diff --git a/lib/core/constants.dart b/lib/core/constants.dart index 075b1ab..8d3c8cc 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -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); +} diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index 5930901..a6c6376 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -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), ); } diff --git a/lib/core/enums.dart b/lib/core/enums.dart index ce06f85..17a01f6 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -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] diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 89354bd..4d86460 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index d567f50..17c3b06 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index aea4457..5080ff3 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -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 diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index bfb9870..c720941 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -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'; } diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 38ea20f..cd71035 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -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'; } diff --git a/lib/main.dart b/lib/main.dart index c1ed977..1dee10b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(), ); } diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 1e38808..a8b18c8 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -17,7 +17,10 @@ class CustomNavigationBar extends StatefulWidget { class _CustomNavigationBarState extends State 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 ); } + /// 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) { diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index cba22ef..f92df0f 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -18,15 +18,17 @@ class CreateGroupView extends StatefulWidget { } class _CreateGroupViewState extends State { - final _groupNameController = TextEditingController(); late final AppDatabase db; + /// Controller for the group name input field + final _groupNameController = TextEditingController(); + + /// List of currently selected players List selectedPlayers = []; @override void initState() { super.initState(); - db = Provider.of(context, listen: false); _groupNameController.addListener(() { setState(() {}); @@ -44,27 +46,16 @@ class _CreateGroupViewState extends State { 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)), 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 { ), ); } - setState(() {}); }, ), const SizedBox(height: 20), diff --git a/lib/presentation/views/main_menu/group_view/groups_view.dart b/lib/presentation/views/main_menu/group_view/groups_view.dart index 3505a3c..57d05a4 100644 --- a/lib/presentation/views/main_menu/group_view/groups_view.dart +++ b/lib/presentation/views/main_menu/group_view/groups_view.dart @@ -21,7 +21,11 @@ class GroupsView extends StatefulWidget { class _GroupsViewState extends State { late final AppDatabase db; + + /// Loaded groups from the database late List loadedGroups; + + /// Loading state bool isLoading = true; List groups = List.filled( @@ -101,7 +105,7 @@ class _GroupsViewState extends State { void loadGroups() { Future.wait([ db.groupDao.getAllGroups(), - Future.delayed(minimumSkeletonDuration), + Future.delayed(Constants.minimumSkeletonDuration), ]).then((results) { loadedGroups = results[0] as List; setState(() { diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index 96280ce..170adb4 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -21,9 +21,17 @@ class HomeView extends StatefulWidget { class _HomeViewState extends State { bool isLoading = true; + + /// 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 loadedRecentMatches = []; + + /// Recent matches to display, initially filled with skeleton matches List recentMatches = List.filled( 2, Match( @@ -42,32 +50,7 @@ class _HomeViewState extends State { @override void initState() { super.initState(); - final db = Provider.of(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; - 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 { ); } + /// 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(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; + 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) { diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 18e1e9d..5976f72 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -20,10 +20,12 @@ class ChooseGameView extends StatefulWidget { } class _ChooseGameViewState extends State { - 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 { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( - backgroundColor: CustomTheme.backgroundColor, - scrolledUnderElevation: 0, leading: IconButton( 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 diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart index 5101db6..97fbcef 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart @@ -38,8 +38,6 @@ class _ChooseGroupViewState extends State { 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 { ); }, ), - title: Text( - loc.choose_group, - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - centerTitle: true, + title: Text(loc.choose_group), ), body: PopScope( // This fixes that the Android Back Gesture didn't return the @@ -136,8 +130,7 @@ class _ChooseGroupViewState extends State { ); } - /// Filters the groups based on the search query. - /// TODO: Maybe implement also targetting player names? + /// Filters the groups based on the search [query]. void filterGroups(String query) { setState(() { if (query.isEmpty) { diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart index 7a41417..ca021af 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart @@ -19,6 +19,7 @@ class ChooseRulesetView extends StatefulWidget { } class _ChooseRulesetViewState extends State { + /// Currently selected ruleset index late int selectedRulesetIndex; @override @@ -36,8 +37,6 @@ class _ChooseRulesetViewState extends State { child: Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( - backgroundColor: CustomTheme.backgroundColor, - scrolledUnderElevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back_ios), onPressed: () { @@ -48,11 +47,7 @@ class _ChooseRulesetViewState extends State { ); }, ), - 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 diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index d3a23ae..dc6690b 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -26,14 +26,13 @@ class CreateMatchView extends StatefulWidget { } class _CreateMatchViewState extends State { - /// 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 groupsList = []; @@ -68,6 +67,9 @@ class _CreateMatchViewState extends State { /// The currently selected players List? 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 { }); } - 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 { 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 { 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 { ? 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 { ? () 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 { ); } - /// 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; } } diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 5c455f6..0d624f0 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -18,8 +18,12 @@ class MatchResultView extends StatefulWidget { } class _MatchResultViewState extends State { - late final List allPlayers; late final AppDatabase db; + + /// List of all players who participated in the match + late final List allPlayers; + + /// Currently selected winner player Player? _selectedPlayer; @override @@ -47,17 +51,7 @@ class _MatchResultViewState extends State { 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), ), body: SafeArea( child: Column( @@ -132,6 +126,8 @@ class _MatchResultViewState extends State { ); } + /// Handles saving or removing the winner in the database + /// based on the current selection. Future _handleWinnerSaving() async { if (_selectedPlayer == null) { await db.matchDao.removeWinner(matchId: widget.match.id); @@ -144,6 +140,10 @@ class _MatchResultViewState extends State { 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 getAllPlayers(Match match) { List players = []; diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 73f596f..45b957f 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -28,6 +28,8 @@ class _MatchViewState extends State { late final AppDatabase db; bool isLoading = true; + /// Loaded matches from the database, + /// initially filled with skeleton matches List matches = List.filled( 4, Match( @@ -44,7 +46,6 @@ class _MatchViewState extends State { @override void initState() { super.initState(); - db = Provider.of(context, listen: false); loadGames(); } @@ -117,10 +118,11 @@ class _MatchViewState extends State { ); } + /// 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(() { diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 6c30483..53569ad 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -25,28 +25,7 @@ class _StatisticsViewState extends State { @override void initState() { super.initState(); - - final db = Provider.of(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; - final players = results[1] as List; - 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 { 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 { width: constraints.maxWidth * 0.95, values: matchCounts, itemCount: 10, - barColor: Colors.green, + barColor: Colors.blue, ), ], ), @@ -118,13 +97,43 @@ class _StatisticsViewState extends State { ); } + /// Loads matches and players from the database + /// and calculates statistics for each player + void loadStatisticData() { + final db = Provider.of(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; + final players = results[1] as List; + 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 matches, - List players, - BuildContext context, - ) { + List<(String, int)> _calculateWinsForAllPlayers({ + required List matches, + required List players, + required BuildContext context, + }) { List<(String, int)> winCounts = []; final loc = AppLocalizations.of(context); @@ -169,11 +178,11 @@ class _StatisticsViewState extends State { /// Calculates the number of matches played for each player /// and returns a sorted list of tuples (playerName, matchCount) - List<(String, int)> _calculateMatchAmountsForAllPlayers( - List matches, - List players, - BuildContext context, - ) { + List<(String, int)> _calculateMatchAmountsForAllPlayers({ + required List matches, + required List players, + required BuildContext context, + }) { List<(String, int)> matchCounts = []; final loc = AppLocalizations.of(context); diff --git a/lib/presentation/widgets/app_skeleton.dart b/lib/presentation/widgets/app_skeleton.dart index 209f1d8..1d74456 100644 --- a/lib/presentation/widgets/app_skeleton.dart +++ b/lib/presentation/widgets/app_skeleton.dart @@ -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 createState() => _AppSkeletonState(); } diff --git a/lib/presentation/widgets/buttons/custom_width_button.dart b/lib/presentation/widgets/buttons/custom_width_button.dart index 17c9dc5..7e52648 100644 --- a/lib/presentation/widgets/buttons/custom_width_button.dart +++ b/lib/presentation/widgets/buttons/custom_width_button.dart @@ -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( diff --git a/lib/presentation/widgets/buttons/quick_create_button.dart b/lib/presentation/widgets/buttons/quick_create_button.dart index 3860f1c..40ebeab 100644 --- a/lib/presentation/widgets/buttons/quick_create_button.dart +++ b/lib/presentation/widgets/buttons/quick_create_button.dart @@ -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 createState() => _QuickCreateButtonState(); } @@ -22,7 +29,9 @@ class _QuickCreateButtonState extends State { 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, diff --git a/lib/presentation/widgets/navbar_item.dart b/lib/presentation/widgets/navbar_item.dart index b249571..13a8d4d 100644 --- a/lib/presentation/widgets/navbar_item.dart +++ b/lib/presentation/widgets/navbar_item.dart @@ -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 createState() => _NavbarItemState(); } diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index eac4480..9280ae0 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -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 value) onChanged; - final List? availablePlayers; - final List? 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? availablePlayers; + + /// An optional list of players that should be pre-selected. + final List? initialSelectedPlayers; + + /// A callback function that is invoked whenever the selection changes, + final Function(List value) onChanged; + @override State createState() => _PlayerSelectionState(); } class _PlayerSelectionState extends State { - List selectedPlayers = []; - List suggestedPlayers = []; - List allPlayers = []; + late final AppDatabase db; bool isLoading = true; + + /// Future that loads all players from the database. + late Future> _allPlayersFuture; + + /// The complete list of all available players. + List allPlayers = []; + + /// The list of players suggested based on the search input. + List suggestedPlayers = []; + + /// The list of currently selected players. + List selectedPlayers = []; + + /// Controller for the search bar input. late final TextEditingController _searchBarController = TextEditingController(); - late final AppDatabase db; - late Future> _allPlayersFuture; + + /// Skeleton data used while loading players. late final List skeletonData = List.filled( 7, Player(name: 'Player 0'), @@ -49,47 +73,11 @@ class _PlayerSelectionState extends State { loadPlayerList(); } - void loadPlayerList() { - _allPlayersFuture = Future.wait([ - db.playerDao.getAllPlayers(), - Future.delayed(minimumSkeletonDuration), - ]).then((results) => results[0] as List); - 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 { ), 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 { ); } + /// 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); + 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 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) { diff --git a/lib/presentation/widgets/text_input/custom_search_bar.dart b/lib/presentation/widgets/text_input/custom_search_bar.dart index 35c11e1..bf7971a 100644 --- a/lib/presentation/widgets/text_input/custom_search_bar.dart +++ b/lib/presentation/widgets/text_input/custom_search_bar.dart @@ -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? 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? onChanged; + + /// The constraints for the search bar. + final BoxConstraints? constraints; + @override Widget build(BuildContext context) { return SearchBar( diff --git a/lib/presentation/widgets/text_input/text_input_field.dart b/lib/presentation/widgets/text_input/text_input_field.dart index 6cd9d75..a409c68 100644 --- a/lib/presentation/widgets/text_input/text_input_field.dart +++ b/lib/presentation/widgets/text_input/text_input_field.dart @@ -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? 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? onChanged; + + /// The hint text displayed in the text input field when it is empty. + final String hintText; + @override Widget build(BuildContext context) { return TextField( diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index 10a695d..f6ec940 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -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 createState() => _ChooseTileState(); } @@ -22,8 +32,8 @@ class _ChooseTileState extends State { 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: [ diff --git a/lib/presentation/widgets/tiles/custom_radio_list_tile.dart b/lib/presentation/widgets/tiles/custom_radio_list_tile.dart index 11e8b40..706aabb 100644 --- a/lib/presentation/widgets/tiles/custom_radio_list_tile.dart +++ b/lib/presentation/widgets/tiles/custom_radio_list_tile.dart @@ -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 extends StatelessWidget { - final String text; - final T value; - final ValueChanged onContainerTap; - const CustomRadioListTile({ super.key, required this.text, @@ -13,6 +13,15 @@ class CustomRadioListTile 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 onContainerTap; + @override Widget build(BuildContext context) { return GestureDetector( @@ -23,7 +32,7 @@ class CustomRadioListTile extends StatelessWidget { decoration: BoxDecoration( color: CustomTheme.boxColor, border: Border.all(color: CustomTheme.boxBorder), - borderRadius: BorderRadius.circular(12), + borderRadius: CustomTheme.standardBorderRadiusAll, ), child: Row( children: [ diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart index 5f870de..64d9caa 100644 --- a/lib/presentation/widgets/tiles/group_tile.dart +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -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 diff --git a/lib/presentation/widgets/tiles/info_tile.dart b/lib/presentation/widgets/tiles/info_tile.dart index ff73e59..3e11679 100644 --- a/lib/presentation/widgets/tiles/info_tile.dart +++ b/lib/presentation/widgets/tiles/info_tile.dart @@ -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 createState() => _InfoTileState(); } diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index c455949..55d81c3 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -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 createState() => _MatchTileState(); } class _MatchTileState extends State { + late final List _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 { 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 { const SizedBox(height: 12), ], - if (allPlayers.isNotEmpty) ...[ + if (_allPlayers.isNotEmpty) ...[ Text( loc.players, style: const TextStyle( @@ -127,7 +142,7 @@ class _MatchTileState extends State { 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 { ); } + /// 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 { } } - List _getAllPlayers() { - final allPlayers = []; + /// Retrieves all unique players associated with the match, + /// combining players from both the match and its group. + List _getCombinedPlayers() { + final allPlayers = []; final playerIds = {}; // Add players from game.players diff --git a/lib/presentation/widgets/tiles/quick_info_tile.dart b/lib/presentation/widgets/tiles/quick_info_tile.dart index d360aba..839f6c3 100644 --- a/lib/presentation/widgets/tiles/quick_info_tile.dart +++ b/lib/presentation/widgets/tiles/quick_info_tile.dart @@ -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 createState() => _QuickInfoTileState(); } diff --git a/lib/presentation/widgets/tiles/settings_list_tile.dart b/lib/presentation/widgets/tiles/settings_list_tile.dart index 6b43557..7fb0f80 100644 --- a/lib/presentation/widgets/tiles/settings_list_tile.dart +++ b/lib/presentation/widgets/tiles/settings_list_tile.dart @@ -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( diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index 598fad0..2c0ced0 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -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, - ), - ), - ), - ], - ), + ); + }), ); - }), + }, ), ), ), diff --git a/lib/presentation/widgets/tiles/text_icon_list_tile.dart b/lib/presentation/widgets/tiles/text_icon_list_tile.dart index b23ef75..7d3fe1c 100644 --- a/lib/presentation/widgets/tiles/text_icon_list_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_list_tile.dart @@ -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( diff --git a/lib/presentation/widgets/tiles/text_icon_tile.dart b/lib/presentation/widgets/tiles/text_icon_tile.dart index 2544837..7142b27 100644 --- a/lib/presentation/widgets/tiles/text_icon_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_tile.dart @@ -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( diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart index 465c94d..781149e 100644 --- a/lib/presentation/widgets/tiles/title_description_list_tile.dart +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -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( diff --git a/lib/presentation/widgets/top_centered_message.dart b/lib/presentation/widgets/top_centered_message.dart index a5deea2..c15c93d 100644 --- a/lib/presentation/widgets/top_centered_message.dart +++ b/lib/presentation/widgets/top_centered_message.dart @@ -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(