Merge remote-tracking branch 'origin/development' into feature/206-Neuer-Regelsatz-Platzierung

# Conflicts:
#	lib/l10n/arb/app_de.arb
#	lib/l10n/arb/app_en.arb
#	lib/l10n/generated/app_localizations.dart
#	lib/l10n/generated/app_localizations_de.dart
#	lib/l10n/generated/app_localizations_en.dart
This commit is contained in:
2026-05-09 19:18:54 +02:00
29 changed files with 1752 additions and 411 deletions

View File

@@ -1,4 +1,4 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
@@ -23,8 +23,71 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) {
} }
} }
/// Counts how many players in the match are not part of the group /// Translates a [GameColor] enum value to its corresponding localized string.
/// Returns the count as a string, or an empty string if there is no group 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) { String getExtraPlayerCount(Match match) {
int count = 0; int count = 0;

View File

@@ -19,4 +19,7 @@ class Constants {
/// Maximum length for team names /// Maximum length for team names
static const int MAX_TEAM_NAME_LENGTH = 32; static const int MAX_TEAM_NAME_LENGTH = 32;
/// Maximum length for game descriptions
static const int MAX_GAME_DESCRIPTION_LENGTH = 256;
} }

View File

@@ -63,9 +63,8 @@ class CustomTheme {
static BoxDecoration highlightedBoxDecoration = BoxDecoration( static BoxDecoration highlightedBoxDecoration = BoxDecoration(
color: boxColor, color: boxColor,
border: Border.all(color: primaryColor), border: Border.all(color: textColor, width: 2),
borderRadius: standardBorderRadiusAll, borderRadius: standardBorderRadiusAll,
boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)],
); );
// ==================== Component Themes ==================== // ==================== Component Themes ====================

View File

@@ -194,4 +194,25 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
/// Retrieves all games with their respective match counts.
/// Returns a list of tuples (Game, matchCount).
Future<List<(Game, int)>> 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;
}
} }

View File

@@ -341,9 +341,20 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
); );
} }
/// Retrieves the number of matches associated with a specific game.
Future<int> 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]. /// Retrieves all matches associated with the given [groupId].
/// Queries the database directly, filtering by [groupId]. /// Queries the database directly, filtering by [groupId].
Future<List<Match>> getGroupMatches({required String groupId}) async { Future<List<Match>> getMatchesByGroup({required String groupId}) async {
final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); final query = select(matchTable)..where((m) => m.groupId.equals(groupId));
final rows = await query.get(); final rows = await query.get();
@@ -478,4 +489,12 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
/// Deletes all matches associated with a specific game.
/// Returns the number of matches deleted.
Future<int> deleteMatchesByGame({required String gameId}) async {
final query = delete(matchTable)..where((m) => m.gameId.equals(gameId));
final rowsAffected = await query.go();
return rowsAffected;
}
} }

View File

