From 2f5b9e5ff2a6ac9738bb37a0082132ec97631b3e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 28 Apr 2026 15:27:52 +0200 Subject: [PATCH 01/33] Cherry picked changes from 119-implementierung-der-games --- lib/core/common.dart | 47 ++- lib/core/constants.dart | 3 + lib/data/models/game.dart | 11 +- lib/l10n/arb/app_de.arb | 15 + lib/l10n/arb/app_en.arb | 36 ++ lib/l10n/generated/app_localizations.dart | 90 +++++ lib/l10n/generated/app_localizations_de.dart | 46 +++ lib/l10n/generated/app_localizations_en.dart | 46 +++ .../create_match/choose_game_view.dart | 123 +++++- .../create_game/choose_color_view.dart | 78 ++++ .../create_game/choose_ruleset_view.dart | 99 +++++ .../create_game/create_game_view.dart | 352 ++++++++++++++++++ .../create_match/create_match_view.dart | 31 +- .../widgets/text_input/text_input_field.dart | 21 +- .../tiles/title_description_list_tile.dart | 14 +- 15 files changed, 978 insertions(+), 34 deletions(-) create mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart create mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart create mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart diff --git a/lib/core/common.dart b/lib/core/common.dart index 8027180..4c02350 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; @@ -21,6 +21,51 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) { } } +/// Translates a [GameColor] enum value to its corresponding localized string. +String translateGameColorToString(GameColor color, BuildContext context) { + final loc = AppLocalizations.of(context); + switch (color) { + case GameColor.red: + return loc.color_red; + case GameColor.blue: + return loc.color_blue; + case GameColor.green: + return loc.color_green; + case GameColor.yellow: + return loc.color_yellow; + case GameColor.purple: + return loc.color_purple; + case GameColor.orange: + return loc.color_orange; + case GameColor.pink: + return loc.color_pink; + case GameColor.teal: + return loc.color_teal; + } +} + +/// Returns the [Color] object corresponding to a [GameColor] enum value. +Color getColorFromGameColor(GameColor color) { + switch (color) { + case GameColor.red: + return Colors.red; + case GameColor.blue: + return Colors.blue; + case GameColor.green: + return Colors.green; + case GameColor.yellow: + return Colors.yellow; + case GameColor.purple: + return Colors.purple; + case GameColor.orange: + return Colors.orange; + case GameColor.pink: + return Colors.pink; + case GameColor.teal: + return Colors.teal; + } +} + /// Counts how many players in the match are not part of the group /// Returns the count as a string, or an empty string if there is no group String getExtraPlayerCount(Match match) { diff --git a/lib/core/constants.dart b/lib/core/constants.dart index c1bc0fe..86e3ad7 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -19,4 +19,7 @@ class Constants { /// Maximum length for team names static const int MAX_TEAM_NAME_LENGTH = 32; + + /// Maximum length for game descriptions + static const int MAX_GAME_DESCRIPTION_LENGTH = 256; } diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index 607db0a..4888df4 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -12,16 +12,17 @@ class Game { final String icon; Game({ - String? id, - DateTime? createdAt, required this.name, required this.ruleset, - String? description, required this.color, - required this.icon, + String? id, + DateTime? createdAt, + String? description, + String? icon, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? clock.now(), - description = description ?? ''; + description = description ?? '', + icon = icon ?? ''; @override String toString() { diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 46c780a..ba4fe38 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -6,10 +6,21 @@ "app_name": "Tallee", "best_player": "Beste:r Spieler:in", "cancel": "Abbrechen", + "choose_color": "Farbe wählen", "choose_game": "Spielvorlage wählen", "choose_group": "Gruppe wählen", "choose_ruleset": "Regelwerk wählen", + "color": "Farbe", + "color_blue": "Blau", + "color_green": "Grün", + "color_orange": "Orange", + "color_pink": "Rosa", + "color_purple": "Lila", + "color_red": "Rot", + "color_teal": "Türkis", + "color_yellow": "Gelb", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", + "create_game": "Spielvorlage erstellen", "create_group": "Gruppe erstellen", "create_match": "Spiel erstellen", "create_new_group": "Neue Gruppe erstellen", @@ -22,13 +33,17 @@ "days_ago": "vor {count} Tagen", "delete": "Löschen", "delete_all_data": "Alle Daten löschen", + "delete_game": "Spielvorlage löschen", "delete_group": "Gruppe löschen", "delete_match": "Spiel löschen", + "description": "Beschreibung", + "edit_game": "Spielvorlage bearbeiten", "edit_group": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten", "enter_points": "Punkte eingeben", "enter_results": "Ergebnisse eintragen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", + "error_deleting_game": "Fehler beim Löschen der Spielvorlage, bitte erneut versuchen", "error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen", "error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen", "error_reading_file": "Fehler beim Lesen der Datei", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a85e1b0..98d1c38 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -18,6 +18,9 @@ "@cancel": { "description": "Cancel button text" }, + "@choose_color": { + "description": "Label for choosing a color" + }, "@choose_game": { "description": "Label for choosing a game" }, @@ -27,9 +30,15 @@ "@choose_ruleset": { "description": "Label for choosing a ruleset" }, + "@color": { + "description": "Color label" + }, "@could_not_add_player": { "description": "Error message when adding a player fails" }, + "@create_game": { + "description": "Button text to create a game" + }, "@create_group": { "description": "Button text to create a group" }, @@ -71,12 +80,21 @@ "@delete_all_data": { "description": "Confirmation dialog for deleting all data" }, + "@delete_game": { + "description": "Button text to delete a game" + }, "@delete_group": { "description": "Confirmation dialog for deleting a group" }, "@delete_match": { "description": "Button text to delete a match" }, + "@description": { + "description": "Description label" + }, + "@edit_game": { + "description": "Button text to edit a game" + }, "@edit_group": { "description": "Button & Appbar label for editing a group" }, @@ -92,6 +110,9 @@ "@error_creating_group": { "description": "Error message when group creation fails" }, + "@error_deleting_game": { + "description": "Error message when game deletion fails" + }, "@error_deleting_group": { "description": "Error message when group deletion fails" }, @@ -340,10 +361,21 @@ "app_name": "Tallee", "best_player": "Best Player", "cancel": "Cancel", + "choose_color": "Choose Color", "choose_game": "Choose Game", "choose_group": "Choose Group", "choose_ruleset": "Choose Ruleset", + "color": "Color", + "color_blue": "Blue", + "color_green": "Green", + "color_orange": "Orange", + "color_pink": "Pink", + "color_purple": "Purple", + "color_red": "Red", + "color_teal": "Teal", + "color_yellow": "Yellow", "could_not_add_player": "Could not add player", + "create_game": "Create Game", "create_group": "Create Group", "create_match": "Create match", "create_new_group": "Create new group", @@ -356,13 +388,17 @@ "days_ago": "{count} days ago", "delete": "Delete", "delete_all_data": "Delete all data", + "delete_game": "Delete Game", "delete_group": "Delete Group", "delete_match": "Delete Match", + "description": "Description", + "edit_game": "Edit Game", "edit_group": "Edit Group", "edit_match": "Edit Match", "enter_points": "Enter points", "enter_results": "Enter Results", "error_creating_group": "Error while creating group, please try again", + "error_deleting_game": "Error while deleting game, please try again", "error_deleting_group": "Error while deleting group, please try again", "error_editing_group": "Error while editing group, please try again", "error_reading_file": "Error reading file", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 99c9317..8e44e7b 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -134,6 +134,12 @@ abstract class AppLocalizations { /// **'Cancel'** String get cancel; + /// Label for choosing a color + /// + /// In en, this message translates to: + /// **'Choose Color'** + String get choose_color; + /// Label for choosing a game /// /// In en, this message translates to: @@ -152,12 +158,72 @@ abstract class AppLocalizations { /// **'Choose Ruleset'** String get choose_ruleset; + /// Color label + /// + /// 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; + /// Error message when adding a player fails /// /// In en, this message translates to: /// **'Could not add player'** String could_not_add_player(Object playerName); + /// Button text to create a game + /// + /// In en, this message translates to: + /// **'Create Game'** + String get create_game; + /// Button text to create a group /// /// In en, this message translates to: @@ -230,6 +296,12 @@ abstract class AppLocalizations { /// **'Delete all data'** String get delete_all_data; + /// Button text to delete a game + /// + /// In en, this message translates to: + /// **'Delete Game'** + String get delete_game; + /// Confirmation dialog for deleting a group /// /// In en, this message translates to: @@ -242,6 +314,18 @@ abstract class AppLocalizations { /// **'Delete Match'** String get delete_match; + /// Description label + /// + /// In en, this message translates to: + /// **'Description'** + String get description; + + /// Button text to edit a game + /// + /// In en, this message translates to: + /// **'Edit Game'** + String get edit_game; + /// Button & Appbar label for editing a group /// /// In en, this message translates to: @@ -272,6 +356,12 @@ abstract class AppLocalizations { /// **'Error while creating group, please try again'** String get error_creating_group; + /// Error message when game deletion fails + /// + /// In en, this message translates to: + /// **'Error while deleting game, please try again'** + String get error_deleting_game; + /// Error message when group deletion fails /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 51b4c62..3c2b4e3 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -26,6 +26,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get cancel => 'Abbrechen'; + @override + String get choose_color => 'Farbe wählen'; + @override String get choose_game => 'Spielvorlage wählen'; @@ -35,11 +38,41 @@ class AppLocalizationsDe extends AppLocalizations { @override String get choose_ruleset => 'Regelwerk wählen'; + @override + String get color => 'Farbe'; + + @override + String get color_blue => 'Blau'; + + @override + String get color_green => 'Grün'; + + @override + String get color_orange => 'Orange'; + + @override + String get color_pink => 'Rosa'; + + @override + String get color_purple => 'Lila'; + + @override + String get color_red => 'Rot'; + + @override + String get color_teal => 'Türkis'; + + @override + String get color_yellow => 'Gelb'; + @override String could_not_add_player(Object playerName) { return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; } + @override + String get create_game => 'Spielvorlage erstellen'; + @override String get create_group => 'Gruppe erstellen'; @@ -78,12 +111,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get delete_all_data => 'Alle Daten löschen'; + @override + String get delete_game => 'Spielvorlage löschen'; + @override String get delete_group => 'Gruppe löschen'; @override String get delete_match => 'Spiel löschen'; + @override + String get description => 'Beschreibung'; + + @override + String get edit_game => 'Spielvorlage bearbeiten'; + @override String get edit_group => 'Gruppe bearbeiten'; @@ -100,6 +142,10 @@ class AppLocalizationsDe extends AppLocalizations { String get error_creating_group => 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; + @override + String get error_deleting_game => + 'Fehler beim Löschen der Spielvorlage, bitte erneut versuchen'; + @override String get error_deleting_group => 'Fehler beim Löschen der Gruppe, bitte erneut versuchen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 2b42e47..e14b7a0 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -26,6 +26,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cancel => 'Cancel'; + @override + String get choose_color => 'Choose Color'; + @override String get choose_game => 'Choose Game'; @@ -35,11 +38,41 @@ class AppLocalizationsEn extends AppLocalizations { @override String get choose_ruleset => 'Choose Ruleset'; + @override + String get color => 'Color'; + + @override + String get color_blue => 'Blue'; + + @override + String get color_green => 'Green'; + + @override + String get color_orange => 'Orange'; + + @override + String get color_pink => 'Pink'; + + @override + String get color_purple => 'Purple'; + + @override + String get color_red => 'Red'; + + @override + String get color_teal => 'Teal'; + + @override + String get color_yellow => 'Yellow'; + @override String could_not_add_player(Object playerName) { return 'Could not add player'; } + @override + String get create_game => 'Create Game'; + @override String get create_group => 'Create Group'; @@ -78,12 +111,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get delete_all_data => 'Delete all data'; + @override + String get delete_game => 'Delete Game'; + @override String get delete_group => 'Delete Group'; @override String get delete_match => 'Delete Match'; + @override + String get description => 'Description'; + + @override + String get edit_game => 'Edit Game'; + @override String get edit_group => 'Edit Group'; @@ -100,6 +142,10 @@ class AppLocalizationsEn extends AppLocalizations { String get error_creating_group => 'Error while creating group, please try again'; + @override + String get error_deleting_game => + 'Error while deleting game, please try again'; + @override String get error_deleting_group => 'Error while deleting group, please try again'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 51512f9..8f3e06e 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -1,19 +1,23 @@ import 'package:flutter/material.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; class ChooseGameView extends StatefulWidget { /// A view that allows the user to choose a game from a list of available games - /// - [games]: A list of tuples containing the game name, description and ruleset - /// - [initialGameIndex]: The index of the initially selected game + /// - [games]: The list of available games + /// - [initialGameId]: The id of the initially selected game + /// - [onGamesUpdated]: Optional callback invoked when the games are updated const ChooseGameView({ super.key, required this.games, required this.initialGameId, + this.onGamesUpdated, }); /// A list of tuples containing the game name, description and ruleset @@ -22,6 +26,9 @@ class ChooseGameView extends StatefulWidget { /// The id of the initially selected game final String initialGameId; + /// Optional callback invoked when the games are updated + final VoidCallback? onGamesUpdated; + @override State createState() => _ChooseGameViewState(); } @@ -33,9 +40,16 @@ class _ChooseGameViewState extends State { /// Currently selected game index late String selectedGameId; + /// Games filtered according to the current search query + late List filteredGames; + @override void initState() { selectedGameId = widget.initialGameId; + + // Start with all games visible + filteredGames = List.from(widget.games); + super.initState(); } @@ -58,6 +72,30 @@ class _ChooseGameViewState extends State { ); }, ), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () async { + final result = await Navigator.push( + context, + adaptivePageRoute( + builder: (context) => CreateGameView( + onGameChanged: () { + widget.onGamesUpdated?.call(); + }, + ), + ), + ); + if (result != null && result.game != null) { + setState(() { + widget.games.insert(0, result.game); + }); + _refreshFromSource(); + } + }, + ), + ], + title: Text(loc.choose_game), ), body: PopScope( @@ -77,30 +115,63 @@ class _ChooseGameViewState extends State { child: CustomSearchBar( controller: searchBarController, hintText: loc.game_name, + onChanged: (value) { + _applySearchFilter(value); + }, ), ), const SizedBox(height: 5), Expanded( child: ListView.builder( - itemCount: widget.games.length, + itemCount: filteredGames.length, itemBuilder: (BuildContext context, int index) { + final game = filteredGames[index]; return TitleDescriptionListTile( - title: widget.games[index].name, - description: widget.games[index].description, - badgeText: translateRulesetToString( - widget.games[index].ruleset, - context, - ), - isHighlighted: selectedGameId == widget.games[index].id, - onPressed: () async { + title: game.name, + description: game.description, + badgeText: translateRulesetToString(game.ruleset, context), + isHighlighted: selectedGameId == game.id, + onTap: () async { setState(() { - if (selectedGameId != widget.games[index].id) { - selectedGameId = widget.games[index].id; - } else { + if (selectedGameId == game.id) { selectedGameId = ''; + } else { + selectedGameId = game.id; } }); }, + onLongPress: () async { + final result = await Navigator.push( + context, + adaptivePageRoute( + builder: (context) => CreateGameView( + gameToEdit: 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(() { + widget.games.removeAt(originalIndex); + }); + } else { + setState(() { + widget.games[originalIndex] = result.game; + }); + } + _refreshFromSource(); + } + }, ); }, ), @@ -110,4 +181,28 @@ class _ChooseGameViewState extends State { ), ); } + + /// Applies the search filter to the games list based on [query]. + void _applySearchFilter(String query) { + final q = query.toLowerCase().trim(); + if (q.isEmpty) { + setState(() { + filteredGames = List.from(widget.games); + }); + return; + } + + setState(() { + filteredGames = widget.games.where((game) { + final name = game.name.toLowerCase(); + final description = game.description.toLowerCase(); + return name.contains(q) || description.contains(q); + }).toList(); + }); + } + + /// Re-applies the current filter after the underlying games list changed. + void _refreshFromSource() { + _applySearchFilter(searchBarController.text); + } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart new file mode 100644 index 0000000..bf764ad --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; + +class ChooseColorView extends StatefulWidget { + /// A view that allows the user to choose a color from a list of available game colors + /// - [initialColor]: The initially selected color + const ChooseColorView({super.key, this.initialColor}); + + /// The initially selected color + final GameColor? initialColor; + + @override + State createState() => _ChooseColorViewState(); +} + +class _ChooseColorViewState extends State { + /// Currently selected color + GameColor? selectedColor; + + @override + void initState() { + selectedColor = widget.initialColor; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + const colors = GameColor.values; + + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () { + Navigator.of(context).pop(selectedColor); + }, + ), + title: Text(loc.choose_color), + ), + body: PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) { + if (didPop) return; + Navigator.of(context).pop(selectedColor); + }, + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: colors.length, + itemBuilder: (BuildContext context, int index) { + final color = colors[index]; + return TitleDescriptionListTile( + onTap: () { + setState(() { + if (selectedColor == color) { + selectedColor = null; + } else { + selectedColor = color; + } + }); + }, + title: translateGameColorToString(color, context), + description: '', + isHighlighted: selectedColor == color, + badgeText: ' ', //Breite für Color Badge + badgeColor: getColorFromGameColor(color), + ); + }, + ), + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart new file mode 100644 index 0000000..6b69b22 --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; + +class ChooseRulesetView extends StatefulWidget { + /// A view that allows the user to choose a ruleset from a list of available rulesets + /// - [rulesets]: A list of tuples containing the ruleset and its description + /// - [initialRulesetIndex]: The index of the initially selected ruleset + const ChooseRulesetView({ + super.key, + required this.rulesets, + required this.initialRulesetIndex, + }); + + /// A list of tuples containing the ruleset and its description + final List<(Ruleset, String)> rulesets; + + /// The index of the initially selected ruleset + final int initialRulesetIndex; + @override + State createState() => _ChooseRulesetViewState(); +} + +class _ChooseRulesetViewState extends State { + /// Currently selected ruleset index + late int selectedRulesetIndex; + + @override + void initState() { + selectedRulesetIndex = widget.initialRulesetIndex; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return DefaultTabController( + length: 2, + initialIndex: 0, + child: Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () { + Navigator.of(context).pop( + selectedRulesetIndex == -1 + ? null + : widget.rulesets[selectedRulesetIndex].$1, + ); + }, + ), + title: Text(loc.choose_ruleset), + ), + body: PopScope( + // This fixes that the Android Back Gesture didn't return the + // selectedRulesetIndex and therefore the selected Ruleset wasn't saved + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) { + if (didPop) { + return; + } + Navigator.of(context).pop( + selectedRulesetIndex == -1 + ? null + : widget.rulesets[selectedRulesetIndex].$1, + ); + }, + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: widget.rulesets.length, + itemBuilder: (BuildContext context, int index) { + return TitleDescriptionListTile( + onTap: () async { + setState(() { + if (selectedRulesetIndex == index) { + selectedRulesetIndex = -1; + } else { + selectedRulesetIndex = index; + } + }); + }, + title: translateRulesetToString( + widget.rulesets[index].$1, + context, + ), + description: widget.rulesets[index].$2, + isHighlighted: selectedRulesetIndex == index, + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart new file mode 100644 index 0000000..2907720 --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -0,0 +1,352 @@ +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/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/views/main_menu/match_view/create_match/create_game/choose_color_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.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, + this.gameToEdit, + required this.onGameChanged, + }); + + /// An optional game to prefill the fields + final Game? gameToEdit; + + /// Callback to invoke when the game is created or edited + final VoidCallback onGameChanged; + + @override + State createState() => _CreateGameViewState(); +} + +class _CreateGameViewState extends State { + /// GlobalKey for ScaffoldMessenger to show snackbars + final _scaffoldMessengerKey = GlobalKey(); + + /// The database instance for accessing game data. + late final AppDatabase db; + + /// The currently selected ruleset for the game. + Ruleset? selectedRuleset; + + /// The index of the currently selected ruleset. + int selectedRulesetIndex = -1; + + /// A list of available rulesets and their localized names. + late List<(Ruleset, String)> _rulesets; + + /// The currently selected color for the game. + GameColor? selectedColor; + + /// Controller for the game name input field. + final _gameNameController = TextEditingController(); + + /// Controller for the game description input field. + final _descriptionController = TextEditingController(); + + /// The ID of the currently selected group. + late String selectedGroupId; + + /// A controller for the search bar input field. + final TextEditingController controller = TextEditingController(); + + /// A list of groups filtered based on the search query. + late final List filteredGroups; + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + _gameNameController.addListener(() => setState(() {})); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _rulesets = [ + ( + Ruleset.singleWinner, + translateRulesetToString(Ruleset.singleWinner, context), + ), + ( + Ruleset.singleLoser, + translateRulesetToString(Ruleset.singleLoser, context), + ), + ( + Ruleset.highestScore, + translateRulesetToString(Ruleset.highestScore, context), + ), + ( + Ruleset.lowestScore, + translateRulesetToString(Ruleset.lowestScore, context), + ), + ( + Ruleset.multipleWinners, + translateRulesetToString(Ruleset.multipleWinners, context), + ), + ]; + + if (widget.gameToEdit != null) { + _gameNameController.text = widget.gameToEdit!.name; + _descriptionController.text = widget.gameToEdit!.description; + selectedRuleset = widget.gameToEdit!.ruleset; + selectedColor = widget.gameToEdit!.color; + + selectedRulesetIndex = _rulesets.indexWhere( + (r) => r.$1 == selectedRuleset, + ); + } + } + + @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: widget.gameToEdit == null + ? [] + : [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + if (widget.gameToEdit != null) { + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: loc.delete_game, + content: Text(loc.this_cannot_be_undone), + actions: [ + CustomDialogAction( + onPressed: () => + Navigator.of(context).pop(false), + text: loc.cancel, + ), + CustomDialogAction( + onPressed: () => + Navigator.of(context).pop(true), + text: loc.delete, + ), + ], + ), + ).then((confirmed) async { + if (confirmed == true && context.mounted) { + 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: [ + Container( + margin: CustomTheme.tileMargin, + child: TextInputField( + controller: _gameNameController, + maxLength: Constants.MAX_MATCH_NAME_LENGTH, + hintText: loc.game_name, + ), + ), + ChooseTile( + title: loc.ruleset, + trailingText: selectedRuleset == null + ? loc.none + : translateRulesetToString(selectedRuleset!, context), + onPressed: () async { + final result = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => ChooseRulesetView( + rulesets: _rulesets, + initialRulesetIndex: selectedRulesetIndex, + ), + ), + ); + if (mounted) { + setState(() { + selectedRuleset = result; + selectedRulesetIndex = result == null + ? -1 + : _rulesets.indexWhere((r) => r.$1 == result); + }); + } + }, + ), + ChooseTile( + title: loc.color, + trailingText: selectedColor == null + ? loc.none + : translateGameColorToString(selectedColor!, context), + onPressed: () async { + final result = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => + ChooseColorView(initialColor: selectedColor), + ), + ); + if (mounted) { + setState(() { + selectedColor = result; + }); + } + }, + ), + 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(), + 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 && + selectedRulesetIndex != -1 && + selectedColor != null + ? () async { + Game newGame = Game( + name: _gameNameController.text.trim(), + description: _descriptionController.text.trim(), + ruleset: selectedRuleset!, + color: selectedColor!, + ); + if (isEditing) { + await handleGameUpdate(newGame); + } else { + await handleGameCreation(newGame); + } + widget.onGameChanged.call(); + if (context.mounted) { + Navigator.of( + context, + ).pop((game: newGame, delete: false)); + } + } + : null, + ), + ), + ], + ), + ), + ), + ); + } + + /// Handles updating an existing game in the database. + /// + /// [newGame] The updated game object. + Future handleGameUpdate(Game newGame) async { + final oldGame = widget.gameToEdit!; + + if (oldGame.name != newGame.name) { + await db.gameDao.updateGameName( + gameId: oldGame.id, + newName: newGame.name, + ); + } + + if (oldGame.description != newGame.description) { + await db.gameDao.updateGameDescription( + gameId: oldGame.id, + newDescription: newGame.description, + ); + } + + if (oldGame.ruleset != newGame.ruleset) { + await db.gameDao.updateGameRuleset( + gameId: oldGame.id, + newRuleset: newGame.ruleset, + ); + } + + if (oldGame.color != newGame.color) { + await db.gameDao.updateGameColor( + gameId: oldGame.id, + newColor: newGame.color, + ); + } + + if (oldGame.icon != newGame.icon) { + await db.gameDao.updateGameIcon( + gameId: oldGame.id, + newIcon: newGame.icon, + ); + } + } + + /// Handles creating a new game in the database. + /// + /// [newGame] The game object to be created. + Future handleGameCreation(Game newGame) async { + await db.gameDao.addGame(game: newGame); + } + + /// Displays a snackbar with the given message and optional action. + /// + /// [message] The message to display in the snackbar. + void showSnackbar({required String message}) { + final messenger = _scaffoldMessengerKey.currentState; + if (messenger != null) { + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message, style: const TextStyle(color: Colors.white)), + backgroundColor: CustomTheme.boxColor, + ), + ); + } + } +} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 1a04c78..b042ebb 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -115,6 +115,7 @@ class _CreateMatchViewState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ + // Match name input field. Container( margin: CustomTheme.tileMargin, child: TextInputField( @@ -123,6 +124,8 @@ class _CreateMatchViewState extends State { maxLength: Constants.MAX_MATCH_NAME_LENGTH, ), ), + + // Game selection tile. ChooseTile( title: loc.game, trailingText: selectedGame == null @@ -146,6 +149,8 @@ class _CreateMatchViewState extends State { }); }, ), + + // Group selection tile. ChooseTile( title: loc.group, trailingText: selectedGroup == null @@ -181,6 +186,8 @@ class _CreateMatchViewState extends State { }); }, ), + + // Player selection widget. Expanded( child: PlayerSelection( key: ValueKey(selectedGroup?.id ?? 'no_group'), @@ -193,6 +200,8 @@ class _CreateMatchViewState extends State { }, ), ), + + // Create or save button. CustomWidthButton( text: buttonText, sizeRelativeToWidth: 0.95, @@ -218,16 +227,16 @@ class _CreateMatchViewState extends State { /// /// Returns `true` if: /// - A ruleset is selected AND - /// - Either a group is selected OR at least 2 players are selected + /// - Either a group is selected OR at least 2 players are selected. bool _enableCreateGameButton() { return (selectedGroup != null || (selectedPlayers.length > 1) && selectedGame != null); } - // If a match was provided to the view, it updates the match in the database - // and navigates back to the previous screen. - // If no match was provided, it creates a new match in the database and - // navigates to the MatchResultView for the newly created match. + /// Handles navigation when the create or save button is pressed. + /// + /// If a match is being edited, updates the match in the database. + /// Otherwise, creates a new match and navigates to the MatchResultView. void buttonNavigation(BuildContext context) async { if (isEditMode()) { await updateMatch(); @@ -252,8 +261,7 @@ class _CreateMatchViewState extends State { } } - /// Updates attributes of the existing match in the database based on the - /// changes made in the edit view. + /// Updates the existing match in the database. Future updateMatch() async { final updatedMatch = Match( id: widget.matchToEdit!.id, @@ -262,7 +270,7 @@ class _CreateMatchViewState extends State { : _matchNameController.text.trim(), group: selectedGroup, players: selectedPlayers, - game: widget.matchToEdit!.game, + game: selectedGame!, createdAt: widget.matchToEdit!.createdAt, endedAt: widget.matchToEdit!.endedAt, notes: widget.matchToEdit!.notes, @@ -282,6 +290,13 @@ class _CreateMatchViewState extends State { ); } + if (widget.matchToEdit!.game.id != updatedMatch.game.id) { + await db.matchDao.updateMatchGame( + matchId: widget.matchToEdit!.id, + gameId: updatedMatch.game.id, + ); + } + // Add players who are in updatedMatch but not in the original match for (var player in updatedMatch.players) { if (!widget.matchToEdit!.players.any((p) => p.id == player.id)) { diff --git a/lib/presentation/widgets/text_input/text_input_field.dart b/lib/presentation/widgets/text_input/text_input_field.dart index 541ae6f..b074638 100644 --- a/lib/presentation/widgets/text_input/text_input_field.dart +++ b/lib/presentation/widgets/text_input/text_input_field.dart @@ -8,12 +8,18 @@ class TextInputField extends StatelessWidget { /// - [onChanged]: Optional callback invoked when the text in the field changes. /// - [hintText]: The hint text displayed in the text input field when it is empty /// - [maxLength]: Optional parameter for maximum length of the input text. + /// - [maxLines]: The maximum number of lines for the text input field. Defaults to 1. + /// - [minLines]: The minimum number of lines for the text input field. Defaults to 1. + /// - [showCounterText]: Whether to show the counter text in the text input field. Defaults to false. const TextInputField({ super.key, required this.controller, required this.hintText, this.onChanged, this.maxLength, + this.maxLines = 1, + this.minLines = 1, + this.showCounterText = false, }); /// The controller for the text input field. @@ -28,6 +34,15 @@ class TextInputField extends StatelessWidget { /// Optional parameter for maximum length of the input text. final int? maxLength; + /// The maximum number of lines for the text input field. + final int? maxLines; + + /// The minimum number of lines for the text input field. + final int? minLines; + + /// Whether to show the counter text in the text input field. + final bool showCounterText; + @override Widget build(BuildContext context) { return TextField( @@ -35,13 +50,15 @@ class TextInputField extends StatelessWidget { onChanged: onChanged, maxLength: maxLength, maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, + maxLines: maxLines, + minLines: minLines, + decoration: InputDecoration( filled: true, fillColor: CustomTheme.boxColor, hintText: hintText, hintStyle: const TextStyle(fontSize: 18), - // Hides the character counter - counterText: '', + counterText: showCounterText ? null : '', enabledBorder: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), borderSide: BorderSide(color: CustomTheme.boxBorderColor), diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart index 9dc8f33..95163f2 100644 --- a/lib/presentation/widgets/tiles/title_description_list_tile.dart +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -5,7 +5,8 @@ class TitleDescriptionListTile 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. - /// - [onPressed]: The callback invoked when the tile is tapped. + /// - [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. @@ -13,7 +14,8 @@ class TitleDescriptionListTile extends StatelessWidget { super.key, required this.title, required this.description, - this.onPressed, + this.onTap, + this.onLongPress, this.isHighlighted = false, this.badgeText, this.badgeColor, @@ -26,7 +28,10 @@ class TitleDescriptionListTile extends StatelessWidget { final String description; /// The callback invoked when the tile is tapped. - final VoidCallback? onPressed; + 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; @@ -40,7 +45,8 @@ class TitleDescriptionListTile extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: onPressed, + onTap: onTap, + onLongPress: onLongPress, child: AnimatedContainer( margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), From c64fd0c9b4d70fcce74f5ff46b805d392c949b3d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 15:33:31 +0200 Subject: [PATCH 02/33] Added game label --- lib/core/common.dart | 2 +- .../match_view/match_detail_view.dart | 35 +++++++++ lib/presentation/widgets/game_label.dart | 71 +++++++++++++++++++ .../widgets/tiles/match_tile.dart | 56 ++------------- 4 files changed, 114 insertions(+), 50 deletions(-) create mode 100644 lib/presentation/widgets/game_label.dart diff --git a/lib/core/common.dart b/lib/core/common.dart index 4c02350..2db8d19 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -54,7 +54,7 @@ Color getColorFromGameColor(GameColor color) { case GameColor.green: return Colors.green; case GameColor.yellow: - return Colors.yellow; + return const Color(0xFFF7CA28); case GameColor.purple: return Colors.purple; case GameColor.orange: diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 2117b77..a0f8760 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.dart'; @@ -14,6 +15,7 @@ import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; +import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -102,6 +104,7 @@ class _MatchDetailViewState extends State { bottom: 100, ), children: [ + // Controller Icon const Center( child: ColoredIconContainer( icon: Icons.sports_esports, @@ -110,6 +113,8 @@ class _MatchDetailViewState extends State { ), ), const SizedBox(height: 10), + + // Match Name Text( match.name, style: const TextStyle( @@ -120,6 +125,8 @@ class _MatchDetailViewState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 5), + + // Creation Date Text( '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}', style: const TextStyle( @@ -129,6 +136,8 @@ class _MatchDetailViewState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 10), + + // Group Name if (match.group != null) ...[ Row( mainAxisAlignment: MainAxisAlignment.center, @@ -143,6 +152,8 @@ class _MatchDetailViewState extends State { ), const SizedBox(height: 20), ], + + // Players InfoTile( title: loc.players, icon: Icons.people, @@ -162,6 +173,30 @@ class _MatchDetailViewState extends State { ), ), const SizedBox(height: 15), + + // Game + InfoTile( + title: loc.game, + icon: RpgAwesome.clovers_card, + horizontalAlignment: CrossAxisAlignment.start, + content: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ), + child: GameLabel( + title: match.game.name, + description: translateRulesetToString( + match.game.ruleset, + context, + ), + color: match.game.color, + ), + ), + ), + const SizedBox(height: 15), + + // Results InfoTile( title: loc.results, icon: Icons.emoji_events, diff --git a/lib/presentation/widgets/game_label.dart b/lib/presentation/widgets/game_label.dart new file mode 100644 index 0000000..553e637 --- /dev/null +++ b/lib/presentation/widgets/game_label.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/enums.dart'; + +class GameLabel extends StatelessWidget { + const GameLabel({ + super.key, + required this.title, + required this.description, + required this.color, + }); + + final String title; + final String description; + final GameColor color; + + @override + Widget build(BuildContext context) { + final backgroundColor = getColorFromGameColor(color); + final fontColor = backgroundColor.computeLuminance() > 0.5 + ? Colors.black + : Colors.white; + + return IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Title + Container( + decoration: BoxDecoration( + color: backgroundColor.withAlpha(230), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Text( + title, + style: TextStyle( + fontSize: 12, + color: fontColor, + fontWeight: FontWeight.bold, + ), + ), + ), + + // Description + Container( + decoration: BoxDecoration( + color: backgroundColor.withAlpha(140), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Text( + description, + style: TextStyle( + fontSize: 12, + color: fontColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index f7585d6..f939601 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -7,6 +7,7 @@ import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; class MatchTile extends StatefulWidget { @@ -116,56 +117,13 @@ class _MatchTileState extends State { // Game + Ruleset Badge if (!widget.compact) - IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // Game - Container( - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(230), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - bottomLeft: Radius.circular(8), - ), - ), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ), - child: Text( - match.game.name, - style: const TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - // Ruleset - Container( - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(140), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(8), - bottomRight: Radius.circular(8), - ), - ), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ), - child: Text( - translateRulesetToString(match.game.ruleset, context), - style: const TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ], + GameLabel( + title: match.game.name, + description: translateRulesetToString( + match.game.ruleset, + context, ), + color: match.game.color, ), const SizedBox(height: 12), From b664bcacda19ab81deaf813561d8ac5ba4aade86 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 16:02:49 +0200 Subject: [PATCH 03/33] Created seperate game tile and simplified title description list tile --- lib/presentation/widgets/tiles/game_tile.dart | 151 ++++++++++++++++++ .../tiles/title_description_list_tile.dart | 77 ++------- 2 files changed, 168 insertions(+), 60 deletions(-) create mode 100644 lib/presentation/widgets/tiles/game_tile.dart diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart new file mode 100644 index 0000000..1d494b9 --- /dev/null +++ b/lib/presentation/widgets/tiles/game_tile.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; + +class GameTile extends StatelessWidget { + /// A list tile widget that displays a title and description, with optional highlighting and badge. + /// - [title]: The title text displayed on the tile. + /// - [description]: The description text displayed below the title. + /// - [onTap]: The callback invoked when the tile is tapped. + /// - [onLongPress]: The callback invoked when the tile is tapped. + /// - [isHighlighted]: A boolean to determine if the tile should be highlighted. + /// - [badgeText]: Optional text to display in a badge on the right side of the title. + /// - [badgeColor]: Optional color for the badge background. + const GameTile({ + super.key, + required this.title, + required this.description, + this.onTap, + this.onLongPress, + this.isHighlighted = false, + this.badgeText, + this.badgeColor, + }); + + /// The title text displayed on the tile. + final String title; + + /// The description text displayed below the title. + final String description; + + /// The callback invoked when the tile is tapped. + final VoidCallback? onTap; + + /// The callback invoked when the tile is long-pressed. + final VoidCallback? onLongPress; + + /// A boolean to determine if the tile should be highlighted. + final bool isHighlighted; + + /// Optional text to display in a badge on the right side of the title. + final String? badgeText; + + /// Optional color for the badge background. + final Color? badgeColor; + + @override + Widget build(BuildContext context) { + final badgeTextColor = badgeColor != null + ? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white) + : Colors.white; + + final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange); + + return GestureDetector( + onTap: onTap, + onLongPress: onLongPress, + child: AnimatedContainer( + margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + decoration: !isHighlighted + ? CustomTheme.standardBoxDecoration + : CustomTheme.highlightedBoxDecoration.copyWith( + border: Border.all( + color: gameColor.withValues(alpha: 0.9), + width: 2, + ), + ), + duration: const Duration(milliseconds: 200), + child: Stack( + children: [ + // Gradient overlay + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + gameColor.withValues(alpha: 0.08), + gameColor.withValues(alpha: 0.02), + Colors.transparent, + ], + stops: const [0.0, 0.5, 1.0], + ), + ), + ), + ), + + // Content + Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // Title + Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + + // Badge + if (badgeText != null) ...[ + const SizedBox(height: 5), + Container( + constraints: const BoxConstraints(maxWidth: 250), + padding: const EdgeInsets.symmetric( + vertical: 2, + horizontal: 6, + ), + decoration: BoxDecoration( + color: gameColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + badgeText!, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: TextStyle( + color: badgeTextColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + + // Description + if (description.isNotEmpty) ...[ + const SizedBox(height: 10), + Text(description, style: const TextStyle(fontSize: 14)), + const SizedBox(height: 2.5), + ], + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart index 95163f2..bf45c1e 100644 --- a/lib/presentation/widgets/tiles/title_description_list_tile.dart +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -2,23 +2,17 @@ import 'package:flutter/material.dart'; import 'package:tallee/core/custom_theme.dart'; class TitleDescriptionListTile extends StatelessWidget { - /// A list tile widget that displays a title and description, with optional highlighting and badge. + /// A list tile widget that displays a title and description /// - [title]: The title text displayed on the tile. /// - [description]: The description text displayed below the title. /// - [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 TitleDescriptionListTile({ 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. @@ -30,23 +24,13 @@ class TitleDescriptionListTile extends StatelessWidget { /// 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) { return GestureDetector( onTap: onTap, - onLongPress: onLongPress, child: AnimatedContainer( margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), @@ -57,53 +41,26 @@ class TitleDescriptionListTile extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 230, - child: Text( - title, - overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), + // Title + SizedBox( + width: 230, + child: Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, ), - if (badgeText != null) ...[ - const Spacer(), - Container( - constraints: const BoxConstraints(maxWidth: 115), - padding: const EdgeInsets.symmetric( - vertical: 2, - horizontal: 6, - ), - decoration: BoxDecoration( - color: badgeColor ?? CustomTheme.primaryColor, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - badgeText!, - overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ], + ), ), + + // Description if (description.isNotEmpty) ...[ - const SizedBox(height: 5), + const SizedBox(height: 10), Text(description, style: const TextStyle(fontSize: 14)), const SizedBox(height: 2.5), ], From e895359dac3c3441ef3f82222edd06b90f0833a1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 16:02:59 +0200 Subject: [PATCH 04/33] Updated highlighting --- lib/core/custom_theme.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index 3274db9..b32ce63 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -63,9 +63,8 @@ class CustomTheme { static BoxDecoration highlightedBoxDecoration = BoxDecoration( color: boxColor, - border: Border.all(color: primaryColor), + border: Border.all(color: textColor, width: 2), borderRadius: standardBorderRadiusAll, - boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)], ); // ==================== Component Themes ==================== From 633a0599eba257c5e010a70908954084c9f44668 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 16:04:20 +0200 Subject: [PATCH 05/33] Implemented game tile in choose game view --- lib/core/common.dart | 8 +++++--- .../match_view/create_match/choose_game_view.dart | 8 ++++++-- .../create_match/create_game/choose_color_view.dart | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 2db8d19..14d90aa 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -58,7 +58,7 @@ Color getColorFromGameColor(GameColor color) { case GameColor.purple: return Colors.purple; case GameColor.orange: - return Colors.orange; + return const Color(0xFFef681f); case GameColor.pink: return Colors.pink; case GameColor.teal: @@ -66,8 +66,10 @@ Color getColorFromGameColor(GameColor color) { } } -/// Counts how many players in the match are not part of the group -/// Returns the count as a string, or an empty string if there is no group +/// Counts how many players in the [match] are not part of the group +/// +/// Returns the text you append after the group name, e.g. " + 5" or an empty +/// string if there are no extra players String getExtraPlayerCount(Match match) { int count = 0; diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 8f3e06e..42d5253 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -6,7 +6,7 @@ import 'package:tallee/data/models/game.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; -import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; class ChooseGameView extends StatefulWidget { /// A view that allows the user to choose a game from a list of available games @@ -110,6 +110,7 @@ class _ChooseGameViewState extends State { }, child: Column( children: [ + // Search Bar Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: CustomSearchBar( @@ -121,15 +122,18 @@ class _ChooseGameViewState extends State { ), ), const SizedBox(height: 5), + + // Game list Expanded( child: ListView.builder( itemCount: filteredGames.length, itemBuilder: (BuildContext context, int index) { final game = filteredGames[index]; - return TitleDescriptionListTile( + return GameTile( title: game.name, description: game.description, badgeText: translateRulesetToString(game.ruleset, context), + badgeColor: getColorFromGameColor(game.color), isHighlighted: selectedGameId == game.id, onTap: () async { setState(() { diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart index bf764ad..e6d0b7e 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart @@ -3,7 +3,7 @@ import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; class ChooseColorView extends StatefulWidget { /// A view that allows the user to choose a color from a list of available game colors @@ -54,7 +54,7 @@ class _ChooseColorViewState extends State { itemCount: colors.length, itemBuilder: (BuildContext context, int index) { final color = colors[index]; - return TitleDescriptionListTile( + return GameTile( onTap: () { setState(() { if (selectedColor == color) { From 2e1314ccd40b081fc17ed66e5f836240c75de9a9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 16:11:15 +0200 Subject: [PATCH 06/33] Implemented consistency changes --- .../create_game/create_game_view.dart | 61 ++++++++++++------- .../create_match/create_match_view.dart | 45 +++++++------- 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart index 2907720..c27ecac 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -183,6 +183,7 @@ class _CreateGameViewState extends State { body: SafeArea( child: Column( children: [ + // Game name input field Container( margin: CustomTheme.tileMargin, child: TextInputField( @@ -191,30 +192,35 @@ class _CreateGameViewState extends State { hintText: loc.game_name, ), ), - ChooseTile( - title: loc.ruleset, - trailingText: selectedRuleset == null - ? loc.none - : translateRulesetToString(selectedRuleset!, context), - onPressed: () async { - final result = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseRulesetView( - rulesets: _rulesets, - initialRulesetIndex: selectedRulesetIndex, + + // Choose ruleset tile + if (!isEditMode()) + ChooseTile( + title: loc.ruleset, + trailingText: selectedRuleset == null + ? loc.none + : translateRulesetToString(selectedRuleset!, context), + onPressed: () async { + final result = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => ChooseRulesetView( + rulesets: _rulesets, + initialRulesetIndex: selectedRulesetIndex, + ), ), - ), - ); - if (mounted) { - setState(() { - selectedRuleset = result; - selectedRulesetIndex = result == null - ? -1 - : _rulesets.indexWhere((r) => r.$1 == result); - }); - } - }, - ), + ); + if (mounted) { + setState(() { + selectedRuleset = result; + selectedRulesetIndex = result == null + ? -1 + : _rulesets.indexWhere((r) => r.$1 == result); + }); + } + }, + ), + + // Choose color tile ChooseTile( title: loc.color, trailingText: selectedColor == null @@ -234,6 +240,8 @@ class _CreateGameViewState extends State { } }, ), + + // Description input field Container( margin: CustomTheme.tileMargin, child: TextInputField( @@ -245,7 +253,10 @@ class _CreateGameViewState extends State { showCounterText: true, ), ), + const Spacer(), + + // Create/Edit game button Padding( padding: const EdgeInsets.all(12.0), child: CustomWidthButton( @@ -349,4 +360,8 @@ class _CreateGameViewState extends State { ); } } + + bool isEditMode() { + return widget.gameToEdit != null; + } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index b042ebb..945e68a 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -126,29 +126,30 @@ class _CreateMatchViewState extends State { ), // Game selection tile. - ChooseTile( - title: loc.game, - trailingText: selectedGame == null - ? loc.none_group - : selectedGame!.name, - onPressed: () async { - selectedGame = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseGameView( - games: gamesList, - initialGameId: selectedGame?.id ?? '', + if (!isEditMode()) + ChooseTile( + title: loc.game, + trailingText: selectedGame == null + ? loc.none_group + : selectedGame!.name, + onPressed: () async { + selectedGame = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => ChooseGameView( + games: gamesList, + initialGameId: selectedGame?.id ?? '', + ), ), - ), - ); - setState(() { - if (selectedGame != null) { - hintText = selectedGame!.name; - } else { - hintText = loc.match_name; - } - }); - }, - ), + ); + setState(() { + if (selectedGame != null) { + hintText = selectedGame!.name; + } else { + hintText = loc.match_name; + } + }); + }, + ), // Group selection tile. ChooseTile( From 92bf74683f4cdeaafcc514f8ad94959b3ca3e479 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 16:32:25 +0200 Subject: [PATCH 07/33] feat: games with match associations cant be deleted --- lib/data/dao/game_dao.dart | 21 ++++++++++++++ .../create_match/choose_game_view.dart | 22 ++++++++++++++ .../create_game/create_game_view.dart | 29 ++++++++++--------- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index f07e2c7..98ac6c3 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -176,4 +176,25 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { final rowsAffected = await query.go(); return rowsAffected > 0; } + + /// Retrieves all games with their respective match counts. + /// Returns a list of tuples (Game, matchCount). + Future> getGameUsage() async { + final games = await getAllGames(); + + final results = <(Game, int)>[]; + + for (final game in games) { + final matchCount = + await (selectOnly(db.matchTable) + ..where(db.matchTable.gameId.equals(game.id)) + ..addColumns([db.matchTable.id.count()])) + .map((row) => row.read(db.matchTable.id.count())) + .getSingle(); + + results.add((game, matchCount ?? 0)); + } + + return results; + } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 42d5253..fca65bb 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart'; @@ -34,6 +36,10 @@ class ChooseGameView extends StatefulWidget { } class _ChooseGameViewState extends State { + late final AppDatabase db; + + late List<(Game, int)> gameCounts = []; + /// Controller for the search bar final TextEditingController searchBarController = TextEditingController(); @@ -45,6 +51,9 @@ class _ChooseGameViewState extends State { @override void initState() { + db = Provider.of(context, listen: false); + fetchGameCounts(); + selectedGameId = widget.initialGameId; // Start with all games visible @@ -150,6 +159,7 @@ class _ChooseGameViewState extends State { adaptivePageRoute( builder: (context) => CreateGameView( gameToEdit: game, + canDelete: canDeleteGame(game), onGameChanged: () { widget.onGamesUpdated?.call(); }, @@ -209,4 +219,16 @@ class _ChooseGameViewState extends State { void _refreshFromSource() { _applySearchFilter(searchBarController.text); } + + Future fetchGameCounts() async { + gameCounts = await db.gameDao.getGameUsage(); + } + + // A game can only be deleted if there are no matches using it + bool canDeleteGame(Game game) { + final count = gameCounts + .firstWhere((gc) => gc.$1.id == game.id, orElse: () => (game, 0)) + .$2; + return count == 0; + } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart index c27ecac..ba4d101 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -23,15 +23,18 @@ import 'package:tallee/presentation/widgets/tiles/choose_tile.dart'; class CreateGameView extends StatefulWidget { const CreateGameView({ super.key, - this.gameToEdit, required this.onGameChanged, + this.gameToEdit, + this.canDelete = false, }); + /// Callback to invoke when the game is created or edited + final VoidCallback onGameChanged; + /// An optional game to prefill the fields final Game? gameToEdit; - /// Callback to invoke when the game is created or edited - final VoidCallback onGameChanged; + final bool canDelete; @override State createState() => _CreateGameViewState(); @@ -41,7 +44,6 @@ class _CreateGameViewState extends State { /// GlobalKey for ScaffoldMessenger to show snackbars final _scaffoldMessengerKey = GlobalKey(); - /// The database instance for accessing game data. late final AppDatabase db; /// The currently selected ruleset for the game. @@ -133,13 +135,12 @@ class _CreateGameViewState extends State { backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( title: Text(isEditing ? loc.edit_game : loc.create_game), - actions: widget.gameToEdit == null - ? [] - : [ - IconButton( - icon: const Icon(Icons.delete), - onPressed: () async { - if (widget.gameToEdit != null) { + actions: [ + if (isEditMode()) + IconButton( + icon: const Icon(Icons.delete), + onPressed: widget.canDelete + ? () async { showDialog( context: context, builder: (context) => CustomAlertDialog( @@ -176,9 +177,9 @@ class _CreateGameViewState extends State { } }); } - }, - ), - ], + : null, + ), + ], ), body: SafeArea( child: Column( From e3aef81ab67dbb17df95a43064e0776c0704d81c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 3 May 2026 01:00:44 +0200 Subject: [PATCH 08/33] feat: Deleting games associated with matches deletes them --- lib/data/dao/match_dao.dart | 19 ++++ lib/l10n/arb/app_de.arb | 8 ++ lib/l10n/arb/app_en.arb | 8 ++ lib/l10n/generated/app_localizations.dart | 6 ++ lib/l10n/generated/app_localizations_de.dart | 11 +++ lib/l10n/generated/app_localizations_en.dart | 11 +++ .../create_match/choose_game_view.dart | 9 +- .../create_game/create_game_view.dart | 94 +++++++++++-------- .../buttons/animated_dialog_button.dart | 65 ++++++++----- .../widgets/dialog/custom_dialog_action.dart | 4 + test/db_tests/aggregates/match_test.dart | 50 ++++++++++ 11 files changed, 218 insertions(+), 67 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 93df7d7..48098ee 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -299,6 +299,25 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return count ?? 0; } + /// Retrieves the number of matches associated with a specific game. + Future getMatchCountByGame({required String gameId}) async { + final count = + await (selectOnly(matchTable) + ..where(matchTable.gameId.equals(gameId)) + ..addColumns([matchTable.id.count()])) + .map((row) => row.read(matchTable.id.count())) + .getSingle(); + return count ?? 0; + } + + /// Deletes all matches associated with a specific game. + /// Returns the number of matches deleted. + Future deleteMatchesByGame({required String gameId}) async { + final query = delete(matchTable)..where((m) => m.gameId.equals(gameId)); + final rowsAffected = await query.go(); + return rowsAffected; + } + /// Retrieves all matches associated with the given [groupId]. /// Queries the database directly, filtering by [groupId]. Future> getGroupMatches({required String groupId}) async { diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index ba4fe38..e518525 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -34,6 +34,14 @@ "delete": "Löschen", "delete_all_data": "Alle Daten löschen", "delete_game": "Spielvorlage löschen", + "delete_game_with_matches_warning": "Wenn du diese Spielvorlage löschst, werden {count, plural, =1{1 Spiel} other{{count} Spiele}} mit dieser Spielvorlage ebenfalls gelöscht.", + "@delete_game_with_matches_warning": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "delete_group": "Gruppe löschen", "delete_match": "Spiel löschen", "description": "Beschreibung", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 98d1c38..c01f0b2 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -389,6 +389,14 @@ "delete": "Delete", "delete_all_data": "Delete all data", "delete_game": "Delete Game", + "delete_game_with_matches_warning": "If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.", + "@delete_game_with_matches_warning": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "delete_group": "Delete Group", "delete_match": "Delete Match", "description": "Description", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 8e44e7b..790597f 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -302,6 +302,12 @@ abstract class AppLocalizations { /// **'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); + /// Confirmation dialog for deleting a group /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 3c2b4e3..2b20848 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -114,6 +114,17 @@ class AppLocalizationsDe extends AppLocalizations { @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: '$count Spiele', + one: '1 Spiel', + ); + return 'Wenn du diese Spielvorlage löschst, werden $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; + } + @override String get delete_group => 'Gruppe löschen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index e14b7a0..323d8c8 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -114,6 +114,17 @@ class AppLocalizationsEn extends AppLocalizations { @override String get delete_game => 'Delete Game'; + @override + String delete_game_with_matches_warning(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count matches', + one: '1 match', + ); + return 'If you delete this game template, $_temp0 using this game template will also be deleted.'; + } + @override String get delete_group => 'Delete Group'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index fca65bb..ef92638 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -159,7 +159,7 @@ class _ChooseGameViewState extends State { adaptivePageRoute( builder: (context) => CreateGameView( gameToEdit: game, - canDelete: canDeleteGame(game), + matchCount: getMatchCount(game), onGameChanged: () { widget.onGamesUpdated?.call(); }, @@ -224,11 +224,10 @@ class _ChooseGameViewState extends State { gameCounts = await db.gameDao.getGameUsage(); } - // A game can only be deleted if there are no matches using it - bool canDeleteGame(Game game) { - final count = gameCounts + // 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; - return count == 0; } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart index ba4d101..52e6c14 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -25,7 +25,7 @@ class CreateGameView extends StatefulWidget { super.key, required this.onGameChanged, this.gameToEdit, - this.canDelete = false, + this.matchCount = 0, }); /// Callback to invoke when the game is created or edited @@ -34,7 +34,7 @@ class CreateGameView extends StatefulWidget { /// An optional game to prefill the fields final Game? gameToEdit; - final bool canDelete; + final int matchCount; @override State createState() => _CreateGameViewState(); @@ -139,45 +139,59 @@ class _CreateGameViewState extends State { if (isEditMode()) IconButton( icon: const Icon(Icons.delete), - onPressed: widget.canDelete - ? () async { - showDialog( - context: context, - builder: (context) => CustomAlertDialog( - title: loc.delete_game, - content: Text(loc.this_cannot_be_undone), - actions: [ - CustomDialogAction( - onPressed: () => - Navigator.of(context).pop(false), - text: loc.cancel, - ), - CustomDialogAction( - onPressed: () => - Navigator.of(context).pop(true), - text: loc.delete, - ), - ], - ), - ).then((confirmed) async { - if (confirmed == true && context.mounted) { - 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); - } - } - }); + onPressed: () async { + if (!context.mounted) return; + + // Build the dialog content based on match count + final String dialogContent = widget.matchCount > 0 + ? loc.delete_game_with_matches_warning(widget.matchCount) + : loc.this_cannot_be_undone; + + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: loc.delete_game, + content: Text(dialogContent), + 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, + ); } - : null, + + // 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); + } + } + }); + }, ), ], ), diff --git a/lib/presentation/widgets/buttons/animated_dialog_button.dart b/lib/presentation/widgets/buttons/animated_dialog_button.dart index 70deea6..8c8765e 100644 --- a/lib/presentation/widgets/buttons/animated_dialog_button.dart +++ b/lib/presentation/widgets/buttons/animated_dialog_button.dart @@ -14,6 +14,7 @@ class AnimatedDialogButton extends StatefulWidget { required this.onPressed, this.buttonConstraints, this.buttonType = ButtonType.primary, + this.isDescructive = false, }); final String buttonText; @@ -24,6 +25,8 @@ class AnimatedDialogButton extends StatefulWidget { final ButtonType buttonType; + final bool isDescructive; + @override State createState() => _AnimatedDialogButtonState(); } @@ -33,28 +36,8 @@ class _AnimatedDialogButtonState extends State { @override Widget build(BuildContext context) { - final textStyling = TextStyle( - color: widget.buttonType == ButtonType.primary - ? Colors.black - : Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ); - - final buttonDecoration = widget.buttonType == ButtonType.primary - // Primary - ? BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ) - : widget.buttonType == ButtonType.secondary - // Secondary - ? BoxDecoration( - border: BoxBorder.all(color: Colors.white, width: 2), - borderRadius: BorderRadius.circular(12), - ) - // Tertiary - : const BoxDecoration(); + final textStyling = _getTextStyling(); + final buttonDecoration = _getButtonDecoration(); return GestureDetector( onTapDown: (_) => setState(() => _isPressed = true), @@ -84,4 +67,42 @@ class _AnimatedDialogButtonState extends State { ), ); } + + TextStyle _getTextStyling() { + late Color textColor; + if (widget.buttonType == ButtonType.primary) { + textColor = widget.isDescructive ? Colors.white : Colors.black; + } else if (widget.buttonType == ButtonType.secondary) { + textColor = widget.isDescructive ? Colors.red : Colors.white; + } else { + textColor = widget.isDescructive ? Colors.red : Colors.white; + } + + return TextStyle( + color: textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ); + } + + BoxDecoration _getButtonDecoration() { + if (widget.buttonType == ButtonType.primary) { + // Primary + return BoxDecoration( + color: widget.isDescructive ? Colors.red : Colors.white, + borderRadius: BorderRadius.circular(12), + ); + } else if (widget.buttonType == ButtonType.secondary) { + // Secondary + return BoxDecoration( + border: BoxBorder.all( + color: widget.isDescructive ? Colors.red : Colors.white, + width: 2, + ), + borderRadius: BorderRadius.circular(12), + ); + } + // Tertiary + return const BoxDecoration(); + } } diff --git a/lib/presentation/widgets/dialog/custom_dialog_action.dart b/lib/presentation/widgets/dialog/custom_dialog_action.dart index aec0dfa..47024dc 100644 --- a/lib/presentation/widgets/dialog/custom_dialog_action.dart +++ b/lib/presentation/widgets/dialog/custom_dialog_action.dart @@ -12,6 +12,7 @@ class CustomDialogAction extends StatelessWidget { required this.onPressed, required this.text, this.buttonType = ButtonType.primary, + this.isDestructive = false, }); final String text; @@ -20,12 +21,15 @@ class CustomDialogAction extends StatelessWidget { final VoidCallback onPressed; + final bool isDestructive; + @override Widget build(BuildContext context) { return AnimatedDialogButton( onPressed: onPressed, buttonText: text, buttonType: buttonType, + isDescructive: isDestructive, buttonConstraints: const BoxConstraints(minWidth: 300), ); } diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 3305b9a..9ba33ac 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -366,5 +366,55 @@ void main() { expect(match.group, isNotNull); expect(match.group!.id, testGroup1.id); }); + + 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); + }); + + test('getMatchCountByGame() returns 0 for non-existent game', () async { + final count = await database.matchDao.getMatchCountByGame( + gameId: 'non-existent-game-id', + ); + expect(count, 0); + }); + + 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); + }); }); } From 5d832c98a79a5cf19cac72ac68d305c45bf2d807 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 3 May 2026 10:45:18 +0200 Subject: [PATCH 09/33] fix: callbacks when game deletes matches --- .../main_menu/match_view/create_match/choose_game_view.dart | 1 + .../match_view/create_match/create_match_view.dart | 4 ++++ lib/presentation/views/main_menu/match_view/match_view.dart | 6 ++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index ef92638..fc9e76c 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -177,6 +177,7 @@ class _ChooseGameViewState extends State { if (result.delete) { setState(() { widget.games.removeAt(originalIndex); + widget.onGamesUpdated?.call(); }); } else { setState(() { diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 945e68a..6f1bf95 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -28,10 +28,13 @@ class CreateMatchView extends StatefulWidget { this.onWinnerChanged, this.matchToEdit, this.onMatchUpdated, + this.onMatchesUpdated, }); final VoidCallback? onWinnerChanged; + final VoidCallback? onMatchesUpdated; + final void Function(Match)? onMatchUpdated; /// An optional match to prefill the fields for editing. @@ -138,6 +141,7 @@ class _CreateMatchViewState extends State { builder: (context) => ChooseGameView( games: gamesList, initialGameId: selectedGame?.id ?? '', + onGamesUpdated: widget.onMatchesUpdated, ), ), ); diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 2fb36e7..b7b9147 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -118,8 +118,10 @@ class _MatchViewState extends State { Navigator.push( context, adaptivePageRoute( - builder: (context) => - CreateMatchView(onWinnerChanged: loadMatches), + builder: (context) => CreateMatchView( + onWinnerChanged: loadMatches, + onMatchesUpdated: loadMatches, + ), ), ); }, From 6272794cc5347f961c108c7973324cb390dad13c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 3 May 2026 10:59:46 +0200 Subject: [PATCH 10/33] feat: showing game color in choose tile --- lib/data/models/game.dart | 10 +++--- .../create_game/create_game_view.dart | 31 ++++++++++++++----- .../create_match/create_match_view.dart | 12 +++---- .../widgets/tiles/choose_tile.dart | 8 ++--- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index 4888df4..1699fc0 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -14,15 +14,13 @@ class Game { Game({ required this.name, required this.ruleset, - required this.color, + this.color = GameColor.orange, + this.description = '', + this.icon = '', String? id, DateTime? createdAt, - String? description, - String? icon, }) : id = id ?? const Uuid().v4(), - createdAt = createdAt ?? clock.now(), - description = description ?? '', - icon = icon ?? ''; + createdAt = createdAt ?? clock.now(); @override String toString() { diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart index 52e6c14..7351b4d 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -56,7 +56,7 @@ class _CreateGameViewState extends State { late List<(Ruleset, String)> _rulesets; /// The currently selected color for the game. - GameColor? selectedColor; + GameColor? selectedColor = GameColor.orange; /// Controller for the game name input field. final _gameNameController = TextEditingController(); @@ -212,9 +212,11 @@ class _CreateGameViewState extends State { if (!isEditMode()) ChooseTile( title: loc.ruleset, - trailingText: selectedRuleset == null - ? loc.none - : translateRulesetToString(selectedRuleset!, context), + trailing: selectedRuleset == null + ? Text(loc.none) + : Text( + translateRulesetToString(selectedRuleset!, context), + ), onPressed: () async { final result = await Navigator.of(context).push( adaptivePageRoute( @@ -238,9 +240,24 @@ class _CreateGameViewState extends State { // Choose color tile ChooseTile( title: loc.color, - trailingText: selectedColor == null - ? loc.none - : translateGameColorToString(selectedColor!, context), + trailing: selectedColor == null + ? Text(loc.none) + : Row( + children: [ + Text( + translateGameColorToString(selectedColor!, context), + ), + Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(left: 12), + decoration: BoxDecoration( + color: getColorFromGameColor(selectedColor!), + shape: BoxShape.circle, + ), + ), + ], + ), onPressed: () async { final result = await Navigator.of(context).push( adaptivePageRoute( diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 6f1bf95..f0c0bf2 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -132,9 +132,9 @@ class _CreateMatchViewState extends State { if (!isEditMode()) ChooseTile( title: loc.game, - trailingText: selectedGame == null - ? loc.none_group - : selectedGame!.name, + trailing: selectedGame == null + ? Text(loc.none_group) + : Text(selectedGame!.name), onPressed: () async { selectedGame = await Navigator.of(context).push( adaptivePageRoute( @@ -158,9 +158,9 @@ class _CreateMatchViewState extends State { // Group selection tile. ChooseTile( title: loc.group, - trailingText: selectedGroup == null - ? loc.none_group - : selectedGroup!.name, + trailing: selectedGroup == null + ? Text(loc.none_group) + : Text(selectedGroup!.name), onPressed: () async { // Remove all players from the previously selected group from // the selected players list, in case the user deselects the diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index 10ded6b..234c663 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -4,12 +4,12 @@ import 'package:tallee/core/custom_theme.dart'; class ChooseTile extends StatefulWidget { /// A tile widget that allows users to choose an option by tapping on it. /// - [title]: The title text displayed on the tile. - /// - [trailingText]: Optional trailing text displayed on the tile. + /// - [trailing]: Optional trailing text displayed on the tile. /// - [onPressed]: The callback invoked when the tile is tapped. const ChooseTile({ super.key, required this.title, - this.trailingText, + this.trailing, this.onPressed, }); @@ -20,7 +20,7 @@ class ChooseTile extends StatefulWidget { final VoidCallback? onPressed; /// Optional trailing text displayed on the tile. - final String? trailingText; + final Widget? trailing; @override State createState() => _ChooseTileState(); @@ -42,7 +42,7 @@ class _ChooseTileState extends State { style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const Spacer(), - if (widget.trailingText != null) Text(widget.trailingText!), + if (widget.trailing != null) widget.trailing!, const SizedBox(width: 10), const Icon(Icons.arrow_forward_ios, size: 16), ], From 8c52db9981e066018d062c55360b18a9f8c3eeb3 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 10:43:43 +0200 Subject: [PATCH 11/33] Added popups to create game view --- .../create_game/create_game_view.dart | 240 ++++++++++++++---- .../widgets/tiles/choose_tile.dart | 9 +- pubspec.yaml | 1 + 3 files changed, 198 insertions(+), 52 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart index 7351b4d..0c3fde6 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_popup/flutter_popup.dart'; import 'package:provider/provider.dart'; -import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/constants.dart'; import 'package:tallee/core/custom_theme.dart'; @@ -9,8 +9,6 @@ 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/views/main_menu/match_view/create_match/create_game/choose_color_view.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.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'; @@ -55,6 +53,9 @@ class _CreateGameViewState extends State { /// A list of available rulesets and their localized names. late List<(Ruleset, String)> _rulesets; + /// A list of available game colors and their localized names. + late List<(GameColor, String)> _colors; + /// The currently selected color for the game. GameColor? selectedColor = GameColor.orange; @@ -105,6 +106,16 @@ class _CreateGameViewState extends State { 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; @@ -212,65 +223,192 @@ class _CreateGameViewState extends State { if (!isEditMode()) ChooseTile( title: loc.ruleset, - trailing: selectedRuleset == null - ? Text(loc.none) - : Text( - translateRulesetToString(selectedRuleset!, context), - ), - onPressed: () async { - final result = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseRulesetView( - rulesets: _rulesets, - initialRulesetIndex: selectedRulesetIndex, + trailing: 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: 250, + 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: [ + Text( + _rulesets[index].$2, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 15, + ), + ), + ], + ), + ), + ), + if (index < _rulesets.length - 1) + const Divider(indent: 15, endIndent: 15), + ], + ), + ), + ), ), ), - ); - if (mounted) { - setState(() { - selectedRuleset = result; - selectedRulesetIndex = result == null - ? -1 - : _rulesets.indexWhere((r) => r.$1 == result); - }); - } - }, + ), + child: selectedRuleset == null + ? Text(loc.none) + : Text( + translateRulesetToString(selectedRuleset!, context), + ), + ), ), // Choose color tile ChooseTile( title: loc.color, - trailing: selectedColor == null - ? Text(loc.none) - : Row( - children: [ - Text( - translateGameColorToString(selectedColor!, context), - ), - Container( - width: 16, - height: 16, - margin: const EdgeInsets.only(left: 12), - decoration: BoxDecoration( - color: getColorFromGameColor(selectedColor!), - shape: BoxShape.circle, + trailing: Row( + spacing: 8, + children: [ + // Selected Color + Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(left: 12), + decoration: BoxDecoration( + color: getColorFromGameColor(selectedColor!), + shape: BoxShape.circle, + ), + ), + + //Popup + 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: Text( + translateGameColorToString(selectedColor!, context), ), - onPressed: () async { - final result = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => - ChooseColorView(initialColor: selectedColor), ), - ); - if (mounted) { - setState(() { - selectedColor = result; - }); - } - }, + ], + ), ), // Description input field diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index 234c663..1f72328 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:tallee/core/custom_theme.dart'; @@ -44,7 +46,12 @@ class _ChooseTileState extends State { const Spacer(), if (widget.trailing != null) widget.trailing!, const SizedBox(width: 10), - const Icon(Icons.arrow_forward_ios, size: 16), + widget.onPressed == null + ? Transform.rotate( + angle: pi / 2, + child: const Icon(Icons.arrow_forward_ios, size: 16), + ) + : const Icon(Icons.arrow_forward_ios, size: 16), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 363ea7f..d9964fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + flutter_popup: ^3.3.9 fluttericon: ^2.0.0 font_awesome_flutter: ^11.0.0 intl: any From 94bb477cd9141da5ed6c3c626953492546b480b1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 10:44:40 +0200 Subject: [PATCH 12/33] Added popups to create game view replacing two screens --- .../create_match/choose_game_view.dart | 2 +- .../create_game/choose_color_view.dart | 78 --------------- .../create_game/choose_ruleset_view.dart | 99 ------------------- .../{create_game => }/create_game_view.dart | 0 4 files changed, 1 insertion(+), 178 deletions(-) delete mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart delete mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart rename lib/presentation/views/main_menu/match_view/create_match/{create_game => }/create_game_view.dart (100%) diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index fc9e76c..cdd73c2 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -6,7 +6,7 @@ import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game_view.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart deleted file mode 100644 index e6d0b7e..0000000 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tallee/core/common.dart'; -import 'package:tallee/core/custom_theme.dart'; -import 'package:tallee/core/enums.dart'; -import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; - -class ChooseColorView extends StatefulWidget { - /// A view that allows the user to choose a color from a list of available game colors - /// - [initialColor]: The initially selected color - const ChooseColorView({super.key, this.initialColor}); - - /// The initially selected color - final GameColor? initialColor; - - @override - State createState() => _ChooseColorViewState(); -} - -class _ChooseColorViewState extends State { - /// Currently selected color - GameColor? selectedColor; - - @override - void initState() { - selectedColor = widget.initialColor; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - const colors = GameColor.values; - - return Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: () { - Navigator.of(context).pop(selectedColor); - }, - ), - title: Text(loc.choose_color), - ), - body: PopScope( - canPop: false, - onPopInvokedWithResult: (bool didPop, Object? result) { - if (didPop) return; - Navigator.of(context).pop(selectedColor); - }, - child: ListView.builder( - padding: const EdgeInsets.only(bottom: 85), - itemCount: colors.length, - itemBuilder: (BuildContext context, int index) { - final color = colors[index]; - return GameTile( - onTap: () { - setState(() { - if (selectedColor == color) { - selectedColor = null; - } else { - selectedColor = color; - } - }); - }, - title: translateGameColorToString(color, context), - description: '', - isHighlighted: selectedColor == color, - badgeText: ' ', //Breite für Color Badge - badgeColor: getColorFromGameColor(color), - ); - }, - ), - ), - ); - } -} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart deleted file mode 100644 index 6b69b22..0000000 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tallee/core/common.dart'; -import 'package:tallee/core/custom_theme.dart'; -import 'package:tallee/core/enums.dart'; -import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; - -class ChooseRulesetView extends StatefulWidget { - /// A view that allows the user to choose a ruleset from a list of available rulesets - /// - [rulesets]: A list of tuples containing the ruleset and its description - /// - [initialRulesetIndex]: The index of the initially selected ruleset - const ChooseRulesetView({ - super.key, - required this.rulesets, - required this.initialRulesetIndex, - }); - - /// A list of tuples containing the ruleset and its description - final List<(Ruleset, String)> rulesets; - - /// The index of the initially selected ruleset - final int initialRulesetIndex; - @override - State createState() => _ChooseRulesetViewState(); -} - -class _ChooseRulesetViewState extends State { - /// Currently selected ruleset index - late int selectedRulesetIndex; - - @override - void initState() { - selectedRulesetIndex = widget.initialRulesetIndex; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return DefaultTabController( - length: 2, - initialIndex: 0, - child: Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: () { - Navigator.of(context).pop( - selectedRulesetIndex == -1 - ? null - : widget.rulesets[selectedRulesetIndex].$1, - ); - }, - ), - title: Text(loc.choose_ruleset), - ), - body: PopScope( - // This fixes that the Android Back Gesture didn't return the - // selectedRulesetIndex and therefore the selected Ruleset wasn't saved - canPop: false, - onPopInvokedWithResult: (bool didPop, Object? result) { - if (didPop) { - return; - } - Navigator.of(context).pop( - selectedRulesetIndex == -1 - ? null - : widget.rulesets[selectedRulesetIndex].$1, - ); - }, - child: ListView.builder( - padding: const EdgeInsets.only(bottom: 85), - itemCount: widget.rulesets.length, - itemBuilder: (BuildContext context, int index) { - return TitleDescriptionListTile( - onTap: () async { - setState(() { - if (selectedRulesetIndex == index) { - selectedRulesetIndex = -1; - } else { - selectedRulesetIndex = index; - } - }); - }, - title: translateRulesetToString( - widget.rulesets[index].$1, - context, - ), - description: widget.rulesets[index].$2, - isHighlighted: selectedRulesetIndex == index, - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart similarity index 100% rename from lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart rename to lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart From 8194fb2f282d7be2f5230e77784ea784a0a3da9e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 11:03:34 +0200 Subject: [PATCH 13/33] Removed selectedRulesetIndex --- .../create_match/create_game_view.dart | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 0c3fde6..415794a 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -44,19 +44,10 @@ class _CreateGameViewState extends State { late final AppDatabase db; - /// The currently selected ruleset for the game. + late List<(Ruleset, String)> _rulesets; Ruleset? selectedRuleset; - /// The index of the currently selected ruleset. - int selectedRulesetIndex = -1; - - /// A list of available rulesets and their localized names. - late List<(Ruleset, String)> _rulesets; - - /// A list of available game colors and their localized names. late List<(GameColor, String)> _colors; - - /// The currently selected color for the game. GameColor? selectedColor = GameColor.orange; /// Controller for the game name input field. @@ -122,10 +113,7 @@ class _CreateGameViewState extends State { _descriptionController.text = widget.gameToEdit!.description; selectedRuleset = widget.gameToEdit!.ruleset; selectedColor = widget.gameToEdit!.color; - - selectedRulesetIndex = _rulesets.indexWhere( - (r) => r.$1 == selectedRuleset, - ); + selectedRuleset = widget.gameToEdit!.ruleset; } } @@ -435,7 +423,7 @@ class _CreateGameViewState extends State { buttonType: ButtonType.primary, onPressed: _gameNameController.text.trim().isNotEmpty && - selectedRulesetIndex != -1 && + selectedRuleset != null && selectedColor != null ? () async { Game newGame = Game( From b53facc16c35f6eafac41d350635b7396f43743f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 11:03:59 +0200 Subject: [PATCH 14/33] Added singleWinner as default ruleset --- .../main_menu/match_view/create_match/create_game_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 415794a..6928c78 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -45,7 +45,7 @@ class _CreateGameViewState extends State { late final AppDatabase db; late List<(Ruleset, String)> _rulesets; - Ruleset? selectedRuleset; + Ruleset? selectedRuleset = Ruleset.singleWinner; late List<(GameColor, String)> _colors; GameColor? selectedColor = GameColor.orange; From a9b86fe7ffeb9ad3ee494d7797ae6e98c79c6167 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 11:09:05 +0200 Subject: [PATCH 15/33] Added icons to rulesets --- lib/core/common.dart | 16 +++++++++++++++ .../create_match/create_game_view.dart | 20 +++++++++++++------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 14d90aa..fc61a94 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -66,6 +66,22 @@ Color getColorFromGameColor(GameColor color) { } } +/// 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 diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 6928c78..aeb2099 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -222,7 +222,7 @@ class _CreateGameViewState extends State { contentDecoration: CustomTheme.standardBoxDecoration, content: StatefulBuilder( builder: (context, setPopupState) => SizedBox( - width: 250, + width: 280, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -258,6 +258,10 @@ class _CreateGameViewState extends State { child: Row( spacing: 8, children: [ + Icon( + getRulesetIcon(_rulesets[index].$1), + size: 16, + ), Text( _rulesets[index].$2, style: const TextStyle( @@ -278,11 +282,15 @@ class _CreateGameViewState extends State { ), ), ), - child: selectedRuleset == null - ? Text(loc.none) - : Text( - translateRulesetToString(selectedRuleset!, context), - ), + child: Row( + children: [ + Icon(getRulesetIcon(selectedRuleset!), size: 16), + SizedBox(width: 5), + Text( + translateRulesetToString(selectedRuleset!, context), + ), + ], + ), ), ), From fef83808600cbfa27a2c3f3edbb3517a1eccf82b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:41:01 +0200 Subject: [PATCH 16/33] added const --- .../main_menu/match_view/create_match/create_game_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index aeb2099..8594bd9 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -285,7 +285,7 @@ class _CreateGameViewState extends State { child: Row( children: [ Icon(getRulesetIcon(selectedRuleset!), size: 16), - SizedBox(width: 5), + const SizedBox(width: 5), Text( translateRulesetToString(selectedRuleset!, context), ), From 98960083350ef50d6cff3527261d9cb252a3076e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 11:42:34 +0200 Subject: [PATCH 17/33] Refactoring --- lib/data/dao/match_dao.dart | 2 +- .../views/main_menu/group_view/create_group_view.dart | 2 +- .../views/main_menu/group_view/group_detail_view.dart | 4 +++- test/db_tests/aggregates/match_test.dart | 6 +++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 340273d..88cca35 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -354,7 +354,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Retrieves all matches associated with the given [groupId]. /// Queries the database directly, filtering by [groupId]. - Future> getGroupMatches({required String groupId}) async { + Future> getMatchesByGroup({required String groupId}) async { final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); final rows = await query.get(); diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 3a2ee60..b4a5b97 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -197,7 +197,7 @@ class _CreateGroupViewState extends State { /// obsolete. For each such match, the group association is removed by setting /// its [groupId] to null. Future deleteObsoleteMatchGroupRelations() async { - final groupMatches = await db.matchDao.getGroupMatches( + final groupMatches = await db.matchDao.getMatchesByGroup( groupId: widget.groupToEdit!.id, ); diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 92c3bba..3d5e805 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -244,7 +244,9 @@ class _GroupDetailViewState extends State { /// Loads statistics for this group Future _loadStatistics() async { isLoading = true; - final groupMatches = await db.matchDao.getGroupMatches(groupId: _group.id); + final groupMatches = await db.matchDao.getMatchesByGroup( + groupId: _group.id, + ); setState(() { totalMatches = groupMatches.length; diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 367f38f..2c9b768 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -241,15 +241,15 @@ void main() { expect(matchExists, isTrue); }); - test('getGroupMatches() works correctly', () async { - var matches = await database.matchDao.getGroupMatches( + test('getMatchesByGroup() works correctly', () async { + var matches = await database.matchDao.getMatchesByGroup( groupId: 'non-existing-id', ); expect(matches, isEmpty); await database.matchDao.addMatch(match: testMatch1); - matches = await database.matchDao.getGroupMatches( + matches = await database.matchDao.getMatchesByGroup( groupId: testGroup1.id, ); expect(matches, isNotEmpty); From eadf05e116583847ecd97ebe2b216b5ebe82c0e6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 11:43:35 +0200 Subject: [PATCH 18/33] Fixed incoming changes --- .../create_match/create_game_view.dart | 16 +++++----------- .../create_match/create_match_view.dart | 7 ------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 8594bd9..2094554 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -469,38 +469,32 @@ class _CreateGameViewState extends State { final oldGame = widget.gameToEdit!; if (oldGame.name != newGame.name) { - await db.gameDao.updateGameName( - gameId: oldGame.id, - newName: newGame.name, - ); + await db.gameDao.updateGameName(gameId: oldGame.id, name: newGame.name); } if (oldGame.description != newGame.description) { await db.gameDao.updateGameDescription( gameId: oldGame.id, - newDescription: newGame.description, + description: newGame.description, ); } if (oldGame.ruleset != newGame.ruleset) { await db.gameDao.updateGameRuleset( gameId: oldGame.id, - newRuleset: newGame.ruleset, + ruleset: newGame.ruleset, ); } if (oldGame.color != newGame.color) { await db.gameDao.updateGameColor( gameId: oldGame.id, - newColor: newGame.color, + color: newGame.color, ); } if (oldGame.icon != newGame.icon) { - await db.gameDao.updateGameIcon( - gameId: oldGame.id, - newIcon: newGame.icon, - ); + await db.gameDao.updateGameIcon(gameId: oldGame.id, icon: newGame.icon); } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index c09ff49..14908b6 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -295,13 +295,6 @@ class _CreateMatchViewState extends State { ); } - if (widget.matchToEdit!.game.id != updatedMatch.game.id) { - await db.matchDao.updateMatchGame( - matchId: widget.matchToEdit!.id, - gameId: updatedMatch.game.id, - ); - } - // Add players who are in updatedMatch but not in the original match for (var player in updatedMatch.players) { if (!widget.matchToEdit!.players.any((p) => p.id == player.id)) { From f3380e6c08d6976424d31636e5c77bc6cc5c3409 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 11:45:22 +0200 Subject: [PATCH 19/33] Reordered tests --- test/db_tests/aggregates/match_test.dart | 98 ++++++++++++------------ 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 2c9b768..7f627f7 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -259,6 +259,57 @@ void main() { expect(match.group, isNotNull); expect(match.group!.id, testGroup1.id); }); + + test('getMatchCount() 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); + }); + + 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); + }); + + 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', () { @@ -408,31 +459,6 @@ void main() { final allMatches = await database.matchDao.getAllMatches(); expect(allMatches, isEmpty); }); - - test('Getting the match count works correctly', () async { - var matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); - - await database.matchDao.addMatch(match: testMatch1); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); - - await database.matchDao.addMatch(match: testMatch2); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 2); - - await database.matchDao.deleteMatch(matchId: testMatch1.id); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); - - await database.matchDao.deleteMatch(matchId: testMatch2.id); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); - }); }); group('DELETE', () { @@ -472,28 +498,6 @@ void main() { }); }); - 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); - }); - - test('getMatchCountByGame() returns 0 for non-existent game', () async { - final count = await database.matchDao.getMatchCountByGame( - gameId: 'non-existent-game-id', - ); - expect(count, 0); - }); - test('deleteMatchesByGame() deletes all matches for a game', () async { await database.matchDao.addMatch(match: testMatch1); await database.matchDao.addMatch(match: testMatch2); From 045d2afa3958db3d12818209264731dc448b7e8d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 6 May 2026 19:54:34 +0200 Subject: [PATCH 20/33] removed descriptions --- lib/l10n/arb/app_de.arb | 1 - lib/l10n/arb/app_en.arb | 366 ++-------------------------------------- 2 files changed, 10 insertions(+), 357 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index e518525..ab8166a 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -97,7 +97,6 @@ "played_matches": "Gespielte Spiele", "player_name": "Spieler:innenname", "players": "Spieler:innen", - "players_count": "{count} Spieler", "point": "Punkt", "points": "Punkte", "privacy_policy": "Datenschutzerklärung", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index c01f0b2..e41bb83 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,360 +1,6 @@ { "@@locale": "en", - "@all_players": { - "description": "Label for all players list" - }, - "@all_players_selected": { - "description": "Message when all players are added to selection" - }, - "@amount_of_matches": { - "description": "Label for amount of matches statistic" - }, - "@app_name": { - "description": "The name of the App" - }, - "@best_player": { - "description": "Label for best player statistic" - }, - "@cancel": { - "description": "Cancel button text" - }, - "@choose_color": { - "description": "Label for choosing a color" - }, - "@choose_game": { - "description": "Label for choosing a game" - }, - "@choose_group": { - "description": "Label for choosing a group" - }, - "@choose_ruleset": { - "description": "Label for choosing a ruleset" - }, - "@color": { - "description": "Color label" - }, - "@could_not_add_player": { - "description": "Error message when adding a player fails" - }, - "@create_game": { - "description": "Button text to create a game" - }, - "@create_group": { - "description": "Button text to create a group" - }, - "@create_match": { - "description": "Button text to create a match" - }, - "@create_new_group": { - "description": "Appbar text to create a new group" - }, - "@create_new_match": { - "description": "Appbar text to create a new match" - }, - "@created_on": { - "description": "Label for creation date" - }, - "@data": { - "description": "Data label" - }, - "@data_successfully_deleted": { - "description": "Success message after deleting data" - }, - "@data_successfully_exported": { - "description": "Success message after exporting data" - }, - "@data_successfully_imported": { - "description": "Success message after importing data" - }, - "@days_ago": { - "description": "Date format for days ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@delete": { - "description": "Delete button text" - }, - "@delete_all_data": { - "description": "Confirmation dialog for deleting all data" - }, - "@delete_game": { - "description": "Button text to delete a game" - }, - "@delete_group": { - "description": "Confirmation dialog for deleting a group" - }, - "@delete_match": { - "description": "Button text to delete a match" - }, - "@description": { - "description": "Description label" - }, - "@edit_game": { - "description": "Button text to edit a game" - }, - "@edit_group": { - "description": "Button & Appbar label for editing a group" - }, - "@edit_match": { - "description": "Button & Appbar label for editing a match" - }, - "@enter_points": { - "description": "Label to enter players points" - }, - "@enter_results": { - "description": "Button text to enter match results" - }, - "@error_creating_group": { - "description": "Error message when group creation fails" - }, - "@error_deleting_game": { - "description": "Error message when game deletion fails" - }, - "@error_deleting_group": { - "description": "Error message when group deletion fails" - }, - "@error_editing_group": { - "description": "Error message when group editing fails" - }, - "@error_reading_file": { - "description": "Error message when file cannot be read" - }, - "@export_canceled": { - "description": "Message when export is canceled" - }, - "@export_data": { - "description": "Export data menu item" - }, - "@format_exception": { - "description": "Error message for format exceptions" - }, - "@game": { - "description": "Game label" - }, - "@game_name": { - "description": "Placeholder for game name search" - }, - "@group": { - "description": "Group label" - }, - "@group_name": { - "description": "Placeholder for group name input" - }, - "@group_profile": { - "description": "Title for group profile view" - }, - "@groups": { - "description": "Label for groups" - }, - "@home": { - "description": "Home tab label" - }, - "@import_canceled": { - "description": "Message when import is canceled" - }, - "@import_data": { - "description": "Import data menu item" - }, - "@info": { - "description": "Info label" - }, - "@invalid_schema": { - "description": "Error message for invalid schema" - }, - "@least_points": { - "description": "Title for least points ruleset" - }, - "@legal": { - "description": "Legal section header" - }, - "@legal_notice": { - "description": "Legal notice menu item" - }, - "@licenses": { - "description": "Licenses menu item" - }, - "@match_in_progress": { - "description": "Message when match is in progress" - }, - "@match_name": { - "description": "Placeholder for match name input" - }, - "@match_profile": { - "description": "Title for match profile view" - }, - "@matches": { - "description": "Label for matches" - }, - "@members": { - "description": "Label for group members" - }, - "@most_points": { - "description": "Title for most points ruleset" - }, - "@no_data_available": { - "description": "Message when no data in the statistic tiles is given" - }, - "@no_groups_created_yet": { - "description": "Message when no groups exist" - }, - "@no_licenses_found": { - "description": "Message when no licenses are found" - }, - "@no_license_text_available": { - "description": "Message when no license text is available" - }, - "@no_matches_created_yet": { - "description": "Message when no matches exist" - }, - "@no_players_created_yet": { - "description": "Message when no players exist" - }, - "@no_players_found_with_that_name": { - "description": "Message when search returns no results" - }, - "@no_players_selected": { - "description": "Message when no players are selected" - }, - "@no_recent_matches_available": { - "description": "Message when no recent matches exist" - }, - "@no_results_entered_yet": { - "description": "Message when no results have been entered yet" - }, - "@no_second_match_available": { - "description": "Message when no second match exists" - }, - "@no_statistics_available": { - "description": "Message when no statistics are available, because no matches were played yet" - }, - "@none": { - "description": "None option label" - }, - "@none_group": { - "description": "None group option label" - }, - "@not_available": { - "description": "Abbreviation for not available" - }, - "@played_matches": { - "description": "Label for played matches statistic" - }, - "@player_name": { - "description": "Placeholder for player name input" - }, - "@players": { - "description": "Players label" - }, - "@players_count": { - "description": "Shows the number of players", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@points": { - "description": "Points label" - }, - "@privacy_policy": { - "description": "Privacy policy menu item" - }, - "@quick_create": { - "description": "Title for quick create section" - }, - "@recent_matches": { - "description": "Title for recent matches section" - }, - "@results": { - "description": "Label for match results" - }, - "@ruleset": { - "description": "Ruleset label" - }, - "@ruleset_least_points": { - "description": "Description for least points ruleset" - }, - "@ruleset_most_points": { - "description": "Description for most points ruleset" - }, - "@ruleset_single_loser": { - "description": "Description for single loser ruleset" - }, - "@ruleset_single_winner": { - "description": "Description for single winner ruleset" - }, - "@save_changes": { - "description": "Save changes button text" - }, - "@search_for_groups": { - "description": "Hint text for group search input field" - }, - "@search_for_players": { - "description": "Hint text for player search input field" - }, - "@select_winner": { - "description": "Label to select the winner" - }, - "@select_loser": { - "description": "Label to select the loser" - }, - "@selected_players": { - "description": "Shows the number of selected players" - }, - "@settings": { - "description": "Label for the App Settings" - }, - "@single_loser": { - "description": "Title for single loser ruleset" - }, - "@single_winner": { - "description": "Title for single winner ruleset" - }, - "@statistics": { - "description": "Statistics tab label" - }, - "@stats": { - "description": "Stats tab label (short)" - }, - "@successfully_added_player": { - "description": "Success message when adding a player", - "placeholders": { - "playerName": { - "type": "String", - "example": "John" - } - } - }, - "@there_is_no_group_matching_your_search": { - "description": "Message when search returns no groups" - }, - "@this_cannot_be_undone": { - "description": "Warning message for irreversible actions" - }, - "@today_at": { - "description": "Date format for today" - }, - "@undo": { - "description": "Undo button text" - }, - "@unknown_exception": { - "description": "Error message for unknown exceptions" - }, - "@winner": { - "description": "Winner label" - }, - "@winrate": { - "description": "Label for winrate statistic" - }, - "@wins": { - "description": "Label for wins statistic" - }, - "@yesterday_at": { - "description": "Date format for yesterday" - }, + "all_players": "All players", "all_players_selected": "All players selected", "amount_of_matches": "Amount of Matches", @@ -452,7 +98,6 @@ "played_matches": "Played Matches", "player_name": "Player name", "players": "Players", - "players_count": "{count} Players", "point": "Point", "points": "Points", "privacy_policy": "Privacy Policy", @@ -480,6 +125,15 @@ "statistics": "Statistics", "stats": "Stats", "successfully_added_player": "Successfully added player {playerName}", + "@successfully_added_player": { + "description": "Success message when adding a player", + "placeholders": { + "playerName": { + "type": "String", + "example": "John" + } + } + }, "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", "tie": "Tie", From 46041be837a31160ab19434dc5f0bbdf803f4bfc Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 6 May 2026 19:55:56 +0200 Subject: [PATCH 21/33] compiling localizations --- lib/l10n/generated/app_localizations.dart | 230 +++++++++---------- lib/l10n/generated/app_localizations_de.dart | 7 +- lib/l10n/generated/app_localizations_en.dart | 7 +- 3 files changed, 114 insertions(+), 130 deletions(-) diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 790597f..dd8f9cb 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -98,67 +98,67 @@ abstract class AppLocalizations { Locale('en'), ]; - /// Label for all players list + /// No description provided for @all_players. /// /// In en, this message translates to: /// **'All players'** String get all_players; - /// Message when all players are added to selection + /// No description provided for @all_players_selected. /// /// In en, this message translates to: /// **'All players selected'** String get all_players_selected; - /// Label for amount of matches statistic + /// No description provided for @amount_of_matches. /// /// In en, this message translates to: /// **'Amount of Matches'** String get amount_of_matches; - /// The name of the App + /// No description provided for @app_name. /// /// In en, this message translates to: /// **'Tallee'** String get app_name; - /// Label for best player statistic + /// No description provided for @best_player. /// /// In en, this message translates to: /// **'Best Player'** String get best_player; - /// Cancel button text + /// No description provided for @cancel. /// /// In en, this message translates to: /// **'Cancel'** String get cancel; - /// Label for choosing a color + /// No description provided for @choose_color. /// /// In en, this message translates to: /// **'Choose Color'** String get choose_color; - /// Label for choosing a game + /// No description provided for @choose_game. /// /// In en, this message translates to: /// **'Choose Game'** String get choose_game; - /// Label for choosing a group + /// No description provided for @choose_group. /// /// In en, this message translates to: /// **'Choose Group'** String get choose_group; - /// Label for choosing a ruleset + /// No description provided for @choose_ruleset. /// /// In en, this message translates to: /// **'Choose Ruleset'** String get choose_ruleset; - /// Color label + /// No description provided for @color. /// /// In en, this message translates to: /// **'Color'** @@ -212,91 +212,91 @@ abstract class AppLocalizations { /// **'Yellow'** String get color_yellow; - /// Error message when adding a player fails + /// No description provided for @could_not_add_player. /// /// In en, this message translates to: /// **'Could not add player'** String could_not_add_player(Object playerName); - /// Button text to create a game + /// No description provided for @create_game. /// /// In en, this message translates to: /// **'Create Game'** String get create_game; - /// Button text to create a group + /// No description provided for @create_group. /// /// In en, this message translates to: /// **'Create Group'** String get create_group; - /// Button text to create a match + /// No description provided for @create_match. /// /// In en, this message translates to: /// **'Create match'** String get create_match; - /// Appbar text to create a new group + /// No description provided for @create_new_group. /// /// In en, this message translates to: /// **'Create new group'** String get create_new_group; - /// Label for creation date + /// No description provided for @created_on. /// /// In en, this message translates to: /// **'Created on'** String get created_on; - /// Appbar text to create a new match + /// No description provided for @create_new_match. /// /// In en, this message translates to: /// **'Create new match'** String get create_new_match; - /// Data label + /// No description provided for @data. /// /// In en, this message translates to: /// **'Data'** String get data; - /// Success message after deleting data + /// No description provided for @data_successfully_deleted. /// /// In en, this message translates to: /// **'Data successfully deleted'** String get data_successfully_deleted; - /// Success message after exporting data + /// No description provided for @data_successfully_exported. /// /// In en, this message translates to: /// **'Data successfully exported'** String get data_successfully_exported; - /// Success message after importing data + /// No description provided for @data_successfully_imported. /// /// In en, this message translates to: /// **'Data successfully imported'** String get data_successfully_imported; - /// Date format for days ago + /// No description provided for @days_ago. /// /// In en, this message translates to: /// **'{count} days ago'** - String days_ago(int count); + String days_ago(Object count); - /// Delete button text + /// No description provided for @delete. /// /// In en, this message translates to: /// **'Delete'** String get delete; - /// Confirmation dialog for deleting all data + /// No description provided for @delete_all_data. /// /// In en, this message translates to: /// **'Delete all data'** String get delete_all_data; - /// Button text to delete a game + /// No description provided for @delete_game. /// /// In en, this message translates to: /// **'Delete Game'** @@ -308,457 +308,451 @@ abstract class AppLocalizations { /// **'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); - /// Confirmation dialog for deleting a group + /// No description provided for @delete_group. /// /// In en, this message translates to: /// **'Delete Group'** String get delete_group; - /// Button text to delete a match + /// No description provided for @delete_match. /// /// In en, this message translates to: /// **'Delete Match'** String get delete_match; - /// Description label + /// No description provided for @description. /// /// In en, this message translates to: /// **'Description'** String get description; - /// Button text to edit a game + /// No description provided for @edit_game. /// /// In en, this message translates to: /// **'Edit Game'** String get edit_game; - /// Button & Appbar label for editing a group + /// No description provided for @edit_group. /// /// In en, this message translates to: /// **'Edit Group'** String get edit_group; - /// Button & Appbar label for editing a match + /// No description provided for @edit_match. /// /// In en, this message translates to: /// **'Edit Match'** String get edit_match; - /// Label to enter players points + /// No description provided for @enter_points. /// /// In en, this message translates to: /// **'Enter points'** String get enter_points; - /// Button text to enter match results + /// No description provided for @enter_results. /// /// In en, this message translates to: /// **'Enter Results'** String get enter_results; - /// Error message when group creation fails + /// No description provided for @error_creating_group. /// /// In en, this message translates to: /// **'Error while creating group, please try again'** String get error_creating_group; - /// Error message when game 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; - /// Error message when group deletion fails + /// No description provided for @error_deleting_group. /// /// In en, this message translates to: /// **'Error while deleting group, please try again'** String get error_deleting_group; - /// Error message when group editing fails + /// No description provided for @error_editing_group. /// /// In en, this message translates to: /// **'Error while editing group, please try again'** String get error_editing_group; - /// Error message when file cannot be read + /// No description provided for @error_reading_file. /// /// In en, this message translates to: /// **'Error reading file'** String get error_reading_file; - /// Message when export is canceled + /// No description provided for @export_canceled. /// /// In en, this message translates to: /// **'Export canceled'** String get export_canceled; - /// Export data menu item + /// No description provided for @export_data. /// /// In en, this message translates to: /// **'Export data'** String get export_data; - /// Error message for format exceptions + /// No description provided for @format_exception. /// /// In en, this message translates to: /// **'Format Exception (see console)'** String get format_exception; - /// Game label + /// No description provided for @game. /// /// In en, this message translates to: /// **'Game'** String get game; - /// Placeholder for game name search + /// No description provided for @game_name. /// /// In en, this message translates to: /// **'Game Name'** String get game_name; - /// Group label + /// No description provided for @group. /// /// In en, this message translates to: /// **'Group'** String get group; - /// Placeholder for group name input + /// No description provided for @group_name. /// /// In en, this message translates to: /// **'Group name'** String get group_name; - /// Title for group profile view + /// No description provided for @group_profile. /// /// In en, this message translates to: /// **'Group Profile'** String get group_profile; - /// Label for groups + /// No description provided for @groups. /// /// In en, this message translates to: /// **'Groups'** String get groups; - /// Home tab label + /// No description provided for @home. /// /// In en, this message translates to: /// **'Home'** String get home; - /// Message when import is canceled + /// No description provided for @import_canceled. /// /// In en, this message translates to: /// **'Import canceled'** String get import_canceled; - /// Import data menu item + /// No description provided for @import_data. /// /// In en, this message translates to: /// **'Import data'** String get import_data; - /// Info label + /// No description provided for @info. /// /// In en, this message translates to: /// **'Info'** String get info; - /// Error message for invalid schema + /// No description provided for @invalid_schema. /// /// In en, this message translates to: /// **'Invalid Schema'** String get invalid_schema; - /// Title for least points ruleset + /// No description provided for @least_points. /// /// In en, this message translates to: /// **'Least Points'** String get least_points; - /// Legal section header + /// No description provided for @legal. /// /// In en, this message translates to: /// **'Legal'** String get legal; - /// Legal notice menu item + /// No description provided for @legal_notice. /// /// In en, this message translates to: /// **'Legal Notice'** String get legal_notice; - /// Licenses menu item + /// No description provided for @licenses. /// /// In en, this message translates to: /// **'Licenses'** String get licenses; - /// Message when match is in progress + /// No description provided for @match_in_progress. /// /// In en, this message translates to: /// **'Match in progress...'** String get match_in_progress; - /// Placeholder for match name input + /// No description provided for @match_name. /// /// In en, this message translates to: /// **'Match name'** String get match_name; - /// Title for match profile view + /// No description provided for @match_profile. /// /// In en, this message translates to: /// **'Match Profile'** String get match_profile; - /// Label for matches + /// No description provided for @matches. /// /// In en, this message translates to: /// **'Matches'** String get matches; - /// Label for group members + /// No description provided for @members. /// /// In en, this message translates to: /// **'Members'** String get members; - /// Title for most points ruleset + /// No description provided for @most_points. /// /// In en, this message translates to: /// **'Most Points'** String get most_points; - /// Message when no data in the statistic tiles is given + /// No description provided for @no_data_available. /// /// In en, this message translates to: /// **'No data available'** String get no_data_available; - /// Message when no groups exist + /// No description provided for @no_groups_created_yet. /// /// In en, this message translates to: /// **'No groups created yet'** String get no_groups_created_yet; - /// Message when no licenses are found + /// No description provided for @no_licenses_found. /// /// In en, this message translates to: /// **'No licenses found'** String get no_licenses_found; - /// Message when no license text is available + /// No description provided for @no_license_text_available. /// /// In en, this message translates to: /// **'No license text available'** String get no_license_text_available; - /// Message when no matches exist + /// No description provided for @no_matches_created_yet. /// /// In en, this message translates to: /// **'No matches created yet'** String get no_matches_created_yet; - /// Message when no players exist + /// No description provided for @no_players_created_yet. /// /// In en, this message translates to: /// **'No players created yet'** String get no_players_created_yet; - /// Message when search returns no results + /// No description provided for @no_players_found_with_that_name. /// /// In en, this message translates to: /// **'No players found with that name'** String get no_players_found_with_that_name; - /// Message when no players are selected + /// No description provided for @no_players_selected. /// /// In en, this message translates to: /// **'No players selected'** String get no_players_selected; - /// Message when no recent matches exist + /// No description provided for @no_recent_matches_available. /// /// In en, this message translates to: /// **'No recent matches available'** String get no_recent_matches_available; - /// Message when no results have been entered yet + /// No description provided for @no_results_entered_yet. /// /// In en, this message translates to: /// **'No results entered yet'** String get no_results_entered_yet; - /// Message when no second match exists + /// No description provided for @no_second_match_available. /// /// In en, this message translates to: /// **'No second match available'** String get no_second_match_available; - /// Message when no statistics are available, because no matches were played yet + /// No description provided for @no_statistics_available. /// /// In en, this message translates to: /// **'No statistics available'** String get no_statistics_available; - /// None option label + /// No description provided for @none. /// /// In en, this message translates to: /// **'None'** String get none; - /// None group option label + /// No description provided for @none_group. /// /// In en, this message translates to: /// **'None'** String get none_group; - /// Abbreviation for not available + /// No description provided for @not_available. /// /// In en, this message translates to: /// **'Not available'** String get not_available; - /// Label for played matches statistic + /// No description provided for @played_matches. /// /// In en, this message translates to: /// **'Played Matches'** String get played_matches; - /// Placeholder for player name input + /// No description provided for @player_name. /// /// In en, this message translates to: /// **'Player name'** String get player_name; - /// Players label + /// No description provided for @players. /// /// In en, this message translates to: /// **'Players'** String get players; - /// Shows the number of players - /// - /// In en, this message translates to: - /// **'{count} Players'** - String players_count(int count); - /// No description provided for @point. /// /// In en, this message translates to: /// **'Point'** String get point; - /// Points label + /// No description provided for @points. /// /// In en, this message translates to: /// **'Points'** String get points; - /// Privacy policy menu item + /// No description provided for @privacy_policy. /// /// In en, this message translates to: /// **'Privacy Policy'** String get privacy_policy; - /// Title for quick create section + /// No description provided for @quick_create. /// /// In en, this message translates to: /// **'Quick Create'** String get quick_create; - /// Title for recent matches section + /// No description provided for @recent_matches. /// /// In en, this message translates to: /// **'Recent Matches'** String get recent_matches; - /// Label for match results + /// No description provided for @results. /// /// In en, this message translates to: /// **'Results'** String get results; - /// Ruleset label + /// No description provided for @ruleset. /// /// In en, this message translates to: /// **'Ruleset'** String get ruleset; - /// Description for least points ruleset + /// No description provided for @ruleset_least_points. /// /// In en, this message translates to: /// **'Inverse scoring: the player with the fewest points wins.'** String get ruleset_least_points; - /// Description for most points ruleset + /// No description provided for @ruleset_most_points. /// /// In en, this message translates to: /// **'Traditional ruleset: the player with the most points wins.'** String get ruleset_most_points; - /// Description for single loser ruleset + /// No description provided for @ruleset_single_loser. /// /// In en, this message translates to: /// **'Exactly one loser is determined; last place receives the penalty or consequence.'** String get ruleset_single_loser; - /// Description for single winner ruleset + /// No description provided for @ruleset_single_winner. /// /// In en, this message translates to: /// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'** String get ruleset_single_winner; - /// Save changes button text + /// No description provided for @save_changes. /// /// In en, this message translates to: /// **'Save Changes'** String get save_changes; - /// Hint text for group search input field + /// No description provided for @search_for_groups. /// /// In en, this message translates to: /// **'Search for groups'** String get search_for_groups; - /// Hint text for player search input field + /// No description provided for @search_for_players. /// /// In en, this message translates to: /// **'Search for players'** String get search_for_players; - /// Label to select the winner + /// No description provided for @select_winner. /// /// In en, this message translates to: /// **'Select Winner'** String get select_winner; - /// Label to select the loser + /// No description provided for @select_loser. /// /// In en, this message translates to: /// **'Select Loser'** String get select_loser; - /// Shows the number of selected players + /// No description provided for @selected_players. /// /// In en, this message translates to: /// **'Selected players'** String get selected_players; - /// Label for the App Settings + /// No description provided for @settings. /// /// In en, this message translates to: /// **'Settings'** String get settings; - /// Title for single loser ruleset + /// No description provided for @single_loser. /// /// In en, this message translates to: /// **'Single Loser'** String get single_loser; - /// Title for single winner ruleset + /// No description provided for @single_winner. /// /// In en, this message translates to: /// **'Single Winner'** @@ -788,13 +782,13 @@ abstract class AppLocalizations { /// **'Multiple Winners'** String get multiple_winners; - /// Statistics tab label + /// No description provided for @statistics. /// /// In en, this message translates to: /// **'Statistics'** String get statistics; - /// Stats tab label (short) + /// No description provided for @stats. /// /// In en, this message translates to: /// **'Stats'** @@ -806,13 +800,13 @@ abstract class AppLocalizations { /// **'Successfully added player {playerName}'** String successfully_added_player(String playerName); - /// Message when search returns no groups + /// No description provided for @there_is_no_group_matching_your_search. /// /// In en, this message translates to: /// **'There is no group matching your search'** String get there_is_no_group_matching_your_search; - /// Warning message for irreversible actions + /// No description provided for @this_cannot_be_undone. /// /// In en, this message translates to: /// **'This can\'t be undone.'** @@ -824,43 +818,43 @@ abstract class AppLocalizations { /// **'Tie'** String get tie; - /// Date format for today + /// No description provided for @today_at. /// /// In en, this message translates to: /// **'Today at'** String get today_at; - /// Undo button text + /// No description provided for @undo. /// /// In en, this message translates to: /// **'Undo'** String get undo; - /// Error message for unknown exceptions + /// No description provided for @unknown_exception. /// /// In en, this message translates to: /// **'Unknown Exception (see console)'** String get unknown_exception; - /// Winner label + /// No description provided for @winner. /// /// In en, this message translates to: /// **'Winner'** String get winner; - /// Label for winrate statistic + /// No description provided for @winrate. /// /// In en, this message translates to: /// **'Winrate'** String get winrate; - /// Label for wins statistic + /// No description provided for @wins. /// /// In en, this message translates to: /// **'Wins'** String get wins; - /// Date format for yesterday + /// No description provided for @yesterday_at. /// /// In en, this message translates to: /// **'Yesterday at'** diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 2b20848..fe846c3 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -101,7 +101,7 @@ class AppLocalizationsDe extends AppLocalizations { String get data_successfully_imported => 'Daten erfolgreich importiert'; @override - String days_ago(int count) { + String days_ago(Object count) { return 'vor $count Tagen'; } @@ -295,11 +295,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get players => 'Spieler:innen'; - @override - String players_count(int count) { - return '$count Spieler'; - } - @override String get point => 'Punkt'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 323d8c8..899e3a5 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -101,7 +101,7 @@ class AppLocalizationsEn extends AppLocalizations { String get data_successfully_imported => 'Data successfully imported'; @override - String days_ago(int count) { + String days_ago(Object count) { return '$count days ago'; } @@ -295,11 +295,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get players => 'Players'; - @override - String players_count(int count) { - return '$count Players'; - } - @override String get point => 'Point'; From 4520281cb65f284e5c60982137acb4de4390323e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 8 May 2026 19:33:22 +0200 Subject: [PATCH 22/33] fix: popup area --- .../create_match/create_game_view.dart | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 2094554..3baa5b3 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -297,20 +297,7 @@ class _CreateGameViewState extends State { // Choose color tile ChooseTile( title: loc.color, - trailing: Row( - spacing: 8, - children: [ - // Selected Color - Container( - width: 16, - height: 16, - margin: const EdgeInsets.only(left: 12), - decoration: BoxDecoration( - color: getColorFromGameColor(selectedColor!), - shape: BoxShape.circle, - ), - ), - + trailing: //Popup CustomPopup( showArrow: true, @@ -399,12 +386,25 @@ class _CreateGameViewState extends State { ), ), ), - child: Text( - translateGameColorToString(selectedColor!, context), + child: Row( + spacing: 8, + children: [ + // Selected Color + Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(left: 12), + decoration: BoxDecoration( + color: getColorFromGameColor(selectedColor!), + shape: BoxShape.circle, + ), + ), + Text( + translateGameColorToString(selectedColor!, context), + ), + ], ), ), - ], - ), ), // Description input field From 0d1ed3e666e56d5eba53633e1d825d3ee47f4afd Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 8 May 2026 19:36:52 +0200 Subject: [PATCH 23/33] fix: no element error --- .../main_menu/match_view/create_match/choose_game_view.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index cdd73c2..78a10f1 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -176,6 +176,10 @@ class _ChooseGameViewState extends State { } if (result.delete) { setState(() { + // deselect the game + if (selectedGameId == game.id) { + selectedGameId = ''; + } widget.games.removeAt(originalIndex); widget.onGamesUpdated?.call(); }); From 28fb608b3023be797586b46b82bd9cae10cd8603 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 8 May 2026 19:45:46 +0200 Subject: [PATCH 24/33] fix: increased font size --- .../main_menu/match_view/create_match/create_game_view.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 3baa5b3..f20817a 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -150,7 +150,10 @@ class _CreateGameViewState extends State { context: context, builder: (context) => CustomAlertDialog( title: loc.delete_game, - content: Text(dialogContent), + content: Text( + dialogContent, + style: const TextStyle(fontSize: 15), + ), actions: [ CustomDialogAction( isDestructive: true, From fd553e1d245c3980178a823ce2179c304935ae55 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 13:27:24 +0200 Subject: [PATCH 25/33] fix: chevron tap --- .../match_view/create_match/create_game_view.dart | 15 +++++++++++++++ lib/presentation/widgets/tiles/choose_tile.dart | 13 ++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index f20817a..274c960 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_popup/flutter_popup.dart'; import 'package:provider/provider.dart'; @@ -292,6 +294,11 @@ class _CreateGameViewState extends State { Text( translateRulesetToString(selectedRuleset!, context), ), + const SizedBox(width: 5), + Transform.rotate( + angle: pi / 2, + child: const Icon(Icons.arrow_forward_ios, size: 16), + ), ], ), ), @@ -405,6 +412,14 @@ class _CreateGameViewState extends State { Text( translateGameColorToString(selectedColor!, context), ), + const SizedBox(width: 5), + Transform.rotate( + angle: pi / 2, + child: const Icon( + Icons.arrow_forward_ios, + size: 16, + ), + ), ], ), ), diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index 1f72328..41cc7f0 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:tallee/core/custom_theme.dart'; @@ -45,13 +43,10 @@ class _ChooseTileState extends State { ), const Spacer(), if (widget.trailing != null) widget.trailing!, - const SizedBox(width: 10), - widget.onPressed == null - ? Transform.rotate( - angle: pi / 2, - child: const Icon(Icons.arrow_forward_ios, size: 16), - ) - : const Icon(Icons.arrow_forward_ios, size: 16), + if (widget.onPressed != null) ...[ + const SizedBox(width: 10), + const Icon(Icons.arrow_forward_ios, size: 16), + ], ], ), ), From df757af7ec5e1df101e1550507380a8a61ce8f3e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 13:33:50 +0200 Subject: [PATCH 26/33] fix: choose tile alignment --- .../create_match/create_game_view.dart | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 274c960..3f4169e 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -288,13 +288,16 @@ class _CreateGameViewState extends State { ), ), child: Row( + spacing: 8, children: [ Icon(getRulesetIcon(selectedRuleset!), size: 16), - const SizedBox(width: 5), - Text( - translateRulesetToString(selectedRuleset!, context), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Text( + translateRulesetToString(selectedRuleset!, context), + textAlign: TextAlign.right, + ), ), - const SizedBox(width: 5), Transform.rotate( angle: pi / 2, child: const Icon(Icons.arrow_forward_ios, size: 16), @@ -403,16 +406,20 @@ class _CreateGameViewState extends State { Container( width: 16, height: 16, - margin: const EdgeInsets.only(left: 12), decoration: BoxDecoration( color: getColorFromGameColor(selectedColor!), shape: BoxShape.circle, ), ), - Text( - translateGameColorToString(selectedColor!, context), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Text( + translateGameColorToString( + selectedColor!, + context, + ), + ), ), - const SizedBox(width: 5), Transform.rotate( angle: pi / 2, child: const Icon( From f9eafa5b3d06d389b4f2eb7b33d938b568971686 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 15:01:21 +0200 Subject: [PATCH 27/33] add: empty game list messages --- lib/l10n/arb/app_de.arb | 2 + lib/l10n/arb/app_en.arb | 2 + lib/l10n/generated/app_localizations.dart | 12 ++ lib/l10n/generated/app_localizations_de.dart | 7 + lib/l10n/generated/app_localizations_en.dart | 7 + .../create_match/choose_game_view.dart | 132 ++++++++++-------- 6 files changed, 107 insertions(+), 55 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index ab8166a..dd8c744 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -80,6 +80,7 @@ "members": "Mitglieder", "most_points": "Höchste Punkte", "no_data_available": "Keine Daten verfügbar", + "no_games_created_yet": "Noch keine Spielvorlagen erstellt", "no_groups_created_yet": "Noch keine Gruppen erstellt", "no_licenses_found": "Keine Lizenzen gefunden", "no_license_text_available": "Kein Lizenztext verfügbar", @@ -125,6 +126,7 @@ "statistics": "Statistiken", "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", + "there_are_no_games_matching_your_search": "Es gibt keine Spielvorlagen, die deiner Suche entspricht", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "tie": "Unentschieden", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e41bb83..11a908e 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -81,6 +81,7 @@ "members": "Members", "most_points": "Most Points", "no_data_available": "No data available", + "no_games_created_yet": "No games created yet", "no_groups_created_yet": "No groups created yet", "no_licenses_found": "No licenses found", "no_license_text_available": "No license text available", @@ -134,6 +135,7 @@ } } }, + "there_are_no_games_matching_your_search": "There are no games matching your search", "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", "tie": "Tie", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index dd8f9cb..1f6abff 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -536,6 +536,12 @@ abstract class AppLocalizations { /// **'No data available'** String get no_data_available; + /// 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: @@ -800,6 +806,12 @@ abstract class AppLocalizations { /// **'Successfully added player {playerName}'** String successfully_added_player(String playerName); + /// 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: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index fe846c3..bff60d0 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -243,6 +243,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get no_data_available => 'Keine Daten verfügbar'; + @override + String get no_games_created_yet => 'Noch keine Spielvorlagen erstellt'; + @override String get no_groups_created_yet => 'Noch keine Gruppen erstellt'; @@ -382,6 +385,10 @@ class AppLocalizationsDe extends AppLocalizations { return 'Spieler:in $playerName erfolgreich hinzugefügt'; } + @override + String get there_are_no_games_matching_your_search => + 'Es gibt keine Spielvorlagen, die deiner Suche entspricht'; + @override String get there_is_no_group_matching_your_search => 'Es gibt keine Gruppe, die deiner Suche entspricht'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 899e3a5..ae7d813 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -243,6 +243,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no_data_available => 'No data available'; + @override + String get no_games_created_yet => 'No games created yet'; + @override String get no_groups_created_yet => 'No groups created yet'; @@ -382,6 +385,10 @@ class AppLocalizationsEn extends AppLocalizations { return 'Successfully added player $playerName'; } + @override + String get there_are_no_games_matching_your_search => + 'There are no games matching your search'; + @override String get there_is_no_group_matching_your_search => 'There is no group matching your search'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 78a10f1..c019213 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -9,6 +9,7 @@ import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game_view.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; +import 'package:tallee/presentation/widgets/top_centered_message.dart'; class ChooseGameView extends StatefulWidget { /// A view that allows the user to choose a game from a list of available games @@ -134,65 +135,86 @@ class _ChooseGameViewState extends State { // Game list Expanded( - child: ListView.builder( - itemCount: filteredGames.length, - itemBuilder: (BuildContext context, int index) { - final game = filteredGames[index]; - return GameTile( - title: game.name, - description: game.description, - badgeText: translateRulesetToString(game.ruleset, context), - badgeColor: getColorFromGameColor(game.color), - isHighlighted: selectedGameId == game.id, - onTap: () async { - setState(() { - if (selectedGameId == game.id) { - selectedGameId = ''; - } else { - selectedGameId = game.id; - } - }); - }, - onLongPress: () async { - final result = await Navigator.push( + child: Visibility( + visible: filteredGames.isNotEmpty, + replacement: Visibility( + visible: widget.games.isNotEmpty, + replacement: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: loc.no_games_created_yet, + ), + child: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: AppLocalizations.of( + context, + ).there_are_no_games_matching_your_search, + ), + ), + child: ListView.builder( + itemCount: filteredGames.length, + itemBuilder: (BuildContext context, int index) { + final game = filteredGames[index]; + return GameTile( + title: game.name, + description: game.description, + badgeText: translateRulesetToString( + game.ruleset, context, - adaptivePageRoute( - builder: (context) => CreateGameView( - gameToEdit: game, - matchCount: getMatchCount(game), - onGameChanged: () { - widget.onGamesUpdated?.call(); - }, + ), + 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 != 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(); } - 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(); - } - }, - ); - }, + }, + ); + }, + ), ), ), ], From fbb83aaf7b4981d01a409c2f22c86904f7500314 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 17:17:33 +0200 Subject: [PATCH 28/33] fix: localization plural --- lib/l10n/arb/app_de.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index dd8c744..9107bff 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -34,7 +34,7 @@ "delete": "Löschen", "delete_all_data": "Alle Daten löschen", "delete_game": "Spielvorlage löschen", - "delete_game_with_matches_warning": "Wenn du diese Spielvorlage löschst, werden {count, plural, =1{1 Spiel} other{{count} Spiele}} mit dieser Spielvorlage ebenfalls gelöscht.", + "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": { From 5c9db7244a5f2d5e7270fa1bb3de6f76865e6f5b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 17:22:16 +0200 Subject: [PATCH 29/33] fix: adjusted test --- test/db_tests/aggregates/match_test.dart | 32 ++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 7f627f7..0c5b1df 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -261,28 +261,28 @@ void main() { }); test('getMatchCount() works correctly', () async { - var matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); + var count = await database.matchDao.getMatchCount(); + expect(count, 0); await database.matchDao.addMatch(match: testMatch1); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); + count = await database.matchDao.getMatchCount(); + expect(count, 1); await database.matchDao.addMatch(match: testMatch2); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 2); + count = await database.matchDao.getMatchCount(); + expect(count, 2); await database.matchDao.deleteMatch(matchId: testMatch1.id); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); + count = await database.matchDao.getMatchCount(); + expect(count, 1); await database.matchDao.deleteMatch(matchId: testMatch2.id); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); + count = await database.matchDao.getMatchCount(); + expect(count, 0); }); test('getMatchCountByGame() works correctly', () async { @@ -302,6 +302,18 @@ void main() { 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 { From f7c8160c581202569fdcd7bacd4c313d7022a5fb Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 17:28:57 +0200 Subject: [PATCH 30/33] fix: removed print in test --- test/db_tests/aggregates/match_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 0c5b1df..37c1cd0 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -449,7 +449,6 @@ void main() { await database.matchDao.addMatch(match: testMatch1); DateTime newEndedAt = DateTime(2030, 1, 1, 12, 0, 0); - print(newEndedAt); await database.matchDao.updateMatchEndedAt( matchId: testMatch1.id, endedAt: newEndedAt, From ae572a5dbd4ed07aa253d801fa1fc2b4445cad17 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 18:55:00 +0200 Subject: [PATCH 31/33] Refactored trailing widgets --- lib/l10n/generated/app_localizations_de.dart | 6 +- .../create_match/create_game_view.dart | 398 ++++++++---------- 2 files changed, 186 insertions(+), 218 deletions(-) diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index bff60d0..3666d11 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -119,10 +119,10 @@ class AppLocalizationsDe extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count Spiele', - one: '1 Spiel', + other: 'werden $count Spiele', + one: 'wird 1 Spiel', ); - return 'Wenn du diese Spielvorlage löschst, werden $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; + return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; } @override diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 3f4169e..e0f9d85 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -214,223 +214,10 @@ class _CreateGameViewState extends State { // Choose ruleset tile if (!isEditMode()) - ChooseTile( - title: loc.ruleset, - trailing: 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), - ), - ], - ), - ), - ), + ChooseTile(title: loc.ruleset, trailing: getColorDropdown(loc)), // Choose color tile - ChooseTile( - title: loc.color, - trailing: - //Popup - 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, - ), - ), - ], - ), - ), - ), + ChooseTile(title: loc.color, trailing: getRulesetDropdown(loc)), // Description input field Container( @@ -549,4 +336,185 @@ class _CreateGameViewState extends State { 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), + ), + ], + ), + ); + } } From 84f8a77c7283843ab7329748c2b67cf2860abf2d Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Sat, 9 May 2026 17:14:43 +0000 Subject: [PATCH 32/33] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 28a008c..5e3ec4c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.27+261 +version: 0.0.28+262 environment: sdk: ^3.8.1 From 9e76acce29c2f5819a3df2eed96c14e3ef1f116e Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Sat, 9 May 2026 17:15:22 +0000 Subject: [PATCH 33/33] Updated licenses [skip ci] --- .../settings_view/licenses/oss_licenses.dart | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 396d7b7..92f3080 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -55,6 +55,7 @@ const allDependencies = [ _flutter_lints, _flutter_localizations, _flutter_plugin_android_lifecycle, + _flutter_popup, _flutter_test, _flutter_web_plugins, _fluttericon, @@ -168,6 +169,7 @@ const dependencies = [ _file_saver, _flutter, _flutter_localizations, + _flutter_popup, _fluttericon, _font_awesome_flutter, _intl, @@ -2628,6 +2630,41 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); +/// flutter_popup 3.3.9 +const _flutter_popup = Package( + name: 'flutter_popup', + description: 'The flutter_popup package is a versatile tool for creating customizable popups in Flutter apps. Its highlight feature effectively guides user attention to specific areas', + homepage: 'https://github.com/herowws/flutter_popup', + authors: [], + version: '3.3.9', + spdxIdentifiers: ['MIT'], + isMarkdown: false, + isSdk: false, + dependencies: [PackageRef('flutter')], + devDependencies: [PackageRef('flutter_lints'), PackageRef('flutter_test')], + license: '''MIT License + +Copyright (c) 2023 mopriestt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''', + ); + /// flutter_test null const _flutter_test = Package( name: 'flutter_test', @@ -37676,16 +37713,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.27+261 +/// tallee 0.0.28+262 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.27+261', + version: '0.0.28+262', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], + dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('flutter_popup'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], devDependencies: [PackageRef('flutter_test'), PackageRef('build_runner'), PackageRef('dart_pubspec_licenses'), PackageRef('drift_dev'), PackageRef('flutter_lints')], license: '''GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007