diff --git a/lib/core/common.dart b/lib/core/common.dart index 8027180..fc61a94 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; @@ -21,8 +21,71 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) { } } -/// Counts how many players in the match are not part of the group -/// Returns the count as a string, or an empty string if there is no group +/// Translates a [GameColor] enum value to its corresponding localized string. +String translateGameColorToString(GameColor color, BuildContext context) { + final loc = AppLocalizations.of(context); + switch (color) { + case GameColor.red: + return loc.color_red; + case GameColor.blue: + return loc.color_blue; + case GameColor.green: + return loc.color_green; + case GameColor.yellow: + return loc.color_yellow; + case GameColor.purple: + return loc.color_purple; + case GameColor.orange: + return loc.color_orange; + case GameColor.pink: + return loc.color_pink; + case GameColor.teal: + return loc.color_teal; + } +} + +/// Returns the [Color] object corresponding to a [GameColor] enum value. +Color getColorFromGameColor(GameColor color) { + switch (color) { + case GameColor.red: + return Colors.red; + case GameColor.blue: + return Colors.blue; + case GameColor.green: + return Colors.green; + case GameColor.yellow: + return const Color(0xFFF7CA28); + case GameColor.purple: + return Colors.purple; + case GameColor.orange: + return const Color(0xFFef681f); + case GameColor.pink: + return Colors.pink; + case GameColor.teal: + return Colors.teal; + } +} + +/// Returns [IconData] corresponding to a [Ruleset] enum value. +IconData getRulesetIcon(Ruleset ruleset) { + switch (ruleset) { + case Ruleset.highestScore: + return Icons.arrow_upward; + case Ruleset.lowestScore: + return Icons.arrow_downward; + case Ruleset.singleWinner: + return Icons.emoji_events; + case Ruleset.singleLoser: + return Icons.sentiment_dissatisfied; + case Ruleset.multipleWinners: + return Icons.group; + } +} + +/// Counts how many players in the [match] are not part of the group +/// +/// Returns the text you append after the group name, e.g. " + 5" or an empty +/// string if there are no extra players String getExtraPlayerCount(Match match) { int count = 0; diff --git a/lib/core/constants.dart b/lib/core/constants.dart index c1bc0fe..86e3ad7 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -19,4 +19,7 @@ class Constants { /// Maximum length for team names static const int MAX_TEAM_NAME_LENGTH = 32; + + /// Maximum length for game descriptions + static const int MAX_GAME_DESCRIPTION_LENGTH = 256; } diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index 3274db9..b32ce63 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -63,9 +63,8 @@ class CustomTheme { static BoxDecoration highlightedBoxDecoration = BoxDecoration( color: boxColor, - border: Border.all(color: primaryColor), + border: Border.all(color: textColor, width: 2), borderRadius: standardBorderRadiusAll, - boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)], ); // ==================== Component Themes ==================== diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index 400f04a..a4c2300 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -194,4 +194,25 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { final rowsAffected = await query.go(); return rowsAffected > 0; } + + /// Retrieves all games with their respective match counts. + /// Returns a list of tuples (Game, matchCount). + Future> getGameUsage() async { + final games = await getAllGames(); + + final results = <(Game, int)>[]; + + for (final game in games) { + final matchCount = + await (selectOnly(db.matchTable) + ..where(db.matchTable.gameId.equals(game.id)) + ..addColumns([db.matchTable.id.count()])) + .map((row) => row.read(db.matchTable.id.count())) + .getSingle(); + + results.add((game, matchCount ?? 0)); + } + + return results; + } } diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 69aaeef..88cca35 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -341,9 +341,20 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); } + /// Retrieves the number of matches associated with a specific game. + Future getMatchCountByGame({required String gameId}) async { + final count = + await (selectOnly(matchTable) + ..where(matchTable.gameId.equals(gameId)) + ..addColumns([matchTable.id.count()])) + .map((row) => row.read(matchTable.id.count())) + .getSingle(); + return count ?? 0; + } + /// Retrieves all matches associated with the given [groupId]. /// Queries the database directly, filtering by [groupId]. - Future> getGroupMatches({required String groupId}) async { + Future> getMatchesByGroup({required String groupId}) async { final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); final rows = await query.get(); @@ -478,4 +489,12 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { final rowsAffected = await query.go(); return rowsAffected > 0; } + + /// Deletes all matches associated with a specific game. + /// Returns the number of matches deleted. + Future deleteMatchesByGame({required String gameId}) async { + final query = delete(matchTable)..where((m) => m.gameId.equals(gameId)); + final rowsAffected = await query.go(); + return rowsAffected; + } } diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index c02f455..89bbd30 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -12,16 +12,15 @@ class Game { final String icon; Game({ - String? id, - DateTime? createdAt, required this.name, required this.ruleset, - String? description, - required this.color, - required this.icon, + this.color = GameColor.orange, + this.description = '', + this.icon = '', + String? id, + DateTime? createdAt, }) : id = id ?? const Uuid().v4(), - createdAt = createdAt ?? clock.now(), - description = description ?? ''; + createdAt = createdAt ?? clock.now(); @override String toString() { diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 65f1813..4033cfe 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -6,10 +6,21 @@ "app_name": "Tallee", "best_player": "Beste:r Spieler:in", "cancel": "Abbrechen", + "choose_color": "Farbe wählen", "choose_game": "Spielvorlage wählen", "choose_group": "Gruppe wählen", "choose_ruleset": "Regelwerk wählen", + "color": "Farbe", + "color_blue": "Blau", + "color_green": "Grün", + "color_orange": "Orange", + "color_pink": "Rosa", + "color_purple": "Lila", + "color_red": "Rot", + "color_teal": "Türkis", + "color_yellow": "Gelb", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", + "create_game": "Spielvorlage erstellen", "create_group": "Gruppe erstellen", "create_match": "Spiel erstellen", "create_new_group": "Neue Gruppe erstellen", @@ -22,13 +33,25 @@ "days_ago": "vor {count} Tagen", "delete": "Löschen", "delete_all_data": "Alle Daten löschen", + "delete_game": "Spielvorlage löschen", + "delete_game_with_matches_warning": "Wenn du diese Spielvorlage löschst, {count, plural, =1{wird 1 Spiel} other{werden {count} Spiele}} mit dieser Spielvorlage ebenfalls gelöscht.", + "@delete_game_with_matches_warning": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "delete_group": "Gruppe löschen", "delete_match": "Spiel löschen", + "description": "Beschreibung", + "edit_game": "Spielvorlage bearbeiten", "edit_group": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten", "enter_points": "Punkte eingeben", "enter_results": "Ergebnisse eintragen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", + "error_deleting_game": "Fehler beim Löschen der Spielvorlage, bitte erneut versuchen", "error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen", "error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen", "error_reading_file": "Fehler beim Lesen der Datei", @@ -59,6 +82,7 @@ "members": "Mitglieder", "most_points": "Höchste Punkte", "no_data_available": "Keine Daten verfügbar", + "no_games_created_yet": "Noch keine Spielvorlagen erstellt", "no_groups_created_yet": "Noch keine Gruppen erstellt", "no_licenses_found": "Keine Lizenzen gefunden", "no_license_text_available": "Kein Lizenztext verfügbar", @@ -76,7 +100,6 @@ "played_matches": "Gespielte Spiele", "player_name": "Spieler:innenname", "players": "Spieler:innen", - "players_count": "{count} Spieler", "point": "Punkt", "points": "Punkte", "privacy_policy": "Datenschutzerklärung", @@ -105,6 +128,7 @@ "statistics": "Statistiken", "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", + "there_are_no_games_matching_your_search": "Es gibt keine Spielvorlagen, die deiner Suche entspricht", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "tie": "Unentschieden", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7d54e92..9bc7318 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,349 +1,27 @@ { "@@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" - }, - "@best_player": { - "description": "Label for best player 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" - }, - "@create_group": { - "description": "Button text to create a group" - }, - "@create_match": { - "description": "Button text to create a match" - }, - "@create_new_group": { - "description": "Appbar text to create a new group" - }, - "@create_new_match": { - "description": "Appbar text to create a new match" - }, - "@created_on": { - "description": "Label for creation date" - }, - "@data": { - "description": "Data label" - }, - "@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" - }, - "@delete_group": { - "description": "Confirmation dialog for deleting a group" - }, - "@delete_match": { - "description": "Button text to delete a match" - }, - "@edit_group": { - "description": "Button & Appbar label for editing a group" - }, - "@edit_match": { - "description": "Button & Appbar label for editing a match" - }, - "@enter_points": { - "description": "Label to enter players points" - }, - "@enter_results": { - "description": "Button text to enter match results" - }, - "@error_creating_group": { - "description": "Error message when group creation fails" - }, - "@error_deleting_group": { - "description": "Error message when group deletion fails" - }, - "@error_editing_group": { - "description": "Error message when group editing 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" - }, - "@group_profile": { - "description": "Title for group profile view" - }, - "@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" - }, - "@legal": { - "description": "Legal section header" - }, - "@legal_notice": { - "description": "Legal notice menu item" - }, - "@licenses": { - "description": "Licenses menu item" - }, - "@match_in_progress": { - "description": "Message when match is in progress" - }, - "@match_name": { - "description": "Placeholder for match name input" - }, - "@match_profile": { - "description": "Title for match profile view" - }, - "@matches": { - "description": "Label for matches" - }, - "@members": { - "description": "Label for group members" - }, - "@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_licenses_found": { - "description": "Message when no licenses are found" - }, - "@no_license_text_available": { - "description": "Message when no license text is available" - }, - "@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_results_entered_yet": { - "description": "Message when no results have been entered yet" - }, - "@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" - }, - "@played_matches": { - "description": "Label for played matches statistic" - }, - "@player_name": { - "description": "Placeholder for player name input" - }, - "@players": { - "description": "Players label" - }, - "@players_count": { - "description": "Shows the number of players", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@points": { - "description": "Points label" - }, - "@privacy_policy": { - "description": "Privacy policy menu item" - }, - "@quick_create": { - "description": "Title for quick create section" - }, - "@recent_matches": { - "description": "Title for recent matches section" - }, - "@results": { - "description": "Label for match results" - }, - "@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" - }, - "@save_changes": { - "description": "Save changes button text" - }, - "@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" - }, - "@select_loser": { - "description": "Label to select the loser" - }, - "@selected_players": { - "description": "Shows the number of selected players" - }, - "@settings": { - "description": "Label for the App Settings" - }, - "@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": "Tallee", "best_player": "Best Player", "cancel": "Cancel", + "choose_color": "Choose Color", "choose_game": "Choose Game", "choose_group": "Choose Group", "choose_ruleset": "Choose Ruleset", + "color": "Color", + "color_blue": "Blue", + "color_green": "Green", + "color_orange": "Orange", + "color_pink": "Pink", + "color_purple": "Purple", + "color_red": "Red", + "color_teal": "Teal", + "color_yellow": "Yellow", "could_not_add_player": "Could not add player", + "create_game": "Create Game", "create_group": "Create Group", "create_match": "Create match", "create_new_group": "Create new group", @@ -356,13 +34,25 @@ "days_ago": "{count} days ago", "delete": "Delete", "delete_all_data": "Delete all data", + "delete_game": "Delete Game", + "delete_game_with_matches_warning": "If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.", + "@delete_game_with_matches_warning": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "delete_group": "Delete Group", "delete_match": "Delete Match", + "description": "Description", + "edit_game": "Edit Game", "edit_group": "Edit Group", "edit_match": "Edit Match", "enter_points": "Enter points", "enter_results": "Enter Results", "error_creating_group": "Error while creating group, please try again", + "error_deleting_game": "Error while deleting game, please try again", "error_deleting_group": "Error while deleting group, please try again", "error_editing_group": "Error while editing group, please try again", "error_reading_file": "Error reading file", @@ -393,6 +83,7 @@ "members": "Members", "most_points": "Most Points", "no_data_available": "No data available", + "no_games_created_yet": "No games created yet", "no_groups_created_yet": "No groups created yet", "no_licenses_found": "No licenses found", "no_license_text_available": "No license text available", @@ -410,7 +101,6 @@ "played_matches": "Played Matches", "player_name": "Player name", "players": "Players", - "players_count": "{count} Players", "point": "Point", "points": "Points", "privacy_policy": "Privacy Policy", @@ -438,6 +128,16 @@ "statistics": "Statistics", "stats": "Stats", "successfully_added_player": "Successfully added player {playerName}", + "@successfully_added_player": { + "description": "Success message when adding a player", + "placeholders": { + "playerName": { + "type": "String", + "example": "John" + } + } + }, + "there_are_no_games_matching_your_search": "There are no games matching your search", "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", "tie": "Tie", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index ea52dfc..1f6abff 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -98,583 +98,667 @@ abstract class AppLocalizations { Locale('en'), ]; - /// Label for all players list + /// No description provided for @all_players. /// /// In en, this message translates to: /// **'All players'** String get all_players; - /// Message when all players are added to selection + /// No description provided for @all_players_selected. /// /// In en, this message translates to: /// **'All players selected'** String get all_players_selected; - /// Label for amount of matches statistic + /// No description provided for @amount_of_matches. /// /// In en, this message translates to: /// **'Amount of Matches'** String get amount_of_matches; - /// The name of the App + /// No description provided for @app_name. /// /// In en, this message translates to: /// **'Tallee'** String get app_name; - /// Label for best player statistic + /// No description provided for @best_player. /// /// In en, this message translates to: /// **'Best Player'** String get best_player; - /// Cancel button text + /// No description provided for @cancel. /// /// In en, this message translates to: /// **'Cancel'** String get cancel; - /// Label for choosing a game + /// No description provided for @choose_color. + /// + /// In en, this message translates to: + /// **'Choose Color'** + String get choose_color; + + /// No description provided for @choose_game. /// /// In en, this message translates to: /// **'Choose Game'** String get choose_game; - /// Label for choosing a group + /// No description provided for @choose_group. /// /// In en, this message translates to: /// **'Choose Group'** String get choose_group; - /// Label for choosing a ruleset + /// No description provided for @choose_ruleset. /// /// In en, this message translates to: /// **'Choose Ruleset'** String get choose_ruleset; - /// Error message when adding a player fails + /// No description provided for @color. + /// + /// In en, this message translates to: + /// **'Color'** + String get color; + + /// No description provided for @color_blue. + /// + /// In en, this message translates to: + /// **'Blue'** + String get color_blue; + + /// No description provided for @color_green. + /// + /// In en, this message translates to: + /// **'Green'** + String get color_green; + + /// No description provided for @color_orange. + /// + /// In en, this message translates to: + /// **'Orange'** + String get color_orange; + + /// No description provided for @color_pink. + /// + /// In en, this message translates to: + /// **'Pink'** + String get color_pink; + + /// No description provided for @color_purple. + /// + /// In en, this message translates to: + /// **'Purple'** + String get color_purple; + + /// No description provided for @color_red. + /// + /// In en, this message translates to: + /// **'Red'** + String get color_red; + + /// No description provided for @color_teal. + /// + /// In en, this message translates to: + /// **'Teal'** + String get color_teal; + + /// No description provided for @color_yellow. + /// + /// In en, this message translates to: + /// **'Yellow'** + String get color_yellow; + + /// No description provided for @could_not_add_player. /// /// In en, this message translates to: /// **'Could not add player'** String could_not_add_player(Object playerName); - /// Button text to create a group + /// No description provided for @create_game. + /// + /// In en, this message translates to: + /// **'Create Game'** + String get create_game; + + /// No description provided for @create_group. /// /// In en, this message translates to: /// **'Create Group'** String get create_group; - /// Button text to create a match + /// No description provided for @create_match. /// /// In en, this message translates to: /// **'Create match'** String get create_match; - /// Appbar text to create a new group + /// No description provided for @create_new_group. /// /// In en, this message translates to: /// **'Create new group'** String get create_new_group; - /// Label for creation date + /// No description provided for @created_on. /// /// In en, this message translates to: /// **'Created on'** String get created_on; - /// Appbar text to create a new match + /// No description provided for @create_new_match. /// /// In en, this message translates to: /// **'Create new match'** String get create_new_match; - /// Data label + /// No description provided for @data. /// /// In en, this message translates to: /// **'Data'** String get data; - /// Success message after deleting data + /// No description provided for @data_successfully_deleted. /// /// In en, this message translates to: /// **'Data successfully deleted'** String get data_successfully_deleted; - /// Success message after exporting data + /// No description provided for @data_successfully_exported. /// /// In en, this message translates to: /// **'Data successfully exported'** String get data_successfully_exported; - /// Success message after importing data + /// No description provided for @data_successfully_imported. /// /// In en, this message translates to: /// **'Data successfully imported'** String get data_successfully_imported; - /// Date format for days ago + /// No description provided for @days_ago. /// /// In en, this message translates to: /// **'{count} days ago'** - String days_ago(int count); + String days_ago(Object count); - /// Delete button text + /// No description provided for @delete. /// /// In en, this message translates to: /// **'Delete'** String get delete; - /// Confirmation dialog for deleting all data + /// No description provided for @delete_all_data. /// /// In en, this message translates to: /// **'Delete all data'** String get delete_all_data; - /// Confirmation dialog for deleting a group + /// No description provided for @delete_game. + /// + /// In en, this message translates to: + /// **'Delete Game'** + String get delete_game; + + /// No description provided for @delete_game_with_matches_warning. + /// + /// In en, this message translates to: + /// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'** + String delete_game_with_matches_warning(int count); + + /// No description provided for @delete_group. /// /// In en, this message translates to: /// **'Delete Group'** String get delete_group; - /// Button text to delete a match + /// No description provided for @delete_match. /// /// In en, this message translates to: /// **'Delete Match'** String get delete_match; - /// Button & Appbar label for editing a group + /// No description provided for @description. + /// + /// In en, this message translates to: + /// **'Description'** + String get description; + + /// No description provided for @edit_game. + /// + /// In en, this message translates to: + /// **'Edit Game'** + String get edit_game; + + /// No description provided for @edit_group. /// /// In en, this message translates to: /// **'Edit Group'** String get edit_group; - /// Button & Appbar label for editing a match + /// No description provided for @edit_match. /// /// In en, this message translates to: /// **'Edit Match'** String get edit_match; - /// Label to enter players points + /// No description provided for @enter_points. /// /// In en, this message translates to: /// **'Enter points'** String get enter_points; - /// Button text to enter match results + /// No description provided for @enter_results. /// /// In en, this message translates to: /// **'Enter Results'** String get enter_results; - /// Error message when group creation fails + /// No description provided for @error_creating_group. /// /// In en, this message translates to: /// **'Error while creating group, please try again'** String get error_creating_group; - /// Error message when group deletion fails + /// No description provided for @error_deleting_game. + /// + /// In en, this message translates to: + /// **'Error while deleting game, please try again'** + String get error_deleting_game; + + /// No description provided for @error_deleting_group. /// /// In en, this message translates to: /// **'Error while deleting group, please try again'** String get error_deleting_group; - /// Error message when group editing fails + /// No description provided for @error_editing_group. /// /// In en, this message translates to: /// **'Error while editing group, please try again'** String get error_editing_group; - /// Error message when file cannot be read + /// No description provided for @error_reading_file. /// /// In en, this message translates to: /// **'Error reading file'** String get error_reading_file; - /// No description provided for @exit_view. - /// - /// In en, this message translates to: - /// **'Exit View'** - String get exit_view; - - /// Message when export is canceled + /// No description provided for @export_canceled. /// /// In en, this message translates to: /// **'Export canceled'** String get export_canceled; - /// Export data menu item + /// No description provided for @export_data. /// /// In en, this message translates to: /// **'Export data'** String get export_data; - /// Error message for format exceptions + /// No description provided for @format_exception. /// /// In en, this message translates to: /// **'Format Exception (see console)'** String get format_exception; - /// Game label + /// No description provided for @game. /// /// In en, this message translates to: /// **'Game'** String get game; - /// Placeholder for game name search + /// No description provided for @game_name. /// /// In en, this message translates to: /// **'Game Name'** String get game_name; - /// Group label + /// No description provided for @group. /// /// In en, this message translates to: /// **'Group'** String get group; - /// Placeholder for group name input + /// No description provided for @group_name. /// /// In en, this message translates to: /// **'Group name'** String get group_name; - /// Title for group profile view + /// No description provided for @group_profile. /// /// In en, this message translates to: /// **'Group Profile'** String get group_profile; - /// Label for groups + /// No description provided for @groups. /// /// In en, this message translates to: /// **'Groups'** String get groups; - /// Home tab label + /// No description provided for @home. /// /// In en, this message translates to: /// **'Home'** String get home; - /// Message when import is canceled + /// No description provided for @import_canceled. /// /// In en, this message translates to: /// **'Import canceled'** String get import_canceled; - /// Import data menu item + /// No description provided for @import_data. /// /// In en, this message translates to: /// **'Import data'** String get import_data; - /// Info label + /// No description provided for @info. /// /// In en, this message translates to: /// **'Info'** String get info; - /// Error message for invalid schema + /// No description provided for @invalid_schema. /// /// In en, this message translates to: /// **'Invalid Schema'** String get invalid_schema; - /// Title for least points ruleset + /// No description provided for @least_points. /// /// In en, this message translates to: /// **'Least Points'** String get least_points; - /// Legal section header + /// No description provided for @legal. /// /// In en, this message translates to: /// **'Legal'** String get legal; - /// Legal notice menu item + /// No description provided for @legal_notice. /// /// In en, this message translates to: /// **'Legal Notice'** String get legal_notice; - /// Licenses menu item + /// No description provided for @licenses. /// /// In en, this message translates to: /// **'Licenses'** String get licenses; - /// No description provided for @live_edit_mode. - /// - /// In en, this message translates to: - /// **'Live Edit Mode'** - String get live_edit_mode; - - /// Message when match is in progress + /// No description provided for @match_in_progress. /// /// In en, this message translates to: /// **'Match in progress...'** String get match_in_progress; - /// Placeholder for match name input + /// No description provided for @match_name. /// /// In en, this message translates to: /// **'Match name'** String get match_name; - /// Title for match profile view + /// No description provided for @match_profile. /// /// In en, this message translates to: /// **'Match Profile'** String get match_profile; - /// Label for matches + /// No description provided for @matches. /// /// In en, this message translates to: /// **'Matches'** String get matches; - /// Label for group members + /// No description provided for @members. /// /// In en, this message translates to: /// **'Members'** String get members; - /// Title for most points ruleset + /// No description provided for @most_points. /// /// In en, this message translates to: /// **'Most Points'** String get most_points; - /// Message when no data in the statistic tiles is given + /// No description provided for @no_data_available. /// /// In en, this message translates to: /// **'No data available'** String get no_data_available; - /// Message when no groups exist + /// No description provided for @no_games_created_yet. + /// + /// In en, this message translates to: + /// **'No games created yet'** + String get no_games_created_yet; + + /// No description provided for @no_groups_created_yet. /// /// In en, this message translates to: /// **'No groups created yet'** String get no_groups_created_yet; - /// Message when no licenses are found + /// No description provided for @no_licenses_found. /// /// In en, this message translates to: /// **'No licenses found'** String get no_licenses_found; - /// Message when no license text is available + /// No description provided for @no_license_text_available. /// /// In en, this message translates to: /// **'No license text available'** String get no_license_text_available; - /// Message when no matches exist + /// No description provided for @no_matches_created_yet. /// /// In en, this message translates to: /// **'No matches created yet'** String get no_matches_created_yet; - /// Message when no players exist + /// No description provided for @no_players_created_yet. /// /// In en, this message translates to: /// **'No players created yet'** String get no_players_created_yet; - /// Message when search returns no results + /// No description provided for @no_players_found_with_that_name. /// /// In en, this message translates to: /// **'No players found with that name'** String get no_players_found_with_that_name; - /// Message when no players are selected + /// No description provided for @no_players_selected. /// /// In en, this message translates to: /// **'No players selected'** String get no_players_selected; - /// Message when no recent matches exist + /// No description provided for @no_recent_matches_available. /// /// In en, this message translates to: /// **'No recent matches available'** String get no_recent_matches_available; - /// Message when no results have been entered yet + /// No description provided for @no_results_entered_yet. /// /// In en, this message translates to: /// **'No results entered yet'** String get no_results_entered_yet; - /// Message when no second match exists + /// No description provided for @no_second_match_available. /// /// In en, this message translates to: /// **'No second match available'** String get no_second_match_available; - /// Message when no statistics are available, because no matches were played yet + /// No description provided for @no_statistics_available. /// /// In en, this message translates to: /// **'No statistics available'** String get no_statistics_available; - /// None option label + /// No description provided for @none. /// /// In en, this message translates to: /// **'None'** String get none; - /// None group option label + /// No description provided for @none_group. /// /// In en, this message translates to: /// **'None'** String get none_group; - /// Abbreviation for not available + /// No description provided for @not_available. /// /// In en, this message translates to: /// **'Not available'** String get not_available; - /// Label for played matches statistic + /// No description provided for @played_matches. /// /// In en, this message translates to: /// **'Played Matches'** String get played_matches; - /// Placeholder for player name input + /// No description provided for @player_name. /// /// In en, this message translates to: /// **'Player name'** String get player_name; - /// Players label + /// No description provided for @players. /// /// In en, this message translates to: /// **'Players'** String get players; - /// Shows the number of players - /// - /// In en, this message translates to: - /// **'{count} Players'** - String players_count(int count); - /// No description provided for @point. /// /// In en, this message translates to: /// **'Point'** String get point; - /// Points label + /// No description provided for @points. /// /// In en, this message translates to: /// **'Points'** String get points; - /// Privacy policy menu item + /// No description provided for @privacy_policy. /// /// In en, this message translates to: /// **'Privacy Policy'** String get privacy_policy; - /// Title for quick create section + /// No description provided for @quick_create. /// /// In en, this message translates to: /// **'Quick Create'** String get quick_create; - /// Title for recent matches section + /// No description provided for @recent_matches. /// /// In en, this message translates to: /// **'Recent Matches'** String get recent_matches; - /// Label for match results + /// No description provided for @results. /// /// In en, this message translates to: /// **'Results'** String get results; - /// Ruleset label + /// No description provided for @ruleset. /// /// In en, this message translates to: /// **'Ruleset'** String get ruleset; - /// Description for least points ruleset + /// No description provided for @ruleset_least_points. /// /// In en, this message translates to: /// **'Inverse scoring: the player with the fewest points wins.'** String get ruleset_least_points; - /// Description for most points ruleset + /// No description provided for @ruleset_most_points. /// /// In en, this message translates to: /// **'Traditional ruleset: the player with the most points wins.'** String get ruleset_most_points; - /// Description for single loser ruleset + /// No description provided for @ruleset_single_loser. /// /// In en, this message translates to: /// **'Exactly one loser is determined; last place receives the penalty or consequence.'** String get ruleset_single_loser; - /// Description for single winner ruleset + /// No description provided for @ruleset_single_winner. /// /// In en, this message translates to: /// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'** String get ruleset_single_winner; - /// Save changes button text + /// No description provided for @save_changes. /// /// In en, this message translates to: /// **'Save Changes'** String get save_changes; - /// Hint text for group search input field + /// No description provided for @search_for_groups. /// /// In en, this message translates to: /// **'Search for groups'** String get search_for_groups; - /// Hint text for player search input field + /// No description provided for @search_for_players. /// /// In en, this message translates to: /// **'Search for players'** String get search_for_players; - /// Label to select the winner + /// No description provided for @select_winner. /// /// In en, this message translates to: /// **'Select Winner'** String get select_winner; - /// Label to select the loser + /// No description provided for @select_loser. /// /// In en, this message translates to: /// **'Select Loser'** String get select_loser; - /// Shows the number of selected players + /// No description provided for @selected_players. /// /// In en, this message translates to: /// **'Selected players'** String get selected_players; - /// Label for the App Settings + /// No description provided for @settings. /// /// In en, this message translates to: /// **'Settings'** String get settings; - /// Title for single loser ruleset + /// No description provided for @single_loser. /// /// In en, this message translates to: /// **'Single Loser'** String get single_loser; - /// Title for single winner ruleset + /// No description provided for @single_winner. /// /// In en, this message translates to: /// **'Single Winner'** @@ -704,13 +788,13 @@ abstract class AppLocalizations { /// **'Multiple Winners'** String get multiple_winners; - /// Statistics tab label + /// No description provided for @statistics. /// /// In en, this message translates to: /// **'Statistics'** String get statistics; - /// Stats tab label (short) + /// No description provided for @stats. /// /// In en, this message translates to: /// **'Stats'** @@ -722,13 +806,19 @@ abstract class AppLocalizations { /// **'Successfully added player {playerName}'** String successfully_added_player(String playerName); - /// Message when search returns no groups + /// No description provided for @there_are_no_games_matching_your_search. + /// + /// In en, this message translates to: + /// **'There are no games matching your search'** + String get there_are_no_games_matching_your_search; + + /// No description provided for @there_is_no_group_matching_your_search. /// /// In en, this message translates to: /// **'There is no group matching your search'** String get there_is_no_group_matching_your_search; - /// Warning message for irreversible actions + /// No description provided for @this_cannot_be_undone. /// /// In en, this message translates to: /// **'This can\'t be undone.'** @@ -740,43 +830,43 @@ abstract class AppLocalizations { /// **'Tie'** String get tie; - /// Date format for today + /// No description provided for @today_at. /// /// In en, this message translates to: /// **'Today at'** String get today_at; - /// Undo button text + /// No description provided for @undo. /// /// In en, this message translates to: /// **'Undo'** String get undo; - /// Error message for unknown exceptions + /// No description provided for @unknown_exception. /// /// In en, this message translates to: /// **'Unknown Exception (see console)'** String get unknown_exception; - /// Winner label + /// No description provided for @winner. /// /// In en, this message translates to: /// **'Winner'** String get winner; - /// Label for winrate statistic + /// No description provided for @winrate. /// /// In en, this message translates to: /// **'Winrate'** String get winrate; - /// Label for wins statistic + /// No description provided for @wins. /// /// In en, this message translates to: /// **'Wins'** String get wins; - /// Date format for yesterday + /// No description provided for @yesterday_at. /// /// In en, this message translates to: /// **'Yesterday at'** diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 45859c0..4883272 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -26,6 +26,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get cancel => 'Abbrechen'; + @override + String get choose_color => 'Farbe wählen'; + @override String get choose_game => 'Spielvorlage wählen'; @@ -35,11 +38,41 @@ class AppLocalizationsDe extends AppLocalizations { @override String get choose_ruleset => 'Regelwerk wählen'; + @override + String get color => 'Farbe'; + + @override + String get color_blue => 'Blau'; + + @override + String get color_green => 'Grün'; + + @override + String get color_orange => 'Orange'; + + @override + String get color_pink => 'Rosa'; + + @override + String get color_purple => 'Lila'; + + @override + String get color_red => 'Rot'; + + @override + String get color_teal => 'Türkis'; + + @override + String get color_yellow => 'Gelb'; + @override String could_not_add_player(Object playerName) { return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; } + @override + String get create_game => 'Spielvorlage erstellen'; + @override String get create_group => 'Gruppe erstellen'; @@ -68,7 +101,7 @@ class AppLocalizationsDe extends AppLocalizations { String get data_successfully_imported => 'Daten erfolgreich importiert'; @override - String days_ago(int count) { + String days_ago(Object count) { return 'vor $count Tagen'; } @@ -78,12 +111,32 @@ class AppLocalizationsDe extends AppLocalizations { @override String get delete_all_data => 'Alle Daten löschen'; + @override + String get delete_game => 'Spielvorlage löschen'; + + @override + String delete_game_with_matches_warning(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'werden $count Spiele', + one: 'wird 1 Spiel', + ); + return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; + } + @override String get delete_group => 'Gruppe löschen'; @override String get delete_match => 'Spiel löschen'; + @override + String get description => 'Beschreibung'; + + @override + String get edit_game => 'Spielvorlage bearbeiten'; + @override String get edit_group => 'Gruppe bearbeiten'; @@ -100,6 +153,10 @@ class AppLocalizationsDe extends AppLocalizations { String get error_creating_group => 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; + @override + String get error_deleting_game => + 'Fehler beim Löschen der Spielvorlage, bitte erneut versuchen'; + @override String get error_deleting_group => 'Fehler beim Löschen der Gruppe, bitte erneut versuchen'; @@ -192,6 +249,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get no_data_available => 'Keine Daten verfügbar'; + @override + String get no_games_created_yet => 'Noch keine Spielvorlagen erstellt'; + @override String get no_groups_created_yet => 'Noch keine Gruppen erstellt'; @@ -244,11 +304,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get players => 'Spieler:innen'; - @override - String players_count(int count) { - return '$count Spieler'; - } - @override String get point => 'Punkt'; @@ -336,6 +391,10 @@ class AppLocalizationsDe extends AppLocalizations { return 'Spieler:in $playerName erfolgreich hinzugefügt'; } + @override + String get there_are_no_games_matching_your_search => + 'Es gibt keine Spielvorlagen, die deiner Suche entspricht'; + @override String get there_is_no_group_matching_your_search => 'Es gibt keine Gruppe, die deiner Suche entspricht'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index d8b0261..b107caa 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -26,6 +26,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cancel => 'Cancel'; + @override + String get choose_color => 'Choose Color'; + @override String get choose_game => 'Choose Game'; @@ -35,11 +38,41 @@ class AppLocalizationsEn extends AppLocalizations { @override String get choose_ruleset => 'Choose Ruleset'; + @override + String get color => 'Color'; + + @override + String get color_blue => 'Blue'; + + @override + String get color_green => 'Green'; + + @override + String get color_orange => 'Orange'; + + @override + String get color_pink => 'Pink'; + + @override + String get color_purple => 'Purple'; + + @override + String get color_red => 'Red'; + + @override + String get color_teal => 'Teal'; + + @override + String get color_yellow => 'Yellow'; + @override String could_not_add_player(Object playerName) { return 'Could not add player'; } + @override + String get create_game => 'Create Game'; + @override String get create_group => 'Create Group'; @@ -68,7 +101,7 @@ class AppLocalizationsEn extends AppLocalizations { String get data_successfully_imported => 'Data successfully imported'; @override - String days_ago(int count) { + String days_ago(Object count) { return '$count days ago'; } @@ -78,12 +111,32 @@ class AppLocalizationsEn extends AppLocalizations { @override String get delete_all_data => 'Delete all data'; + @override + String get delete_game => 'Delete Game'; + + @override + String delete_game_with_matches_warning(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count matches', + one: '1 match', + ); + return 'If you delete this game template, $_temp0 using this game template will also be deleted.'; + } + @override String get delete_group => 'Delete Group'; @override String get delete_match => 'Delete Match'; + @override + String get description => 'Description'; + + @override + String get edit_game => 'Edit Game'; + @override String get edit_group => 'Edit Group'; @@ -100,6 +153,10 @@ class AppLocalizationsEn extends AppLocalizations { String get error_creating_group => 'Error while creating group, please try again'; + @override + String get error_deleting_game => + 'Error while deleting game, please try again'; + @override String get error_deleting_group => 'Error while deleting group, please try again'; @@ -192,6 +249,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no_data_available => 'No data available'; + @override + String get no_games_created_yet => 'No games created yet'; + @override String get no_groups_created_yet => 'No groups created yet'; @@ -244,11 +304,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get players => 'Players'; - @override - String players_count(int count) { - return '$count Players'; - } - @override String get point => 'Point'; @@ -336,6 +391,10 @@ class AppLocalizationsEn extends AppLocalizations { return 'Successfully added player $playerName'; } + @override + String get there_are_no_games_matching_your_search => + 'There are no games matching your search'; + @override String get there_is_no_group_matching_your_search => 'There is no group matching your search'; 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 3a2ee60..b4a5b97 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 @@ -197,7 +197,7 @@ class _CreateGroupViewState extends State { /// obsolete. For each such match, the group association is removed by setting /// its [groupId] to null. Future deleteObsoleteMatchGroupRelations() async { - final groupMatches = await db.matchDao.getGroupMatches( + final groupMatches = await db.matchDao.getMatchesByGroup( groupId: widget.groupToEdit!.id, ); diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 92c3bba..3d5e805 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -244,7 +244,9 @@ class _GroupDetailViewState extends State { /// Loads statistics for this group Future _loadStatistics() async { isLoading = true; - final groupMatches = await db.matchDao.getGroupMatches(groupId: _group.id); + final groupMatches = await db.matchDao.getMatchesByGroup( + groupId: _group.id, + ); setState(() { totalMatches = groupMatches.length; 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 51512f9..c019213 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 @@ -1,19 +1,26 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game_view.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; -import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; +import 'package:tallee/presentation/widgets/top_centered_message.dart'; class ChooseGameView extends StatefulWidget { /// A view that allows the user to choose a game from a list of available games - /// - [games]: A list of tuples containing the game name, description and ruleset - /// - [initialGameIndex]: The index of the initially selected game + /// - [games]: The list of available games + /// - [initialGameId]: The id of the initially selected game + /// - [onGamesUpdated]: Optional callback invoked when the games are updated const ChooseGameView({ super.key, required this.games, required this.initialGameId, + this.onGamesUpdated, }); /// A list of tuples containing the game name, description and ruleset @@ -22,20 +29,37 @@ class ChooseGameView extends StatefulWidget { /// The id of the initially selected game final String initialGameId; + /// Optional callback invoked when the games are updated + final VoidCallback? onGamesUpdated; + @override State createState() => _ChooseGameViewState(); } class _ChooseGameViewState extends State { + late final AppDatabase db; + + late List<(Game, int)> gameCounts = []; + /// Controller for the search bar final TextEditingController searchBarController = TextEditingController(); /// Currently selected game index late String selectedGameId; + /// Games filtered according to the current search query + late List filteredGames; + @override void initState() { + db = Provider.of(context, listen: false); + fetchGameCounts(); + selectedGameId = widget.initialGameId; + + // Start with all games visible + filteredGames = List.from(widget.games); + super.initState(); } @@ -58,6 +82,30 @@ class _ChooseGameViewState extends State { ); }, ), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () async { + final result = await Navigator.push( + context, + adaptivePageRoute( + builder: (context) => CreateGameView( + onGameChanged: () { + widget.onGamesUpdated?.call(); + }, + ), + ), + ); + if (result != null && result.game != null) { + setState(() { + widget.games.insert(0, result.game); + }); + _refreshFromSource(); + } + }, + ), + ], + title: Text(loc.choose_game), ), body: PopScope( @@ -72,37 +120,101 @@ class _ChooseGameViewState extends State { }, child: Column( children: [ + // Search Bar Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: CustomSearchBar( controller: searchBarController, hintText: loc.game_name, + onChanged: (value) { + _applySearchFilter(value); + }, ), ), const SizedBox(height: 5), + + // Game list Expanded( - child: ListView.builder( - itemCount: widget.games.length, - itemBuilder: (BuildContext context, int index) { - return TitleDescriptionListTile( - title: widget.games[index].name, - description: widget.games[index].description, - badgeText: translateRulesetToString( - widget.games[index].ruleset, + child: Visibility( + visible: filteredGames.isNotEmpty, + replacement: Visibility( + visible: widget.games.isNotEmpty, + replacement: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: loc.no_games_created_yet, + ), + child: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: AppLocalizations.of( context, - ), - isHighlighted: selectedGameId == widget.games[index].id, - onPressed: () async { - setState(() { - if (selectedGameId != widget.games[index].id) { - selectedGameId = widget.games[index].id; - } else { - selectedGameId = ''; + ).there_are_no_games_matching_your_search, + ), + ), + child: ListView.builder( + itemCount: filteredGames.length, + itemBuilder: (BuildContext context, int index) { + final game = filteredGames[index]; + return GameTile( + title: game.name, + description: game.description, + badgeText: translateRulesetToString( + game.ruleset, + context, + ), + badgeColor: getColorFromGameColor(game.color), + isHighlighted: selectedGameId == game.id, + onTap: () async { + setState(() { + if (selectedGameId == game.id) { + selectedGameId = ''; + } else { + selectedGameId = game.id; + } + }); + }, + onLongPress: () async { + final result = await Navigator.push( + context, + adaptivePageRoute( + builder: (context) => CreateGameView( + gameToEdit: game, + matchCount: getMatchCount(game), + onGameChanged: () { + widget.onGamesUpdated?.call(); + }, + ), + ), + ); + if (result != null && result.game != null) { + // Find the index in the original list to mutate + final originalIndex = widget.games.indexWhere( + (g) => g.id == game.id, + ); + if (originalIndex == -1) { + return; + } + if (result.delete) { + setState(() { + // deselect the game + if (selectedGameId == game.id) { + selectedGameId = ''; + } + widget.games.removeAt(originalIndex); + widget.onGamesUpdated?.call(); + }); + } else { + setState(() { + widget.games[originalIndex] = result.game; + }); + } + _refreshFromSource(); } - }); - }, - ); - }, + }, + ); + }, + ), ), ), ], @@ -110,4 +222,39 @@ class _ChooseGameViewState extends State { ), ); } + + /// Applies the search filter to the games list based on [query]. + void _applySearchFilter(String query) { + final q = query.toLowerCase().trim(); + if (q.isEmpty) { + setState(() { + filteredGames = List.from(widget.games); + }); + return; + } + + setState(() { + filteredGames = widget.games.where((game) { + final name = game.name.toLowerCase(); + final description = game.description.toLowerCase(); + return name.contains(q) || description.contains(q); + }).toList(); + }); + } + + /// Re-applies the current filter after the underlying games list changed. + void _refreshFromSource() { + _applySearchFilter(searchBarController.text); + } + + Future fetchGameCounts() async { + gameCounts = await db.gameDao.getGameUsage(); + } + + // Returns the number of matches that use the given [game]. + int getMatchCount(Game game) { + return gameCounts + .firstWhere((gc) => gc.$1.id == game.id, orElse: () => (game, 0)) + .$2; + } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart new file mode 100644 index 0000000..e0f9d85 --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -0,0 +1,520 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_popup/flutter_popup.dart'; +import 'package:provider/provider.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; +import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; +import 'package:tallee/presentation/widgets/tiles/choose_tile.dart'; + +/// A stateful widget for creating or editing a game. +/// - [gameToEdit] An optional game to prefill the fields +/// - [onGameChanged] Callback to invoke when the game is created or edited +class CreateGameView extends StatefulWidget { + const CreateGameView({ + super.key, + required this.onGameChanged, + this.gameToEdit, + this.matchCount = 0, + }); + + /// Callback to invoke when the game is created or edited + final VoidCallback onGameChanged; + + /// An optional game to prefill the fields + final Game? gameToEdit; + + final int matchCount; + + @override + State createState() => _CreateGameViewState(); +} + +class _CreateGameViewState extends State { + /// GlobalKey for ScaffoldMessenger to show snackbars + final _scaffoldMessengerKey = GlobalKey(); + + late final AppDatabase db; + + late List<(Ruleset, String)> _rulesets; + Ruleset? selectedRuleset = Ruleset.singleWinner; + + late List<(GameColor, String)> _colors; + GameColor? selectedColor = GameColor.orange; + + /// Controller for the game name input field. + final _gameNameController = TextEditingController(); + + /// Controller for the game description input field. + final _descriptionController = TextEditingController(); + + /// The ID of the currently selected group. + late String selectedGroupId; + + /// A controller for the search bar input field. + final TextEditingController controller = TextEditingController(); + + /// A list of groups filtered based on the search query. + late final List filteredGroups; + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + _gameNameController.addListener(() => setState(() {})); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _rulesets = [ + ( + Ruleset.singleWinner, + translateRulesetToString(Ruleset.singleWinner, context), + ), + ( + Ruleset.singleLoser, + translateRulesetToString(Ruleset.singleLoser, context), + ), + ( + Ruleset.highestScore, + translateRulesetToString(Ruleset.highestScore, context), + ), + ( + Ruleset.lowestScore, + translateRulesetToString(Ruleset.lowestScore, context), + ), + ( + Ruleset.multipleWinners, + translateRulesetToString(Ruleset.multipleWinners, context), + ), + ]; + _colors = [ + (GameColor.green, translateGameColorToString(GameColor.green, context)), + (GameColor.teal, translateGameColorToString(GameColor.teal, context)), + (GameColor.blue, translateGameColorToString(GameColor.blue, context)), + (GameColor.purple, translateGameColorToString(GameColor.purple, context)), + (GameColor.pink, translateGameColorToString(GameColor.pink, context)), + (GameColor.red, translateGameColorToString(GameColor.red, context)), + (GameColor.orange, translateGameColorToString(GameColor.orange, context)), + (GameColor.yellow, translateGameColorToString(GameColor.yellow, context)), + ]; + + if (widget.gameToEdit != null) { + _gameNameController.text = widget.gameToEdit!.name; + _descriptionController.text = widget.gameToEdit!.description; + selectedRuleset = widget.gameToEdit!.ruleset; + selectedColor = widget.gameToEdit!.color; + selectedRuleset = widget.gameToEdit!.ruleset; + } + } + + @override + void dispose() { + _gameNameController.dispose(); + _descriptionController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var loc = AppLocalizations.of(context); + final isEditing = widget.gameToEdit != null; + + return ScaffoldMessenger( + child: Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + title: Text(isEditing ? loc.edit_game : loc.create_game), + actions: [ + if (isEditMode()) + IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + if (!context.mounted) return; + + // Build the dialog content based on match count + final String dialogContent = widget.matchCount > 0 + ? loc.delete_game_with_matches_warning(widget.matchCount) + : loc.this_cannot_be_undone; + + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: loc.delete_game, + content: Text( + dialogContent, + style: const TextStyle(fontSize: 15), + ), + actions: [ + CustomDialogAction( + isDestructive: true, + onPressed: () => Navigator.of(context).pop(true), + text: loc.delete, + ), + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(false), + buttonType: ButtonType.secondary, + text: loc.cancel, + ), + ], + ), + ).then((confirmed) async { + if (confirmed == true && context.mounted) { + // Delete assocaited matches + if (widget.matchCount > 0) { + await db.matchDao.deleteMatchesByGame( + gameId: widget.gameToEdit!.id, + ); + } + + // Delete the targetted game + bool success = await db.gameDao.deleteGame( + gameId: widget.gameToEdit!.id, + ); + + if (!context.mounted) return; + if (success) { + widget.onGameChanged.call(); + Navigator.of( + context, + ).pop((game: widget.gameToEdit, delete: true)); + } else { + if (!mounted) return; + showSnackbar(message: loc.error_deleting_game); + } + } + }); + }, + ), + ], + ), + body: SafeArea( + child: Column( + children: [ + // Game name input field + Container( + margin: CustomTheme.tileMargin, + child: TextInputField( + controller: _gameNameController, + maxLength: Constants.MAX_MATCH_NAME_LENGTH, + hintText: loc.game_name, + ), + ), + + // Choose ruleset tile + if (!isEditMode()) + ChooseTile(title: loc.ruleset, trailing: getColorDropdown(loc)), + + // Choose color tile + ChooseTile(title: loc.color, trailing: getRulesetDropdown(loc)), + + // Description input field + Container( + margin: CustomTheme.tileMargin, + child: TextInputField( + controller: _descriptionController, + hintText: loc.description, + minLines: 6, + maxLines: 6, + maxLength: Constants.MAX_GAME_DESCRIPTION_LENGTH, + showCounterText: true, + ), + ), + + const Spacer(), + + // Create/Edit game button + Padding( + padding: const EdgeInsets.all(12.0), + child: CustomWidthButton( + text: isEditing ? loc.edit_game : loc.create_game, + sizeRelativeToWidth: 1, + buttonType: ButtonType.primary, + onPressed: + _gameNameController.text.trim().isNotEmpty && + selectedRuleset != null && + selectedColor != null + ? () async { + Game newGame = Game( + name: _gameNameController.text.trim(), + description: _descriptionController.text.trim(), + ruleset: selectedRuleset!, + color: selectedColor!, + ); + if (isEditing) { + await handleGameUpdate(newGame); + } else { + await handleGameCreation(newGame); + } + widget.onGameChanged.call(); + if (context.mounted) { + Navigator.of( + context, + ).pop((game: newGame, delete: false)); + } + } + : null, + ), + ), + ], + ), + ), + ), + ); + } + + /// Handles updating an existing game in the database. + /// + /// [newGame] The updated game object. + Future handleGameUpdate(Game newGame) async { + final oldGame = widget.gameToEdit!; + + if (oldGame.name != newGame.name) { + await db.gameDao.updateGameName(gameId: oldGame.id, name: newGame.name); + } + + if (oldGame.description != newGame.description) { + await db.gameDao.updateGameDescription( + gameId: oldGame.id, + description: newGame.description, + ); + } + + if (oldGame.ruleset != newGame.ruleset) { + await db.gameDao.updateGameRuleset( + gameId: oldGame.id, + ruleset: newGame.ruleset, + ); + } + + if (oldGame.color != newGame.color) { + await db.gameDao.updateGameColor( + gameId: oldGame.id, + color: newGame.color, + ); + } + + if (oldGame.icon != newGame.icon) { + await db.gameDao.updateGameIcon(gameId: oldGame.id, icon: newGame.icon); + } + } + + /// Handles creating a new game in the database. + /// + /// [newGame] The game object to be created. + Future handleGameCreation(Game newGame) async { + await db.gameDao.addGame(game: newGame); + } + + /// Displays a snackbar with the given message and optional action. + /// + /// [message] The message to display in the snackbar. + void showSnackbar({required String message}) { + final messenger = _scaffoldMessengerKey.currentState; + if (messenger != null) { + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message, style: const TextStyle(color: Colors.white)), + backgroundColor: CustomTheme.boxColor, + ), + ); + } + } + + bool isEditMode() { + return widget.gameToEdit != null; + } + + Widget getRulesetDropdown(AppLocalizations loc) { + return CustomPopup( + showArrow: true, + arrowColor: CustomTheme.boxBorderColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10), + barrierColor: Colors.transparent, + contentDecoration: CustomTheme.standardBoxDecoration, + content: StatefulBuilder( + builder: (context, setPopupState) => SizedBox( + width: 280, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + _rulesets.length, + (index) => GestureDetector( + onTap: () { + setState(() { + selectedRuleset = _rulesets[index].$1; + }); + setPopupState(() {}); + }, + child: Column( + children: [ + Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + color: selectedRuleset == _rulesets[index].$1 + ? CustomTheme.textColor.withAlpha(20) + : Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: Row( + spacing: 8, + children: [ + Icon(getRulesetIcon(_rulesets[index].$1), size: 16), + Text( + _rulesets[index].$2, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 15, + ), + ), + ], + ), + ), + ), + if (index < _rulesets.length - 1) + const Divider(indent: 15, endIndent: 15), + ], + ), + ), + ), + ), + ), + ), + child: Row( + spacing: 8, + children: [ + Icon(getRulesetIcon(selectedRuleset!), size: 16), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Text( + translateRulesetToString(selectedRuleset!, context), + textAlign: TextAlign.right, + ), + ), + Transform.rotate( + angle: pi / 2, + child: const Icon(Icons.arrow_forward_ios, size: 16), + ), + ], + ), + ); + } + + Widget getColorDropdown(AppLocalizations loc) { + return CustomPopup( + showArrow: true, + arrowColor: CustomTheme.boxBorderColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10), + barrierColor: Colors.transparent, + contentDecoration: CustomTheme.standardBoxDecoration, + content: StatefulBuilder( + builder: (context, setPopupState) => SizedBox( + width: 150, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + _colors.length, + (index) => GestureDetector( + onTap: () { + setState(() { + selectedColor = _colors[index].$1; + }); + setPopupState(() {}); + }, + child: Column( + children: [ + // Selected Highlighting + Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + color: selectedColor == _colors[index].$1 + ? CustomTheme.textColor.withAlpha(20) + : Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + spacing: 8, + children: selectedColor == null + ? [Text(loc.none)] + : [ + Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(left: 12), + decoration: BoxDecoration( + color: getColorFromGameColor( + _colors[index].$1, + ), + shape: BoxShape.circle, + ), + ), + Text( + _colors[index].$2, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 15, + ), + ), + ], + ), + ), + ), + if (index < _colors.length - 1) + const Divider(indent: 15, endIndent: 15), + ], + ), + ), + ), + ), + ), + ), + child: Row( + spacing: 8, + children: [ + // Selected Color + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: getColorFromGameColor(selectedColor!), + shape: BoxShape.circle, + ), + ), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Text(translateGameColorToString(selectedColor!, context)), + ), + Transform.rotate( + angle: pi / 2, + child: const Icon(Icons.arrow_forward_ios, size: 16), + ), + ], + ), + ); + } +} 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 cb26de8..14908b6 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 @@ -28,10 +28,13 @@ class CreateMatchView extends StatefulWidget { this.onWinnerChanged, this.matchToEdit, this.onMatchUpdated, + this.onMatchesUpdated, }); final VoidCallback? onWinnerChanged; + final VoidCallback? onMatchesUpdated; + final void Function(Match)? onMatchUpdated; /// An optional match to prefill the fields for editing. @@ -115,6 +118,7 @@ class _CreateMatchViewState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ + // Match name input field. Container( margin: CustomTheme.tileMargin, child: TextInputField( @@ -123,34 +127,40 @@ class _CreateMatchViewState extends State { maxLength: Constants.MAX_MATCH_NAME_LENGTH, ), ), - ChooseTile( - title: loc.game, - trailingText: selectedGame == null - ? loc.none_group - : selectedGame!.name, - onPressed: () async { - selectedGame = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseGameView( - games: gamesList, - initialGameId: selectedGame?.id ?? '', + + // Game selection tile. + if (!isEditMode()) + ChooseTile( + title: loc.game, + trailing: selectedGame == null + ? Text(loc.none_group) + : Text(selectedGame!.name), + onPressed: () async { + selectedGame = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => ChooseGameView( + games: gamesList, + initialGameId: selectedGame?.id ?? '', + onGamesUpdated: widget.onMatchesUpdated, + ), ), - ), - ); - setState(() { - if (selectedGame != null) { - hintText = selectedGame!.name; - } else { - hintText = loc.match_name; - } - }); - }, - ), + ); + setState(() { + if (selectedGame != null) { + hintText = selectedGame!.name; + } else { + hintText = loc.match_name; + } + }); + }, + ), + + // Group selection tile. ChooseTile( title: loc.group, - trailingText: selectedGroup == null - ? loc.none_group - : selectedGroup!.name, + trailing: selectedGroup == null + ? Text(loc.none_group) + : Text(selectedGroup!.name), onPressed: () async { // Remove all players from the previously selected group from // the selected players list, in case the user deselects the @@ -181,6 +191,8 @@ class _CreateMatchViewState extends State { }); }, ), + + // Player selection widget. Expanded( child: PlayerSelection( key: ValueKey(selectedGroup?.id ?? 'no_group'), @@ -193,6 +205,8 @@ class _CreateMatchViewState extends State { }, ), ), + + // Create or save button. CustomWidthButton( text: buttonText, sizeRelativeToWidth: 0.95, @@ -218,16 +232,16 @@ class _CreateMatchViewState extends State { /// /// Returns `true` if: /// - A ruleset is selected AND - /// - Either a group is selected OR at least 2 players are selected + /// - Either a group is selected OR at least 2 players are selected. bool _enableCreateGameButton() { return (selectedGroup != null || (selectedPlayers.length > 1) && selectedGame != null); } - // If a match was provided to the view, it updates the match in the database - // and navigates back to the previous screen. - // If no match was provided, it creates a new match in the database and - // navigates to the MatchResultView for the newly created match. + /// Handles navigation when the create or save button is pressed. + /// + /// If a match is being edited, updates the match in the database. + /// Otherwise, creates a new match and navigates to the MatchResultView. void buttonNavigation(BuildContext context) async { if (isEditMode()) { await updateMatch(); @@ -252,8 +266,7 @@ class _CreateMatchViewState extends State { } } - /// Updates attributes of the existing match in the database based on the - /// changes made in the edit view. + /// Updates the existing match in the database. Future updateMatch() async { final updatedMatch = Match( id: widget.matchToEdit!.id, @@ -262,7 +275,7 @@ class _CreateMatchViewState extends State { : _matchNameController.text.trim(), group: selectedGroup, players: selectedPlayers, - game: widget.matchToEdit!.game, + game: selectedGame!, createdAt: widget.matchToEdit!.createdAt, endedAt: widget.matchToEdit!.endedAt, notes: widget.matchToEdit!.notes, diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 9b53b15..26f0f2b 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.dart'; @@ -14,6 +15,7 @@ import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; +import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -102,6 +104,7 @@ class _MatchDetailViewState extends State { bottom: 100, ), children: [ + // Controller Icon const Center( child: ColoredIconContainer( icon: Icons.sports_esports, @@ -110,6 +113,8 @@ class _MatchDetailViewState extends State { ), ), const SizedBox(height: 10), + + // Match Name Text( match.name, style: const TextStyle( @@ -120,6 +125,8 @@ class _MatchDetailViewState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 5), + + // Creation Date Text( '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}', style: const TextStyle( @@ -129,6 +136,8 @@ class _MatchDetailViewState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 10), + + // Group Name if (match.group != null) ...[ Row( mainAxisAlignment: MainAxisAlignment.center, @@ -143,6 +152,8 @@ class _MatchDetailViewState extends State { ), const SizedBox(height: 20), ], + + // Players InfoTile( title: loc.players, icon: Icons.people, @@ -162,6 +173,30 @@ class _MatchDetailViewState extends State { ), ), const SizedBox(height: 15), + + // Game + InfoTile( + title: loc.game, + icon: RpgAwesome.clovers_card, + horizontalAlignment: CrossAxisAlignment.start, + content: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ), + child: GameLabel( + title: match.game.name, + description: translateRulesetToString( + match.game.ruleset, + context, + ), + color: match.game.color, + ), + ), + ), + const SizedBox(height: 15), + + // Results InfoTile( title: loc.results, icon: Icons.emoji_events, 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 4f70347..a7f60c6 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -126,8 +126,10 @@ class _MatchViewState extends State { Navigator.push( context, adaptivePageRoute( - builder: (context) => - CreateMatchView(onWinnerChanged: loadMatches), + builder: (context) => CreateMatchView( + onWinnerChanged: loadMatches, + onMatchesUpdated: loadMatches, + ), ), ); }, diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 396d7b7..92f3080 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -55,6 +55,7 @@ const allDependencies = [ _flutter_lints, _flutter_localizations, _flutter_plugin_android_lifecycle, + _flutter_popup, _flutter_test, _flutter_web_plugins, _fluttericon, @@ -168,6 +169,7 @@ const dependencies = [ _file_saver, _flutter, _flutter_localizations, + _flutter_popup, _fluttericon, _font_awesome_flutter, _intl, @@ -2628,6 +2630,41 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); +/// flutter_popup 3.3.9 +const _flutter_popup = Package( + name: 'flutter_popup', + description: 'The flutter_popup package is a versatile tool for creating customizable popups in Flutter apps. Its highlight feature effectively guides user attention to specific areas', + homepage: 'https://github.com/herowws/flutter_popup', + authors: [], + version: '3.3.9', + spdxIdentifiers: ['MIT'], + isMarkdown: false, + isSdk: false, + dependencies: [PackageRef('flutter')], + devDependencies: [PackageRef('flutter_lints'), PackageRef('flutter_test')], + license: '''MIT License + +Copyright (c) 2023 mopriestt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''', + ); + /// flutter_test null const _flutter_test = Package( name: 'flutter_test', @@ -37676,16 +37713,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.27+261 +/// tallee 0.0.28+262 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.27+261', + version: '0.0.28+262', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], + dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('flutter_popup'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], devDependencies: [PackageRef('flutter_test'), PackageRef('build_runner'), PackageRef('dart_pubspec_licenses'), PackageRef('drift_dev'), PackageRef('flutter_lints')], license: '''GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 diff --git a/lib/presentation/widgets/buttons/animated_dialog_button.dart b/lib/presentation/widgets/buttons/animated_dialog_button.dart index 70deea6..8c8765e 100644 --- a/lib/presentation/widgets/buttons/animated_dialog_button.dart +++ b/lib/presentation/widgets/buttons/animated_dialog_button.dart @@ -14,6 +14,7 @@ class AnimatedDialogButton extends StatefulWidget { required this.onPressed, this.buttonConstraints, this.buttonType = ButtonType.primary, + this.isDescructive = false, }); final String buttonText; @@ -24,6 +25,8 @@ class AnimatedDialogButton extends StatefulWidget { final ButtonType buttonType; + final bool isDescructive; + @override State createState() => _AnimatedDialogButtonState(); } @@ -33,28 +36,8 @@ class _AnimatedDialogButtonState extends State { @override Widget build(BuildContext context) { - final textStyling = TextStyle( - color: widget.buttonType == ButtonType.primary - ? Colors.black - : Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ); - - final buttonDecoration = widget.buttonType == ButtonType.primary - // Primary - ? BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ) - : widget.buttonType == ButtonType.secondary - // Secondary - ? BoxDecoration( - border: BoxBorder.all(color: Colors.white, width: 2), - borderRadius: BorderRadius.circular(12), - ) - // Tertiary - : const BoxDecoration(); + final textStyling = _getTextStyling(); + final buttonDecoration = _getButtonDecoration(); return GestureDetector( onTapDown: (_) => setState(() => _isPressed = true), @@ -84,4 +67,42 @@ class _AnimatedDialogButtonState extends State { ), ); } + + TextStyle _getTextStyling() { + late Color textColor; + if (widget.buttonType == ButtonType.primary) { + textColor = widget.isDescructive ? Colors.white : Colors.black; + } else if (widget.buttonType == ButtonType.secondary) { + textColor = widget.isDescructive ? Colors.red : Colors.white; + } else { + textColor = widget.isDescructive ? Colors.red : Colors.white; + } + + return TextStyle( + color: textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ); + } + + BoxDecoration _getButtonDecoration() { + if (widget.buttonType == ButtonType.primary) { + // Primary + return BoxDecoration( + color: widget.isDescructive ? Colors.red : Colors.white, + borderRadius: BorderRadius.circular(12), + ); + } else if (widget.buttonType == ButtonType.secondary) { + // Secondary + return BoxDecoration( + border: BoxBorder.all( + color: widget.isDescructive ? Colors.red : Colors.white, + width: 2, + ), + borderRadius: BorderRadius.circular(12), + ); + } + // Tertiary + return const BoxDecoration(); + } } diff --git a/lib/presentation/widgets/dialog/custom_dialog_action.dart b/lib/presentation/widgets/dialog/custom_dialog_action.dart index aec0dfa..47024dc 100644 --- a/lib/presentation/widgets/dialog/custom_dialog_action.dart +++ b/lib/presentation/widgets/dialog/custom_dialog_action.dart @@ -12,6 +12,7 @@ class CustomDialogAction extends StatelessWidget { required this.onPressed, required this.text, this.buttonType = ButtonType.primary, + this.isDestructive = false, }); final String text; @@ -20,12 +21,15 @@ class CustomDialogAction extends StatelessWidget { final VoidCallback onPressed; + final bool isDestructive; + @override Widget build(BuildContext context) { return AnimatedDialogButton( onPressed: onPressed, buttonText: text, buttonType: buttonType, + isDescructive: isDestructive, buttonConstraints: const BoxConstraints(minWidth: 300), ); } diff --git a/lib/presentation/widgets/game_label.dart b/lib/presentation/widgets/game_label.dart new file mode 100644 index 0000000..553e637 --- /dev/null +++ b/lib/presentation/widgets/game_label.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/enums.dart'; + +class GameLabel extends StatelessWidget { + const GameLabel({ + super.key, + required this.title, + required this.description, + required this.color, + }); + + final String title; + final String description; + final GameColor color; + + @override + Widget build(BuildContext context) { + final backgroundColor = getColorFromGameColor(color); + final fontColor = backgroundColor.computeLuminance() > 0.5 + ? Colors.black + : Colors.white; + + return IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Title + Container( + decoration: BoxDecoration( + color: backgroundColor.withAlpha(230), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Text( + title, + style: TextStyle( + fontSize: 12, + color: fontColor, + fontWeight: FontWeight.bold, + ), + ), + ), + + // Description + Container( + decoration: BoxDecoration( + color: backgroundColor.withAlpha(140), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Text( + description, + style: TextStyle( + fontSize: 12, + color: fontColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/text_input/text_input_field.dart b/lib/presentation/widgets/text_input/text_input_field.dart index 541ae6f..b074638 100644 --- a/lib/presentation/widgets/text_input/text_input_field.dart +++ b/lib/presentation/widgets/text_input/text_input_field.dart @@ -8,12 +8,18 @@ class TextInputField extends StatelessWidget { /// - [onChanged]: Optional callback invoked when the text in the field changes. /// - [hintText]: The hint text displayed in the text input field when it is empty /// - [maxLength]: Optional parameter for maximum length of the input text. + /// - [maxLines]: The maximum number of lines for the text input field. Defaults to 1. + /// - [minLines]: The minimum number of lines for the text input field. Defaults to 1. + /// - [showCounterText]: Whether to show the counter text in the text input field. Defaults to false. const TextInputField({ super.key, required this.controller, required this.hintText, this.onChanged, this.maxLength, + this.maxLines = 1, + this.minLines = 1, + this.showCounterText = false, }); /// The controller for the text input field. @@ -28,6 +34,15 @@ class TextInputField extends StatelessWidget { /// Optional parameter for maximum length of the input text. final int? maxLength; + /// The maximum number of lines for the text input field. + final int? maxLines; + + /// The minimum number of lines for the text input field. + final int? minLines; + + /// Whether to show the counter text in the text input field. + final bool showCounterText; + @override Widget build(BuildContext context) { return TextField( @@ -35,13 +50,15 @@ class TextInputField extends StatelessWidget { onChanged: onChanged, maxLength: maxLength, maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, + maxLines: maxLines, + minLines: minLines, + decoration: InputDecoration( filled: true, fillColor: CustomTheme.boxColor, hintText: hintText, hintStyle: const TextStyle(fontSize: 18), - // Hides the character counter - counterText: '', + counterText: showCounterText ? null : '', enabledBorder: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), borderSide: BorderSide(color: CustomTheme.boxBorderColor), diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index 10ded6b..41cc7f0 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -4,12 +4,12 @@ import 'package:tallee/core/custom_theme.dart'; class ChooseTile extends StatefulWidget { /// 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. + /// - [trailing]: Optional trailing text displayed on the tile. /// - [onPressed]: The callback invoked when the tile is tapped. const ChooseTile({ super.key, required this.title, - this.trailingText, + this.trailing, this.onPressed, }); @@ -20,7 +20,7 @@ class ChooseTile extends StatefulWidget { final VoidCallback? onPressed; /// Optional trailing text displayed on the tile. - final String? trailingText; + final Widget? trailing; @override State createState() => _ChooseTileState(); @@ -42,9 +42,11 @@ class _ChooseTileState extends State { style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const Spacer(), - if (widget.trailingText != null) Text(widget.trailingText!), - const SizedBox(width: 10), - const Icon(Icons.arrow_forward_ios, size: 16), + if (widget.trailing != null) widget.trailing!, + if (widget.onPressed != null) ...[ + const SizedBox(width: 10), + const Icon(Icons.arrow_forward_ios, size: 16), + ], ], ), ), diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart new file mode 100644 index 0000000..1d494b9 --- /dev/null +++ b/lib/presentation/widgets/tiles/game_tile.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; + +class GameTile extends StatelessWidget { + /// 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. + /// - [onTap]: The callback invoked when the tile is tapped. + /// - [onLongPress]: 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. + const GameTile({ + super.key, + required this.title, + required this.description, + this.onTap, + this.onLongPress, + this.isHighlighted = false, + this.badgeText, + 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? onTap; + + /// The callback invoked when the tile is long-pressed. + final VoidCallback? onLongPress; + + /// 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) { + final badgeTextColor = badgeColor != null + ? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white) + : Colors.white; + + final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange); + + return GestureDetector( + onTap: onTap, + onLongPress: onLongPress, + child: AnimatedContainer( + margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + decoration: !isHighlighted + ? CustomTheme.standardBoxDecoration + : CustomTheme.highlightedBoxDecoration.copyWith( + border: Border.all( + color: gameColor.withValues(alpha: 0.9), + width: 2, + ), + ), + duration: const Duration(milliseconds: 200), + child: Stack( + children: [ + // Gradient overlay + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + gameColor.withValues(alpha: 0.08), + gameColor.withValues(alpha: 0.02), + Colors.transparent, + ], + stops: const [0.0, 0.5, 1.0], + ), + ), + ), + ), + + // Content + Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // Title + Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + + // Badge + if (badgeText != null) ...[ + const SizedBox(height: 5), + Container( + constraints: const BoxConstraints(maxWidth: 250), + padding: const EdgeInsets.symmetric( + vertical: 2, + horizontal: 6, + ), + decoration: BoxDecoration( + color: gameColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + badgeText!, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: TextStyle( + color: badgeTextColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + + // Description + if (description.isNotEmpty) ...[ + const SizedBox(height: 10), + Text(description, style: const TextStyle(fontSize: 14)), + const SizedBox(height: 2.5), + ], + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index f7585d6..f939601 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -7,6 +7,7 @@ import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; class MatchTile extends StatefulWidget { @@ -116,56 +117,13 @@ class _MatchTileState extends State { // Game + Ruleset Badge if (!widget.compact) - IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // Game - Container( - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(230), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - bottomLeft: Radius.circular(8), - ), - ), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ), - child: Text( - match.game.name, - style: const TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - // Ruleset - Container( - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(140), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(8), - bottomRight: Radius.circular(8), - ), - ), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ), - child: Text( - translateRulesetToString(match.game.ruleset, context), - style: const TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ], + GameLabel( + title: match.game.name, + description: translateRulesetToString( + match.game.ruleset, + context, ), + color: match.game.color, ), const SizedBox(height: 12), diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart index 9dc8f33..bf45c1e 100644 --- a/lib/presentation/widgets/tiles/title_description_list_tile.dart +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -2,21 +2,17 @@ import 'package:flutter/material.dart'; import 'package:tallee/core/custom_theme.dart'; class TitleDescriptionListTile extends StatelessWidget { - /// A list tile widget that displays a title and description, with optional highlighting and badge. + /// A list tile widget that displays a title and description /// - [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. + /// - [onTap]: 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. const TitleDescriptionListTile({ super.key, required this.title, required this.description, - this.onPressed, + this.onTap, this.isHighlighted = false, - this.badgeText, - this.badgeColor, }); /// The title text displayed on the tile. @@ -26,21 +22,15 @@ class TitleDescriptionListTile extends StatelessWidget { final String description; /// The callback invoked when the tile is tapped. - final VoidCallback? onPressed; + final VoidCallback? onTap; /// 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( - onTap: onPressed, + onTap: onTap, child: AnimatedContainer( margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), @@ -51,53 +41,26 @@ class TitleDescriptionListTile extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 230, - child: Text( - title, - overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), + // Title + SizedBox( + width: 230, + child: Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, ), - if (badgeText != null) ...[ - const Spacer(), - Container( - constraints: const BoxConstraints(maxWidth: 115), - padding: const EdgeInsets.symmetric( - vertical: 2, - horizontal: 6, - ), - decoration: BoxDecoration( - color: badgeColor ?? CustomTheme.primaryColor, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - badgeText!, - overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ], + ), ), + + // Description if (description.isNotEmpty) ...[ - const SizedBox(height: 5), + const SizedBox(height: 10), Text(description, style: const TextStyle(fontSize: 14)), const SizedBox(height: 2.5), ], diff --git a/pubspec.yaml b/pubspec.yaml index 668e664..1b3d91c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.27+261 +version: 0.0.28+262 environment: sdk: ^3.8.1 @@ -19,6 +19,7 @@ dependencies: flutter_localizations: sdk: flutter flutter_numeric_text: ^1.3.3 + flutter_popup: ^3.3.9 fluttericon: ^2.0.0 font_awesome_flutter: ^11.0.0 intl: any diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index fccecfe..37c1cd0 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -241,15 +241,15 @@ void main() { expect(matchExists, isTrue); }); - test('getGroupMatches() works correctly', () async { - var matches = await database.matchDao.getGroupMatches( + test('getMatchesByGroup() works correctly', () async { + var matches = await database.matchDao.getMatchesByGroup( groupId: 'non-existing-id', ); expect(matches, isEmpty); await database.matchDao.addMatch(match: testMatch1); - matches = await database.matchDao.getGroupMatches( + matches = await database.matchDao.getMatchesByGroup( groupId: testGroup1.id, ); expect(matches, isNotEmpty); @@ -259,6 +259,69 @@ void main() { expect(match.group, isNotNull); expect(match.group!.id, testGroup1.id); }); + + test('getMatchCount() works correctly', () async { + var count = await database.matchDao.getMatchCount(); + expect(count, 0); + + await database.matchDao.addMatch(match: testMatch1); + + count = await database.matchDao.getMatchCount(); + expect(count, 1); + + await database.matchDao.addMatch(match: testMatch2); + + count = await database.matchDao.getMatchCount(); + expect(count, 2); + + await database.matchDao.deleteMatch(matchId: testMatch1.id); + + count = await database.matchDao.getMatchCount(); + expect(count, 1); + + await database.matchDao.deleteMatch(matchId: testMatch2.id); + + count = await database.matchDao.getMatchCount(); + expect(count, 0); + }); + + test('getMatchCountByGame() works correctly', () async { + var count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 0); + + await database.matchDao.addMatch(match: testMatch1); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 1); + + await database.matchDao.addMatch(match: testMatch2); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 2); + + await database.matchDao.deleteMatch(matchId: testMatch1.id); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 1); + + await database.matchDao.deleteMatch(matchId: testMatch2.id); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 0); + }); + + test('getMatchCountByGame() returns 0 for non-existent game', () async { + final count = await database.matchDao.getMatchCountByGame( + gameId: 'non-existent-game-id', + ); + expect(count, 0); + }); }); group('UPDATE', () { @@ -386,7 +449,6 @@ void main() { await database.matchDao.addMatch(match: testMatch1); DateTime newEndedAt = DateTime(2030, 1, 1, 12, 0, 0); - print(newEndedAt); await database.matchDao.updateMatchEndedAt( matchId: testMatch1.id, endedAt: newEndedAt, @@ -408,31 +470,6 @@ void main() { final allMatches = await database.matchDao.getAllMatches(); expect(allMatches, isEmpty); }); - - test('Getting the match count works correctly', () async { - var matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); - - await database.matchDao.addMatch(match: testMatch1); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); - - await database.matchDao.addMatch(match: testMatch2); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 2); - - await database.matchDao.deleteMatch(matchId: testMatch1.id); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); - - await database.matchDao.deleteMatch(matchId: testMatch2.id); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); - }); }); group('DELETE', () { @@ -471,5 +508,33 @@ void main() { expect(deleted, isFalse); }); }); + + test('deleteMatchesByGame() deletes all matches for a game', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.matchDao.addMatch(match: testMatch2); + + var count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 2); + + final deletedCount = await database.matchDao.deleteMatchesByGame( + gameId: testGame.id, + ); + expect(deletedCount, 2); + + count = await database.matchDao.getMatchCountByGame(gameId: testGame.id); + expect(count, 0); + + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); + + test('deleteMatchesByGame() returns 0 for non-existent game', () async { + final deletedCount = await database.matchDao.deleteMatchesByGame( + gameId: 'non-existent-game-id', + ); + expect(deletedCount, 0); + }); }); }