@@ -12,16 +12,15 @@ class Game {
final String icon; final String icon;
Game({ Game({
String? id,
DateTime? createdAt,
required this.name, required this.name,
required this.ruleset, required this.ruleset,
String? description, this.color = GameColor.orange,
required this.color, this.description = '',
required this.icon, this.icon = '',
String? id,
DateTime? createdAt,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(), createdAt = createdAt ?? clock.now();
description = description ?? '';
@override @override
String toString() { String toString() {

View File

@@ -6,10 +6,21 @@
"app_name": "Tallee", "app_name": "Tallee",
"best_player": "Beste:r Spieler:in", "best_player": "Beste:r Spieler:in",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"choose_color": "Farbe wählen",
"choose_game": "Spielvorlage wählen", "choose_game": "Spielvorlage wählen",
"choose_group": "Gruppe wählen", "choose_group": "Gruppe wählen",
"choose_ruleset": "Regelwerk wählen", "choose_ruleset": "Regelwerk wählen",
"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", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
"create_game": "Spielvorlage erstellen",
"create_group": "Gruppe erstellen", "create_group": "Gruppe erstellen",
"create_match": "Spiel erstellen", "create_match": "Spiel erstellen",
"create_new_group": "Neue Gruppe erstellen", "create_new_group": "Neue Gruppe erstellen",
@@ -22,14 +33,26 @@
"days_ago": "vor {count} Tagen", "days_ago": "vor {count} Tagen",
"delete": "Löschen", "delete": "Löschen",
"delete_all_data": "Alle Daten 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_group": "Gruppe löschen",
"delete_match": "Spiel löschen", "delete_match": "Spiel löschen",
"drag_to_set_placement": "Ziehen um Platzierung zu setzen", "drag_to_set_placement": "Ziehen um Platzierung zu setzen",
"description": "Beschreibung",
"edit_game": "Spielvorlage bearbeiten",
"edit_group": "Gruppe bearbeiten", "edit_group": "Gruppe bearbeiten",
"edit_match": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten",
"enter_points": "Punkte eingeben", "enter_points": "Punkte eingeben",
"enter_results": "Ergebnisse eintragen", "enter_results": "Ergebnisse eintragen",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "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_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen",
"error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen", "error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen",
"error_reading_file": "Fehler beim Lesen der Datei", "error_reading_file": "Fehler beim Lesen der Datei",
@@ -58,6 +81,7 @@
"members": "Mitglieder", "members": "Mitglieder",
"most_points": "Höchste Punkte", "most_points": "Höchste Punkte",
"no_data_available": "Keine Daten verfügbar", "no_data_available": "Keine Daten verfügbar",
"no_games_created_yet": "Noch keine Spielvorlagen erstellt",
"no_groups_created_yet": "Noch keine Gruppen erstellt", "no_groups_created_yet": "Noch keine Gruppen erstellt",
"no_licenses_found": "Keine Lizenzen gefunden", "no_licenses_found": "Keine Lizenzen gefunden",
"no_license_text_available": "Kein Lizenztext verfügbar", "no_license_text_available": "Kein Lizenztext verfügbar",
@@ -77,7 +101,6 @@
"played_matches": "Gespielte Spiele", "played_matches": "Gespielte Spiele",
"player_name": "Spieler:innenname", "player_name": "Spieler:innenname",
"players": "Spieler:innen", "players": "Spieler:innen",
"players_count": "{count} Spieler",
"point": "Punkt", "point": "Punkt",
"points": "Punkte", "points": "Punkte",
"privacy_policy": "Datenschutzerklärung", "privacy_policy": "Datenschutzerklärung",
@@ -107,6 +130,7 @@
"statistics": "Statistiken", "statistics": "Statistiken",
"stats": "Statistiken", "stats": "Statistiken",
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", "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", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht",
"this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.",
"tie": "Unentschieden", "tie": "Unentschieden",

View File

@@ -1,5 +1,6 @@
{ {
"@@locale": "en", "@@locale": "en",
"@all_players": { "@all_players": {
"description": "Label for all players list" "description": "Label for all players list"
}, },
@@ -352,10 +353,21 @@
"app_name": "Tallee", "app_name": "Tallee",
"best_player": "Best Player", "best_player": "Best Player",
"cancel": "Cancel", "cancel": "Cancel",
"choose_color": "Choose Color",
"choose_game": "Choose Game", "choose_game": "Choose Game",
"choose_group": "Choose Group", "choose_group": "Choose Group",
"choose_ruleset": "Choose Ruleset", "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", "could_not_add_player": "Could not add player",
"create_game": "Create Game",
"create_group": "Create Group", "create_group": "Create Group",
"create_match": "Create match", "create_match": "Create match",
"create_new_group": "Create new group", "create_new_group": "Create new group",
@@ -368,14 +380,26 @@
"days_ago": "{count} days ago", "days_ago": "{count} days ago",
"delete": "Delete", "delete": "Delete",
"delete_all_data": "Delete all data", "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_group": "Delete Group",
"delete_match": "Delete Match", "delete_match": "Delete Match",
"drag_to_set_placement": "Drag to set placement", "drag_to_set_placement": "Drag to set placement",
"description": "Description",
"edit_game": "Edit Game",
"edit_group": "Edit Group", "edit_group": "Edit Group",
"edit_match": "Edit Match", "edit_match": "Edit Match",
"enter_points": "Enter points", "enter_points": "Enter points",
"enter_results": "Enter Results", "enter_results": "Enter Results",
"error_creating_group": "Error while creating group, please try again", "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_deleting_group": "Error while deleting group, please try again",
"error_editing_group": "Error while editing group, please try again", "error_editing_group": "Error while editing group, please try again",
"error_reading_file": "Error reading file", "error_reading_file": "Error reading file",
@@ -404,6 +428,7 @@
"members": "Members", "members": "Members",
"most_points": "Most Points", "most_points": "Most Points",
"no_data_available": "No data available", "no_data_available": "No data available",
"no_games_created_yet": "No games created yet",
"no_groups_created_yet": "No groups created yet", "no_groups_created_yet": "No groups created yet",
"no_licenses_found": "No licenses found", "no_licenses_found": "No licenses found",
"no_license_text_available": "No license text available", "no_license_text_available": "No license text available",
@@ -423,7 +448,6 @@
"played_matches": "Played Matches", "played_matches": "Played Matches",
"player_name": "Player name", "player_name": "Player name",
"players": "Players", "players": "Players",
"players_count": "{count} Players",
"point": "Point", "point": "Point",
"points": "Points", "points": "Points",
"privacy_policy": "Privacy Policy", "privacy_policy": "Privacy Policy",
@@ -452,6 +476,16 @@
"statistics": "Statistics", "statistics": "Statistics",
"stats": "Stats", "stats": "Stats",
"successfully_added_player": "Successfully added player {playerName}", "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", "there_is_no_group_matching_your_search": "There is no group matching your search",
"this_cannot_be_undone": "This can't be undone.", "this_cannot_be_undone": "This can't be undone.",
"tie": "Tie", "tie": "Tie",

View File

@@ -98,595 +98,667 @@ abstract class AppLocalizations {
Locale('en'), Locale('en'),
]; ];
/// Label for all players list /// No description provided for @all_players.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'All players'** /// **'All players'**
String get all_players; String get all_players;
/// Message when all players are added to selection /// No description provided for @all_players_selected.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'All players selected'** /// **'All players selected'**
String get 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: /// In en, this message translates to:
/// **'Amount of Matches'** /// **'Amount of Matches'**
String get 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: /// In en, this message translates to:
/// **'Tallee'** /// **'Tallee'**
String get app_name; String get app_name;
/// Label for best player statistic /// No description provided for @best_player.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Best Player'** /// **'Best Player'**
String get best_player; String get best_player;
/// Cancel button text /// No description provided for @cancel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Cancel'** /// **'Cancel'**
String get 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: /// In en, this message translates to:
/// **'Choose Game'** /// **'Choose Game'**
String get choose_game; String get choose_game;
/// Label for choosing a group /// No description provided for @choose_group.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Choose Group'** /// **'Choose Group'**
String get choose_group; String get choose_group;
/// Label for choosing a ruleset /// No description provided for @choose_ruleset.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Choose Ruleset'** /// **'Choose Ruleset'**
String get 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: /// In en, this message translates to:
/// **'Could not add player'** /// **'Could not add player'**
String could_not_add_player(Object playerName); 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: /// In en, this message translates to:
/// **'Create Group'** /// **'Create Group'**
String get create_group; String get create_group;
/// Button text to create a match /// No description provided for @create_match.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Create match'** /// **'Create match'**
String get 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: /// In en, this message translates to:
/// **'Create new group'** /// **'Create new group'**
String get create_new_group; String get create_new_group;
/// Label for creation date /// No description provided for @created_on.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Created on'** /// **'Created on'**
String get 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: /// In en, this message translates to:
/// **'Create new match'** /// **'Create new match'**
String get create_new_match; String get create_new_match;
/// Data label /// No description provided for @data.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Data'** /// **'Data'**
String get data; String get data;
/// Success message after deleting data /// No description provided for @data_successfully_deleted.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Data successfully deleted'** /// **'Data successfully deleted'**
String get 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: /// In en, this message translates to:
/// **'Data successfully exported'** /// **'Data successfully exported'**
String get 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: /// In en, this message translates to:
/// **'Data successfully imported'** /// **'Data successfully imported'**
String get 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: /// In en, this message translates to:
/// **'{count} days ago'** /// **'{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: /// In en, this message translates to:
/// **'Delete'** /// **'Delete'**
String get delete; String get delete;
/// Confirmation dialog for deleting all data /// No description provided for @delete_all_data.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Delete all data'** /// **'Delete all data'**
String get 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: /// In en, this message translates to:
/// **'Delete Group'** /// **'Delete Group'**
String get delete_group; String get delete_group;
/// Button text to delete a match /// No description provided for @delete_match.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Delete Match'** /// **'Delete Match'**
String get delete_match; String get delete_match;
/// Label for dragging to set placement /// No description provided for @description.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Drag to set placement'** /// **'Description'**
String get drag_to_set_placement; String get description;
/// Button & Appbar label for editing a group /// 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: /// In en, this message translates to:
/// **'Edit Group'** /// **'Edit Group'**
String get 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: /// In en, this message translates to:
/// **'Edit Match'** /// **'Edit Match'**
String get edit_match; String get edit_match;
/// Label to enter players points /// No description provided for @enter_points.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Enter points'** /// **'Enter points'**
String get enter_points; String get enter_points;
/// Button text to enter match results /// No description provided for @enter_results.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Enter Results'** /// **'Enter Results'**
String get 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: /// In en, this message translates to:
/// **'Error while creating group, please try again'** /// **'Error while creating group, please try again'**
String get error_creating_group; 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: /// In en, this message translates to:
/// **'Error while deleting group, please try again'** /// **'Error while deleting group, please try again'**
String get error_deleting_group; String get error_deleting_group;
/// Error message when group editing fails /// No description provided for @error_editing_group.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Error while editing group, please try again'** /// **'Error while editing group, please try again'**
String get error_editing_group; 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: /// In en, this message translates to:
/// **'Error reading file'** /// **'Error reading file'**
String get error_reading_file; String get error_reading_file;
/// Message when export is canceled /// No description provided for @export_canceled.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Export canceled'** /// **'Export canceled'**
String get export_canceled; String get export_canceled;
/// Export data menu item /// No description provided for @export_data.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Export data'** /// **'Export data'**
String get export_data; String get export_data;
/// Error message for format exceptions /// No description provided for @format_exception.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Format Exception (see console)'** /// **'Format Exception (see console)'**
String get format_exception; String get format_exception;
/// Game label /// No description provided for @game.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Game'** /// **'Game'**
String get game; String get game;
/// Placeholder for game name search /// No description provided for @game_name.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Game Name'** /// **'Game Name'**
String get game_name; String get game_name;
/// Group label /// No description provided for @group.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Group'** /// **'Group'**
String get group; String get group;
/// Placeholder for group name input /// No description provided for @group_name.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Group name'** /// **'Group name'**
String get group_name; String get group_name;
/// Title for group profile view /// No description provided for @group_profile.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Group Profile'** /// **'Group Profile'**
String get group_profile; String get group_profile;
/// Label for groups /// No description provided for @groups.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Groups'** /// **'Groups'**
String get groups; String get groups;
/// Home tab label /// No description provided for @home.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Home'** /// **'Home'**
String get home; String get home;
/// Message when import is canceled /// No description provided for @import_canceled.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Import canceled'** /// **'Import canceled'**
String get import_canceled; String get import_canceled;
/// Import data menu item /// No description provided for @import_data.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Import data'** /// **'Import data'**
String get import_data; String get import_data;
/// Info label /// No description provided for @info.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Info'** /// **'Info'**
String get info; String get info;
/// Error message for invalid schema /// No description provided for @invalid_schema.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Invalid Schema'** /// **'Invalid Schema'**
String get invalid_schema; String get invalid_schema;
/// Title for least points ruleset /// No description provided for @least_points.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Least Points'** /// **'Least Points'**
String get least_points; String get least_points;
/// Legal section header /// No description provided for @legal.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Legal'** /// **'Legal'**
String get legal; String get legal;
/// Legal notice menu item /// No description provided for @legal_notice.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Legal Notice'** /// **'Legal Notice'**
String get legal_notice; String get legal_notice;
/// Licenses menu item /// No description provided for @licenses.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Licenses'** /// **'Licenses'**
String get licenses; String get licenses;
/// Message when match is in progress /// No description provided for @match_in_progress.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Match in progress...'** /// **'Match in progress...'**
String get 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: /// In en, this message translates to:
/// **'Match name'** /// **'Match name'**
String get match_name; String get match_name;
/// Title for match profile view /// No description provided for @match_profile.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Match Profile'** /// **'Match Profile'**
String get match_profile; String get match_profile;
/// Label for matches /// No description provided for @matches.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Matches'** /// **'Matches'**
String get matches; String get matches;
/// Label for group members /// No description provided for @members.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Members'** /// **'Members'**
String get members; String get members;
/// Title for most points ruleset /// No description provided for @most_points.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Most Points'** /// **'Most Points'**
String get 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: /// In en, this message translates to:
/// **'No data available'** /// **'No data available'**
String get 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: /// In en, this message translates to:
/// **'No groups created yet'** /// **'No groups created yet'**
String get 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: /// In en, this message translates to:
/// **'No licenses found'** /// **'No licenses found'**
String get 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: /// In en, this message translates to:
/// **'No license text available'** /// **'No license text available'**
String get 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: /// In en, this message translates to:
/// **'No matches created yet'** /// **'No matches created yet'**
String get 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: /// In en, this message translates to:
/// **'No players created yet'** /// **'No players created yet'**
String get 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: /// In en, this message translates to:
/// **'No players found with that name'** /// **'No players found with that name'**
String get 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: /// In en, this message translates to:
/// **'No players selected'** /// **'No players selected'**
String get 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: /// In en, this message translates to:
/// **'No recent matches available'** /// **'No recent matches available'**
String get 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: /// In en, this message translates to:
/// **'No results entered yet'** /// **'No results entered yet'**
String get 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: /// In en, this message translates to:
/// **'No second match available'** /// **'No second match available'**
String get 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: /// In en, this message translates to:
/// **'No statistics available'** /// **'No statistics available'**
String get no_statistics_available; String get no_statistics_available;
/// None option label /// No description provided for @none.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'None'** /// **'None'**
String get none; String get none;
/// None group option label /// No description provided for @none_group.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'None'** /// **'None'**
String get none_group; String get none_group;
/// Abbreviation for not available /// No description provided for @not_available.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Not available'** /// **'Not available'**
String get not_available; String get not_available;
/// Title for placement ruleset /// No description provided for @played_matches.
///
/// In en, this message translates to:
/// **'Placement'**
String get placement;
/// Label for placement text in match detail view
///
/// In en, this message translates to:
/// **'place'**
String get place;
/// Label for played matches statistic
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Played Matches'** /// **'Played Matches'**
String get played_matches; String get played_matches;
/// Placeholder for player name input /// No description provided for @player_name.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Player name'** /// **'Player name'**
String get player_name; String get player_name;
/// Players label /// No description provided for @players.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Players'** /// **'Players'**
String get 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. /// No description provided for @point.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Point'** /// **'Point'**
String get point; String get point;
/// Points label /// No description provided for @points.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Points'** /// **'Points'**
String get points; String get points;
/// Privacy policy menu item /// No description provided for @privacy_policy.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Privacy Policy'** /// **'Privacy Policy'**
String get privacy_policy; String get privacy_policy;
/// Title for quick create section /// No description provided for @quick_create.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Quick Create'** /// **'Quick Create'**
String get quick_create; String get quick_create;
/// Title for recent matches section /// No description provided for @recent_matches.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Recent Matches'** /// **'Recent Matches'**
String get recent_matches; String get recent_matches;
/// Label for match results /// No description provided for @results.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Results'** /// **'Results'**
String get results; String get results;
/// Ruleset label /// No description provided for @ruleset.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Ruleset'** /// **'Ruleset'**
String get ruleset; String get ruleset;
/// Description for least points ruleset /// No description provided for @ruleset_least_points.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Inverse scoring: the player with the fewest points wins.'** /// **'Inverse scoring: the player with the fewest points wins.'**
String get ruleset_least_points; String get ruleset_least_points;
/// Description for most points ruleset /// No description provided for @ruleset_most_points.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Traditional ruleset: the player with the most points wins.'** /// **'Traditional ruleset: the player with the most points wins.'**
String get ruleset_most_points; String get ruleset_most_points;
/// Description for placement ruleset /// No description provided for @ruleset_single_loser.
///
/// In en, this message translates to:
/// **'Players can be arranged in an order, which reflects their placement.'**
String get ruleset_placement;
/// Description for single loser ruleset
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Exactly one loser is determined; last place receives the penalty or consequence.'** /// **'Exactly one loser is determined; last place receives the penalty or consequence.'**
String get ruleset_single_loser; String get ruleset_single_loser;
/// Description for single winner ruleset /// No description provided for @ruleset_single_winner.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'** /// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'**
String get ruleset_single_winner; String get ruleset_single_winner;
/// Save changes button text /// No description provided for @save_changes.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Save Changes'** /// **'Save Changes'**
String get 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: /// In en, this message translates to:
/// **'Search for groups'** /// **'Search for groups'**
String get 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: /// In en, this message translates to:
/// **'Search for players'** /// **'Search for players'**
String get 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: /// In en, this message translates to:
/// **'Select Winner'** /// **'Select Winner'**
String get select_winner; String get select_winner;
/// Label to select the loser /// No description provided for @select_loser.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Select Loser'** /// **'Select Loser'**
String get select_loser; String get select_loser;
/// Shows the number of selected players /// No description provided for @selected_players.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Selected players'** /// **'Selected players'**
String get selected_players; String get selected_players;
/// Label for the App Settings /// No description provided for @settings.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Settings'** /// **'Settings'**
String get settings; String get settings;
/// Title for single loser ruleset /// No description provided for @single_loser.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Single Loser'** /// **'Single Loser'**
String get single_loser; String get single_loser;
/// Title for single winner ruleset /// No description provided for @single_winner.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Single Winner'** /// **'Single Winner'**
@@ -716,13 +788,13 @@ abstract class AppLocalizations {
/// **'Multiple Winners'** /// **'Multiple Winners'**
String get multiple_winners; String get multiple_winners;
/// Statistics tab label /// No description provided for @statistics.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Statistics'** /// **'Statistics'**
String get statistics; String get statistics;
/// Stats tab label (short) /// No description provided for @stats.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Stats'** /// **'Stats'**
@@ -734,13 +806,19 @@ abstract class AppLocalizations {
/// **'Successfully added player {playerName}'** /// **'Successfully added player {playerName}'**
String successfully_added_player(String 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: /// In en, this message translates to:
/// **'There is no group matching your search'** /// **'There is no group matching your search'**
String get 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: /// In en, this message translates to:
/// **'This can\'t be undone.'** /// **'This can\'t be undone.'**
@@ -752,43 +830,43 @@ abstract class AppLocalizations {
/// **'Tie'** /// **'Tie'**
String get tie; String get tie;
/// Date format for today /// No description provided for @today_at.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Today at'** /// **'Today at'**
String get today_at; String get today_at;
/// Undo button text /// No description provided for @undo.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Undo'** /// **'Undo'**
String get undo; String get undo;
/// Error message for unknown exceptions /// No description provided for @unknown_exception.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Unknown Exception (see console)'** /// **'Unknown Exception (see console)'**
String get unknown_exception; String get unknown_exception;
/// Winner label /// No description provided for @winner.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Winner'** /// **'Winner'**
String get winner; String get winner;
/// Label for winrate statistic /// No description provided for @winrate.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Winrate'** /// **'Winrate'**
String get winrate; String get winrate;
/// Label for wins statistic /// No description provided for @wins.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Wins'** /// **'Wins'**
String get wins; String get wins;
/// Date format for yesterday /// No description provided for @yesterday_at.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Yesterday at'** /// **'Yesterday at'**

View File

@@ -26,6 +26,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get cancel => 'Abbrechen'; String get cancel => 'Abbrechen';
@override
String get choose_color => 'Farbe wählen';
@override @override
String get choose_game => 'Spielvorlage wählen'; String get choose_game => 'Spielvorlage wählen';
@@ -35,11 +38,41 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get choose_ruleset => 'Regelwerk wählen'; 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 @override
String could_not_add_player(Object playerName) { String could_not_add_player(Object playerName) {
return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
} }
@override
String get create_game => 'Spielvorlage erstellen';
@override @override
String get create_group => 'Gruppe erstellen'; String get create_group => 'Gruppe erstellen';
@@ -68,7 +101,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get data_successfully_imported => 'Daten erfolgreich importiert'; String get data_successfully_imported => 'Daten erfolgreich importiert';
@override @override
String days_ago(int count) { String days_ago(Object count) {
return 'vor $count Tagen'; return 'vor $count Tagen';
} }
@@ -78,6 +111,20 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get delete_all_data => 'Alle Daten löschen'; 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 @override
String get delete_group => 'Gruppe löschen'; String get delete_group => 'Gruppe löschen';
@@ -85,7 +132,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get delete_match => 'Spiel löschen'; String get delete_match => 'Spiel löschen';
@override @override
String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen'; String get description => 'Beschreibung';
@override
String get edit_game => 'Spielvorlage bearbeiten';
@override @override
String get edit_group => 'Gruppe bearbeiten'; String get edit_group => 'Gruppe bearbeiten';
@@ -103,6 +153,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get error_creating_group => String get error_creating_group =>
'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen';
@override
String get error_deleting_game =>
'Fehler beim Löschen der Spielvorlage, bitte erneut versuchen';
@override @override
String get error_deleting_group => String get error_deleting_group =>
'Fehler beim Löschen der Gruppe, bitte erneut versuchen'; 'Fehler beim Löschen der Gruppe, bitte erneut versuchen';
@@ -189,6 +243,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get no_data_available => 'Keine Daten verfügbar'; String get no_data_available => 'Keine Daten verfügbar';
@override
String get no_games_created_yet => 'Noch keine Spielvorlagen erstellt';
@override @override
String get no_groups_created_yet => 'Noch keine Gruppen erstellt'; String get no_groups_created_yet => 'Noch keine Gruppen erstellt';
@@ -232,12 +289,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get not_available => 'Nicht verfügbar'; String get not_available => 'Nicht verfügbar';
@override
String get placement => 'Platzierung';
@override
String get place => 'Platz';
@override @override
String get played_matches => 'Gespielte Spiele'; String get played_matches => 'Gespielte Spiele';
@@ -247,11 +298,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get players => 'Spieler:innen'; String get players => 'Spieler:innen';
@override
String players_count(int count) {
return '$count Spieler';
}
@override @override
String get point => 'Punkt'; String get point => 'Punkt';
@@ -281,10 +327,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get ruleset_most_points => String get ruleset_most_points =>
'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.'; 'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.';
@override
String get ruleset_placement =>
'Spieler:innen können in einer Reihenfolge angeordnet werden, die ihre Platzierung reflektiert.';
@override @override
String get ruleset_single_loser => String get ruleset_single_loser =>
'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.'; 'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.';
@@ -343,6 +385,10 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Spieler:in $playerName erfolgreich hinzugefügt'; 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 @override
String get there_is_no_group_matching_your_search => String get there_is_no_group_matching_your_search =>
'Es gibt keine Gruppe, die deiner Suche entspricht'; 'Es gibt keine Gruppe, die deiner Suche entspricht';

View File

@@ -26,6 +26,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get cancel => 'Cancel'; String get cancel => 'Cancel';
@override
String get choose_color => 'Choose Color';
@override @override
String get choose_game => 'Choose Game'; String get choose_game => 'Choose Game';
@@ -35,11 +38,41 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get choose_ruleset => 'Choose Ruleset'; 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 @override
String could_not_add_player(Object playerName) { String could_not_add_player(Object playerName) {
return 'Could not add player'; return 'Could not add player';
} }
@override
String get create_game => 'Create Game';
@override @override
String get create_group => 'Create Group'; String get create_group => 'Create Group';
@@ -68,7 +101,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get data_successfully_imported => 'Data successfully imported'; String get data_successfully_imported => 'Data successfully imported';
@override @override
String days_ago(int count) { String days_ago(Object count) {
return '$count days ago'; return '$count days ago';
} }
@@ -78,6 +111,20 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get delete_all_data => 'Delete all data'; 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 @override
String get delete_group => 'Delete Group'; String get delete_group => 'Delete Group';
@@ -85,7 +132,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get delete_match => 'Delete Match'; String get delete_match => 'Delete Match';
@override @override
String get drag_to_set_placement => 'Drag to set placement'; String get description => 'Description';
@override
String get edit_game => 'Edit Game';
@override @override
String get edit_group => 'Edit Group'; String get edit_group => 'Edit Group';
@@ -103,6 +153,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get error_creating_group => String get error_creating_group =>
'Error while creating group, please try again'; 'Error while creating group, please try again';
@override
String get error_deleting_game =>
'Error while deleting game, please try again';
@override @override
String get error_deleting_group => String get error_deleting_group =>
'Error while deleting group, please try again'; 'Error while deleting group, please try again';
@@ -189,6 +243,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get no_data_available => 'No data available'; String get no_data_available => 'No data available';
@override
String get no_games_created_yet => 'No games created yet';
@override @override
String get no_groups_created_yet => 'No groups created yet'; String get no_groups_created_yet => 'No groups created yet';
@@ -232,12 +289,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get not_available => 'Not available'; String get not_available => 'Not available';
@override
String get placement => 'Placement';
@override
String get place => 'place';
@override @override
String get played_matches => 'Played Matches'; String get played_matches => 'Played Matches';
@@ -247,11 +298,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get players => 'Players'; String get players => 'Players';
@override
String players_count(int count) {
return '$count Players';
}
@override @override
String get point => 'Point'; String get point => 'Point';
@@ -281,10 +327,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get ruleset_most_points => String get ruleset_most_points =>
'Traditional ruleset: the player with the most points wins.'; 'Traditional ruleset: the player with the most points wins.';
@override
String get ruleset_placement =>
'Players can be arranged in an order, which reflects their placement.';
@override @override
String get ruleset_single_loser => String get ruleset_single_loser =>
'Exactly one loser is determined; last place receives the penalty or consequence.'; 'Exactly one loser is determined; last place receives the penalty or consequence.';
@@ -343,6 +385,10 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Successfully added player $playerName'; return 'Successfully added player $playerName';
} }
@override
String get there_are_no_games_matching_your_search =>
'There are no games matching your search';
@override @override
String get there_is_no_group_matching_your_search => String get there_is_no_group_matching_your_search =>
'There is no group matching your search'; 'There is no group matching your search';

View File

@@ -197,7 +197,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
/// obsolete. For each such match, the group association is removed by setting /// obsolete. For each such match, the group association is removed by setting
/// its [groupId] to null. /// its [groupId] to null.
Future<void> deleteObsoleteMatchGroupRelations() async { Future<void> deleteObsoleteMatchGroupRelations() async {
final groupMatches = await db.matchDao.getGroupMatches( final groupMatches = await db.matchDao.getMatchesByGroup(
groupId: widget.groupToEdit!.id, groupId: widget.groupToEdit!.id,
); );

View File

@@ -244,7 +244,9 @@ class _GroupDetailViewState extends State<GroupDetailView> {
/// Loads statistics for this group /// Loads statistics for this group
Future<void> _loadStatistics() async { Future<void> _loadStatistics() async {
isLoading = true; isLoading = true;
final groupMatches = await db.matchDao.getGroupMatches(groupId: _group.id); final groupMatches = await db.matchDao.getMatchesByGroup(
groupId: _group.id,
);
setState(() { setState(() {
totalMatches = groupMatches.length; totalMatches = groupMatches.length;

View File

@@ -1,19 +1,26 @@
import 'package:flutter/material.dart'; 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/common.dart';
import 'package:tallee/core/custom_theme.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/data/models/game.dart';
import 'package:tallee/l10n/generated/app_localizations.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/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 { class ChooseGameView extends StatefulWidget {
/// A view that allows the user to choose a game from a list of available games /// 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 /// - [games]: The list of available games
/// - [initialGameIndex]: The index of the initially selected game /// - [initialGameId]: The id of the initially selected game
/// - [onGamesUpdated]: Optional callback invoked when the games are updated
const ChooseGameView({ const ChooseGameView({
super.key, super.key,
required this.games, required this.games,
required this.initialGameId, required this.initialGameId,
this.onGamesUpdated,
}); });
/// A list of tuples containing the game name, description and ruleset /// 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 /// The id of the initially selected game
final String initialGameId; final String initialGameId;
/// Optional callback invoked when the games are updated
final VoidCallback? onGamesUpdated;
@override @override
State<ChooseGameView> createState() => _ChooseGameViewState(); State<ChooseGameView> createState() => _ChooseGameViewState();
} }
class _ChooseGameViewState extends State<ChooseGameView> { class _ChooseGameViewState extends State<ChooseGameView> {
late final AppDatabase db;
late List<(Game, int)> gameCounts = [];
/// Controller for the search bar /// Controller for the search bar
final TextEditingController searchBarController = TextEditingController(); final TextEditingController searchBarController = TextEditingController();
/// Currently selected game index /// Currently selected game index
late String selectedGameId; late String selectedGameId;
/// Games filtered according to the current search query
late List<Game> filteredGames;
@override @override
void initState() { void initState() {
db = Provider.of<AppDatabase>(context, listen: false);
fetchGameCounts();
selectedGameId = widget.initialGameId; selectedGameId = widget.initialGameId;
// Start with all games visible
filteredGames = List<Game>.from(widget.games);
super.initState(); super.initState();
} }
@@ -58,6 +82,30 @@ class _ChooseGameViewState extends State<ChooseGameView> {
); );
}, },
), ),
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), title: Text(loc.choose_game),
), ),
body: PopScope( body: PopScope(
@@ -72,37 +120,101 @@ class _ChooseGameViewState extends State<ChooseGameView> {
}, },
child: Column( child: Column(
children: [ children: [
// Search Bar
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: CustomSearchBar( child: CustomSearchBar(
controller: searchBarController, controller: searchBarController,
hintText: loc.game_name, hintText: loc.game_name,
onChanged: (value) {
_applySearchFilter(value);
},
), ),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
// Game list
Expanded( Expanded(
child: ListView.builder( child: Visibility(
itemCount: widget.games.length, visible: filteredGames.isNotEmpty,
itemBuilder: (BuildContext context, int index) { replacement: Visibility(
return TitleDescriptionListTile( visible: widget.games.isNotEmpty,
title: widget.games[index].name, replacement: TopCenteredMessage(
description: widget.games[index].description, icon: Icons.info,
badgeText: translateRulesetToString( title: loc.info,
widget.games[index].ruleset, message: loc.no_games_created_yet,
),
child: TopCenteredMessage(
icon: Icons.info,
title: loc.info,
message: AppLocalizations.of(
context, context,
), ).there_are_no_games_matching_your_search,
isHighlighted: selectedGameId == widget.games[index].id, ),
onPressed: () async { ),
setState(() { child: ListView.builder(
if (selectedGameId != widget.games[index].id) { itemCount: filteredGames.length,
selectedGameId = widget.games[index].id; itemBuilder: (BuildContext context, int index) {
} else { final game = filteredGames[index];
selectedGameId = ''; 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<ChooseGameView> {
), ),
); );
} }
/// 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<Game>.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<void> 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;
}
} }

View File

@@ -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<CreateGameView> createState() => _CreateGameViewState();
}
class _CreateGameViewState extends State<CreateGameView> {
/// GlobalKey for ScaffoldMessenger to show snackbars
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
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<Group> filteredGroups;
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(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<bool>(
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<void> 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<void> 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),
),
],
),
);
}
}

View File

@@ -28,10 +28,13 @@ class CreateMatchView extends StatefulWidget {
this.onWinnerChanged, this.onWinnerChanged,
this.matchToEdit, this.matchToEdit,
this.onMatchUpdated, this.onMatchUpdated,
this.onMatchesUpdated,
}); });
final VoidCallback? onWinnerChanged; final VoidCallback? onWinnerChanged;
final VoidCallback? onMatchesUpdated;
final void Function(Match)? onMatchUpdated; final void Function(Match)? onMatchUpdated;
/// An optional match to prefill the fields for editing. /// An optional match to prefill the fields for editing.
@@ -115,6 +118,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
// Match name input field.
Container( Container(
margin: CustomTheme.tileMargin, margin: CustomTheme.tileMargin,
child: TextInputField( child: TextInputField(
@@ -123,34 +127,40 @@ class _CreateMatchViewState extends State<CreateMatchView> {
maxLength: Constants.MAX_MATCH_NAME_LENGTH, maxLength: Constants.MAX_MATCH_NAME_LENGTH,
), ),
), ),
ChooseTile(
title: loc.game, // Game selection tile.
trailingText: selectedGame == null if (!isEditMode())
? loc.none_group ChooseTile(
: selectedGame!.name, title: loc.game,
onPressed: () async { trailing: selectedGame == null
selectedGame = await Navigator.of(context).push( ? Text(loc.none_group)
adaptivePageRoute( : Text(selectedGame!.name),
builder: (context) => ChooseGameView( onPressed: () async {
games: gamesList, selectedGame = await Navigator.of(context).push(
initialGameId: selectedGame?.id ?? '', adaptivePageRoute(
builder: (context) => ChooseGameView(
games: gamesList,
initialGameId: selectedGame?.id ?? '',
onGamesUpdated: widget.onMatchesUpdated,
),
), ),
), );
); setState(() {
setState(() { if (selectedGame != null) {
if (selectedGame != null) { hintText = selectedGame!.name;
hintText = selectedGame!.name; } else {
} else { hintText = loc.match_name;
hintText = loc.match_name; }
} });
}); },
}, ),
),
// Group selection tile.
ChooseTile( ChooseTile(
title: loc.group, title: loc.group,
trailingText: selectedGroup == null trailing: selectedGroup == null
? loc.none_group ? Text(loc.none_group)
: selectedGroup!.name, : Text(selectedGroup!.name),
onPressed: () async { onPressed: () async {
// Remove all players from the previously selected group from // Remove all players from the previously selected group from
// the selected players list, in case the user deselects the // the selected players list, in case the user deselects the
@@ -181,6 +191,8 @@ class _CreateMatchViewState extends State<CreateMatchView> {
}); });
}, },
), ),
// Player selection widget.
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'), key: ValueKey(selectedGroup?.id ?? 'no_group'),
@@ -193,6 +205,8 @@ class _CreateMatchViewState extends State<CreateMatchView> {
}, },
), ),
), ),
// Create or save button.
CustomWidthButton( CustomWidthButton(
text: buttonText, text: buttonText,
sizeRelativeToWidth: 0.95, sizeRelativeToWidth: 0.95,
@@ -218,16 +232,16 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// ///
/// Returns `true` if: /// Returns `true` if:
/// - A ruleset is selected AND /// - 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() { bool _enableCreateGameButton() {
return (selectedGroup != null || return (selectedGroup != null ||
(selectedPlayers.length > 1) && selectedGame != null); (selectedPlayers.length > 1) && selectedGame != null);
} }
// If a match was provided to the view, it updates the match in the database /// Handles navigation when the create or save button is pressed.
// and navigates back to the previous screen. ///
// If no match was provided, it creates a new match in the database and /// If a match is being edited, updates the match in the database.
// navigates to the MatchResultView for the newly created match. /// Otherwise, creates a new match and navigates to the MatchResultView.
void buttonNavigation(BuildContext context) async { void buttonNavigation(BuildContext context) async {
if (isEditMode()) { if (isEditMode()) {
await updateMatch(); await updateMatch();
@@ -252,8 +266,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
} }
} }
/// Updates attributes of the existing match in the database based on the /// Updates the existing match in the database.
/// changes made in the edit view.
Future<void> updateMatch() async { Future<void> updateMatch() async {
final updatedMatch = Match( final updatedMatch = Match(
id: widget.matchToEdit!.id, id: widget.matchToEdit!.id,
@@ -262,7 +275,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
: _matchNameController.text.trim(), : _matchNameController.text.trim(),
group: selectedGroup, group: selectedGroup,
players: selectedPlayers, players: selectedPlayers,
game: widget.matchToEdit!.game, game: selectedGame!,
createdAt: widget.matchToEdit!.createdAt, createdAt: widget.matchToEdit!.createdAt,
endedAt: widget.matchToEdit!.endedAt, endedAt: widget.matchToEdit!.endedAt,
notes: widget.matchToEdit!.notes, notes: widget.matchToEdit!.notes,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttericon/rpg_awesome_icons.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/adaptive_page_route.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/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.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/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/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -102,6 +104,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
bottom: 100, bottom: 100,
), ),
children: [ children: [
// Controller Icon
const Center( const Center(
child: ColoredIconContainer( child: ColoredIconContainer(
icon: Icons.sports_esports, icon: Icons.sports_esports,
@@ -110,6 +113,8 @@ class _MatchDetailViewState extends State<MatchDetailView> {
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Match Name
Text( Text(
match.name, match.name,
style: const TextStyle( style: const TextStyle(
@@ -120,6 +125,8 @@ class _MatchDetailViewState extends State<MatchDetailView> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
// Creation Date
Text( Text(
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}', '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}',
style: const TextStyle( style: const TextStyle(
@@ -129,6 +136,8 @@ class _MatchDetailViewState extends State<MatchDetailView> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Group Name
if (match.group != null) ...[ if (match.group != null) ...[
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -143,6 +152,8 @@ class _MatchDetailViewState extends State<MatchDetailView> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
// Players
InfoTile( InfoTile(
title: loc.players, title: loc.players,
icon: Icons.people, icon: Icons.people,
@@ -162,6 +173,30 @@ class _MatchDetailViewState extends State<MatchDetailView> {
), ),
), ),
const SizedBox(height: 15), 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( InfoTile(
title: loc.results, title: loc.results,
icon: Icons.emoji_events, icon: Icons.emoji_events,

View File

@@ -126,8 +126,10 @@ class _MatchViewState extends State<MatchView> {
Navigator.push( Navigator.push(
context, context,
adaptivePageRoute( adaptivePageRoute(
builder: (context) => builder: (context) => CreateMatchView(
CreateMatchView(onWinnerChanged: loadMatches), onWinnerChanged: loadMatches,
onMatchesUpdated: loadMatches,
),
), ),
); );
}, },

View File

@@ -55,6 +55,7 @@ const allDependencies = <Package>[
_flutter_lints, _flutter_lints,
_flutter_localizations, _flutter_localizations,
_flutter_plugin_android_lifecycle, _flutter_plugin_android_lifecycle,
_flutter_popup,
_flutter_test, _flutter_test,
_flutter_web_plugins, _flutter_web_plugins,
_fluttericon, _fluttericon,
@@ -168,6 +169,7 @@ const dependencies = <Package>[
_file_saver, _file_saver,
_flutter, _flutter,
_flutter_localizations, _flutter_localizations,
_flutter_popup,
_fluttericon, _fluttericon,
_font_awesome_flutter, _font_awesome_flutter,
_intl, _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.''', 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 /// flutter_test null
const _flutter_test = Package( const _flutter_test = Package(
name: 'flutter_test', name: 'flutter_test',
@@ -37676,16 +37713,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''', SOFTWARE.''',
); );
/// tallee 0.0.27+261 /// tallee 0.0.28+262
const _tallee = Package( const _tallee = Package(
name: 'tallee', name: 'tallee',
description: 'Tracking App for Card Games', description: 'Tracking App for Card Games',
authors: [], authors: [],
version: '0.0.27+261', version: '0.0.28+262',
spdxIdentifiers: ['LGPL-3.0'], spdxIdentifiers: ['LGPL-3.0'],
isMarkdown: false, isMarkdown: false,
isSdk: 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')], devDependencies: [PackageRef('flutter_test'), PackageRef('build_runner'), PackageRef('dart_pubspec_licenses'), PackageRef('drift_dev'), PackageRef('flutter_lints')],
license: '''GNU LESSER GENERAL PUBLIC LICENSE license: '''GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 29 June 2007

View File

@@ -14,6 +14,7 @@ class AnimatedDialogButton extends StatefulWidget {
required this.onPressed, required this.onPressed,
this.buttonConstraints, this.buttonConstraints,
this.buttonType = ButtonType.primary, this.buttonType = ButtonType.primary,
this.isDescructive = false,
}); });
final String buttonText; final String buttonText;
@@ -24,6 +25,8 @@ class AnimatedDialogButton extends StatefulWidget {
final ButtonType buttonType; final ButtonType buttonType;
final bool isDescructive;
@override @override
State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState(); State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState();
} }
@@ -33,28 +36,8 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textStyling = TextStyle( final textStyling = _getTextStyling();
color: widget.buttonType == ButtonType.primary final buttonDecoration = _getButtonDecoration();
? 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();
return GestureDetector( return GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true), onTapDown: (_) => setState(() => _isPressed = true),
@@ -84,4 +67,42 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
), ),
); );
} }
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();
}
} }

View File

@@ -12,6 +12,7 @@ class CustomDialogAction extends StatelessWidget {
required this.onPressed, required this.onPressed,
required this.text, required this.text,
this.buttonType = ButtonType.primary, this.buttonType = ButtonType.primary,
this.isDestructive = false,
}); });
final String text; final String text;
@@ -20,12 +21,15 @@ class CustomDialogAction extends StatelessWidget {
final VoidCallback onPressed; final VoidCallback onPressed;
final bool isDestructive;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedDialogButton( return AnimatedDialogButton(
onPressed: onPressed, onPressed: onPressed,
buttonText: text, buttonText: text,
buttonType: buttonType, buttonType: buttonType,
isDescructive: isDestructive,
buttonConstraints: const BoxConstraints(minWidth: 300), buttonConstraints: const BoxConstraints(minWidth: 300),
); );
} }

View File

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

View File

@@ -8,12 +8,18 @@ class TextInputField extends StatelessWidget {
/// - [onChanged]: Optional callback invoked when the text in the field changes. /// - [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 /// - [hintText]: The hint text displayed in the text input field when it is empty
/// - [maxLength]: Optional parameter for maximum length of the input text. /// - [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({ const TextInputField({
super.key, super.key,
required this.controller, required this.controller,
required this.hintText, required this.hintText,
this.onChanged, this.onChanged,
this.maxLength, this.maxLength,
this.maxLines = 1,
this.minLines = 1,
this.showCounterText = false,
}); });
/// The controller for the text input field. /// The controller for the text input field.
@@ -28,6 +34,15 @@ class TextInputField extends StatelessWidget {
/// Optional parameter for maximum length of the input text. /// Optional parameter for maximum length of the input text.
final int? maxLength; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextField( return TextField(
@@ -35,13 +50,15 @@ class TextInputField extends StatelessWidget {
onChanged: onChanged, onChanged: onChanged,
maxLength: maxLength, maxLength: maxLength,
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
maxLines: maxLines,
minLines: minLines,
decoration: InputDecoration( decoration: InputDecoration(
filled: true, filled: true,
fillColor: CustomTheme.boxColor, fillColor: CustomTheme.boxColor,
hintText: hintText, hintText: hintText,
hintStyle: const TextStyle(fontSize: 18), hintStyle: const TextStyle(fontSize: 18),
// Hides the character counter counterText: showCounterText ? null : '',
counterText: '',
enabledBorder: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorderColor), borderSide: BorderSide(color: CustomTheme.boxBorderColor),

View File

@@ -4,12 +4,12 @@ import 'package:tallee/core/custom_theme.dart';
class ChooseTile extends StatefulWidget { class ChooseTile extends StatefulWidget {
/// A tile widget that allows users to choose an option by tapping on it. /// A tile widget that allows users to choose an option by tapping on it.
/// - [title]: The title text displayed on the tile. /// - [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. /// - [onPressed]: The callback invoked when the tile is tapped.
const ChooseTile({ const ChooseTile({
super.key, super.key,
required this.title, required this.title,
this.trailingText, this.trailing,
this.onPressed, this.onPressed,
}); });
@@ -20,7 +20,7 @@ class ChooseTile extends StatefulWidget {
final VoidCallback? onPressed; final VoidCallback? onPressed;
/// Optional trailing text displayed on the tile. /// Optional trailing text displayed on the tile.
final String? trailingText; final Widget? trailing;
@override @override
State<ChooseTile> createState() => _ChooseTileState(); State<ChooseTile> createState() => _ChooseTileState();
@@ -42,9 +42,11 @@ class _ChooseTileState extends State<ChooseTile> {
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
), ),
const Spacer(), const Spacer(),
if (widget.trailingText != null) Text(widget.trailingText!), if (widget.trailing != null) widget.trailing!,
const SizedBox(width: 10), if (widget.onPressed != null) ...[
const Icon(Icons.arrow_forward_ios, size: 16), const SizedBox(width: 10),
const Icon(Icons.arrow_forward_ios, size: 16),
],
], ],
), ),
), ),

View File

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

View File

@@ -7,6 +7,7 @@ import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.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'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class MatchTile extends StatefulWidget { class MatchTile extends StatefulWidget {
@@ -116,56 +117,13 @@ class _MatchTileState extends State<MatchTile> {
// Game + Ruleset Badge // Game + Ruleset Badge
if (!widget.compact) if (!widget.compact)
IntrinsicHeight( GameLabel(
child: Row( title: match.game.name,
mainAxisSize: MainAxisSize.min, description: translateRulesetToString(
children: [ match.game.ruleset,
// Game context,
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,
),
),
),
],
), ),
color: match.game.color,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),

View File

@@ -2,21 +2,17 @@ import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
class TitleDescriptionListTile extends StatelessWidget { 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. /// - [title]: The title text displayed on the tile.
/// - [description]: The description text displayed below the title. /// - [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. /// - [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({ const TitleDescriptionListTile({
super.key, super.key,
required this.title, required this.title,
required this.description, required this.description,
this.onPressed, this.onTap,
this.isHighlighted = false, this.isHighlighted = false,
this.badgeText,
this.badgeColor,
}); });
/// The title text displayed on the tile. /// The title text displayed on the tile.
@@ -26,21 +22,15 @@ class TitleDescriptionListTile extends StatelessWidget {
final String description; final String description;
/// The callback invoked when the tile is tapped. /// The callback invoked when the tile is tapped.
final VoidCallback? onPressed; final VoidCallback? onTap;
/// A boolean to determine if the tile should be highlighted. /// A boolean to determine if the tile should be highlighted.
final bool isHighlighted; final bool isHighlighted;
/// Optional text to display in a badge on the right side of the title.
final String? badgeText;
/// Optional color for the badge background.
final Color? badgeColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: onPressed, onTap: onTap,
child: AnimatedContainer( child: AnimatedContainer(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
@@ -51,53 +41,26 @@ class TitleDescriptionListTile extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [ children: [
Row( // Title
mainAxisAlignment: MainAxisAlignment.start, SizedBox(
crossAxisAlignment: CrossAxisAlignment.center, width: 230,
children: [ child: Text(
SizedBox( title,
width: 230, overflow: TextOverflow.ellipsis,
child: Text( maxLines: 1,
title, softWrap: false,
overflow: TextOverflow.ellipsis, style: const TextStyle(
maxLines: 1, fontWeight: FontWeight.bold,
softWrap: false, fontSize: 18,
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) ...[ if (description.isNotEmpty) ...[
const SizedBox(height: 5), const SizedBox(height: 10),
Text(description, style: const TextStyle(fontSize: 14)), Text(description, style: const TextStyle(fontSize: 14)),
const SizedBox(height: 2.5), const SizedBox(height: 2.5),
], ],

View File

@@ -1,7 +1,7 @@
name: tallee name: tallee
description: "Tracking App for Card Games" description: "Tracking App for Card Games"
publish_to: 'none' publish_to: 'none'
version: 0.0.27+261 version: 0.0.28+262
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1
@@ -18,6 +18,7 @@ dependencies:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
flutter_popup: ^3.3.9
fluttericon: ^2.0.0 fluttericon: ^2.0.0
font_awesome_flutter: ^11.0.0 font_awesome_flutter: ^11.0.0
intl: any intl: any

View File

@@ -241,15 +241,15 @@ void main() {
expect(matchExists, isTrue); expect(matchExists, isTrue);
}); });
test('getGroupMatches() works correctly', () async { test('getMatchesByGroup() works correctly', () async {
var matches = await database.matchDao.getGroupMatches( var matches = await database.matchDao.getMatchesByGroup(
groupId: 'non-existing-id', groupId: 'non-existing-id',
); );
expect(matches, isEmpty); expect(matches, isEmpty);
await database.matchDao.addMatch(match: testMatch1); await database.matchDao.addMatch(match: testMatch1);
matches = await database.matchDao.getGroupMatches( matches = await database.matchDao.getMatchesByGroup(
groupId: testGroup1.id, groupId: testGroup1.id,
); );
expect(matches, isNotEmpty); expect(matches, isNotEmpty);
@@ -259,6 +259,69 @@ void main() {
expect(match.group, isNotNull); expect(match.group, isNotNull);
expect(match.group!.id, testGroup1.id); 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', () { group('UPDATE', () {
@@ -386,7 +449,6 @@ void main() {
await database.matchDao.addMatch(match: testMatch1); await database.matchDao.addMatch(match: testMatch1);
DateTime newEndedAt = DateTime(2030, 1, 1, 12, 0, 0); DateTime newEndedAt = DateTime(2030, 1, 1, 12, 0, 0);
print(newEndedAt);
await database.matchDao.updateMatchEndedAt( await database.matchDao.updateMatchEndedAt(
matchId: testMatch1.id, matchId: testMatch1.id,
endedAt: newEndedAt, endedAt: newEndedAt,
@@ -408,31 +470,6 @@ void main() {
final allMatches = await database.matchDao.getAllMatches(); final allMatches = await database.matchDao.getAllMatches();
expect(allMatches, isEmpty); 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', () { group('DELETE', () {
@@ -471,5 +508,33 @@ void main() {
expect(deleted, isFalse); 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);
});
}); });
} }