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
Duration minimumSkeletonDuration = const Duration(milliseconds: 250);
class Constants {
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';
class CustomTheme {
CustomTheme._(); // Private constructor to prevent instantiation
// ==================== Colors ====================
static Color primaryColor = const Color(0xFF7505E4);
static Color secondaryColor = const Color(0xFFAFA2FF);
static Color backgroundColor = const Color(0xFF0B0B0B);
static Color boxColor = const Color(0xFF101010);
static Color onBoxColor = const Color(0xFF181818);
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(
color: boxColor,
border: Border.all(color: boxBorder),
borderRadius: BorderRadius.circular(12),
borderRadius: standardBorderRadiusAll,
);
static BoxDecoration highlightedBoxDecoration = BoxDecoration(
color: boxColor,
border: Border.all(color: primaryColor),
borderRadius: BorderRadius.circular(12),
borderRadius: standardBorderRadiusAll,
boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)],
);
// ==================== App Bar Theme ====================
static AppBarTheme appBarTheme = AppBarTheme(
backgroundColor: backgroundColor,
foregroundColor: Colors.white,
foregroundColor: textColor,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
titleTextStyle: const TextStyle(
color: Colors.white,
color: textColor,
fontSize: 20,
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';
/// 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 }
/// Result types for import operations in the [SettingsView]

View File

@@ -1,17 +1,17 @@
{
"@@locale": "de",
"all_players": "Alle Spieler:innen:",
"all_players": "Alle Spieler:innen",
"all_players_selected": "Alle Spieler:innen ausgewählt",
"amount_of_matches": "Anzahl der Matches",
"amount_of_matches": "Anzahl der Spiele",
"cancel": "Abbrechen",
"choose_game": "Spielvorlage wählen",
"choose_group": "Gruppe wählen",
"choose_ruleset": "Regelwerk wählen",
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
"create_group": "Gruppe erstellen",
"create_match": "Match erstellen",
"create_match": "Spiel 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_exported": "Daten erfolgreich exportiert",
"data_successfully_imported": "Daten erfolgreich importiert",
@@ -34,28 +34,28 @@
"info": "Info",
"invalid_schema": "Ungültiges Schema",
"least_points": "Niedrigste Punkte",
"match_in_progress": "Match läuft...",
"match_name": "Matchname",
"matches": "Matches",
"match_in_progress": "Spiel läuft...",
"match_name": "Spieltitel",
"matches": "Spiele",
"menu": "Menü",
"most_points": "Höchste Punkte",
"no_data_available": "Keine Daten verfügbar",
"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_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
"no_players_selected": "Keine Spieler:in ausgewählt",
"no_recent_matches_available": "Keine letzten Matches verfügbar",
"no_second_match_available": "Kein zweites Match verfügbar",
"no_players_selected": "Keine Spieler:innen ausgewählt",
"no_recent_matches_available": "Keine letzten Spiele verfügbar",
"no_second_match_available": "Kein zweites Spiel verfügbar",
"no_statistics_available": "Keine Statistiken verfügbar",
"none": "Kein",
"none_group": "Keine",
"not_available": "Nicht verfügbar",
"player_name": "Spieler:innenname",
"players": "Spieler:in",
"players": "Spieler:innen",
"players_count": "{count} Spieler",
"quick_create": "Schnellzugriff",
"recent_matches": "Letzte Matches",
"recent_matches": "Letzte Spiele",
"ruleset": "Regelwerk",
"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.",
@@ -64,7 +64,7 @@
"search_for_groups": "Nach Gruppen suchen",
"search_for_players": "Nach Spieler:innen suchen",
"select_winner": "Gewinner:in wählen:",
"selected_players": "Ausgewählte Spieler:in: {count}",
"selected_players": "Ausgewählte Spieler:innen",
"settings": "Einstellungen",
"single_loser": "Ein:e Verlierer:in",
"single_winner": "Ein:e Gewinner:in",
@@ -73,11 +73,11 @@
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
"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",
"today_at": "Heute um {time}",
"today_at": "Heute um",
"undo": "Rückgängig",
"unknown_exception": "Unbekannter Fehler (siehe Konsole)",
"winner": "Gewinner:in: {winnerName}",
"winner": "Gewinner:in",
"winrate": "Siegquote",
"wins": "Siege",
"yesterday_at": "Gestern um {time}"
"yesterday_at": "Gestern um"
}

View File

@@ -1,367 +1,343 @@
{
"@@locale": "en",
"@all_players": {
"description": "Label for all players list"
},
"@all_players_selected": {
"description": "Message when all players are added to selection"
},
"@amount_of_matches": {
"description": "Label for amount of matches statistic"
},
"@cancel": {
"description": "Cancel button text"
},
"@choose_game": {
"description": "Label for choosing a game"
},
"@choose_group": {
"description": "Label for choosing a group"
},
"@choose_ruleset": {
"description": "Label for choosing a ruleset"
},
"@could_not_add_player": {
"description": "Error message when adding a player fails",
"placeholders": {
"playerName": {
"type": "String",
"example": "John"
}
}
},
"@create_group": {
"description": "Button text to create a group"
},
"@create_match": {
"description": "Button text to create a match"
},
"@create_new_group": {
"description": "Button text to create a new group"
},
"@create_new_match": {
"description": "Button text to create a new match"
},
"@data_successfully_deleted": {
"description": "Success message after deleting data"
},
"@data_successfully_exported": {
"description": "Success message after exporting data"
},
"@data_successfully_imported": {
"description": "Success message after importing data"
},
"@days_ago": {
"description": "Date format for days ago",
"placeholders": {
"count": {
"type": "int"
}
}
},
"@delete": {
"description": "Delete button text"
},
"@delete_all_data": {
"description": "Confirmation dialog for deleting all data"
},
"@error_creating_group": {
"description": "Error message when group creation fails"
},
"@error_reading_file": {
"description": "Error message when file cannot be read"
},
"@export_canceled": {
"description": "Message when export is canceled"
},
"@export_data": {
"description": "Export data menu item"
},
"@format_exception": {
"description": "Error message for format exceptions"
},
"@game": {
"description": "Game label"
},
"@game_name": {
"description": "Placeholder for game name search"
},
"@game_tracker": {
"description": "App Name"
},
"@group": {
"description": "Group label"
},
"@group_name": {
"description": "Placeholder for group name input"
},
"@groups": {
"description": "Label for groups"
},
"@home": {
"description": "Home tab label"
},
"@import_canceled": {
"description": "Message when import is canceled"
},
"@import_data": {
"description": "Import data menu item"
},
"@info": {
"description": "Info label"
},
"@invalid_schema": {
"description": "Error message for invalid schema"
},
"@least_points": {
"description": "Title for least points ruleset"
},
"@match_in_progress": {
"description": "Message when match is in progress"
},
"@match_name": {
"description": "Placeholder for match name input"
},
"@matches": {
"description": "Label for matches"
},
"@menu": {
"description": "Menu label"
},
"@most_points": {
"description": "Title for most points ruleset"
},
"@no_data_available": {
"description": "Message when no data in the statistic tiles is given"
},
"@no_groups_created_yet": {
"description": "Message when no groups exist"
},
"@no_matches_created_yet": {
"description": "Message when no matches exist"
},
"@no_players_created_yet": {
"description": "Message when no players exist"
},
"@no_players_found_with_that_name": {
"description": "Message when search returns no results"
},
"@no_players_selected": {
"description": "Message when no players are selected"
},
"@no_recent_matches_available": {
"description": "Message when no recent matches exist"
},
"@no_second_match_available": {
"description": "Message when no second match exists"
},
"@no_statistics_available": {
"description": "Message when no statistics are available, because no matches were played yet"
},
"@none": {
"description": "None option label"
},
"@none_group": {
"description": "None group option label"
},
"@not_available": {
"description": "Abbreviation for not available"
},
"@player_name": {
"description": "Placeholder for player name input"
},
"@players": {
"description": "Players label"
},
"@players_count": {
"description": "Shows the number of players",
"placeholders": {
"count": {
"type": "int"
}
}
},
"@quick_create": {
"description": "Title for quick create section"
},
"@recent_matches": {
"description": "Title for recent matches section"
},
"@ruleset": {
"description": "Ruleset label"
},
"@ruleset_least_points": {
"description": "Description for least points ruleset"
},
"@ruleset_most_points": {
"description": "Description for most points ruleset"
},
"@ruleset_single_loser": {
"description": "Description for single loser ruleset"
},
"@ruleset_single_winner": {
"description": "Description for single winner ruleset"
},
"@search_for_groups": {
"description": "Hint text for group search input field"
},
"@search_for_players": {
"description": "Hint text for player search input field"
},
"@select_winner": {
"description": "Label to select the winner"
},
"@selected_players": {
"description": "Shows the number of selected players",
"placeholders": {
"count": {
"type": "int",
"format": "compact"
}
}
},
"@settings": {
"description": "Settings label"
},
"@single_loser": {
"description": "Title for single loser ruleset"
},
"@single_winner": {
"description": "Title for single winner ruleset"
},
"@statistics": {
"description": "Statistics tab label"
},
"@stats": {
"description": "Stats tab label (short)"
},
"@successfully_added_player": {
"description": "Success message when adding a player",
"placeholders": {
"playerName": {
"type": "String",
"example": "John"
}
}
},
"@there_is_no_group_matching_your_search": {
"description": "Message when search returns no groups"
},
"@this_cannot_be_undone": {
"description": "Warning message for irreversible actions"
},
"@today_at": {
"description": "Date format for today",
"placeholders": {
"time": {
"type": "String",
"example": "14:30"
}
}
},
"@undo": {
"description": "Undo button text"
},
"@unknown_exception": {
"description": "Error message for unknown exceptions"
},
"@winner": {
"description": "Winner label"
},
"@winrate": {
"description": "Label for winrate statistic"
},
"@wins": {
"description": "Label for wins statistic"
},
"@yesterday_at": {
"description": "Date format for yesterday",
"placeholders": {
"time": {
"type": "String",
"example": "14:30"
}
}
},
"all_players": "All players:",
"all_players_selected": "All players selected",
"amount_of_matches": "Amount of Matches",
"cancel": "Cancel",
"choose_game": "Choose Game",
"choose_group": "Choose Group",
"choose_ruleset": "Choose Ruleset",
"could_not_add_player": "Could not add player {playerName}",
"create_group": "Create Group",
"create_match": "Create match",
"create_new_group": "Create new group",
"create_new_match": "Create new match",
"data_successfully_deleted": "Data successfully deleted",
"data_successfully_exported": "Data successfully exported",
"data_successfully_imported": "Data successfully imported",
"days_ago": "{count} days ago",
"delete": "Delete",
"delete_all_data": "Delete all data?",
"error_creating_group": "Error while creating group, please try again",
"error_reading_file": "Error reading file",
"export_canceled": "Export canceled",
"export_data": "Export data",
"format_exception": "Format Exception (see console)",
"game": "Game",
"game_name": "Game Name",
"game_tracker": "Game Tracker",
"group": "Group",
"group_name": "Group name",
"groups": "Groups",
"home": "Home",
"import_canceled": "Import canceled",
"import_data": "Import data",
"info": "Info",
"invalid_schema": "Invalid Schema",
"least_points": "Least Points",
"match_in_progress": "Match in progress...",
"match_name": "Match name",
"matches": "Matches",
"menu": "Menu",
"most_points": "Most Points",
"no_data_available": "No data available",
"no_groups_created_yet": "No groups created yet",
"no_matches_created_yet": "No matches created yet",
"no_players_created_yet": "No players created yet",
"no_players_found_with_that_name": "No players found with that name",
"no_players_selected": "No players selected",
"no_recent_matches_available": "No recent matches available",
"no_second_match_available": "No second match available",
"no_statistics_available": "No statistics available",
"none": "None",
"none_group": "None",
"not_available": "Not available",
"player_name": "Player name",
"players": "Players",
"players_count": "{count} Players",
"quick_create": "Quick Create",
"recent_matches": "Recent Matches",
"ruleset": "Ruleset",
"ruleset_least_points": "Inverse scoring: the player with the fewest points wins.",
"ruleset_most_points": "Traditional ruleset: the player with the most points wins.",
"ruleset_single_loser": "Exactly one loser is determined; last place receives the penalty or consequence.",
"ruleset_single_winner": "Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.",
"search_for_groups": "Search for groups",
"search_for_players": "Search for players",
"select_winner": "Select Winner:",
"selected_players": "Selected players: {count}",
"settings": "Settings",
"single_loser": "Single Loser",
"single_winner": "Single Winner",
"statistics": "Statistics",
"stats": "Stats",
"successfully_added_player": "Successfully added player {playerName}",
"there_is_no_group_matching_your_search": "There is no group matching your search",
"this_cannot_be_undone": "This can't be undone",
"today_at": "Today at {time}",
"undo": "Undo",
"unknown_exception": "Unknown Exception (see console)",
"winner": "Winner",
"winrate": "Winrate",
"wins": "Wins",
"yesterday_at": "Yesterday at {time}"
"@@locale": "en",
"@all_players": {
"description": "Label for all players list"
},
"@all_players_selected": {
"description": "Message when all players are added to selection"
},
"@amount_of_matches": {
"description": "Label for amount of matches statistic"
},
"@app_name": {
"description": "The name of the App"
},
"@cancel": {
"description": "Cancel button text"
},
"@choose_game": {
"description": "Label for choosing a game"
},
"@choose_group": {
"description": "Label for choosing a group"
},
"@choose_ruleset": {
"description": "Label for choosing a ruleset"
},
"@could_not_add_player": {
"description": "Error message when adding a player fails"
},
"@create_group": {
"description": "Button text to create a group"
},
"@create_match": {
"description": "Button text to create a match"
},
"@create_new_group": {
"description": "Button text to create a new group"
},
"@create_new_match": {
"description": "Button text to create a new match"
},
"@data_successfully_deleted": {
"description": "Success message after deleting data"
},
"@data_successfully_exported": {
"description": "Success message after exporting data"
},
"@data_successfully_imported": {
"description": "Success message after importing data"
},
"@days_ago": {
"description": "Date format for days ago",
"placeholders": {
"count": {
"type": "int"
}
}
},
"@delete": {
"description": "Delete button text"
},
"@delete_all_data": {
"description": "Confirmation dialog for deleting all data"
},
"@error_creating_group": {
"description": "Error message when group creation fails"
},
"@error_reading_file": {
"description": "Error message when file cannot be read"
},
"@export_canceled": {
"description": "Message when export is canceled"
},
"@export_data": {
"description": "Export data menu item"
},
"@format_exception": {
"description": "Error message for format exceptions"
},
"@game": {
"description": "Game label"
},
"@game_name": {
"description": "Placeholder for game name search"
},
"@group": {
"description": "Group label"
},
"@group_name": {
"description": "Placeholder for group name input"
},
"@groups": {
"description": "Label for groups"
},
"@home": {
"description": "Home tab label"
},
"@import_canceled": {
"description": "Message when import is canceled"
},
"@import_data": {
"description": "Import data menu item"
},
"@info": {
"description": "Info label"
},
"@invalid_schema": {
"description": "Error message for invalid schema"
},
"@least_points": {
"description": "Title for least points ruleset"
},
"@match_in_progress": {
"description": "Message when match is in progress"
},
"@match_name": {
"description": "Placeholder for match name input"
},
"@matches": {
"description": "Label for matches"
},
"@menu": {
"description": "Menu label"
},
"@most_points": {
"description": "Title for most points ruleset"
},
"@no_data_available": {
"description": "Message when no data in the statistic tiles is given"
},
"@no_groups_created_yet": {
"description": "Message when no groups exist"
},
"@no_matches_created_yet": {
"description": "Message when no matches exist"
},
"@no_players_created_yet": {
"description": "Message when no players exist"
},
"@no_players_found_with_that_name": {
"description": "Message when search returns no results"
},
"@no_players_selected": {
"description": "Message when no players are selected"
},
"@no_recent_matches_available": {
"description": "Message when no recent matches exist"
},
"@no_second_match_available": {
"description": "Message when no second match exists"
},
"@no_statistics_available": {
"description": "Message when no statistics are available, because no matches were played yet"
},
"@none": {
"description": "None option label"
},
"@none_group": {
"description": "None group option label"
},
"@not_available": {
"description": "Abbreviation for not available"
},
"@player_name": {
"description": "Placeholder for player name input"
},
"@players": {
"description": "Players label"
},
"@players_count": {
"description": "Shows the number of players",
"placeholders": {
"count": {
"type": "int"
}
}
},
"@quick_create": {
"description": "Title for quick create section"
},
"@recent_matches": {
"description": "Title for recent matches section"
},
"@ruleset": {
"description": "Ruleset label"
},
"@ruleset_least_points": {
"description": "Description for least points ruleset"
},
"@ruleset_most_points": {
"description": "Description for most points ruleset"
},
"@ruleset_single_loser": {
"description": "Description for single loser ruleset"
},
"@ruleset_single_winner": {
"description": "Description for single winner ruleset"
},
"@search_for_groups": {
"description": "Hint text for group search input field"
},
"@search_for_players": {
"description": "Hint text for player search input field"
},
"@select_winner": {
"description": "Label to select the winner"
},
"@selected_players": {
"description": "Shows the number of selected players"
},
"@settings": {
"description": "Settings label"
},
"@single_loser": {
"description": "Title for single loser ruleset"
},
"@single_winner": {
"description": "Title for single winner ruleset"
},
"@statistics": {
"description": "Statistics tab label"
},
"@stats": {
"description": "Stats tab label (short)"
},
"@successfully_added_player": {
"description": "Success message when adding a player",
"placeholders": {
"playerName": {
"type": "String",
"example": "John"
}
}
},
"@there_is_no_group_matching_your_search": {
"description": "Message when search returns no groups"
},
"@this_cannot_be_undone": {
"description": "Warning message for irreversible actions"
},
"@today_at": {
"description": "Date format for today"
},
"@undo": {
"description": "Undo button text"
},
"@unknown_exception": {
"description": "Error message for unknown exceptions"
},
"@winner": {
"description": "Winner label"
},
"@winrate": {
"description": "Label for winrate statistic"
},
"@wins": {
"description": "Label for wins statistic"
},
"@yesterday_at": {
"description": "Date format for yesterday"
},
"all_players": "All players",
"all_players_selected": "All players selected",
"amount_of_matches": "Amount of Matches",
"app_name": "Game Tracker",
"cancel": "Cancel",
"choose_game": "Choose Game",
"choose_group": "Choose Group",
"choose_ruleset": "Choose Ruleset",
"could_not_add_player": "Could not add player",
"create_group": "Create Group",
"create_match": "Create match",
"create_new_group": "Create new group",
"create_new_match": "Create new match",
"data_successfully_deleted": "Data successfully deleted",
"data_successfully_exported": "Data successfully exported",
"data_successfully_imported": "Data successfully imported",
"days_ago": "{count} days ago",
"delete": "Delete",
"delete_all_data": "Delete all data?",
"error_creating_group": "Error while creating group, please try again",
"error_reading_file": "Error reading file",
"export_canceled": "Export canceled",
"export_data": "Export data",
"format_exception": "Format Exception (see console)",
"game": "Game",
"game_name": "Game Name",
"group": "Group",
"group_name": "Group name",
"groups": "Groups",
"home": "Home",
"import_canceled": "Import canceled",
"import_data": "Import data",
"info": "Info",
"invalid_schema": "Invalid Schema",
"least_points": "Least Points",
"match_in_progress": "Match in progress...",
"match_name": "Match name",
"matches": "Matches",
"menu": "Menu",
"most_points": "Most Points",
"no_data_available": "No data available",
"no_groups_created_yet": "No groups created yet",
"no_matches_created_yet": "No matches created yet",
"no_players_created_yet": "No players created yet",
"no_players_found_with_that_name": "No players found with that name",
"no_players_selected": "No players selected",
"no_recent_matches_available": "No recent matches available",
"no_second_match_available": "No second match available",
"no_statistics_available": "No statistics available",
"none": "None",
"none_group": "None",
"not_available": "Not available",
"player_name": "Player name",
"players": "Players",
"players_count": "{count} Players",
"quick_create": "Quick Create",
"recent_matches": "Recent Matches",
"ruleset": "Ruleset",
"ruleset_least_points": "Inverse scoring: the player with the fewest points wins.",
"ruleset_most_points": "Traditional ruleset: the player with the most points wins.",
"ruleset_single_loser": "Exactly one loser is determined; last place receives the penalty or consequence.",
"ruleset_single_winner": "Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.",
"search_for_groups": "Search for groups",
"search_for_players": "Search for players",
"select_winner": "Select Winner:",
"selected_players": "Selected players",
"settings": "Settings",
"single_loser": "Single Loser",
"single_winner": "Single Winner",
"statistics": "Statistics",
"stats": "Stats",
"successfully_added_player": "Successfully added player {playerName}",
"there_is_no_group_matching_your_search": "There is no group matching your search",
"this_cannot_be_undone": "This can't be undone",
"today_at": "Today at",
"undo": "Undo",
"unknown_exception": "Unknown Exception (see console)",
"winner": "Winner",
"winrate": "Winrate",
"wins": "Wins",
"yesterday_at": "Yesterday at"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,15 +18,17 @@ class CreateGroupView extends StatefulWidget {
}
class _CreateGroupViewState extends State<CreateGroupView> {
final _groupNameController = TextEditingController();
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 = [];
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_groupNameController.addListener(() {
setState(() {});
@@ -44,27 +46,16 @@ class _CreateGroupViewState extends State<CreateGroupView> {
final loc = AppLocalizations.of(context);
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
title: Text(
loc.create_new_group,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
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
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
margin: CustomTheme.standardMargin,
child: TextInputField(
controller: _groupNameController,
hintText: loc.group_name,
onChanged: (value) {
setState(() {});
},
),
),
Expanded(
@@ -109,7 +100,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
),
);
}
setState(() {});
},
),
const SizedBox(height: 20),

View File

@@ -21,7 +21,11 @@ class GroupsView extends StatefulWidget {
class _GroupsViewState extends State<GroupsView> {
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;
/// Loading state
bool isLoading = true;
List<Group> groups = List.filled(
@@ -101,7 +105,7 @@ class _GroupsViewState extends State<GroupsView> {
void loadGroups() {
Future.wait([
db.groupDao.getAllGroups(),
Future.delayed(minimumSkeletonDuration),
Future.delayed(Constants.minimumSkeletonDuration),
]).then((results) {
loadedGroups = results[0] as List<Group>;
setState(() {

View File

@@ -21,9 +21,17 @@ class HomeView extends StatefulWidget {
class _HomeViewState extends State<HomeView> {
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;
/// Amount of groups in the database
int groupCount = 0;
/// Loaded recent matches from the database
List<Match> loadedRecentMatches = [];
/// Recent matches to display, initially filled with skeleton matches
List<Match> recentMatches = List.filled(
2,
Match(
@@ -42,32 +50,7 @@ class _HomeViewState extends State<HomeView> {
@override
void initState() {
super.initState();
final db = Provider.of<AppDatabase>(context, listen: false);
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;
});
}
});
loadHomeViewData();
}
@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) {
final loc = AppLocalizations.of(context);
if (game.group == null) {

View File

@@ -20,10 +20,12 @@ class ChooseGameView extends StatefulWidget {
}
class _ChooseGameViewState extends State<ChooseGameView> {
late int selectedGameIndex;
/// Controller for the search bar
final TextEditingController searchBarController = TextEditingController();
/// Currently selected game index
late int selectedGameIndex;
@override
void initState() {
selectedGameIndex = widget.initialGameIndex;
@@ -36,19 +38,13 @@ class _ChooseGameViewState extends State<ChooseGameView> {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
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),
onPressed: () {
Navigator.of(context).pop(selectedGameIndex);
},
),
title: Text(
loc.choose_game,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
title: Text(loc.choose_game),
),
body: PopScope(
// This fixes that the Android Back Gesture didn't return the

View File

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

grafik.png

![grafik.png](/attachments/a9c6941a-0805-49e8-8585-7c0a56c3ce4d)
body: PopScope(
// 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.
/// TODO: Maybe implement also targetting player names?
/// 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
void filterGroups(String query) {
setState(() {
if (query.isEmpty) {

View File

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

View File

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

grafik.png

![grafik.png](/attachments/06706384-fe63-4ea8-8886-07737018c7f5)
body: SafeArea(
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 {
if (_selectedPlayer == null) {
await db.matchDao.removeWinner(matchId: widget.match.id);
@@ -144,6 +140,10 @@ class _MatchResultViewState extends State<MatchResultView> {
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> players = [];

View File

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

kein comment?

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

View File

@@ -25,28 +25,7 @@ class _StatisticsViewState extends State<StatisticsView> {
@override
void initState() {
super.initState();
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;
});
});
loadStatisticData();
}
@override
@@ -78,7 +57,7 @@ class _StatisticsViewState extends State<StatisticsView> {
width: constraints.maxWidth * 0.95,
values: winCounts,
itemCount: 3,
barColor: Colors.blue,
barColor: Colors.green,
),
SizedBox(height: constraints.maxHeight * 0.02),
StatisticsTile(
@@ -96,7 +75,7 @@ class _StatisticsViewState extends State<StatisticsView> {
width: constraints.maxWidth * 0.95,
values: matchCounts,
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
/// and returns a sorted list of tuples (playerName, winCount)
List<(String, int)> _calculateWinsForAllPlayers(
List<Match> matches,
List<Player> players,
BuildContext context,
) {
List<(String, int)> _calculateWinsForAllPlayers({
required List<Match> matches,
required List<Player> players,
required BuildContext context,
}) {
List<(String, int)> winCounts = [];
final loc = AppLocalizations.of(context);
@@ -169,11 +178,11 @@ class _StatisticsViewState extends State<StatisticsView> {
/// Calculates the number of matches played for each player
/// and returns a sorted list of tuples (playerName, matchCount)
List<(String, int)> _calculateMatchAmountsForAllPlayers(
List<Match> matches,
List<Player> players,
BuildContext context,
) {
List<(String, int)> _calculateMatchAmountsForAllPlayers({
required List<Match> matches,
required List<Player> players,
required BuildContext context,
}) {
List<(String, int)> matchCounts = [];
final loc = AppLocalizations.of(context);

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.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 {
final Widget child;
final bool enabled;
final bool fixLayoutBuilder;
const AppSkeleton({
super.key,
required this.child,
@@ -13,6 +13,15 @@ class AppSkeleton extends StatefulWidget {
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
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/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 {
const CustomWidthButton({
super.key,
@@ -11,9 +17,16 @@ class CustomWidthButton extends StatelessWidget {
this.onPressed,
});
/// The text to display on the button.
final String text;
/// The size of the button relative to the width of the screen.
final double sizeRelativeToWidth;
/// The callback to be invoked when the button is pressed.
final VoidCallback? onPressed;
/// The type of button to display. Depends on the enum [ButtonType].
final ButtonType buttonType;
@override
@@ -47,7 +60,7 @@ class CustomWidthButton extends StatelessWidget {
60,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: CustomTheme.standardBorderRadiusAll,
),
),
child: Text(
@@ -78,7 +91,7 @@ class CustomWidthButton extends StatelessWidget {
),
side: BorderSide(color: borderSideColor, width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: CustomTheme.standardBorderRadiusAll,
),
),
child: Text(

View File

@@ -1,15 +1,22 @@
import 'package:flutter/material.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 {
final String text;
final VoidCallback? onPressed;
const QuickCreateButton({
super.key,
required this.text,
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
State<QuickCreateButton> createState() => _QuickCreateButtonState();
}
@@ -22,7 +29,9 @@ class _QuickCreateButtonState extends State<QuickCreateButton> {
style: ElevatedButton.styleFrom(
minimumSize: const Size(140, 45),
backgroundColor: CustomTheme.primaryColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: RoundedRectangleBorder(
borderRadius: CustomTheme.standardBorderRadiusAll,
),
),
child: Text(
widget.text,

View File

@@ -1,12 +1,12 @@
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 {
final int index;
final bool isSelected;
final IconData icon;
final String label;
final Function(int) onTabTapped;
const NavbarItem({
super.key,
required this.index,
@@ -16,6 +16,21 @@ class NavbarItem extends StatefulWidget {
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
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: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 {
final Function(List<Player> value) onChanged;
final List<Player>? availablePlayers;
final List<Player>? initialSelectedPlayers;
const PlayerSelection({
super.key,
required this.onChanged,
this.availablePlayers,
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
State<PlayerSelection> createState() => _PlayerSelectionState();
}
class _PlayerSelectionState extends State<PlayerSelection> {
List<Player> selectedPlayers = [];
List<Player> suggestedPlayers = [];
List<Player> allPlayers = [];
late final AppDatabase db;
flixcoo marked this conversation as resolved
Review

kein comment?

kein comment?
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 =
TextEditingController();
late final AppDatabase db;
late Future<List<Player>> _allPlayersFuture;
/// Skeleton data used while loading players.
late final List<Player> skeletonData = List.filled(
7,
Player(name: 'Player 0'),
@@ -49,47 +73,11 @@ class _PlayerSelectionState extends State<PlayerSelection> {
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
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
margin: CustomTheme.standardMargin,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
decoration: CustomTheme.standardBoxDecoration,
child: Column(
@@ -131,9 +119,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
),
const SizedBox(height: 10),
Text(
AppLocalizations.of(
context,
).selected_players(selectedPlayers.length),
loc.selected_players,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
@@ -227,53 +213,97 @@ 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.
/// Shows a snackbar indicating success or failure.
/// [context] - BuildContext to show the snackbar.
void addNewPlayerFromSearch({required BuildContext context}) async {
Future<void> addNewPlayerFromSearch({required BuildContext context}) async {
final loc = AppLocalizations.of(context);
String playerName = _searchBarController.text.trim();
Player createdPlayer = Player(name: playerName);
bool success = await db.playerDao.addPlayer(player: createdPlayer);
final playerName = _searchBarController.text.trim();
final createdPlayer = Player(name: playerName);
final success = await db.playerDao.addPlayer(player: createdPlayer);
if (!context.mounted) return;
if (success) {
selectedPlayers.insert(0, createdPlayer);
widget.onChanged([...selectedPlayers]);
allPlayers.add(createdPlayer);
setState(() {
_searchBarController.clear();
suggestedPlayers = allPlayers.where((player) {
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),
),
),
),
);
_handleSuccessfulPlayerCreation(createdPlayer);
showSnackBarMessage(loc.successfully_added_player(playerName));
} 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),
),
),
),
);
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]);
allPlayers.add(player);
setState(() {
_searchBarController.clear();
_updateSuggestedPlayers();
});
}
/// 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
/// are available in the suggested players list.
String _getInfoText(BuildContext context) {

View File

@@ -1,16 +1,16 @@
import 'package:flutter/material.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 {
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({
super.key,
required this.controller,
@@ -23,6 +23,30 @@ class CustomSearchBar extends StatelessWidget {
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
Widget build(BuildContext context) {
return SearchBar(

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.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 {
final TextEditingController controller;
final ValueChanged<String>? onChanged;
final String hintText;
const TextInputField({
super.key,
required this.controller,
@@ -13,6 +13,15 @@ class TextInputField extends StatelessWidget {
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
Widget build(BuildContext context) {
return TextField(

View File

@@ -1,10 +1,11 @@
import 'package:flutter/material.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 {
final String title;
final VoidCallback? onPressed;
final String? trailingText;
const ChooseTile({
super.key,
required this.title,
@@ -12,6 +13,15 @@ class ChooseTile extends StatefulWidget {
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
State<ChooseTile> createState() => _ChooseTileState();
}
@@ -22,8 +32,8 @@ class _ChooseTileState extends State<ChooseTile> {
return GestureDetector(
onTap: widget.onPressed,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
margin: CustomTheme.tileMargin,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: CustomTheme.standardBoxDecoration,
child: Row(
children: [

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.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 {
final String text;
final T value;
final ValueChanged<T> onContainerTap;
const CustomRadioListTile({
super.key,
required this.text,
@@ -13,6 +13,15 @@ class CustomRadioListTile<T> extends StatelessWidget {
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
Widget build(BuildContext context) {
return GestureDetector(
@@ -23,7 +32,7 @@ class CustomRadioListTile<T> extends StatelessWidget {
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
borderRadius: CustomTheme.standardBorderRadiusAll,
),
child: Row(
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/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 {
const GroupTile({super.key, required this.group, this.isHighlighted = false});
/// The group data to be displayed.
final Group group;
/// Whether the tile should be highlighted.
final bool isHighlighted;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
margin: CustomTheme.standardMargin,
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
decoration: isHighlighted
? CustomTheme.highlightedBoxDecoration

View File

@@ -1,13 +1,14 @@
import 'package:flutter/material.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 {
final String title;
final IconData icon;
final Widget content;
final EdgeInsets? padding;
final double? height;
final double? width;
const InfoTile({
super.key,
required this.title,
@@ -18,6 +19,24 @@ class InfoTile extends StatefulWidget {
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
State<InfoTile> createState() => _InfoTileState();
}

View File

@@ -1,33 +1,48 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.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/presentation/widgets/tiles/text_icon_tile.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 {
final Match match;
final VoidCallback 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
State<MatchTile> createState() => _MatchTileState();
}
class _MatchTileState extends State<MatchTile> {
late final List<Player> _allPlayers;
@override
void initState() {
super.initState();
_allPlayers = _getCombinedPlayers();
}
@override
Widget build(BuildContext context) {
final group = widget.match.group;
final winner = widget.match.winner;
final allPlayers = _getAllPlayers();
final loc = AppLocalizations.of(context);
return GestureDetector(
onTap: widget.onTap,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.all(16),
margin: CustomTheme.tileMargin,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
@@ -103,7 +118,7 @@ class _MatchTileState extends State<MatchTile> {
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
color: CustomTheme.textColor,
),
overflow: TextOverflow.ellipsis,
),
@@ -114,7 +129,7 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(height: 12),
],
if (allPlayers.isNotEmpty) ...[
if (_allPlayers.isNotEmpty) ...[
Text(
loc.players,
style: const TextStyle(
@@ -127,7 +142,7 @@ class _MatchTileState extends State<MatchTile> {
Wrap(
spacing: 6,
runSpacing: 6,
children: allPlayers.map((player) {
children: _allPlayers.map((player) {
return TextIconTile(text: player.name, iconEnabled: false);
}).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) {
final now = DateTime.now();
final difference = now.difference(dateTime);
final loc = AppLocalizations.of(context);
if (difference.inDays == 0) {
return AppLocalizations.of(
context,
).today_at(DateFormat('HH:mm').format(dateTime));
return "${loc.today_at} ${DateFormat('HH:mm').format(dateTime)}";
} else if (difference.inDays == 1) {
return AppLocalizations.of(
context,
).yesterday_at(DateFormat('HH:mm').format(dateTime));
return "${loc.yesterday_at} ${DateFormat('HH:mm').format(dateTime)}";
} else if (difference.inDays < 7) {
return loc.days_ago(difference.inDays);
} else {
@@ -158,8 +171,10 @@ class _MatchTileState extends State<MatchTile> {
}
}
List<dynamic> _getAllPlayers() {
final allPlayers = <dynamic>[];
/// Retrieves all unique players associated with the match,
/// combining players from both the match and its group.
List<Player> _getCombinedPlayers() {
final allPlayers = <Player>[];
final playerIds = <String>{};
// Add players from game.players

View File

@@ -1,13 +1,14 @@
import 'package:flutter/material.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 {
final String title;
final IconData icon;
final int value;
final double? height;
final double? width;
final EdgeInsets? padding;
const QuickInfoTile({
super.key,
required this.title,
@@ -18,6 +19,24 @@ class QuickInfoTile extends StatefulWidget {
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
State<QuickInfoTile> createState() => _QuickInfoTileState();
}

View File

@@ -1,19 +1,32 @@
import 'package:flutter/material.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 {
final VoidCallback? onPressed;
final IconData icon;
final String title;
final Widget? suffixWidget;
const SettingsListTile({
super.key,
required this.title,
required this.icon,
required this.title,
this.suffixWidget,
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
Widget build(BuildContext context) {
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/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 {
const StatisticsTile({
super.key,
@@ -15,16 +22,26 @@ class StatisticsTile extends StatelessWidget {
required this.barColor,
});
/// The icon displayed next to the title.
final IconData icon;
/// The title text displayed on the tile.
final String title;
/// The width of the tile.
final double width;
/// A list of tuples containing labels and their corresponding numeric values.
final List<(String, num)> values;
/// The maximum number of items to display.
final int itemCount;
/// The color of the bars representing the values.
final Color barColor;
@override
Widget build(BuildContext context) {
final maxBarWidth = MediaQuery.of(context).size.width * 0.65;
final loc = AppLocalizations.of(context);
return InfoTile(
@@ -39,38 +56,56 @@ class StatisticsTile extends StatelessWidget {
heightFactor: 4,
child: Text(loc.no_data_available),
),
child: Column(
children: List.generate(min(values.length, itemCount), (index) {
/// The maximum wins among all players
final maxMatches = values.isNotEmpty ? values[0].$2 : 0;
child: LayoutBuilder(
builder: (context, constraints) {
final maxBarWidth = constraints.maxWidth * 0.65;
return Column(
children: List.generate(min(values.length, itemCount), (index) {
/// The maximum wins among all players
final maxMatches = values.isNotEmpty ? values[0].$2 : 0;
/// Fraction of wins
final double fraction = (maxMatches > 0)
? (values[index].$2 / maxMatches)
: 0.0;
/// Fraction of wins
final double fraction = (maxMatches > 0)
? (values[index].$2 / maxMatches)
: 0.0;
/// Calculated width for current the bar
final double barWidth = maxBarWidth * fraction;
/// Calculated width for current the bar
final double barWidth = maxBarWidth * fraction;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Stack(
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
height: 24,
width: barWidth,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: barColor,
),
Stack(
children: [
Container(
height: 24,
width: barWidth,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: barColor,
),
),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
values[index].$1,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
Padding(
padding: const EdgeInsets.only(left: 4.0),
const Spacer(),
Center(
child: Text(
values[index].$1,
values[index].$2 <= 1 && values[index].$2 is double
? values[index].$2.toStringAsFixed(2)
: values[index].$2.toString(),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -79,23 +114,10 @@ class StatisticsTile extends StatelessWidget {
),
],
),
const Spacer(),
Center(
child: Text(
values[index].$2 <= 1 && values[index].$2 is double
? values[index].$2.toStringAsFixed(2)
: values[index].$2.toString(),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}),
);
}),
},
),
),
),

View File

@@ -1,18 +1,27 @@
import 'package:flutter/material.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 {
final String text;
final VoidCallback? onPressed;
final bool iconEnabled;
const TextIconListTile({
super.key,
required this.text,
this.onPressed,
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
Widget build(BuildContext context) {
return Container(

View File

@@ -1,18 +1,27 @@
import 'package:flutter/material.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 {
final String text;
final bool iconEnabled;
final VoidCallback? onIconTap;
const TextIconTile({
super.key,
required this.text,
this.onIconTap,
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
Widget build(BuildContext context) {
return Container(

View File

@@ -1,14 +1,14 @@
import 'package:flutter/material.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 {
final String title;
final String description;
final VoidCallback? onPressed;
final bool isHighlighted;
final String? badgeText;
final Color? badgeColor;
const TitleDescriptionListTile({
super.key,
required this.title,
@@ -19,6 +19,24 @@ class TitleDescriptionListTile extends StatelessWidget {
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
Widget build(BuildContext context) {
return GestureDetector(

View File

@@ -1,5 +1,9 @@
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 {
const TopCenteredMessage({
super.key,
@@ -8,10 +12,15 @@ class TopCenteredMessage extends StatelessWidget {
required this.message,
});
final String title;
final String message;
/// The icon to display above the title.
final IconData icon;
/// The title text to display.
final String title;
/// The message text to display below the title.
final String message;
@override
Widget build(BuildContext context) {
return Container(