From 975679b048fab162d10fdadf5eeca57308c3efb9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 8 Mar 2026 08:24:35 +0100 Subject: [PATCH 01/60] Updated dependencie --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e13f0cc..6192380 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dev_dependencies: build_runner: ^2.5.4 dart_pubspec_licenses: ^3.0.14 drift_dev: ^2.27.0 - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 flutter: uses-material-design: true From 494dec8c614b98591ca05eb8a1532018cab775c3 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 8 Mar 2026 08:29:15 +0100 Subject: [PATCH 02/60] Added localizations --- lib/l10n/arb/app_de.arb | 4 +++- lib/l10n/arb/app_en.arb | 10 +++++++++- lib/l10n/generated/app_localizations.dart | 14 +++++++++++++- lib/l10n/generated/app_localizations_de.dart | 9 ++++++++- lib/l10n/generated/app_localizations_en.dart | 9 ++++++++- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index e0efc2c..0a114a6 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -24,6 +24,7 @@ "delete_all_data": "Alle Daten löschen", "delete_group": "Gruppe löschen", "edit_group": "Gruppe bearbeiten", + "enter_points": "Punkte eingeben", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_reading_file": "Fehler beim Lesen der Datei", "export_canceled": "Export abgebrochen", @@ -77,7 +78,8 @@ "ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.", "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", - "select_winner": "Gewinner:in wählen:", + "select_winner": "Gewinner:in wählen", + "select_loser": "Verlierer:in wählen", "selected_players": "Ausgewählte Spieler:innen", "settings": "Einstellungen", "single_loser": "Ein:e Verlierer:in", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6a64a1b..ecc5384 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -77,6 +77,9 @@ "@edit_group": { "description": "Button text to edit a group" }, + "@enter_points": { + "description": "Label to enter players points" + }, "@error_creating_group": { "description": "Error message when group creation fails" }, @@ -244,6 +247,9 @@ "@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" }, @@ -322,6 +328,7 @@ "delete_all_data": "Delete all data", "delete_group": "Delete Group", "edit_group": "Edit Group", + "enter_points": "Enter points", "error_creating_group": "Error while creating group, please try again", "error_reading_file": "Error reading file", "export_canceled": "Export canceled", @@ -375,7 +382,8 @@ "ruleset_single_winner": "Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.", "search_for_groups": "Search for groups", "search_for_players": "Search for players", - "select_winner": "Select Winner:", + "select_winner": "Select Winner", + "select_loser": "Select Loser", "selected_players": "Selected players", "settings": "Settings", "single_loser": "Single Loser", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 87fab99..71a2805 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -242,6 +242,12 @@ abstract class AppLocalizations { /// **'Edit Group'** String get edit_group; + /// Label to enter players points + /// + /// In en, this message translates to: + /// **'Enter points'** + String get enter_points; + /// Error message when group creation fails /// /// In en, this message translates to: @@ -563,9 +569,15 @@ abstract class AppLocalizations { /// Label to select the winner /// /// In en, this message translates to: - /// **'Select Winner:'** + /// **'Select Winner'** String get select_winner; + /// Label to select the loser + /// + /// In en, this message translates to: + /// **'Select Loser'** + String get select_loser; + /// Shows the number of selected players /// /// 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 54f3bc7..67a68d6 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -84,6 +85,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get edit_group => 'Gruppe bearbeiten'; + @override + String get enter_points => 'Punkte eingeben'; + @override String get error_creating_group => 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; @@ -252,7 +256,10 @@ class AppLocalizationsDe extends AppLocalizations { String get search_for_players => 'Nach Spieler:innen suchen'; @override - String get select_winner => 'Gewinner:in wählen:'; + String get select_winner => 'Gewinner:in wählen'; + + @override + String get select_loser => 'Verlierer:in wählen'; @override String get selected_players => 'Ausgewählte Spieler:innen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 440ac10..93ed947 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -84,6 +85,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get edit_group => 'Edit Group'; + @override + String get enter_points => 'Enter points'; + @override String get error_creating_group => 'Error while creating group, please try again'; @@ -252,7 +256,10 @@ class AppLocalizationsEn extends AppLocalizations { String get search_for_players => 'Search for players'; @override - String get select_winner => 'Select Winner:'; + String get select_winner => 'Select Winner'; + + @override + String get select_loser => 'Select Loser'; @override String get selected_players => 'Selected players'; From c50ad288fa06b0c5b469421b4b4645aaaa6185eb Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 8 Mar 2026 08:29:45 +0100 Subject: [PATCH 03/60] Implemented basic structure --- .../match_view/match_result_view.dart | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 4f3f0c0..e6b55e5 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.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/dto/match.dart'; import 'package:tallee/data/dto/player.dart'; @@ -11,11 +12,18 @@ class MatchResultView extends StatefulWidget { /// A view that allows selecting and saving the winner of a match /// [match]: The match for which the winner is to be selected /// [onWinnerChanged]: Optional callback invoked when the winner is changed - const MatchResultView({super.key, required this.match, this.onWinnerChanged}); + const MatchResultView({ + super.key, + required this.match, + this.ruleset = Ruleset.singleWinner, + this.onWinnerChanged, + }); /// The match for which the winner is to be selected final Match match; + final Ruleset ruleset; + /// Optional callback invoked when the winner is changed final VoidCallback? onWinnerChanged; @@ -47,6 +55,7 @@ class _MatchResultViewState extends State { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); + return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( @@ -82,7 +91,7 @@ class _MatchResultViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - loc.select_winner, + '${getTitleForRuleset(loc)}:', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -96,7 +105,7 @@ class _MatchResultViewState extends State { setState(() { _selectedPlayer = value; }); - await _handleWinnerSaving(); + await _handleSaving(); }, child: ListView.builder( itemCount: allPlayers.length, @@ -115,7 +124,7 @@ class _MatchResultViewState extends State { (_selectedPlayer = value); } }); - await _handleWinnerSaving(); + await _handleSaving(); }, ); }, @@ -134,16 +143,42 @@ class _MatchResultViewState extends State { /// Handles saving or removing the winner in the database /// based on the current selection. - Future _handleWinnerSaving() async { + Future _handleSaving() async { + if (widget.ruleset == Ruleset.singleWinner) { + await _handleWinner(); + } else if (widget.ruleset == Ruleset.singleLoser) { + await _handleLoser(); + } else if (widget.ruleset == Ruleset.lowestScore || + widget.ruleset == Ruleset.highestScore) { + await _handleScores(); + } + + widget.onWinnerChanged?.call(); + } + + Future _handleWinner() async { if (_selectedPlayer == null) { - await db.matchDao.removeWinner(matchId: widget.match.id); + return await db.matchDao.removeWinner(matchId: widget.match.id); } else { - await db.matchDao.setWinner( + return await db.matchDao.setWinner( matchId: widget.match.id, winnerId: _selectedPlayer!.id, ); } - widget.onWinnerChanged?.call(); + } + + Future _handleLoser() async { + if (_selectedPlayer == null) { + //TODO: removeLoser() method + return false; + } else { + //TODO: setLoser() method + return false; + } + } + + Future _handleScores() async { + return false; } /// Retrieves all players associated with the given [match]. @@ -162,4 +197,15 @@ class _MatchResultViewState extends State { players.sort((a, b) => a.name.compareTo(b.name)); return players; } + + String getTitleForRuleset(AppLocalizations loc) { + switch (widget.ruleset) { + case Ruleset.singleWinner: + return loc.select_winner; + case Ruleset.singleLoser: + return loc.select_loser; + default: + return loc.enter_points; + } + } } From 84b8541822122538b0cc1d500bd2bdbbff041dce Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 8 Mar 2026 11:00:42 +0100 Subject: [PATCH 04/60] Fixed match edit error with game --- .../main_menu/match_view/create_match/create_match_view.dart | 1 + 1 file changed, 1 insertion(+) 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 1bf732c..8138957 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 @@ -363,6 +363,7 @@ class _CreateMatchViewState extends State { final match = widget.matchToEdit!; _matchNameController.text = match.name; selectedPlayers = match.players; + selectedGameIndex = 0; if (match.group != null) { selectedGroup = match.group; From d5a7bb320f9d0ff776cd64fa639e4074b72a6c18 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 8 Mar 2026 22:25:23 +0100 Subject: [PATCH 05/60] Implememented different result tiles in match detail view for different rulesets --- lib/core/custom_theme.dart | 12 +- lib/l10n/arb/app_de.arb | 2 + lib/l10n/arb/app_en.arb | 5 + lib/l10n/generated/app_localizations.dart | 12 ++ lib/l10n/generated/app_localizations_de.dart | 6 + lib/l10n/generated/app_localizations_en.dart | 6 + .../match_view/match_detail_view.dart | 120 +++++++++++++----- 7 files changed, 126 insertions(+), 37 deletions(-) diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index d1b158e..3274db9 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -85,21 +85,21 @@ class CustomTheme { ); static const SearchBarThemeData searchBarTheme = SearchBarThemeData( - textStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.textColor)), - hintStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.hintColor)), + textStyle: WidgetStatePropertyAll(TextStyle(color: textColor)), + hintStyle: WidgetStatePropertyAll(TextStyle(color: hintColor)), ); static final RadioThemeData radioTheme = RadioThemeData( fillColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { - return CustomTheme.primaryColor; + return primaryColor; } - return CustomTheme.textColor; + return textColor; }), ); static const InputDecorationTheme inputDecorationTheme = InputDecorationTheme( - labelStyle: TextStyle(color: CustomTheme.textColor), - hintStyle: TextStyle(color: CustomTheme.hintColor), + labelStyle: TextStyle(color: textColor), + hintStyle: TextStyle(color: hintColor), ); } diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 2046dde..47a092f 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -75,6 +75,7 @@ "player_name": "Spieler:innenname", "players": "Spieler:innen", "players_count": "{count} Spieler", + "points": "Punkte", "privacy_policy": "Datenschutzerklärung", "quick_create": "Schnellzugriff", "recent_matches": "Letzte Spiele", @@ -95,6 +96,7 @@ "single_loser": "Ein:e Verlierer:in", "single_winner": "Ein:e Gewinner:in", "highest_score": "Höchste Punkte", + "loser": "Verlierer:in", "lowest_score": "Niedrigste Punkte", "multiple_winners": "Mehrere Gewinner:innen", "statistics": "Statistiken", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index c992a01..a30d376 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -235,6 +235,9 @@ } } }, + "@points": { + "description": "Points label" + }, "@privacy_policy": { "description": "Privacy policy menu item" }, @@ -406,6 +409,7 @@ "player_name": "Player name", "players": "Players", "players_count": "{count} Players", + "points": "Points", "privacy_policy": "Privacy Policy", "quick_create": "Quick Create", "recent_matches": "Recent Matches", @@ -425,6 +429,7 @@ "single_loser": "Single Loser", "single_winner": "Single Winner", "highest_score": "Highest Score", + "loser": "Loser", "lowest_score": "Lowest Score", "multiple_winners": "Multiple Winners", "statistics": "Statistics", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index f5ba224..456a6fc 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -548,6 +548,12 @@ abstract class AppLocalizations { /// **'{count} Players'** String players_count(int count); + /// Points label + /// + /// In en, this message translates to: + /// **'Points'** + String get points; + /// Privacy policy menu item /// /// In en, this message translates to: @@ -662,6 +668,12 @@ abstract class AppLocalizations { /// **'Highest Score'** String get highest_score; + /// No description provided for @loser. + /// + /// In en, this message translates to: + /// **'Loser'** + String get loser; + /// No description provided for @lowest_score. /// /// 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 7a574c0..a3a1b26 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 { return '$count Spieler'; } + @override + String get points => 'Punkte'; + @override String get privacy_policy => 'Datenschutzerklärung'; @@ -304,6 +307,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get highest_score => 'Höchste Punkte'; + @override + String get loser => 'Verlierer:in'; + @override String get lowest_score => 'Niedrigste Punkte'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 48bc6ab..61b8934 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 { return '$count Players'; } + @override + String get points => 'Points'; + @override String get privacy_policy => 'Privacy Policy'; @@ -304,6 +307,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get highest_score => 'Highest Score'; + @override + String get loser => 'Loser'; + @override String get lowest_score => 'Lowest Score'; 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 1deba18..eab3899 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 @@ -4,6 +4,7 @@ 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/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/dto/match.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; @@ -175,37 +176,7 @@ class _MatchDetailViewState extends State { vertical: 4, horizontal: 8, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - /// TODO: Implement different ruleset results display - if (match.winner != null) ...[ - Text( - loc.winner, - style: const TextStyle( - fontSize: 16, - color: CustomTheme.textColor, - ), - ), - Text( - match.winner!.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, - ), - ), - ] else ...[ - Text( - loc.no_results_entered_yet, - style: const TextStyle( - fontSize: 14, - color: CustomTheme.textColor, - ), - ), - ], - ], - ), + child: getResultWidget(loc), ), ), ], @@ -264,4 +235,91 @@ class _MatchDetailViewState extends State { }); widget.onMatchUpdate.call(); } + + /// Returns the widget to be displayed in the result [InfoTile] + /// TODO: Update when score logic is overhauled + Widget getResultWidget(AppLocalizations loc) { + if (isSingleRowResult()) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: getResultRow(loc), + ); + } else { + return Column( + children: [ + for (var player in match.players) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + player.name, + style: const TextStyle( + fontSize: 16, + color: CustomTheme.textColor, + ), + ), + Text( + '0 ${loc.points}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), + ), + ], + ), + ], + ); + } + } + + /// Returns the result row for single winner/loser rulesets or a placeholder + /// if no result is entered yet + /// TODO: Update when score logic is overhauled + List getResultRow(AppLocalizations loc) { + if (match.winner != null && match.game.ruleset == Ruleset.singleWinner) { + return [ + Text( + loc.winner, + style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), + ), + Text( + match.winner!.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), + ), + ]; + } else if (match.game.ruleset == Ruleset.singleLoser) { + return [ + Text( + loc.loser, + style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), + ), + Text( + match.winner!.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), + ), + ]; + } else { + return [ + Text( + loc.no_results_entered_yet, + style: const TextStyle(fontSize: 14, color: CustomTheme.textColor), + ), + ]; + } + } + + // Returns if the result can be displayed in a single row + bool isSingleRowResult() { + return match.game.ruleset == Ruleset.singleWinner || + match.game.ruleset == Ruleset.singleLoser; + } } From f0c575d2c9eed6001dc73b8bff39ce469f193b55 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 8 Mar 2026 22:25:55 +0100 Subject: [PATCH 06/60] Implemented different result view depending on ruleset --- .../match_view/match_result_view.dart | 128 +++++++++++++----- .../widgets/tiles/score_list_tile.dart | 91 +++++++++++++ 2 files changed, 188 insertions(+), 31 deletions(-) create mode 100644 lib/presentation/widgets/tiles/score_list_tile.dart diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index a3904b7..94e392b 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -6,7 +6,9 @@ import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/dto/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart'; class MatchResultView extends StatefulWidget { /// A view that allows selecting and saving the winner of a match @@ -22,6 +24,8 @@ class MatchResultView extends StatefulWidget { /// The match for which the winner is to be selected final Match match; + /// The ruleset of the match, determines how the winner is selected or how + /// scores are entered final Ruleset ruleset; /// Optional callback invoked when the winner is changed @@ -37,6 +41,9 @@ class _MatchResultViewState extends State { /// List of all players who participated in the match late final List allPlayers; + /// List of text controllers for score entry, one for each player + late final List controller; + /// Currently selected winner player Player? _selectedPlayer; @@ -47,10 +54,19 @@ class _MatchResultViewState extends State { allPlayers = widget.match.players; allPlayers.sort((a, b) => a.name.compareTo(b.name)); + controller = List.generate( + allPlayers.length, + (index) => TextEditingController(), + ); + if (widget.match.winner != null) { - _selectedPlayer = allPlayers.firstWhere( - (p) => p.id == widget.match.winner!.id, - ); + if (rulesetSupportsWinnerSelection()) { + _selectedPlayer = allPlayers.firstWhere( + (p) => p.id == widget.match.winner!.id, + ); + } else if (rulesetSupportsScoreEntry()) { + /// TODO: Update when score logic is overhauled + } } super.initState(); } @@ -101,43 +117,70 @@ class _MatchResultViewState extends State { ), ), const SizedBox(height: 10), - Expanded( - child: RadioGroup( - groupValue: _selectedPlayer, - onChanged: (Player? value) async { - setState(() { - _selectedPlayer = value; - }); - await _handleSaving(); - }, - child: ListView.builder( + if (rulesetSupportsWinnerSelection()) + Expanded( + child: RadioGroup( + groupValue: _selectedPlayer, + onChanged: (Player? value) async { + setState(() { + _selectedPlayer = value; + }); + }, + child: ListView.builder( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomRadioListTile( + text: allPlayers[index].name, + value: allPlayers[index], + onContainerTap: (value) async { + setState(() { + // Check if the already selected player is the same as the newly tapped player. + if (_selectedPlayer == value) { + // If yes deselected the player by setting it to null. + _selectedPlayer = null; + } else { + // If no assign the newly tapped player to the selected player. + (_selectedPlayer = value); + } + }); + }, + ); + }, + ), + ), + ), + if (rulesetSupportsScoreEntry()) + Expanded( + child: ListView.separated( itemCount: allPlayers.length, itemBuilder: (context, index) { - return CustomRadioListTile( + print(allPlayers[index].name); + return ScoreListTile( text: allPlayers[index].name, - value: allPlayers[index], - onContainerTap: (value) async { - setState(() { - // Check if the already selected player is the same as the newly tapped player. - if (_selectedPlayer == value) { - // If yes deselected the player by setting it to null. - _selectedPlayer = null; - } else { - // If no assign the newly tapped player to the selected player. - (_selectedPlayer = value); - } - }); - await _handleSaving(); - }, + controller: controller[index], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Divider(indent: 20), ); }, ), ), - ), ], ), ), ), + CustomWidthButton( + text: loc.save_changes, + sizeRelativeToWidth: 0.95, + onPressed: () async { + await _handleSaving(); + if (!context.mounted) return; + Navigator.of(context).pop(_selectedPlayer); + }, + ), ], ), ), @@ -172,15 +215,28 @@ class _MatchResultViewState extends State { Future _handleLoser() async { if (_selectedPlayer == null) { - //TODO: removeLoser() method + /// TODO: Update when score logic is overhauled return false; } else { - //TODO: setLoser() method + /// TODO: Update when score logic is overhauled return false; } } + /// Handles saving the scores for each player in the database. Future _handleScores() async { + for (int i = 0; i < allPlayers.length; i++) { + var text = controller[i].text; + if (text.isEmpty) { + text = '0'; + } + final score = int.parse(text); + await db.playerMatchDao.updatePlayerScore( + matchId: widget.match.id, + playerId: allPlayers[i].id, + newScore: score, + ); + } return false; } @@ -194,4 +250,14 @@ class _MatchResultViewState extends State { return loc.enter_points; } } + + bool rulesetSupportsWinnerSelection() { + return widget.ruleset == Ruleset.singleWinner || + widget.ruleset == Ruleset.singleLoser; + } + + bool rulesetSupportsScoreEntry() { + return widget.ruleset == Ruleset.lowestScore || + widget.ruleset == Ruleset.highestScore; + } } diff --git a/lib/presentation/widgets/tiles/score_list_tile.dart b/lib/presentation/widgets/tiles/score_list_tile.dart new file mode 100644 index 0000000..d6aafe3 --- /dev/null +++ b/lib/presentation/widgets/tiles/score_list_tile.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; + +class ScoreListTile extends StatelessWidget { + /// A custom list tile widget that has a text field for inputting a score. + /// - [text]: The leading text to be displayed. + /// - [controller]: The controller for the text field to input the score. + const ScoreListTile({ + super.key, + required this.text, + required this.controller, + /* + required this.onContainerTap, +*/ + }); + + /// The text to display next to the radio button. + final String text; + + final TextEditingController controller; + + /// The callback invoked when the container is tapped. + /* + final ValueChanged onContainerTap; +*/ + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 20), + decoration: const BoxDecoration(color: CustomTheme.boxColor), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + text, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500), + ), + SizedBox( + width: 100, + height: 40, + child: TextField( + controller: controller, + keyboardType: TextInputType.number, + maxLength: 4, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: CustomTheme.textColor, + ), + cursorColor: CustomTheme.textColor, + decoration: InputDecoration( + hintText: loc.points, + counterText: '', + filled: true, + fillColor: CustomTheme.onBoxColor, + contentPadding: const EdgeInsets.symmetric( + horizontal: 0, + vertical: 0, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: CustomTheme.textColor.withAlpha(100), + width: 2, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + color: CustomTheme.primaryColor, + width: 2, + ), + ), + ), + ), + ), + ], + ), + ); + } +} From 9b2fcf18608f2be8138c15734163177faae5907f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 8 Mar 2026 22:27:27 +0100 Subject: [PATCH 07/60] Fix: Linter exclude --- analysis_options.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/analysis_options.yaml b/analysis_options.yaml index 04172d4..dc1e1c5 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,5 +1,9 @@ include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart + linter: rules: avoid_print: false From ddf32797aa240dc1e672fc6f8b2c8da53f2966b3 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 14 Apr 2026 23:26:26 +0200 Subject: [PATCH 08/60] Implemented ruleset in match view --- lib/l10n/generated/app_localizations.dart | 37 +++++++++++-------- lib/l10n/generated/app_localizations_de.dart | 30 ++++++++++----- lib/l10n/generated/app_localizations_en.dart | 27 +++++++++----- .../widgets/tiles/match_tile.dart | 18 +++++++++ 4 files changed, 77 insertions(+), 35 deletions(-) diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 66d5b88..456a6fc 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -62,7 +62,8 @@ import 'app_localizations_en.dart'; /// be consistent with the languages listed in the AppLocalizations.supportedLocales /// property. abstract class AppLocalizations { - AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -70,7 +71,8 @@ abstract class AppLocalizations { return Localizations.of(context, AppLocalizations)!; } - static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. @@ -82,17 +84,18 @@ abstract class AppLocalizations { /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. - static const List> localizationsDelegates = >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ Locale('de'), - Locale('en') + Locale('en'), ]; /// Label for all players list @@ -756,7 +759,8 @@ abstract class AppLocalizations { String get yesterday_at; } -class _AppLocalizationsDelegate extends LocalizationsDelegate { +class _AppLocalizationsDelegate + extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override @@ -765,25 +769,26 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate } @override - bool isSupported(Locale locale) => ['de', 'en'].contains(locale.languageCode); + bool isSupported(Locale locale) => + ['de', 'en'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; } AppLocalizations lookupAppLocalizations(Locale locale) { - - // Lookup logic when only language code is specified. switch (locale.languageCode) { - case 'de': return AppLocalizationsDe(); - case 'en': return AppLocalizationsEn(); + case 'de': + return AppLocalizationsDe(); + case 'en': + return AppLocalizationsEn(); } throw FlutterError( 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 'an issue with the localizations generation tool. Please file an issue ' 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.' + 'that was used.', ); } diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 82c0081..a3a1b26 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -97,13 +97,16 @@ class AppLocalizationsDe extends AppLocalizations { String get enter_results => 'Ergebnisse eintragen'; @override - String get error_creating_group => 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; + String get error_creating_group => + 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; @override - String get error_deleting_group => 'Fehler beim Löschen der Gruppe, bitte erneut versuchen'; + String get error_deleting_group => + 'Fehler beim Löschen der Gruppe, bitte erneut versuchen'; @override - String get error_editing_group => 'Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen'; + String get error_editing_group => + 'Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen'; @override String get error_reading_file => 'Fehler beim Lesen der Datei'; @@ -199,7 +202,8 @@ class AppLocalizationsDe extends AppLocalizations { String get no_players_created_yet => 'Noch keine Spieler:in erstellt'; @override - String get no_players_found_with_that_name => 'Keine Spieler:in mit diesem Namen gefunden'; + String get no_players_found_with_that_name => + 'Keine Spieler:in mit diesem Namen gefunden'; @override String get no_players_selected => 'Keine Spieler:innen ausgewählt'; @@ -258,16 +262,20 @@ class AppLocalizationsDe extends AppLocalizations { String get ruleset => 'Regelwerk'; @override - String get ruleset_least_points => 'Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.'; + String get ruleset_least_points => + 'Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.'; @override - String get ruleset_most_points => 'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.'; + String get ruleset_most_points => + 'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.'; @override - String get ruleset_single_loser => 'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.'; + String get ruleset_single_loser => + 'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.'; @override - String get ruleset_single_winner => 'Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.'; + String get ruleset_single_winner => + 'Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.'; @override String get save_changes => 'Änderungen speichern'; @@ -320,10 +328,12 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get there_is_no_group_matching_your_search => 'Es gibt keine Gruppe, die deiner Suche entspricht'; + String get there_is_no_group_matching_your_search => + 'Es gibt keine Gruppe, die deiner Suche entspricht'; @override - String get this_cannot_be_undone => 'Dies kann nicht rückgängig gemacht werden.'; + String get this_cannot_be_undone => + 'Dies kann nicht rückgängig gemacht werden.'; @override String get today_at => 'Heute um'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index cbbfb3c..61b8934 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -97,13 +97,16 @@ class AppLocalizationsEn extends AppLocalizations { String get enter_results => 'Enter Results'; @override - String get error_creating_group => 'Error while creating group, please try again'; + String get error_creating_group => + 'Error while creating group, please try again'; @override - String get error_deleting_group => 'Error while deleting group, please try again'; + String get error_deleting_group => + 'Error while deleting group, please try again'; @override - String get error_editing_group => 'Error while editing group, please try again'; + String get error_editing_group => + 'Error while editing group, please try again'; @override String get error_reading_file => 'Error reading file'; @@ -199,7 +202,8 @@ class AppLocalizationsEn extends AppLocalizations { String get no_players_created_yet => 'No players created yet'; @override - String get no_players_found_with_that_name => 'No players found with that name'; + String get no_players_found_with_that_name => + 'No players found with that name'; @override String get no_players_selected => 'No players selected'; @@ -258,16 +262,20 @@ class AppLocalizationsEn extends AppLocalizations { String get ruleset => 'Ruleset'; @override - String get ruleset_least_points => 'Inverse scoring: the player with the fewest points wins.'; + String get ruleset_least_points => + 'Inverse scoring: the player with the fewest points wins.'; @override - String get ruleset_most_points => 'Traditional ruleset: the player with the most points wins.'; + String get ruleset_most_points => + 'Traditional ruleset: the player with the most points wins.'; @override - String get ruleset_single_loser => 'Exactly one loser is determined; last place receives the penalty or consequence.'; + String get ruleset_single_loser => + 'Exactly one loser is determined; last place receives the penalty or consequence.'; @override - String get ruleset_single_winner => 'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'; + String get ruleset_single_winner => + 'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'; @override String get save_changes => 'Save Changes'; @@ -320,7 +328,8 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get there_is_no_group_matching_your_search => 'There is no group matching your search'; + String get there_is_no_group_matching_your_search => + 'There is no group matching your search'; @override String get this_cannot_be_undone => 'This can\'t be undone.'; diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 39f9cdf..4977b8e 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -79,6 +79,24 @@ class _MatchTileState extends State { ], ), + const SizedBox(height: 4), + + Container( + decoration: BoxDecoration( + color: CustomTheme.primaryColor, + borderRadius: BorderRadius.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, + ), + ), + ), + const SizedBox(height: 8), if (group != null) ...[ From 15702a108dac210ed4548a3a64abca71c95297da Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 15:18:27 +0200 Subject: [PATCH 09/60] Add: custom dialog action --- .../widgets/dialog/custom_dialog_action.dart | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/presentation/widgets/dialog/custom_dialog_action.dart diff --git a/lib/presentation/widgets/dialog/custom_dialog_action.dart b/lib/presentation/widgets/dialog/custom_dialog_action.dart new file mode 100644 index 0000000..dbf47f4 --- /dev/null +++ b/lib/presentation/widgets/dialog/custom_dialog_action.dart @@ -0,0 +1,29 @@ +import 'package:flutter/cupertino.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/presentation/widgets/dialog/animated_dialog_button.dart'; + +class CustomDialogAction extends StatelessWidget { + const CustomDialogAction({ + super.key, + required this.onPressed, + required this.text, + this.buttonType = ButtonType.primary, + }); + + // The text displaed on the button + final String text; + + // The type of the button, which determines its styling + final ButtonType buttonType; + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return AnimatedDialogButton( + onPressed: onPressed, + text: text, + buttonType: buttonType, + ); + } +} From 55437d83c4fc6004859deed45cdf3f3867a5ff66 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 15:19:30 +0200 Subject: [PATCH 10/60] Changed popup class structure --- .../buttons/animated_dialog_button.dart | 50 ----------- .../dialog/animated_dialog_button.dart | 84 +++++++++++++++++++ .../{ => dialog}/custom_alert_dialog.dart | 18 ++-- 3 files changed, 95 insertions(+), 57 deletions(-) delete mode 100644 lib/presentation/widgets/buttons/animated_dialog_button.dart create mode 100644 lib/presentation/widgets/dialog/animated_dialog_button.dart rename lib/presentation/widgets/{ => dialog}/custom_alert_dialog.dart (74%) diff --git a/lib/presentation/widgets/buttons/animated_dialog_button.dart b/lib/presentation/widgets/buttons/animated_dialog_button.dart deleted file mode 100644 index 798edfa..0000000 --- a/lib/presentation/widgets/buttons/animated_dialog_button.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tallee/core/custom_theme.dart'; - -class AnimatedDialogButton extends StatefulWidget { - /// A custom animated button widget that provides a scaling and opacity effect - /// when pressed. - /// - [onPressed]: Callback function that is triggered when the button is pressed. - /// - [child]: The child widget to be displayed inside the button, typically a text or icon. - const AnimatedDialogButton({ - super.key, - required this.onPressed, - required this.child, - }); - - /// Callback function that is triggered when the button is pressed. - final VoidCallback onPressed; - - /// The child widget to be displayed inside the button, typically a text or icon. - final Widget child; - - @override - State createState() => _AnimatedDialogButtonState(); -} - -class _AnimatedDialogButtonState extends State { - bool _isPressed = false; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTapDown: (_) => setState(() => _isPressed = true), - onTapUp: (_) => setState(() => _isPressed = false), - onTapCancel: () => setState(() => _isPressed = false), - onTap: widget.onPressed, - child: AnimatedScale( - scale: _isPressed ? 0.95 : 1.0, - duration: const Duration(milliseconds: 100), - child: AnimatedOpacity( - opacity: _isPressed ? 0.6 : 1.0, - duration: const Duration(milliseconds: 100), - child: Container( - decoration: CustomTheme.standardBoxDecoration, - padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 6), - child: widget.child, - ), - ), - ), - ); - } -} diff --git a/lib/presentation/widgets/dialog/animated_dialog_button.dart b/lib/presentation/widgets/dialog/animated_dialog_button.dart new file mode 100644 index 0000000..3875995 --- /dev/null +++ b/lib/presentation/widgets/dialog/animated_dialog_button.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/enums.dart'; + +class AnimatedDialogButton extends StatefulWidget { + /// A custom animated button widget that provides a scaling and opacity effect + /// when pressed. + /// - [onPressed]: Callback function that is triggered when the button is pressed. + /// - [child]: The child widget to be displayed inside the button, typically a text or icon. + const AnimatedDialogButton({ + super.key, + required this.onPressed, + required this.text, + this.buttonType = ButtonType.primary, + }); + + /// Callback function that is triggered when the button is pressed. + final VoidCallback onPressed; + + /// The text to be displayed on the button. + final String text; + + final ButtonType buttonType; + + @override + State createState() => _AnimatedDialogButtonState(); +} + +class _AnimatedDialogButtonState extends State { + bool _isPressed = false; + + @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(); + + return GestureDetector( + onTapDown: (_) => setState(() => _isPressed = true), + onTapUp: (_) => setState(() => _isPressed = false), + onTapCancel: () => setState(() => _isPressed = false), + onTap: widget.onPressed, + child: AnimatedScale( + scale: _isPressed ? 0.95 : 1.0, + duration: const Duration(milliseconds: 100), + child: AnimatedOpacity( + opacity: _isPressed ? 0.6 : 1.0, + duration: const Duration(milliseconds: 100), + child: Center( + child: Container( + constraints: const BoxConstraints(minWidth: 300), + decoration: buttonDecoration, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + margin: const EdgeInsets.symmetric(vertical: 8), + child: Text( + widget.text, + style: textStyling, + textAlign: TextAlign.center, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/custom_alert_dialog.dart b/lib/presentation/widgets/dialog/custom_alert_dialog.dart similarity index 74% rename from lib/presentation/widgets/custom_alert_dialog.dart rename to lib/presentation/widgets/dialog/custom_alert_dialog.dart index bf98f2c..606fc49 100644 --- a/lib/presentation/widgets/custom_alert_dialog.dart +++ b/lib/presentation/widgets/dialog/custom_alert_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; class CustomAlertDialog extends StatelessWidget { /// A custom alert dialog widget that provides a os unspecific AlertDialog, @@ -16,20 +17,23 @@ class CustomAlertDialog extends StatelessWidget { }); final String title; - final String content; - final List actions; + final Widget content; + final List actions; @override Widget build(BuildContext context) { return AlertDialog( - title: Text(title, style: const TextStyle(color: CustomTheme.textColor)), - content: Text( - content, - style: const TextStyle(color: CustomTheme.textColor), + title: Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: CustomTheme.textColor, + ), ), + content: content, actions: actions, backgroundColor: CustomTheme.boxColor, - actionsAlignment: MainAxisAlignment.spaceAround, + actionsAlignment: MainAxisAlignment.center, shape: RoundedRectangleBorder( borderRadius: CustomTheme.standardBorderRadiusAll, side: const BorderSide(color: CustomTheme.boxBorderColor), From 32e1c587d46952ea9101d7d8900d6cadcbfb6b32 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 15:19:38 +0200 Subject: [PATCH 11/60] Updated popups in views --- .../group_view/group_detail_view.dart | 28 +++++++----------- .../match_view/match_detail_view.dart | 28 +++++++----------- .../settings_view/settings_view.dart | 29 +++++++------------ 3 files changed, 32 insertions(+), 53 deletions(-) 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 1ef89ef..ab70d1a 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 @@ -3,6 +3,7 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.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/group.dart'; import 'package:tallee/data/models/match.dart'; @@ -10,10 +11,10 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; -import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart'; -import 'package:tallee/presentation/widgets/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -70,23 +71,16 @@ class _GroupDetailViewState extends State { context: context, builder: (context) => CustomAlertDialog( title: '${loc.delete_group}?', - content: loc.this_cannot_be_undone, + content: Text(loc.this_cannot_be_undone), actions: [ - AnimatedDialogButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text( - loc.cancel, - style: const TextStyle(color: CustomTheme.textColor), - ), - ), - AnimatedDialogButton( + CustomDialogAction( onPressed: () => Navigator.of(context).pop(true), - child: Text( - loc.delete, - style: const TextStyle( - color: CustomTheme.secondaryColor, - ), - ), + text: loc.delete, + ), + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(false), + buttonType: ButtonType.secondary, + text: loc.cancel, ), ], ), 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 fc53aa8..2ca6925 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 @@ -4,15 +4,16 @@ 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/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; -import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart'; -import 'package:tallee/presentation/widgets/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -64,23 +65,16 @@ class _MatchDetailViewState extends State { context: context, builder: (context) => CustomAlertDialog( title: '${loc.delete_match}?', - content: loc.this_cannot_be_undone, + content: Text(loc.this_cannot_be_undone), actions: [ - AnimatedDialogButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text( - loc.cancel, - style: const TextStyle(color: CustomTheme.textColor), - ), - ), - AnimatedDialogButton( + CustomDialogAction( onPressed: () => Navigator.of(context).pop(true), - child: Text( - loc.delete, - style: const TextStyle( - color: CustomTheme.secondaryColor, - ), - ), + text: loc.delete, + ), + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(false), + buttonType: ButtonType.secondary, + text: loc.cancel, ), ], ), diff --git a/lib/presentation/views/main_menu/settings_view/settings_view.dart b/lib/presentation/views/main_menu/settings_view/settings_view.dart index 6a558ad..8e1cbdc 100644 --- a/lib/presentation/views/main_menu/settings_view/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view/settings_view.dart @@ -9,8 +9,8 @@ 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/views/main_menu/settings_view/licenses_view.dart'; -import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; -import 'package:tallee/presentation/widgets/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; import 'package:tallee/presentation/widgets/tiles/settings_list_tile.dart'; import 'package:tallee/services/data_transfer_service.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -122,25 +122,16 @@ class _SettingsViewState extends State { context: context, builder: (context) => CustomAlertDialog( title: '${loc.delete_all_data}?', - content: loc.this_cannot_be_undone, + content: Text(loc.this_cannot_be_undone), actions: [ - AnimatedDialogButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text( - loc.cancel, - style: const TextStyle( - color: CustomTheme.textColor, - ), - ), - ), - AnimatedDialogButton( + CustomDialogAction( onPressed: () => Navigator.of(context).pop(true), - child: Text( - loc.delete, - style: const TextStyle( - color: CustomTheme.secondaryColor, - ), - ), + text: loc.delete, + ), + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(false), + buttonType: ButtonType.secondary, + text: loc.cancel, ), ], ), From 2ad369806744105e23c3a93af20167abc0ea9f0d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 15:25:22 +0200 Subject: [PATCH 12/60] Updated documentation --- .../dialog/animated_dialog_button.dart | 20 ++++++++++--------- .../widgets/dialog/custom_dialog_action.dart | 9 ++++++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/presentation/widgets/dialog/animated_dialog_button.dart b/lib/presentation/widgets/dialog/animated_dialog_button.dart index 3875995..624d20a 100644 --- a/lib/presentation/widgets/dialog/animated_dialog_button.dart +++ b/lib/presentation/widgets/dialog/animated_dialog_button.dart @@ -5,22 +5,24 @@ class AnimatedDialogButton extends StatefulWidget { /// A custom animated button widget that provides a scaling and opacity effect /// when pressed. /// - [onPressed]: Callback function that is triggered when the button is pressed. - /// - [child]: The child widget to be displayed inside the button, typically a text or icon. + /// - [buttonText]: The text to be displayed on the button. + /// - [buttonType]: The type of the button, which determines its styling. const AnimatedDialogButton({ super.key, required this.onPressed, - required this.text, + required this.buttonText, + this.constraints, this.buttonType = ButtonType.primary, }); - /// Callback function that is triggered when the button is pressed. - final VoidCallback onPressed; - - /// The text to be displayed on the button. - final String text; + final BoxConstraints? constraints; final ButtonType buttonType; + final String buttonText; + + final VoidCallback onPressed; + @override State createState() => _AnimatedDialogButtonState(); } @@ -66,12 +68,12 @@ class _AnimatedDialogButtonState extends State { duration: const Duration(milliseconds: 100), child: Center( child: Container( - constraints: const BoxConstraints(minWidth: 300), + constraints: widget.constraints, decoration: buttonDecoration, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), margin: const EdgeInsets.symmetric(vertical: 8), child: Text( - widget.text, + widget.buttonText, style: textStyling, textAlign: TextAlign.center, ), diff --git a/lib/presentation/widgets/dialog/custom_dialog_action.dart b/lib/presentation/widgets/dialog/custom_dialog_action.dart index dbf47f4..2882c76 100644 --- a/lib/presentation/widgets/dialog/custom_dialog_action.dart +++ b/lib/presentation/widgets/dialog/custom_dialog_action.dart @@ -3,6 +3,10 @@ import 'package:tallee/core/enums.dart'; import 'package:tallee/presentation/widgets/dialog/animated_dialog_button.dart'; class CustomDialogAction extends StatelessWidget { + /// A custom dialog action widget that represents a button in a dialog. + /// - [text]: The text to be displayed on the button. + /// - [buttonType]: The type of the button, which determines its styling. + /// - [onPressed]: Callback function that is triggered when the button is pressed. const CustomDialogAction({ super.key, required this.onPressed, @@ -10,10 +14,8 @@ class CustomDialogAction extends StatelessWidget { this.buttonType = ButtonType.primary, }); - // The text displaed on the button final String text; - // The type of the button, which determines its styling final ButtonType buttonType; final VoidCallback onPressed; @@ -22,8 +24,9 @@ class CustomDialogAction extends StatelessWidget { Widget build(BuildContext context) { return AnimatedDialogButton( onPressed: onPressed, - text: text, + buttonText: text, buttonType: buttonType, + constraints: const BoxConstraints(minWidth: 300), ); } } From ad2e4bc398b8e31e71a4e288a8912fb96ced963c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 15:56:11 +0200 Subject: [PATCH 13/60] Added constraint parameter --- .../widgets/dialog/animated_dialog_button.dart | 15 ++++++++------- .../widgets/dialog/custom_dialog_action.dart | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/presentation/widgets/dialog/animated_dialog_button.dart b/lib/presentation/widgets/dialog/animated_dialog_button.dart index 624d20a..70deea6 100644 --- a/lib/presentation/widgets/dialog/animated_dialog_button.dart +++ b/lib/presentation/widgets/dialog/animated_dialog_button.dart @@ -7,22 +7,23 @@ class AnimatedDialogButton extends StatefulWidget { /// - [onPressed]: Callback function that is triggered when the button is pressed. /// - [buttonText]: The text to be displayed on the button. /// - [buttonType]: The type of the button, which determines its styling. + /// - [buttonConstraints]: Optional constraints to control the button's size. const AnimatedDialogButton({ super.key, - required this.onPressed, required this.buttonText, - this.constraints, + required this.onPressed, + this.buttonConstraints, this.buttonType = ButtonType.primary, }); - final BoxConstraints? constraints; - - final ButtonType buttonType; - final String buttonText; final VoidCallback onPressed; + final BoxConstraints? buttonConstraints; + + final ButtonType buttonType; + @override State createState() => _AnimatedDialogButtonState(); } @@ -68,7 +69,7 @@ class _AnimatedDialogButtonState extends State { duration: const Duration(milliseconds: 100), child: Center( child: Container( - constraints: widget.constraints, + constraints: widget.buttonConstraints, decoration: buttonDecoration, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), margin: const EdgeInsets.symmetric(vertical: 8), diff --git a/lib/presentation/widgets/dialog/custom_dialog_action.dart b/lib/presentation/widgets/dialog/custom_dialog_action.dart index 2882c76..9718a03 100644 --- a/lib/presentation/widgets/dialog/custom_dialog_action.dart +++ b/lib/presentation/widgets/dialog/custom_dialog_action.dart @@ -26,7 +26,7 @@ class CustomDialogAction extends StatelessWidget { onPressed: onPressed, buttonText: text, buttonType: buttonType, - constraints: const BoxConstraints(minWidth: 300), + buttonConstraints: const BoxConstraints(minWidth: 300), ); } } From 9c4eff5056b0595b529f54c9dc3e5cf285f86080 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 16:02:14 +0200 Subject: [PATCH 14/60] Moved button --- .../widgets/{dialog => buttons}/animated_dialog_button.dart | 0 lib/presentation/widgets/dialog/custom_dialog_action.dart | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/presentation/widgets/{dialog => buttons}/animated_dialog_button.dart (100%) diff --git a/lib/presentation/widgets/dialog/animated_dialog_button.dart b/lib/presentation/widgets/buttons/animated_dialog_button.dart similarity index 100% rename from lib/presentation/widgets/dialog/animated_dialog_button.dart rename to lib/presentation/widgets/buttons/animated_dialog_button.dart diff --git a/lib/presentation/widgets/dialog/custom_dialog_action.dart b/lib/presentation/widgets/dialog/custom_dialog_action.dart index 9718a03..aec0dfa 100644 --- a/lib/presentation/widgets/dialog/custom_dialog_action.dart +++ b/lib/presentation/widgets/dialog/custom_dialog_action.dart @@ -1,6 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:tallee/core/enums.dart'; -import 'package:tallee/presentation/widgets/dialog/animated_dialog_button.dart'; +import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; class CustomDialogAction extends StatelessWidget { /// A custom dialog action widget that represents a button in a dialog. From a1398623b0b8188afa8d773a2970b3d18adacd4d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 22:49:06 +0200 Subject: [PATCH 15/60] Added database functionality + tests --- lib/data/dao/player_dao.dart | 172 ++++++++++++++-- lib/data/db/database.g.dart | 249 +++++++++++++++--------- lib/data/db/tables/player_table.dart | 5 +- lib/data/models/player.dart | 3 + test/db_tests/entities/player_test.dart | 159 ++++++++++++++- 5 files changed, 484 insertions(+), 104 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index c58cb9a..74b2960 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:flutter/cupertino.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/player_table.dart'; import 'package:tallee/data/models/player.dart'; @@ -20,6 +21,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { name: row.name, description: row.description, createdAt: row.createdAt, + nameCount: row.nameCount, ), ) .toList(); @@ -34,6 +36,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { name: result.name, description: result.description, createdAt: result.createdAt, + nameCount: result.nameCount, ); } @@ -42,12 +45,15 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /// the new one. Future addPlayer({required Player player}) async { if (!await playerExists(playerId: player.id)) { + final int nameCount = await calculateNameCount(name: player.name); + await into(playerTable).insert( PlayerTableCompanion.insert( id: player.id, name: player.name, description: player.description, createdAt: player.createdAt, + nameCount: Value(nameCount), ), mode: InsertMode.insertOrReplace, ); @@ -62,20 +68,67 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { Future addPlayersAsList({required List players}) async { if (players.isEmpty) return false; + // Filter out players that already exist + final newPlayers = []; + for (final player in players) { + if (!await playerExists(playerId: player.id)) { + newPlayers.add(player); + } + } + + if (newPlayers.isEmpty) return false; + + // Group players by name + final nameGroups = >{}; + for (final player in newPlayers) { + nameGroups.putIfAbsent(player.name, () => []).add(player); + } + + final playersToInsert = []; + + // Process each group of players with the same name + for (final entry in nameGroups.entries) { + final name = entry.key; + final playersWithName = entry.value; + + // Get the current nameCount + var nameCount = await calculateNameCount(name: name); + if (nameCount == 0) nameCount++; + + // One player with the same name + if (playersWithName.length == 1) { + final player = playersWithName[0]; + playersToInsert.add( + PlayerTableCompanion.insert( + id: player.id, + name: player.name, + description: player.description, + createdAt: player.createdAt, + nameCount: Value(nameCount), + ), + ); + } else { + // Multiple players with the same name + for (var i = 0; i < playersWithName.length; i++) { + final player = playersWithName[i]; + playersToInsert.add( + PlayerTableCompanion.insert( + id: player.id, + name: player.name, + description: player.description, + createdAt: player.createdAt, + nameCount: Value(nameCount + i), + ), + ); + } + } + } + await db.batch( (b) => b.insertAll( playerTable, - players - .map( - (player) => PlayerTableCompanion.insert( - id: player.id, - name: player.name, - description: player.description, - createdAt: player.createdAt, - ), - ) - .toList(), - mode: InsertMode.insertOrIgnore, + playersToInsert, + mode: InsertMode.insertOrReplace, ), ); @@ -103,9 +156,36 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { required String playerId, required String newName, }) async { + // Get previous name and name count for the player before updating + final previousPlayerName = await (select( + playerTable, + )..where((p) => p.id.equals(playerId))).map((row) => row.name).getSingle(); + final previousNameCount = await getNameCount(name: previousPlayerName); + await (update(playerTable)..where((p) => p.id.equals(playerId))).write( PlayerTableCompanion(name: Value(newName)), ); + + // Update name count for the new name + final count = await calculateNameCount(name: newName); + if (count > 0) { + await (update(playerTable)..where((p) => p.name.equals(newName))).write( + PlayerTableCompanion(nameCount: Value(count)), + ); + } + + if (previousNameCount > 0) { + // Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount + final player = await getPlayerWithHighestNameCount( + name: previousPlayerName, + ); + if (player != null) { + await updateNameCount( + playerId: player.id, + nameCount: previousNameCount, + ); + } + } } /// Retrieves the total count of players in the database. @@ -117,6 +197,76 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return count ?? 0; } + /// Retrieves the count of players with the given [name] in the database. + Future getNameCount({required String name}) async { + final query = select(playerTable)..where((p) => p.name.equals(name)); + final result = await query.get(); + return result.length; + } + + /// Updates the nameCount for the player with the given [playerId] to [nameCount]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future updateNameCount({ + required String playerId, + required int nameCount, + }) async { + final query = update(playerTable)..where((p) => p.id.equals(playerId)); + final rowsAffected = await query.write( + PlayerTableCompanion(nameCount: Value(nameCount)), + ); + return rowsAffected > 0; + } + + @visibleForTesting + Future getPlayerWithHighestNameCount({required String name}) async { + final query = select(playerTable) + ..where((p) => p.name.equals(name)) + ..orderBy([(p) => OrderingTerm.desc(p.nameCount)]) + ..limit(1); + final result = await query.getSingleOrNull(); + if (result != null) { + return Player( + id: result.id, + name: result.name, + description: result.description, + createdAt: result.createdAt, + nameCount: result.nameCount, + ); + } + return null; + } + + @visibleForTesting + Future calculateNameCount({required String name}) async { + final count = await getNameCount(name: name); + final int nameCount; + + if (count == 1) { + // If one other player exists with the same name, initialize the nameCount + await initializeNameCount(name: name); + // And for the new player, set nameCount to 2 + nameCount = 2; + } else if (count > 1) { + // If more than one player exists with the same name, just increment + // the nameCount for the new player + nameCount = count + 1; + } else { + // If no other players exist with the same name, set nameCount to 0 + nameCount = 0; + } + + return nameCount; + } + + @visibleForTesting + Future initializeNameCount({required String name}) async { + final rowsAffected = + await (update(playerTable)..where((p) => p.name.equals(name))).write( + const PlayerTableCompanion(nameCount: Value(1)), + ); + return rowsAffected > 0; + } + /// Deletes all players from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteAllPlayers() async { diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index 58175df..2190c3d 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -18,6 +18,17 @@ class $PlayerTableTable extends PlayerTable type: DriftSqlType.string, requiredDuringInsert: true, ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); static const VerificationMeta _nameMeta = const VerificationMeta('name'); @override late final GeneratedColumn name = GeneratedColumn( @@ -27,6 +38,18 @@ class $PlayerTableTable extends PlayerTable type: DriftSqlType.string, requiredDuringInsert: true, ); + static const VerificationMeta _nameCountMeta = const VerificationMeta( + 'nameCount', + ); + @override + late final GeneratedColumn nameCount = GeneratedColumn( + 'name_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); static const VerificationMeta _descriptionMeta = const VerificationMeta( 'description', ); @@ -38,19 +61,14 @@ class $PlayerTableTable extends PlayerTable type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _createdAtMeta = const VerificationMeta( - 'createdAt', - ); @override - late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', - aliasedName, - false, - type: DriftSqlType.dateTime, - requiredDuringInsert: true, - ); - @override - List get $columns => [id, name, description, createdAt]; + List get $columns => [ + id, + createdAt, + name, + nameCount, + description, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -68,6 +86,14 @@ class $PlayerTableTable extends PlayerTable } else if (isInserting) { context.missing(_idMeta); } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } if (data.containsKey('name')) { context.handle( _nameMeta, @@ -76,6 +102,12 @@ class $PlayerTableTable extends PlayerTable } else if (isInserting) { context.missing(_nameMeta); } + if (data.containsKey('name_count')) { + context.handle( + _nameCountMeta, + nameCount.isAcceptableOrUnknown(data['name_count']!, _nameCountMeta), + ); + } if (data.containsKey('description')) { context.handle( _descriptionMeta, @@ -87,14 +119,6 @@ class $PlayerTableTable extends PlayerTable } else if (isInserting) { context.missing(_descriptionMeta); } - if (data.containsKey('created_at')) { - context.handle( - _createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), - ); - } else if (isInserting) { - context.missing(_createdAtMeta); - } return context; } @@ -108,18 +132,22 @@ class $PlayerTableTable extends PlayerTable DriftSqlType.string, data['${effectivePrefix}id'], )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, name: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}name'], )!, + nameCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}name_count'], + )!, description: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}description'], )!, - createdAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, - data['${effectivePrefix}created_at'], - )!, ); } @@ -131,31 +159,35 @@ class $PlayerTableTable extends PlayerTable class PlayerTableData extends DataClass implements Insertable { final String id; - final String name; - final String description; final DateTime createdAt; + final String name; + final int nameCount; + final String description; const PlayerTableData({ required this.id, - required this.name, - required this.description, required this.createdAt, + required this.name, + required this.nameCount, + required this.description, }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); - map['name'] = Variable(name); - map['description'] = Variable(description); map['created_at'] = Variable(createdAt); + map['name'] = Variable(name); + map['name_count'] = Variable(nameCount); + map['description'] = Variable(description); return map; } PlayerTableCompanion toCompanion(bool nullToAbsent) { return PlayerTableCompanion( id: Value(id), - name: Value(name), - description: Value(description), createdAt: Value(createdAt), + name: Value(name), + nameCount: Value(nameCount), + description: Value(description), ); } @@ -166,9 +198,10 @@ class PlayerTableData extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return PlayerTableData( id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - description: serializer.fromJson(json['description']), createdAt: serializer.fromJson(json['createdAt']), + name: serializer.fromJson(json['name']), + nameCount: serializer.fromJson(json['nameCount']), + description: serializer.fromJson(json['description']), ); } @override @@ -176,31 +209,35 @@ class PlayerTableData extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'description': serializer.toJson(description), 'createdAt': serializer.toJson(createdAt), + 'name': serializer.toJson(name), + 'nameCount': serializer.toJson(nameCount), + 'description': serializer.toJson(description), }; } PlayerTableData copyWith({ String? id, - String? name, - String? description, DateTime? createdAt, + String? name, + int? nameCount, + String? description, }) => PlayerTableData( id: id ?? this.id, - name: name ?? this.name, - description: description ?? this.description, createdAt: createdAt ?? this.createdAt, + name: name ?? this.name, + nameCount: nameCount ?? this.nameCount, + description: description ?? this.description, ); PlayerTableData copyWithCompanion(PlayerTableCompanion data) { return PlayerTableData( id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, name: data.name.present ? data.name.value : this.name, + nameCount: data.nameCount.present ? data.nameCount.value : this.nameCount, description: data.description.present ? data.description.value : this.description, - createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, ); } @@ -208,76 +245,85 @@ class PlayerTableData extends DataClass implements Insertable { String toString() { return (StringBuffer('PlayerTableData(') ..write('id: $id, ') + ..write('createdAt: $createdAt, ') ..write('name: $name, ') - ..write('description: $description, ') - ..write('createdAt: $createdAt') + ..write('nameCount: $nameCount, ') + ..write('description: $description') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, name, description, createdAt); + int get hashCode => Object.hash(id, createdAt, name, nameCount, description); @override bool operator ==(Object other) => identical(this, other) || (other is PlayerTableData && other.id == this.id && + other.createdAt == this.createdAt && other.name == this.name && - other.description == this.description && - other.createdAt == this.createdAt); + other.nameCount == this.nameCount && + other.description == this.description); } class PlayerTableCompanion extends UpdateCompanion { final Value id; - final Value name; - final Value description; final Value createdAt; + final Value name; + final Value nameCount; + final Value description; final Value rowid; const PlayerTableCompanion({ this.id = const Value.absent(), - this.name = const Value.absent(), - this.description = const Value.absent(), this.createdAt = const Value.absent(), + this.name = const Value.absent(), + this.nameCount = const Value.absent(), + this.description = const Value.absent(), this.rowid = const Value.absent(), }); PlayerTableCompanion.insert({ required String id, - required String name, - required String description, required DateTime createdAt, + required String name, + this.nameCount = const Value.absent(), + required String description, this.rowid = const Value.absent(), }) : id = Value(id), + createdAt = Value(createdAt), name = Value(name), - description = Value(description), - createdAt = Value(createdAt); + description = Value(description); static Insertable custom({ Expression? id, - Expression? name, - Expression? description, Expression? createdAt, + Expression? name, + Expression? nameCount, + Expression? description, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, - if (name != null) 'name': name, - if (description != null) 'description': description, if (createdAt != null) 'created_at': createdAt, + if (name != null) 'name': name, + if (nameCount != null) 'name_count': nameCount, + if (description != null) 'description': description, if (rowid != null) 'rowid': rowid, }); } PlayerTableCompanion copyWith({ Value? id, - Value? name, - Value? description, Value? createdAt, + Value? name, + Value? nameCount, + Value? description, Value? rowid, }) { return PlayerTableCompanion( id: id ?? this.id, - name: name ?? this.name, - description: description ?? this.description, createdAt: createdAt ?? this.createdAt, + name: name ?? this.name, + nameCount: nameCount ?? this.nameCount, + description: description ?? this.description, rowid: rowid ?? this.rowid, ); } @@ -288,15 +334,18 @@ class PlayerTableCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } if (name.present) { map['name'] = Variable(name.value); } + if (nameCount.present) { + map['name_count'] = Variable(nameCount.value); + } if (description.present) { map['description'] = Variable(description.value); } - if (createdAt.present) { - map['created_at'] = Variable(createdAt.value); - } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -307,9 +356,10 @@ class PlayerTableCompanion extends UpdateCompanion { String toString() { return (StringBuffer('PlayerTableCompanion(') ..write('id: $id, ') - ..write('name: $name, ') - ..write('description: $description, ') ..write('createdAt: $createdAt, ') + ..write('name: $name, ') + ..write('nameCount: $nameCount, ') + ..write('description: $description, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -2790,17 +2840,19 @@ abstract class _$AppDatabase extends GeneratedDatabase { typedef $$PlayerTableTableCreateCompanionBuilder = PlayerTableCompanion Function({ required String id, - required String name, - required String description, required DateTime createdAt, + required String name, + Value nameCount, + required String description, Value rowid, }); typedef $$PlayerTableTableUpdateCompanionBuilder = PlayerTableCompanion Function({ Value id, - Value name, - Value description, Value createdAt, + Value name, + Value nameCount, + Value description, Value rowid, }); @@ -2892,18 +2944,23 @@ class $$PlayerTableTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get name => $composableBuilder( column: $table.name, builder: (column) => ColumnFilters(column), ); - ColumnFilters get description => $composableBuilder( - column: $table.description, + ColumnFilters get nameCount => $composableBuilder( + column: $table.nameCount, builder: (column) => ColumnFilters(column), ); - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, + ColumnFilters get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnFilters(column), ); @@ -2997,18 +3054,23 @@ class $$PlayerTableTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get name => $composableBuilder( column: $table.name, builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get description => $composableBuilder( - column: $table.description, + ColumnOrderings get nameCount => $composableBuilder( + column: $table.nameCount, builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, + ColumnOrderings get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnOrderings(column), ); } @@ -3025,17 +3087,20 @@ class $$PlayerTableTableAnnotationComposer GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); + GeneratedColumn get nameCount => + $composableBuilder(column: $table.nameCount, builder: (column) => column); + GeneratedColumn get description => $composableBuilder( column: $table.description, builder: (column) => column, ); - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => column); - Expression playerGroupTableRefs( Expression Function($$PlayerGroupTableTableAnnotationComposer a) f, ) { @@ -3145,29 +3210,33 @@ class $$PlayerTableTableTableManager updateCompanionCallback: ({ Value id = const Value.absent(), - Value name = const Value.absent(), - Value description = const Value.absent(), Value createdAt = const Value.absent(), + Value name = const Value.absent(), + Value nameCount = const Value.absent(), + Value description = const Value.absent(), Value rowid = const Value.absent(), }) => PlayerTableCompanion( id: id, - name: name, - description: description, createdAt: createdAt, + name: name, + nameCount: nameCount, + description: description, rowid: rowid, ), createCompanionCallback: ({ required String id, - required String name, - required String description, required DateTime createdAt, + required String name, + Value nameCount = const Value.absent(), + required String description, Value rowid = const Value.absent(), }) => PlayerTableCompanion.insert( id: id, - name: name, - description: description, createdAt: createdAt, + name: name, + nameCount: nameCount, + description: description, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/lib/data/db/tables/player_table.dart b/lib/data/db/tables/player_table.dart index 15b29a5..ce4f931 100644 --- a/lib/data/db/tables/player_table.dart +++ b/lib/data/db/tables/player_table.dart @@ -2,9 +2,10 @@ import 'package:drift/drift.dart'; class PlayerTable extends Table { TextColumn get id => text()(); - TextColumn get name => text()(); - TextColumn get description => text()(); DateTimeColumn get createdAt => dateTime()(); + TextColumn get name => text()(); + IntColumn get nameCount => integer().withDefault(const Constant(0))(); + TextColumn get description => text()(); @override Set> get primaryKey => {id}; diff --git a/lib/data/models/player.dart b/lib/data/models/player.dart index c405de9..b7de429 100644 --- a/lib/data/models/player.dart +++ b/lib/data/models/player.dart @@ -5,12 +5,14 @@ class Player { final String id; final DateTime createdAt; final String name; + final int nameCount; final String description; Player({ String? id, DateTime? createdAt, required this.name, + this.nameCount = 0, String? description, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? clock.now(), @@ -26,6 +28,7 @@ class Player { : id = json['id'], createdAt = DateTime.parse(json['createdAt']), name = json['name'], + nameCount = 0, description = json['description']; /// Converts the Player instance to a JSON object. diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index 3042b33..45eacf0 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -1,5 +1,5 @@ import 'package:clock/clock.dart'; -import 'package:drift/drift.dart' hide isNull; +import 'package:drift/drift.dart' hide isNull, isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:tallee/data/db/database.dart'; @@ -381,5 +381,162 @@ void main() { ); expect(playerExists, true); }); + + group('Name Count Tests', () { + test('Single player gets initialized wih name count 0', () async { + await database.playerDao.addPlayer(player: testPlayer1); + + final player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.nameCount, 0); + }); + + test('Multiple players get initialized wih name count 0', () async { + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2], + ); + + final players = await database.playerDao.getAllPlayers(); + + expect(players.length, 2); + for (Player p in players) { + expect(p.nameCount, 0); + } + }); + + test( + 'Seperatly added players nameCount gets increased correctly', + () async { + await database.playerDao.addPlayer(player: testPlayer1); + + final player1 = Player(name: testPlayer1.name, description: ''); + await database.playerDao.addPlayer(player: player1); + + var players = await database.playerDao.getAllPlayers(); + + expect(players.length, 2); + players.sort((a, b) => a.nameCount.compareTo(b.nameCount)); + + for (int i = 0; i < players.length - 1; i++) { + expect(players[i].nameCount, i + 1); + } + }, + ); + + test( + 'Together added players nameCount gets increased correctly', + () async { + final player1 = Player(name: testPlayer1.name, description: ''); + final player2 = Player(name: testPlayer1.name, description: ''); + final player3 = Player(name: testPlayer1.name, description: ''); + + // addPlayersAsList() with multiple players and with one player + await database.playerDao.addPlayersAsList(players: [testPlayer1]); + await database.playerDao.addPlayersAsList( + players: [player1, player2, player3], + ); + + var players = await database.playerDao.getAllPlayers(); + + expect(players.length, 4); + players.sort((a, b) => a.nameCount.compareTo(b.nameCount)); + + for (int i = 0; i < players.length - 1; i++) { + expect(players[i].nameCount, i + 1); + } + }, + ); + + test('getNameCount works correctly', () async { + final player2 = Player(name: testPlayer1.name, description: ''); + final player3 = Player(name: testPlayer1.name, description: ''); + + await database.playerDao.addPlayersAsList( + players: [testPlayer1, player2, player3], + ); + + final nameCount = await database.playerDao.getNameCount( + name: testPlayer1.name, + ); + + expect(nameCount, 2); + }); + + test('updateNameCount works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + + final success = await database.playerDao.updateNameCount( + playerId: testPlayer1.id, + nameCount: 2, + ); + + expect(success, true); + + final nameCount = await database.playerDao.getNameCount( + name: testPlayer1.name, + ); + + expect(nameCount, 2); + }); + + test('getPlayerWithHighestNameCount works correctly', () async { + final player2 = Player(name: testPlayer1.name, description: ''); + final player3 = Player(name: testPlayer1.name, description: ''); + + await database.playerDao.addPlayersAsList( + players: [testPlayer1, player2, player3], + ); + + final player = await database.playerDao.getPlayerWithHighestNameCount( + name: testPlayer1.name, + ); + + expect(player, isNotNull); + expect(player!.nameCount, 3); + }); + + test('getPlayerWithHighestNameCount with non existing player', () async { + final player = await database.playerDao.getPlayerWithHighestNameCount( + name: 'non-existing-name', + ); + expect(player, isNull); + }); + + test('calculateNameCount works correctly', () async { + // Case 1: No existing players with the name + var count = await database.playerDao.calculateNameCount( + name: testPlayer1.name, + ); + expect(count, 0); + + // Case 2: One existing player with the name. Should update that + // player's nameCount to 1 and return 2 for the new player + await database.playerDao.addPlayer(player: testPlayer1); + + count = await database.playerDao.calculateNameCount( + name: testPlayer1.name, + ); + expect(count, 2); + + // Case 3: Multiple existing players with the name. + final player2 = Player(name: testPlayer1.name, nameCount: count); + await database.playerDao.addPlayer(player: player2); + + count = await database.playerDao.calculateNameCount( + name: testPlayer1.name, + ); + expect(count, 3); + }); + + test('getPlayerWithHighestNameCount with non existing player', () async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.initializeNameCount(name: testPlayer1.name); + final player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.nameCount, 1); + }); + }); }); } From 9a2afbfd3b67f49d08a7dcd2c3810fb3f8171049 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 22:49:21 +0200 Subject: [PATCH 16/60] Added ui implementation --- lib/core/common.dart | 8 +++++ .../group_view/group_detail_view.dart | 2 ++ .../match_view/match_detail_view.dart | 1 + .../widgets/player_selection.dart | 3 ++ .../widgets/tiles/group_tile.dart | 7 ++++- .../widgets/tiles/match_tile.dart | 6 +++- .../widgets/tiles/text_icon_list_tile.dart | 29 +++++++++++++++---- .../widgets/tiles/text_icon_tile.dart | 27 +++++++++++++++-- 8 files changed, 73 insertions(+), 10 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 20b0225..399872c 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; +import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; /// Translates a [Ruleset] enum value to its corresponding localized string. @@ -43,3 +44,10 @@ String getExtraPlayerCount(Match match) { } return ' + ${count.toString()}'; } + +String getNameCountText(Player player) { + if (player.nameCount >= 1) { + return ' #${player.nameCount}'; + } + return ''; +} 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 1ef89ef..0847ee5 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 @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.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/group.dart'; @@ -153,6 +154,7 @@ class _GroupDetailViewState extends State { children: _group.members.map((member) { return TextIconTile( text: member.name, + suffixText: getNameCountText(member), iconEnabled: false, ); }).toList(), 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 fc53aa8..6671196 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 @@ -161,6 +161,7 @@ class _MatchDetailViewState extends State { children: match.players.map((player) { return TextIconTile( text: player.name, + suffixText: getNameCountText(player), iconEnabled: false, ); }).toList(), diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 6d8769d..31bf942 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/common.dart'; import 'package:tallee/core/constants.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/db/database.dart'; @@ -140,6 +141,7 @@ class _PlayerSelectionState extends State { padding: const EdgeInsets.only(right: 8.0), child: TextIconTile( text: player.name, + suffixText: getNameCountText(player), onIconTap: () { setState(() { // Removes the player from the selection and notifies the parent. @@ -193,6 +195,7 @@ class _PlayerSelectionState extends State { itemBuilder: (BuildContext context, int index) { return TextIconListTile( text: suggestedPlayers[index].name, + suffixText: getNameCountText(suggestedPlayers[index]), onPressed: () { setState(() { // If the player is not already selected diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart index b62f3ce..f4ace65 100644 --- a/lib/presentation/widgets/tiles/group_tile.dart +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -81,7 +82,11 @@ class _GroupTileState extends State { for (var member in [ ...widget.group.members, ]..sort((a, b) => a.name.compareTo(b.name))) - TextIconTile(text: member.name, iconEnabled: false), + TextIconTile( + text: member.name, + suffixText: getNameCountText(member), + iconEnabled: false, + ), ], ), const SizedBox(height: 2.5), diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 39f9cdf..eaf6a4f 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -203,7 +203,11 @@ class _MatchTileState extends State { spacing: 6, runSpacing: 6, children: players.map((player) { - return TextIconTile(text: player.name, iconEnabled: false); + return TextIconTile( + text: player.name, + suffixText: getNameCountText(player), + iconEnabled: false, + ); }).toList(), ), ], diff --git a/lib/presentation/widgets/tiles/text_icon_list_tile.dart b/lib/presentation/widgets/tiles/text_icon_list_tile.dart index 2b29d41..a31f2ae 100644 --- a/lib/presentation/widgets/tiles/text_icon_list_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_list_tile.dart @@ -9,6 +9,7 @@ class TextIconListTile extends StatelessWidget { const TextIconListTile({ super.key, required this.text, + this.suffixText = '', this.iconEnabled = true, this.onPressed, }); @@ -16,6 +17,9 @@ class TextIconListTile extends StatelessWidget { /// The text to display in the tile. final String text; + /// An optional suffix text to display after the main text. + final String suffixText; + /// A boolean to determine if the icon should be displayed. final bool iconEnabled; @@ -35,12 +39,27 @@ class TextIconListTile extends StatelessWidget { Flexible( child: Container( padding: const EdgeInsets.symmetric(vertical: 12.5), - child: Text( - text, + child: RichText( overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: text, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + TextSpan( + text: suffixText, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: CustomTheme.textColor.withAlpha(100), + ), + ), + ], ), ), ), diff --git a/lib/presentation/widgets/tiles/text_icon_tile.dart b/lib/presentation/widgets/tiles/text_icon_tile.dart index f98e0a7..541b6ae 100644 --- a/lib/presentation/widgets/tiles/text_icon_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_tile.dart @@ -9,6 +9,7 @@ class TextIconTile extends StatelessWidget { const TextIconTile({ super.key, required this.text, + this.suffixText = '', this.iconEnabled = true, this.onIconTap, }); @@ -16,6 +17,8 @@ class TextIconTile extends StatelessWidget { /// The text to display in the tile. final String text; + final String suffixText; + /// A boolean to determine if the icon should be displayed. final bool iconEnabled; @@ -36,10 +39,28 @@ class TextIconTile extends StatelessWidget { children: [ if (iconEnabled) const SizedBox(width: 3), Flexible( - child: Text( - text, + child: RichText( overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: text, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + TextSpan( + text: suffixText, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: CustomTheme.textColor.withAlpha(120), + ), + ), + ], + ), ), ), if (iconEnabled) ...[ From 653b85d28d55cc557e28bc6191422328a4a823b9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 23:11:17 +0200 Subject: [PATCH 17/60] Fixed tests --- lib/data/dao/player_dao.dart | 2 +- test/db_tests/entities/player_test.dart | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 74b2960..6d4cff7 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -93,7 +93,6 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { // Get the current nameCount var nameCount = await calculateNameCount(name: name); - if (nameCount == 0) nameCount++; // One player with the same name if (playersWithName.length == 1) { @@ -108,6 +107,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { ), ); } else { + if (nameCount == 0) nameCount++; // Multiple players with the same name for (var i = 0; i < playersWithName.length; i++) { final player = playersWithName[i]; diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index 45eacf0..7462bbf 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -449,8 +449,8 @@ void main() { ); test('getNameCount works correctly', () async { - final player2 = Player(name: testPlayer1.name, description: ''); - final player3 = Player(name: testPlayer1.name, description: ''); + final player2 = Player(name: testPlayer1.name); + final player3 = Player(name: testPlayer1.name); await database.playerDao.addPlayersAsList( players: [testPlayer1, player2, player3], @@ -460,7 +460,7 @@ void main() { name: testPlayer1.name, ); - expect(nameCount, 2); + expect(nameCount, 3); }); test('updateNameCount works correctly', () async { @@ -470,14 +470,12 @@ void main() { playerId: testPlayer1.id, nameCount: 2, ); - expect(success, true); - final nameCount = await database.playerDao.getNameCount( - name: testPlayer1.name, + final player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, ); - - expect(nameCount, 2); + expect(player.nameCount, 2); }); test('getPlayerWithHighestNameCount works correctly', () async { From fcf845af4d775006088cd56c66baed12d1fc971c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 23:22:14 +0200 Subject: [PATCH 18/60] Implemented name count update in player selection --- lib/data/models/player.dart | 4 ++-- .../widgets/player_selection.dart | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/data/models/player.dart b/lib/data/models/player.dart index b7de429..12d17f0 100644 --- a/lib/data/models/player.dart +++ b/lib/data/models/player.dart @@ -5,7 +5,7 @@ class Player { final String id; final DateTime createdAt; final String name; - final int nameCount; + int nameCount; final String description; Player({ @@ -20,7 +20,7 @@ class Player { @override String toString() { - return 'Player{id: $id, name: $name, description: $description}'; + return 'Player{id: $id, createdAt: $createdAt, name: $name, nameCount: $nameCount, description: $description}'; } /// Creates a Player instance from a JSON object. diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 31bf942..0aa6653 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -285,7 +285,8 @@ class _PlayerSelectionState extends State { final loc = AppLocalizations.of(context); final playerName = _searchBarController.text.trim(); - final createdPlayer = Player(name: playerName, description: ''); + int nameCount = _calculateNameCount(playerName); + final createdPlayer = Player(name: playerName, nameCount: nameCount); final success = await db.playerDao.addPlayer(player: createdPlayer); if (!context.mounted) return; @@ -298,6 +299,22 @@ class _PlayerSelectionState extends State { } } + int _calculateNameCount(String playerName) { + final playersWithSameName = + allPlayers.where((player) => player.name == playerName).toList() + ..sort((a, b) => a.nameCount.compareTo(b.nameCount)); + + if (playersWithSameName.isEmpty) { + return 0; + } else if (playersWithSameName.length == 1) { + // Initialize nameCount + playersWithSameName[0].nameCount = 1; + } + + // Return following count + return playersWithSameName.length + 1; + } + /// Updates the state after successfully adding a new player. void _handleSuccessfulPlayerCreation(Player player) { selectedPlayers.insert(0, player); From 9a0386f22d7c9059c745dfa2e0c1be76a1226758 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 23:41:10 +0200 Subject: [PATCH 19/60] Added case for not fetching a player --- lib/data/dao/player_dao.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 6d4cff7..5d46343 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -143,7 +143,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return rowsAffected > 0; } - /// Checks if a player with the given [id] exists in the database. + /// Checks if a player with the given [playerId] exists in the database. /// Returns `true` if the player exists, `false` otherwise. Future playerExists({required String playerId}) async { final query = select(playerTable)..where((p) => p.id.equals(playerId)); @@ -157,9 +157,11 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { required String newName, }) async { // Get previous name and name count for the player before updating - final previousPlayerName = await (select( - playerTable, - )..where((p) => p.id.equals(playerId))).map((row) => row.name).getSingle(); + final previousPlayerName = + await (select(playerTable)..where((p) => p.id.equals(playerId))) + .map((row) => row.name) + .getSingleOrNull() ?? + ''; final previousNameCount = await getNameCount(name: previousPlayerName); await (update(playerTable)..where((p) => p.id.equals(playerId))).write( @@ -197,7 +199,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return count ?? 0; } - /// Retrieves the count of players with the given [name] in the database. + /// Retrieves the count of players with the given [name]. Future getNameCount({required String name}) async { final query = select(playerTable)..where((p) => p.name.equals(name)); final result = await query.get(); From 4e97f6723a49a6de1853e4af8f99e0c84fdaa825 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 20 Apr 2026 16:39:33 +0200 Subject: [PATCH 20/60] Added nameCount to statistic tiles --- .../views/main_menu/statistics_view.dart | 108 +++++++++--------- .../widgets/tiles/statistics_tile.dart | 33 +++++- 2 files changed, 82 insertions(+), 59 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 3a55115..221ffee 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -18,9 +18,18 @@ class StatisticsView extends StatefulWidget { } class _StatisticsViewState extends State { - List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1)); - List<(String, int)> matchCounts = List.filled(6, ('Skeleton Player', 1)); - List<(String, double)> winRates = List.filled(6, ('Skeleton Player', 1)); + List<(Player, int)> winCounts = List.filled(6, ( + Player(name: 'Skeleton Player'), + 1, + )); + List<(Player, int)> matchCounts = List.filled(6, ( + Player(name: 'Skeleton Player'), + 1, + )); + List<(Player, double)> winRates = List.filled(6, ( + Player(name: 'Skeleton Player'), + 1, + )); bool isLoading = true; @override @@ -121,7 +130,10 @@ class _StatisticsViewState extends State { players: players, context: context, ); - winRates = computeWinRatePercent(wins: winCounts, matches: matchCounts); + winRates = computeWinRatePercent( + winCounts: winCounts, + matchCounts: matchCounts, + ); setState(() { isLoading = false; }); @@ -130,47 +142,47 @@ class _StatisticsViewState extends State { /// Calculates the number of wins for each player /// and returns a sorted list of tuples (playerName, winCount) - List<(String, int)> _calculateWinsForAllPlayers({ + List<(Player, int)> _calculateWinsForAllPlayers({ required List matches, required List players, required BuildContext context, }) { - List<(String, int)> winCounts = []; + List<(Player, int)> winCounts = []; final loc = AppLocalizations.of(context); // Getting the winners for (var match in matches) { final winner = match.winner; if (winner != null) { - final index = winCounts.indexWhere((entry) => entry.$1 == winner.id); + final index = winCounts.indexWhere((entry) => entry.$1.id == winner.id); // -1 means winner not found in winCounts if (index != -1) { final current = winCounts[index].$2; - winCounts[index] = (winner.id, current + 1); + winCounts[index] = (winner, current + 1); } else { - winCounts.add((winner.id, 1)); + winCounts.add((winner, 1)); } } } // Adding all players with zero wins for (var player in players) { - final index = winCounts.indexWhere((entry) => entry.$1 == player.id); + final index = winCounts.indexWhere((entry) => entry.$1.id == player.id); // -1 means player not found in winCounts if (index == -1) { - winCounts.add((player.id, 0)); + winCounts.add((player, 0)); } } // Replace player IDs with names for (int i = 0; i < winCounts.length; i++) { - final playerId = winCounts[i].$1; + final playerId = winCounts[i].$1.id; final player = players.firstWhere( (p) => p.id == playerId, orElse: () => Player(id: playerId, name: loc.not_available, description: ''), ); - winCounts[i] = (player.name, winCounts[i].$2); + winCounts[i] = (player, winCounts[i].$2); } winCounts.sort((a, b) => b.$2.compareTo(a.$2)); @@ -180,60 +192,51 @@ class _StatisticsViewState extends State { /// Calculates the number of matches played for each player /// and returns a sorted list of tuples (playerName, matchCount) - List<(String, int)> _calculateMatchAmountsForAllPlayers({ + List<(Player, int)> _calculateMatchAmountsForAllPlayers({ required List matches, required List players, required BuildContext context, }) { - List<(String, int)> matchCounts = []; + List<(Player, int)> matchCounts = []; final loc = AppLocalizations.of(context); // Counting matches for each player for (var match in matches) { - if (match.group != null) { - final members = match.group!.members.map((p) => p.id).toList(); - for (var playerId in members) { - final index = matchCounts.indexWhere((entry) => entry.$1 == playerId); - // -1 means player not found in matchCounts - if (index != -1) { - final current = matchCounts[index].$2; - matchCounts[index] = (playerId, current + 1); - } else { - matchCounts.add((playerId, 1)); - } - } - } - final members = match.players.map((p) => p.id).toList(); - for (var playerId in members) { - final index = matchCounts.indexWhere((entry) => entry.$1 == playerId); - // -1 means player not found in matchCounts - if (index != -1) { - final current = matchCounts[index].$2; - matchCounts[index] = (playerId, current + 1); + for (Player player in match.players) { + // Check if the player is already in matchCounts + final index = matchCounts.indexWhere( + (entry) => entry.$1.id == player.id, + ); + + // -1 -> not found + if (index == -1) { + // Add new entry + matchCounts.add((player, 1)); } else { - matchCounts.add((playerId, 1)); + // Update existing entry + final currentMatchAmount = matchCounts[index].$2; + matchCounts[index] = (player, currentMatchAmount + 1); } } } // Adding all players with zero matches for (var player in players) { - final index = matchCounts.indexWhere((entry) => entry.$1 == player.id); + final index = matchCounts.indexWhere((entry) => entry.$1.id == player.id); // -1 means player not found in matchCounts if (index == -1) { - matchCounts.add((player.id, 0)); + matchCounts.add((player, 0)); } } // Replace player IDs with names for (int i = 0; i < matchCounts.length; i++) { - final playerId = matchCounts[i].$1; + final playerId = matchCounts[i].$1.id; final player = players.firstWhere( (p) => p.id == playerId, - orElse: () => - Player(id: playerId, name: loc.not_available, description: ''), + orElse: () => Player(id: playerId, name: loc.not_available), ); - matchCounts[i] = (player.name, matchCounts[i].$2); + matchCounts[i] = (player, matchCounts[i].$2); } matchCounts.sort((a, b) => b.$2.compareTo(a.$2)); @@ -241,25 +244,24 @@ class _StatisticsViewState extends State { return matchCounts; } - // dart - List<(String, double)> computeWinRatePercent({ - required List<(String, int)> wins, - required List<(String, int)> matches, + List<(Player, double)> computeWinRatePercent({ + required List<(Player, int)> winCounts, + required List<(Player, int)> matchCounts, }) { - final Map winsMap = {for (var e in wins) e.$1: e.$2}; - final Map matchesMap = {for (var e in matches) e.$1: e.$2}; + final Map winsMap = {for (var e in winCounts) e.$1: e.$2}; + final Map matchesMap = {for (var e in matchCounts) e.$1: e.$2}; // Get all unique player names - final names = {...winsMap.keys, ...matchesMap.keys}; + final player = {...matchesMap.keys}; // Calculate win rates - final result = names.map((name) { + final result = player.map((name) { final int w = winsMap[name] ?? 0; - final int g = matchesMap[name] ?? 0; + final int m = matchesMap[name] ?? 0; // Calculate percentage and round to 2 decimal places // Avoid division by zero - final double percent = (g > 0) - ? double.parse(((w / g)).toStringAsFixed(2)) + final double percent = (m > 0) + ? double.parse(((w / m)).toStringAsFixed(2)) : 0; return (name, percent); }).toList(); diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index bc2f7b6..ea9cb49 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -1,6 +1,9 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; @@ -32,7 +35,7 @@ class StatisticsTile extends StatelessWidget { final double width; /// A list of tuples containing labels and their corresponding numeric values. - final List<(String, num)> values; + final List<(Player, num)> values; /// The maximum number of items to display. final int itemCount; @@ -89,11 +92,29 @@ class StatisticsTile extends StatelessWidget { ), Padding( padding: const EdgeInsets.only(left: 4.0), - child: Text( - values[index].$1, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + child: RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: values[index].$1.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: getNameCountText(values[index].$1), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor.withAlpha( + 150, + ), + ), + ), + ], ), ), ), From b2eeabe1ef3386404e0f483ee22570379586ede9 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Tue, 21 Apr 2026 08:47:45 +0000 Subject: [PATCH 21/60] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 641b37b..b51c02a 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.20+254 +version: 0.0.21+255 environment: sdk: ^3.8.1 From 32ed7ac3b5dd07da38d3a29cb4f0417d770a595a Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Tue, 21 Apr 2026 08:48:30 +0000 Subject: [PATCH 22/60] Updated licenses [skip ci] --- .../main_menu/settings_view/licenses/oss_licenses.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 cafec67..d712475 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 @@ -37480,13 +37480,13 @@ freely, subject to the following restrictions: 3. This notice may not be removed or altered from any source distribution.''', ); -/// vm_service 15.0.2 +/// vm_service 15.1.0 const _vm_service = Package( name: 'vm_service', description: 'A library to communicate with a service implementing the Dart VM service protocol.', repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/vm_service', authors: [], - version: '15.0.2', + version: '15.1.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -37880,12 +37880,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.20+254 +/// tallee 0.0.21+255 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.20+254', + version: '0.0.21+255', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, From 1363c1fb06bee6628877b1fc94bae911bd1ccf48 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 17:19:01 +0200 Subject: [PATCH 23/60] Added function for point label --- lib/core/common.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/core/common.dart b/lib/core/common.dart index 20b0225..96187b0 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -43,3 +43,11 @@ String getExtraPlayerCount(Match match) { } return ' + ${count.toString()}'; } + +String getPointLabel(AppLocalizations loc, int points) { + if (points == 1) { + return '$points ${loc.point}'; + } else { + return '$points ${loc.points}'; + } +} From 316a50dad00af2a8e6199cc25f46d9f8616e69e8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 17:19:11 +0200 Subject: [PATCH 24/60] Added point localizations --- lib/l10n/arb/app_de.arb | 1 + lib/l10n/arb/app_en.arb | 1 + lib/l10n/generated/app_localizations.dart | 6 ++++++ lib/l10n/generated/app_localizations_de.dart | 4 ++++ lib/l10n/generated/app_localizations_en.dart | 4 ++++ 5 files changed, 16 insertions(+) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 47a092f..b72685d 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -75,6 +75,7 @@ "player_name": "Spieler:innenname", "players": "Spieler:innen", "players_count": "{count} Spieler", + "point": "Punkt", "points": "Punkte", "privacy_policy": "Datenschutzerklärung", "quick_create": "Schnellzugriff", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a30d376..14e277f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -409,6 +409,7 @@ "player_name": "Player name", "players": "Players", "players_count": "{count} Players", + "point": "Point", "points": "Points", "privacy_policy": "Privacy Policy", "quick_create": "Quick Create", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 456a6fc..a37722c 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -548,6 +548,12 @@ abstract class AppLocalizations { /// **'{count} Players'** String players_count(int count); + /// No description provided for @point. + /// + /// In en, this message translates to: + /// **'Point'** + String get point; + /// Points label /// /// 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 a3a1b26..b4cd428 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -243,6 +244,9 @@ class AppLocalizationsDe extends AppLocalizations { return '$count Spieler'; } + @override + String get point => 'Punkt'; + @override String get points => 'Punkte'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 61b8934..2b9b205 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -243,6 +244,9 @@ class AppLocalizationsEn extends AppLocalizations { return '$count Players'; } + @override + String get point => 'Point'; + @override String get points => 'Points'; From 522441b0caea2311ebc0ba4a93f416c6d62afcfe Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 18:35:40 +0200 Subject: [PATCH 25/60] Added tie localization --- lib/l10n/arb/app_de.arb | 1 + lib/l10n/arb/app_en.arb | 1 + lib/l10n/generated/app_localizations.dart | 6 ++++++ lib/l10n/generated/app_localizations_de.dart | 4 +++- lib/l10n/generated/app_localizations_en.dart | 4 +++- 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index b72685d..46c780a 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -105,6 +105,7 @@ "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", + "tie": "Unentschieden", "today_at": "Heute um", "undo": "Rückgängig", "unknown_exception": "Unbekannter Fehler (siehe Konsole)", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 14e277f..a85e1b0 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -438,6 +438,7 @@ "successfully_added_player": "Successfully added player {playerName}", "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", + "tie": "Tie", "today_at": "Today at", "undo": "Undo", "unknown_exception": "Unknown Exception (see console)", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index a37722c..99c9317 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -722,6 +722,12 @@ abstract class AppLocalizations { /// **'This can\'t be undone.'** String get this_cannot_be_undone; + /// No description provided for @tie. + /// + /// In en, this message translates to: + /// **'Tie'** + String get tie; + /// Date format for today /// /// 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 b4cd428..51b4c62 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1,6 +1,5 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -339,6 +338,9 @@ class AppLocalizationsDe extends AppLocalizations { String get this_cannot_be_undone => 'Dies kann nicht rückgängig gemacht werden.'; + @override + String get tie => 'Unentschieden'; + @override String get today_at => 'Heute um'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 2b9b205..2b42e47 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1,6 +1,5 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -338,6 +337,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get this_cannot_be_undone => 'This can\'t be undone.'; + @override + String get tie => 'Tie'; + @override String get today_at => 'Today at'; From 9364f0d9d6b024afef444f010e598d9bf4d6047b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 18:38:00 +0200 Subject: [PATCH 26/60] Updated score and winner handling --- lib/data/dao/game_dao.dart | 22 ++++- lib/data/dao/game_dao.g.dart | 6 ++ lib/data/dao/match_dao.dart | 28 ++----- lib/data/dao/score_entry_dao.dart | 28 +++++-- lib/data/models/match.dart | 76 +++++++++++++---- .../group_view/group_detail_view.dart | 42 ++++++---- .../views/main_menu/home_view.dart | 15 ++-- .../create_match/create_match_view.dart | 4 - .../match_view/match_detail_view.dart | 83 +++++++++++-------- .../match_view/match_result_view.dart | 68 +++++++-------- .../main_menu/match_view/match_view.dart | 12 +-- .../views/main_menu/statistics_view.dart | 4 +- .../widgets/tiles/match_tile.dart | 23 ++++- lib/services/data_transfer_service.dart | 15 +--- test/db_tests/aggregates/match_test.dart | 12 ++- test/db_tests/aggregates/team_test.dart | 12 ++- .../relationships/player_match_test.dart | 1 + test/db_tests/values/score_test.dart | 4 +- test/services/data_transfer_service_test.dart | 10 +-- 19 files changed, 286 insertions(+), 179 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index f07e2c7..5632abd 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -2,11 +2,12 @@ import 'package:drift/drift.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/game_table.dart'; +import 'package:tallee/data/db/tables/match_table.dart'; import 'package:tallee/data/models/game.dart'; part 'game_dao.g.dart'; -@DriftAccessor(tables: [GameTable]) +@DriftAccessor(tables: [MatchTable, GameTable]) class GameDao extends DatabaseAccessor with _$GameDaoMixin { GameDao(super.db); @@ -44,6 +45,25 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { ); } + Future getGameByMatchId({required String matchId}) async { + final query = select(gameTable).join([ + innerJoin(matchTable, matchTable.gameId.equalsExp(gameTable.id)), + ])..where(matchTable.id.equals(matchId)); + + final result = await query.getSingle(); + final gameRow = result.readTable(gameTable); + + return Game( + id: gameRow.id, + name: gameRow.name, + ruleset: Ruleset.values.firstWhere((e) => e.name == gameRow.ruleset), + description: gameRow.description, + color: GameColor.values.firstWhere((e) => e.name == gameRow.color), + icon: gameRow.icon, + createdAt: gameRow.createdAt, + ); + } + /// Adds a new [game] to the database. /// If a game with the same ID already exists, no action is taken. /// Returns `true` if the game was added, `false` otherwise. diff --git a/lib/data/dao/game_dao.g.dart b/lib/data/dao/game_dao.g.dart index a998fe7..7b86d21 100644 --- a/lib/data/dao/game_dao.g.dart +++ b/lib/data/dao/game_dao.g.dart @@ -5,6 +5,8 @@ part of 'game_dao.dart'; // ignore_for_file: type=lint mixin _$GameDaoMixin on DatabaseAccessor { $GameTableTable get gameTable => attachedDatabase.gameTable; + $GroupTableTable get groupTable => attachedDatabase.groupTable; + $MatchTableTable get matchTable => attachedDatabase.matchTable; GameDaoManager get managers => GameDaoManager(this); } @@ -13,4 +15,8 @@ class GameDaoManager { GameDaoManager(this._db); $$GameTableTableTableManager get gameTable => $$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable); + $$GroupTableTableTableManager get groupTable => + $$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable); + $$MatchTableTableTableManager get matchTable => + $$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable); } diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 81925c7..0b9300e 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -34,7 +34,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { matchId: row.id, ); - final winner = await db.scoreEntryDao.getWinner(matchId: row.id); return Match( id: row.id, name: row.name, @@ -45,7 +44,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { createdAt: row.createdAt, endedAt: row.endedAt, scores: scores, - winner: winner, ); }), ); @@ -68,8 +66,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId); - final winner = await db.scoreEntryDao.getWinner(matchId: matchId); - return Match( id: result.id, name: result.name, @@ -80,7 +76,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { createdAt: result.createdAt, endedAt: result.endedAt, scores: scores, - winner: winner, ); } @@ -110,19 +105,14 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { } for (final pid in match.scores.keys) { - final playerScores = match.scores[pid]!; - await db.scoreEntryDao.addScoresAsList( - entrys: playerScores, - playerId: pid, - matchId: match.id, - ); - } - - if (match.winner != null) { - await db.scoreEntryDao.setWinner( - matchId: match.id, - playerId: match.winner!.id, - ); + final playerScores = match.scores[pid]; + if (playerScores != null) { + await db.scoreEntryDao.addScore( + entry: playerScores, + playerId: pid, + matchId: match.id, + ); + } } }); } @@ -300,7 +290,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { final group = await db.groupDao.getGroupById(groupId: groupId); final players = await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; - final winner = await db.scoreEntryDao.getWinner(matchId: row.id); return Match( id: row.id, name: row.name, @@ -310,7 +299,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { notes: row.notes ?? '', createdAt: row.createdAt, endedAt: row.endedAt, - winner: winner, ); }), ); diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index cdd42f9..9aa3ca2 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:drift/drift.dart'; +import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/score_entry_table.dart'; import 'package:tallee/data/models/player.dart'; @@ -83,21 +84,21 @@ class ScoreEntryDao extends DatabaseAccessor } /// Retrieves all scores for a specific match. - Future>> getAllMatchScores({ + Future> getAllMatchScores({ required String matchId, }) async { final query = select(scoreEntryTable) ..where((s) => s.matchId.equals(matchId)); final result = await query.get(); - final Map> scoresByPlayer = {}; + final Map scoresByPlayer = {}; for (final row in result) { final score = ScoreEntry( roundNumber: row.roundNumber, score: row.score, change: row.change, ); - scoresByPlayer.putIfAbsent(row.playerId, () => []).add(score); + scoresByPlayer[row.playerId] = score; } return scoresByPlayer; @@ -237,10 +238,25 @@ class ScoreEntryDao extends DatabaseAccessor // Retrieves the winner of a match based on the highest score. Future getWinner({required String matchId}) async { + // Check the ruleset of the match + final ruleset = await db.gameDao + .getGameByMatchId(matchId: matchId) + .then((game) => game.ruleset); + final query = select(scoreEntryTable) - ..where((s) => s.matchId.equals(matchId)) - ..orderBy([(s) => OrderingTerm.desc(s.score)]) - ..limit(1); + ..where((s) => s.matchId.equals(matchId)); + + // If the ruleset is lowestScore, the winner is the player with the lowest + // score so we order by ascending score. + if (ruleset == Ruleset.lowestScore) { + query + ..orderBy([(s) => OrderingTerm.asc(s.score)]) + ..limit(1); + } else { + query + ..orderBy([(s) => OrderingTerm.desc(s.score)]) + ..limit(1); + } final result = await query.getSingleOrNull(); if (result == null) return null; diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 60103de..5c37cbe 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -15,27 +15,25 @@ class Match { final Group? group; final List players; final String notes; - Map> scores; - Player? winner; + Map scores; Match({ - String? id, - DateTime? createdAt, - this.endedAt, required this.name, required this.game, + required this.players, + this.endedAt, this.group, - this.players = const [], this.notes = '', - Map>? scores, - this.winner, + String? id, + DateTime? createdAt, + Map? scores, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? clock.now(), - scores = scores ?? {for (var player in players) player.id: []}; + scores = scores ?? {for (Player p in players) p.id: null}; @override String toString() { - return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, winner: $winner}'; + return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, mvp: $mvp}'; } /// Creates a Match instance from a JSON object where related objects are @@ -71,10 +69,60 @@ class Match { 'gameId': game.id, 'groupId': group?.id, 'playerIds': players.map((player) => player.id).toList(), - 'scores': scores.map( - (playerId, scoreList) => - MapEntry(playerId, scoreList.map((score) => score.toJson()).toList()), - ), + 'scores': scores, 'notes': notes, }; + + List get mvp { + if (players.isEmpty || scores.isEmpty) return []; + + switch (game.ruleset) { + case Ruleset.highestScore: + return _getPlayersWithHighestScore(); + + case Ruleset.lowestScore: + return _getPlayersWithLowestScore(); + + case Ruleset.singleWinner: + return [_getPlayersWithHighestScore().first]; + + case Ruleset.singleLoser: + return [_getPlayersWithLowestScore().first]; + + case Ruleset.multipleWinners: + return []; + } + } + + List _getPlayersWithHighestScore() { + if (players.isEmpty || scores.isEmpty) return []; + + final int highestScore = players + .map((player) => scores[player.id]?.score) + .whereType() + .reduce((max, score) => score > max ? score : max); + + return players.where((player) { + final playerScores = scores[player.id]; + if (playerScores == null) return false; + return playerScores.score == highestScore; + }).toList(); + } + + List _getPlayersWithLowestScore() { + if (players.isEmpty || scores.values.every((score) => score == null)) { + return []; + } + + final int lowestScore = players + .map((player) => scores[player.id]?.score) + .whereType() + .reduce((min, score) => score < min ? score : min); + + return players.where((player) { + final playerScore = scores[player.id]; + if (playerScore == null) return false; + return playerScore.score == lowestScore; + }).toList(); + } } 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 1ef89ef..7feae0f 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 @@ -36,6 +36,7 @@ class GroupDetailView extends StatefulWidget { } class _GroupDetailViewState extends State { + late final AppLocalizations loc; late final AppDatabase db; bool isLoading = true; late Group _group; @@ -51,13 +52,12 @@ class _GroupDetailViewState extends State { super.initState(); _group = widget.group; db = Provider.of(context, listen: false); + loc = AppLocalizations.of(context); _loadStatistics(); } @override Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( @@ -259,28 +259,36 @@ class _GroupDetailViewState extends State { /// Determines the best player in the group based on match wins String _getBestPlayer(List matches) { - final bestPlayerCounts = {}; + final mvpCounts = {}; - // Count wins for each player for (var match in matches) { - if (match.winner != null && - _group.members.any((m) => m.id == match.winner?.id)) { - print(match.winner); - bestPlayerCounts.update( - match.winner!, - (value) => value + 1, - ifAbsent: () => 1, - ); + final mvps = match.mvp; + for (final mvpPlayer in mvps) { + if (_group.members.any((m) => m.id == mvpPlayer.id)) { + mvpCounts.update(mvpPlayer, (value) => value + 1, ifAbsent: () => 1); + } } } - // Sort players by win count - final sortedPlayers = bestPlayerCounts.entries.toList() + final sortedMvps = mvpCounts.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); - // Get the best player - bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-'; + if (sortedMvps.isEmpty) { + return '-'; + } - return bestPlayer; + // Check if there are multiple players with the same value + final highestMvpCount = sortedMvps.first.value; + final topPlayers = sortedMvps + .where((entry) => entry.value == highestMvpCount) + .toList(); + switch (topPlayers.length) { + case 0: + return '-'; + case 1: + return topPlayers.first.key.name; + default: + return loc.tie; + } } } diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index 09cec54..d6b9ee6 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -43,21 +43,25 @@ class _HomeViewState extends State { Match( name: 'Skeleton Match', game: Game( - name: '', + name: 'Skeleton Game', ruleset: Ruleset.singleWinner, - description: '', + description: 'This is a skeleton game description.', color: GameColor.blue, icon: '', ), group: Group( name: 'Skeleton Group', - description: '', + description: 'This is a skeleton group description.', members: [ Player(name: 'Skeleton Player 1', description: ''), Player(name: 'Skeleton Player 2', description: ''), ], ), - notes: '', + notes: 'These are skeleton notes.', + players: [ + Player(name: 'Skeleton Player 1', description: ''), + Player(name: 'Skeleton Player 2', description: ''), + ], ), ); @@ -231,7 +235,8 @@ class _HomeViewState extends State { final matchIndex = recentMatches.indexWhere((match) => match.id == matchId); if (matchIndex != -1) { setState(() { - recentMatches[matchIndex].winner = winner; + // TODO: fix + //recentMatches[matchIndex].winner = winner; }); } } 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 950b3a8..83e5d0c 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 @@ -277,7 +277,6 @@ class _CreateMatchViewState extends State { group: selectedGroup, players: selectedPlayers, game: tempGame, - winner: widget.matchToEdit!.winner, createdAt: widget.matchToEdit!.createdAt, endedAt: widget.matchToEdit!.endedAt, notes: widget.matchToEdit!.notes, @@ -314,9 +313,6 @@ class _CreateMatchViewState extends State { matchId: widget.matchToEdit!.id, playerId: player.id, ); - if (widget.matchToEdit!.winner?.id == player.id) { - updatedMatch.winner = null; - } } } 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 81c736d..1ac8a77 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 @@ -203,7 +203,7 @@ class _MatchDetailViewState extends State { text: loc.enter_results, icon: Icons.emoji_events, onPressed: () async { - match.winner = await Navigator.push( + await Navigator.push( context, adaptivePageRoute( fullscreenDialog: true, @@ -237,54 +237,29 @@ class _MatchDetailViewState extends State { } /// Returns the widget to be displayed in the result [InfoTile] - /// TODO: Update when score logic is overhauled Widget getResultWidget(AppLocalizations loc) { if (isSingleRowResult()) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: getResultRow(loc), + children: getSingleResultRow(loc), ); } else { - return Column( - children: [ - for (var player in match.players) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - player.name, - style: const TextStyle( - fontSize: 16, - color: CustomTheme.textColor, - ), - ), - Text( - '0 ${loc.points}', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, - ), - ), - ], - ), - ], - ); + return getScoreResultWidget(loc); } } /// Returns the result row for single winner/loser rulesets or a placeholder /// if no result is entered yet - /// TODO: Update when score logic is overhauled - List getResultRow(AppLocalizations loc) { - if (match.winner != null && match.game.ruleset == Ruleset.singleWinner) { + List getSingleResultRow(AppLocalizations loc) { + // Single Winner + if (match.mvp.isNotEmpty && match.game.ruleset == Ruleset.singleWinner) { return [ Text( loc.winner, style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), ), Text( - match.winner!.name, + match.mvp.first.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -292,6 +267,7 @@ class _MatchDetailViewState extends State { ), ), ]; + // Single Loser } else if (match.game.ruleset == Ruleset.singleLoser) { return [ Text( @@ -299,7 +275,7 @@ class _MatchDetailViewState extends State { style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), ), Text( - match.winner!.name, + match.mvp.first.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -307,6 +283,7 @@ class _MatchDetailViewState extends State { ), ), ]; + // No result entered yet } else { return [ Text( @@ -317,6 +294,46 @@ class _MatchDetailViewState extends State { } } + /// Returns the result widget for scores + Widget getScoreResultWidget(AppLocalizations loc) { + List<(String, int)> playerScores = []; + for (var player in match.players) { + int score = match.scores[player.id]?.score ?? 0; + playerScores.add((player.name, score)); + } + if (widget.match.game.ruleset == Ruleset.highestScore) { + playerScores.sort((a, b) => b.$2.compareTo(a.$2)); + } else if (widget.match.game.ruleset == Ruleset.lowestScore) { + playerScores.sort((a, b) => a.$2.compareTo(b.$2)); + } + + return Column( + children: [ + for (var score in playerScores) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + score.$1, + style: const TextStyle( + fontSize: 16, + color: CustomTheme.textColor, + ), + ), + Text( + getPointLabel(loc, score.$2), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), + ), + ], + ), + ], + ); + } + // Returns if the result can be displayed in a single row bool isSingleRowResult() { return match.game.ruleset == Ruleset.singleWinner || diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 92424ec..af59cf6 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -5,6 +5,7 @@ import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart'; @@ -14,20 +15,11 @@ class MatchResultView extends StatefulWidget { /// A view that allows selecting and saving the winner of a match /// [match]: The match for which the winner is to be selected /// [onWinnerChanged]: Optional callback invoked when the winner is changed - const MatchResultView({ - super.key, - required this.match, - this.ruleset = Ruleset.singleWinner, - this.onWinnerChanged, - }); + const MatchResultView({super.key, required this.match, this.onWinnerChanged}); /// The match for which the winner is to be selected final Match match; - /// The ruleset of the match, determines how the winner is selected or how - /// scores are entered - final Ruleset ruleset; - /// Optional callback invoked when the winner is changed final VoidCallback? onWinnerChanged; @@ -38,6 +30,8 @@ class MatchResultView extends StatefulWidget { class _MatchResultViewState extends State { late final AppDatabase db; + late final Ruleset ruleset; + /// List of all players who participated in the match late final List allPlayers; @@ -51,6 +45,8 @@ class _MatchResultViewState extends State { void initState() { db = Provider.of(context, listen: false); + ruleset = widget.match.game.ruleset; + allPlayers = widget.match.players; allPlayers.sort((a, b) => a.name.compareTo(b.name)); @@ -59,13 +55,17 @@ class _MatchResultViewState extends State { (index) => TextEditingController(), ); - if (widget.match.winner != null) { + if (widget.match.mvp.isNotEmpty) { if (rulesetSupportsWinnerSelection()) { _selectedPlayer = allPlayers.firstWhere( - (p) => p.id == widget.match.winner!.id, + (p) => p.id == widget.match.mvp.first.id, ); } else if (rulesetSupportsScoreEntry()) { - /// TODO: Update when score logic is overhauled + for (int i = 0; i < allPlayers.length; i++) { + final scoreList = widget.match.scores[allPlayers[i].id]; + final score = scoreList?.score ?? 0; + controller[i].text = score.toString(); + } } } super.initState(); @@ -154,7 +154,6 @@ class _MatchResultViewState extends State { child: ListView.separated( itemCount: allPlayers.length, itemBuilder: (context, index) { - print(allPlayers[index].name); return ScoreListTile( text: allPlayers[index].name, controller: controller[index], @@ -176,6 +175,11 @@ class _MatchResultViewState extends State { text: loc.save_changes, sizeRelativeToWidth: 0.95, onPressed: () async { + final ending = DateTime.now(); + await db.matchDao.updateMatchEndedAt( + matchId: widget.match.id, + endedAt: ending, + ); await _handleSaving(); if (!context.mounted) return; Navigator.of(context).pop(_selectedPlayer); @@ -190,12 +194,12 @@ class _MatchResultViewState extends State { /// Handles saving or removing the winner in the database /// based on the current selection. Future _handleSaving() async { - if (widget.ruleset == Ruleset.singleWinner) { + if (ruleset == Ruleset.singleWinner) { await _handleWinner(); - } else if (widget.ruleset == Ruleset.singleLoser) { + } else if (ruleset == Ruleset.singleLoser) { await _handleLoser(); - } else if (widget.ruleset == Ruleset.lowestScore || - widget.ruleset == Ruleset.highestScore) { + } else if (ruleset == Ruleset.lowestScore || + ruleset == Ruleset.highestScore) { await _handleScores(); } @@ -204,9 +208,9 @@ class _MatchResultViewState extends State { Future _handleWinner() async { if (_selectedPlayer == null) { - await db.scoreEntryDao.removeWinner(matchId: widget.match.id); + return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); } else { - await db.scoreEntryDao.setWinner( + return await db.scoreEntryDao.setWinner( matchId: widget.match.id, playerId: _selectedPlayer!.id, ); @@ -215,33 +219,33 @@ class _MatchResultViewState extends State { Future _handleLoser() async { if (_selectedPlayer == null) { - /// TODO: Update when score logic is overhauled - return false; + return await db.scoreEntryDao.removeLooser(matchId: widget.match.id); } else { - /// TODO: Update when score logic is overhauled - return false; + return await db.scoreEntryDao.setLooser( + matchId: widget.match.id, + playerId: _selectedPlayer!.id, + ); } } /// Handles saving the scores for each player in the database. - Future _handleScores() async { + Future _handleScores() async { for (int i = 0; i < allPlayers.length; i++) { var text = controller[i].text; if (text.isEmpty) { text = '0'; } final score = int.parse(text); - await db.playerMatchDao.updatePlayerScore( + await db.scoreEntryDao.addScore( matchId: widget.match.id, playerId: allPlayers[i].id, - newScore: score, + entry: ScoreEntry(roundNumber: 0, score: score, change: 0), ); } - return false; } String getTitleForRuleset(AppLocalizations loc) { - switch (widget.ruleset) { + switch (ruleset) { case Ruleset.singleWinner: return loc.select_winner; case Ruleset.singleLoser: @@ -252,12 +256,10 @@ class _MatchResultViewState extends State { } bool rulesetSupportsWinnerSelection() { - return widget.ruleset == Ruleset.singleWinner || - widget.ruleset == Ruleset.singleLoser; + return ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser; } bool rulesetSupportsScoreEntry() { - return widget.ruleset == Ruleset.lowestScore || - widget.ruleset == Ruleset.highestScore; + return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore; } } 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 1a202c4..2fb36e7 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -37,20 +37,16 @@ class _MatchViewState extends State { Match( name: 'Skeleton match name', game: Game( - name: '', + name: 'Game name', ruleset: Ruleset.singleWinner, - description: '', color: GameColor.blue, icon: '', ), group: Group( name: 'Group name', - description: '', - members: List.filled(5, Player(name: 'Player', description: '')), + members: List.filled(5, Player(name: 'Player')), ), - winner: Player(name: 'Player', description: ''), - players: [Player(name: 'Player', description: '')], - notes: '', + players: [Player(name: 'Player')], ), ); @@ -116,7 +112,7 @@ class _MatchViewState extends State { Positioned( bottom: MediaQuery.paddingOf(context).bottom + 20, child: MainMenuButton( - text: 'Spiel erstellen', + text: loc.create_match, icon: RpgAwesome.clovers_card, onPressed: () async { Navigator.push( diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 3a55115..b0b9c22 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -140,8 +140,8 @@ class _StatisticsViewState extends State { // Getting the winners for (var match in matches) { - final winner = match.winner; - if (winner != null) { + final mvps = match.mvp; + for (var winner in mvps) { final index = winCounts.indexWhere((entry) => entry.$1 == winner.id); // -1 means winner not found in winCounts if (index != -1) { diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 4977b8e..5727c71 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tallee/core/common.dart'; 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/tiles/text_icon_tile.dart'; @@ -44,7 +45,6 @@ class _MatchTileState extends State { Widget build(BuildContext context) { final match = widget.match; final group = match.group; - final winner = match.winner; final players = [...match.players] ..sort((a, b) => a.name.compareTo(b.name)); final loc = AppLocalizations.of(context); @@ -131,7 +131,7 @@ class _MatchTileState extends State { const SizedBox(height: 12), ], - if (winner != null) ...[ + if (match.mvp.isNotEmpty) ...[ Container( padding: const EdgeInsets.symmetric( vertical: 8, @@ -155,7 +155,7 @@ class _MatchTileState extends State { const SizedBox(width: 8), Expanded( child: Text( - '${loc.winner}: ${winner.name}', + getWinner(loc), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -248,4 +248,21 @@ class _MatchTileState extends State { return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}'; } } + + String getWinner(AppLocalizations loc) { + if (widget.match.mvp.isEmpty) return ''; + final ruleset = widget.match.game.ruleset; + + if (ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser) { + return '${loc.winner}: ${widget.match.mvp.first.name}'; + } else if (ruleset == Ruleset.lowestScore || + ruleset == Ruleset.lowestScore) { + final mvp = widget.match.mvp; + final mvpScore = widget.match.scores[mvp.first.id]?.score ?? 0; + final mvpNames = mvp.map((player) => player.name).join(', '); + + return '${loc.winner}: $mvpNames (${getPointLabel(loc, mvpScore)})'; + } + return '${loc.winner}: n.A.'; + } } diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index a0fd57b..05896ea 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -71,20 +71,7 @@ class DataTransferService { 'gameId': m.game.id, 'groupId': m.group?.id, 'playerIds': m.players.map((p) => p.id).toList(), - 'scores': m.scores.map( - (playerId, scores) => MapEntry( - playerId, - scores - .map( - (s) => { - 'roundNumber': s.roundNumber, - 'score': s.score, - 'change': s.change, - }, - ) - .toList(), - ), - ), + 'scores': m.scores, 'notes': m.notes, }, ) diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index dee0eb9..245cca1 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -63,7 +63,6 @@ void main() { game: testGame, group: testGroup1, players: [testPlayer4, testPlayer5], - winner: testPlayer4, notes: '', ); testMatch2 = Match( @@ -71,20 +70,19 @@ void main() { game: testGame, group: testGroup2, players: [testPlayer1, testPlayer2, testPlayer3], - winner: testPlayer2, notes: '', ); testMatchOnlyPlayers = Match( name: 'Test Match with Players', game: testGame, players: [testPlayer1, testPlayer2, testPlayer3], - winner: testPlayer3, notes: '', ); testMatchOnlyGroup = Match( name: 'Test Match with Group', game: testGame, group: testGroup2, + players: testGroup2.members, notes: '', ); }); @@ -289,8 +287,8 @@ void main() { matchId: testMatch1.id, ); - expect(fetchedMatch.winner, isNotNull); - expect(fetchedMatch.winner!.id, testPlayer4.id); + expect(fetchedMatch.mvp, isNotNull); + expect(fetchedMatch.mvp.first.id, testPlayer4.id); }); test('Setting a winner works correctly', () async { @@ -304,8 +302,8 @@ void main() { final fetchedMatch = await database.matchDao.getMatchById( matchId: testMatch1.id, ); - expect(fetchedMatch.winner, isNotNull); - expect(fetchedMatch.winner!.id, testPlayer5.id); + expect(fetchedMatch.mvp, isNotNull); + expect(fetchedMatch.mvp.first.id, testPlayer5.id); }); test( diff --git a/test/db_tests/aggregates/team_test.dart b/test/db_tests/aggregates/team_test.dart index 327bc8f..0850936 100644 --- a/test/db_tests/aggregates/team_test.dart +++ b/test/db_tests/aggregates/team_test.dart @@ -343,8 +343,16 @@ void main() { // Verifies that teams with overlapping members are independent. test('Teams with overlapping members are independent', () async { // Create two matches since player_match has primary key {playerId, matchId} - final match1 = Match(name: 'Match 1', game: testGame1, notes: ''); - final match2 = Match(name: 'Match 2', game: testGame2, notes: ''); + final match1 = Match( + name: 'Match 1', + game: testGame1, + players: [testPlayer1, testPlayer2], + ); + final match2 = Match( + name: 'Match 2', + game: testGame2, + players: [testPlayer1, testPlayer2], + ); await database.matchDao.addMatch(match: match1); await database.matchDao.addMatch(match: match2); diff --git a/test/db_tests/relationships/player_match_test.dart b/test/db_tests/relationships/player_match_test.dart index 3db48de..3071edf 100644 --- a/test/db_tests/relationships/player_match_test.dart +++ b/test/db_tests/relationships/player_match_test.dart @@ -58,6 +58,7 @@ void main() { testMatchOnlyGroup = Match( name: 'Test Match with Group', game: testGame, + players: testGroup.members, group: testGroup, notes: '', ); diff --git a/test/db_tests/values/score_test.dart b/test/db_tests/values/score_test.dart index fc87cc4..c5e2209 100644 --- a/test/db_tests/values/score_test.dart +++ b/test/db_tests/values/score_test.dart @@ -231,8 +231,8 @@ void main() { ); expect(scores.length, 2); - expect(scores[testPlayer1.id]!.length, 2); - expect(scores[testPlayer2.id]!.length, 1); + expect(scores[testPlayer1.id]!, isNotNull); + expect(scores[testPlayer2.id]!, isNotNull); }); test('getAllMatchScores() with no scores saved', () async { diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index 575e52f..12eb4b1 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -64,14 +64,8 @@ void main() { players: [testPlayer1, testPlayer2], notes: 'Test notes', scores: { - testPlayer1.id: [ - ScoreEntry(roundNumber: 1, score: 10, change: 10), - ScoreEntry(roundNumber: 2, score: 20, change: 10), - ], - testPlayer2.id: [ - ScoreEntry(roundNumber: 1, score: 15, change: 15), - ScoreEntry(roundNumber: 2, score: 25, change: 10), - ], + testPlayer1.id: ScoreEntry(roundNumber: 1, score: 10, change: 10), + testPlayer2.id: ScoreEntry(roundNumber: 1, score: 15, change: 15), }, ); }); From c6a9e53cff48b92cc8383edfee16c4144f921b46 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 18:53:03 +0200 Subject: [PATCH 27/60] Updated getting winner & looser functions --- lib/data/dao/score_entry_dao.dart | 76 +++++++++++++++---------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index 9aa3ca2..016e2f9 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:drift/drift.dart'; -import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/score_entry_table.dart'; import 'package:tallee/data/models/player.dart'; @@ -236,37 +235,29 @@ class ScoreEntryDao extends DatabaseAccessor return rowsAffected > 0; } - // Retrieves the winner of a match based on the highest score. + // Retrieves the winner of a match by looking for a score entry where score + /// is 1. Returns `null` if no player found, else the first with the score. Future getWinner({required String matchId}) async { - // Check the ruleset of the match - final ruleset = await db.gameDao - .getGameByMatchId(matchId: matchId) - .then((game) => game.ruleset); + final query = + select(scoreEntryTable).join([ + innerJoin( + db.playerTable, + db.playerTable.id.equalsExp(scoreEntryTable.playerId), + ), + ])..where( + scoreEntryTable.matchId.equals(matchId) & + scoreEntryTable.score.equals(1), + ); - final query = select(scoreEntryTable) - ..where((s) => s.matchId.equals(matchId)); + final result = await query.get(); + if (result.isNotEmpty) return null; - // If the ruleset is lowestScore, the winner is the player with the lowest - // score so we order by ascending score. - if (ruleset == Ruleset.lowestScore) { - query - ..orderBy([(s) => OrderingTerm.asc(s.score)]) - ..limit(1); - } else { - query - ..orderBy([(s) => OrderingTerm.desc(s.score)]) - ..limit(1); - } - final result = await query.getSingleOrNull(); - - if (result == null) return null; - - final player = await db.playerDao.getPlayerById(playerId: result.playerId); + final playerData = result.first.readTable(db.playerTable); return Player( - id: player.id, - name: player.name, - createdAt: player.createdAt, - description: player.description, + id: playerData.id, + name: playerData.name, + createdAt: playerData.createdAt, + description: playerData.description, ); } @@ -311,20 +302,29 @@ class ScoreEntryDao extends DatabaseAccessor return rowsAffected > 0; } - /// Retrieves the looser of a match based on the score 0. + /// Retrieves the looser of a match by looking for a score entry where score + /// is 0. Returns `null` if no player found, else the first with the score. Future getLooser({required String matchId}) async { - final query = select(scoreEntryTable) - ..where((s) => s.matchId.equals(matchId) & s.score.equals(0)); - final result = await query.getSingleOrNull(); + final query = + select(scoreEntryTable).join([ + innerJoin( + db.playerTable, + db.playerTable.id.equalsExp(scoreEntryTable.playerId), + ), + ])..where( + scoreEntryTable.matchId.equals(matchId) & + scoreEntryTable.score.equals(0), + ); - if (result == null) return null; + final result = await query.get(); + if (result.isNotEmpty) return null; - final player = await db.playerDao.getPlayerById(playerId: result.playerId); + final playerData = result.first.readTable(db.playerTable); return Player( - id: player.id, - name: player.name, - createdAt: player.createdAt, - description: player.description, + id: playerData.id, + name: playerData.name, + createdAt: playerData.createdAt, + description: playerData.description, ); } From 86f2ba01e5e53af4954a4f346adcef4214aa8128 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 20:06:14 +0200 Subject: [PATCH 28/60] Updated localization declaration --- .../views/main_menu/group_view/group_detail_view.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 7feae0f..fa5e276 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 @@ -36,7 +36,6 @@ class GroupDetailView extends StatefulWidget { } class _GroupDetailViewState extends State { - late final AppLocalizations loc; late final AppDatabase db; bool isLoading = true; late Group _group; @@ -52,12 +51,13 @@ class _GroupDetailViewState extends State { super.initState(); _group = widget.group; db = Provider.of(context, listen: false); - loc = AppLocalizations.of(context); _loadStatistics(); } @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( @@ -288,6 +288,7 @@ class _GroupDetailViewState extends State { case 1: return topPlayers.first.key.name; default: + final loc = AppLocalizations.of(context); return loc.tie; } } From 68141a3da2a5a90473146591991f0e9edd68f978 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 20:06:33 +0200 Subject: [PATCH 29/60] Updated game + ruleset display --- .../widgets/tiles/match_tile.dart | 81 ++++++++++++++----- 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 5727c71..eb82927 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -79,26 +79,6 @@ class _MatchTileState extends State { ], ), - const SizedBox(height: 4), - - Container( - decoration: BoxDecoration( - color: CustomTheme.primaryColor, - borderRadius: BorderRadius.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, - ), - ), - ), - - const SizedBox(height: 8), - if (group != null) ...[ Row( children: [ @@ -113,7 +93,7 @@ class _MatchTileState extends State { ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 4), ] else if (widget.compact) ...[ Row( children: [ @@ -128,9 +108,66 @@ class _MatchTileState extends State { ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 6), + ] else ...[ + const SizedBox(height: 8), ], + // Game + Ruleset Badge + IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Game + Container( + decoration: BoxDecoration( + color: CustomTheme.primaryColor.withAlpha(200), + 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(120), + 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, + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 12), + if (match.mvp.isNotEmpty) ...[ Container( padding: const EdgeInsets.symmetric( From 9230b0cabae8f879721054ffefcb3412ba5ba3ce Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 20:06:41 +0200 Subject: [PATCH 30/60] Removed dead code --- lib/presentation/widgets/tiles/score_list_tile.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/presentation/widgets/tiles/score_list_tile.dart b/lib/presentation/widgets/tiles/score_list_tile.dart index d6aafe3..52103fa 100644 --- a/lib/presentation/widgets/tiles/score_list_tile.dart +++ b/lib/presentation/widgets/tiles/score_list_tile.dart @@ -11,9 +11,6 @@ class ScoreListTile extends StatelessWidget { super.key, required this.text, required this.controller, - /* - required this.onContainerTap, -*/ }); /// The text to display next to the radio button. @@ -21,11 +18,6 @@ class ScoreListTile extends StatelessWidget { final TextEditingController controller; - /// The callback invoked when the container is tapped. - /* - final ValueChanged onContainerTap; -*/ - @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); From 5e6cf22a9fb73b1afae9dc29826cd925d3fef17b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 20:17:17 +0200 Subject: [PATCH 31/60] Added empty check for score fields --- .../match_view/match_result_view.dart | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index af59cf6..8b41920 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -38,21 +38,23 @@ class _MatchResultViewState extends State { /// List of text controllers for score entry, one for each player late final List controller; + late bool canSave; + /// Currently selected winner player Player? _selectedPlayer; @override void initState() { db = Provider.of(context, listen: false); - ruleset = widget.match.game.ruleset; + canSave = !rulesetSupportsScoreEntry(); allPlayers = widget.match.players; allPlayers.sort((a, b) => a.name.compareTo(b.name)); controller = List.generate( allPlayers.length, - (index) => TextEditingController(), + (index) => TextEditingController()..addListener(() => onTextEnter()), ); if (widget.match.mvp.isNotEmpty) { @@ -67,8 +69,16 @@ class _MatchResultViewState extends State { controller[i].text = score.toString(); } } + super.initState(); } - super.initState(); + } + + @override + void dispose() { + for (final c in controller) { + c.dispose(); + } + super.dispose(); } @override @@ -174,16 +184,18 @@ class _MatchResultViewState extends State { CustomWidthButton( text: loc.save_changes, sizeRelativeToWidth: 0.95, - onPressed: () async { - final ending = DateTime.now(); - await db.matchDao.updateMatchEndedAt( - matchId: widget.match.id, - endedAt: ending, - ); - await _handleSaving(); - if (!context.mounted) return; - Navigator.of(context).pop(_selectedPlayer); - }, + onPressed: canSave + ? () async { + final ending = DateTime.now(); + await db.matchDao.updateMatchEndedAt( + matchId: widget.match.id, + endedAt: ending, + ); + await _handleSaving(); + if (!context.mounted) return; + Navigator.of(context).pop(_selectedPlayer); + } + : null, ), ], ), @@ -191,6 +203,15 @@ class _MatchResultViewState extends State { ); } + /// Updated [canSave] everytime a text is entered in one of the score entry fields. + void onTextEnter() { + if (rulesetSupportsScoreEntry()) { + setState(() { + canSave = controller.every((c) => c.text.isNotEmpty); + }); + } + } + /// Handles saving or removing the winner in the database /// based on the current selection. Future _handleSaving() async { @@ -206,6 +227,7 @@ class _MatchResultViewState extends State { widget.onWinnerChanged?.call(); } + /// Handles saving or removing the winner in the database. Future _handleWinner() async { if (_selectedPlayer == null) { return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); @@ -217,6 +239,7 @@ class _MatchResultViewState extends State { } } + /// Handles saving or removing the loser in the database. Future _handleLoser() async { if (_selectedPlayer == null) { return await db.scoreEntryDao.removeLooser(matchId: widget.match.id); From 2c2bb582fd41b6a42d5f2758e2bfcc6a87aeac1c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 20:17:35 +0200 Subject: [PATCH 32/60] Added null & empty handling --- lib/data/models/match.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 5c37cbe..8cfaf05 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -84,10 +84,10 @@ class Match { return _getPlayersWithLowestScore(); case Ruleset.singleWinner: - return [_getPlayersWithHighestScore().first]; + return _getPlayersWithHighestScore().take(1).toList(); case Ruleset.singleLoser: - return [_getPlayersWithLowestScore().first]; + return _getPlayersWithLowestScore().take(1).toList(); case Ruleset.multipleWinners: return []; @@ -95,7 +95,9 @@ class Match { } List _getPlayersWithHighestScore() { - if (players.isEmpty || scores.isEmpty) return []; + if (players.isEmpty || scores.values.every((score) => score == null)) { + return []; + } final int highestScore = players .map((player) => scores[player.id]?.score) From 4322e758111674e29216c90127de12af735667f0 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 20:17:54 +0200 Subject: [PATCH 33/60] Implemented basic game choosing functionality --- .../create_match/choose_game_view.dart | 38 ++++---- .../create_match/create_match_view.dart | 89 ++++++------------- 2 files changed, 50 insertions(+), 77 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 d4d7f4d..51512f9 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,7 @@ 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/data/models/game.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; @@ -13,14 +13,14 @@ class ChooseGameView extends StatefulWidget { const ChooseGameView({ super.key, required this.games, - required this.initialGameIndex, + required this.initialGameId, }); /// A list of tuples containing the game name, description and ruleset - final List<(String, String, Ruleset)> games; + final List games; - /// The index of the initially selected game - final int initialGameIndex; + /// The id of the initially selected game + final String initialGameId; @override State createState() => _ChooseGameViewState(); @@ -31,11 +31,11 @@ class _ChooseGameViewState extends State { final TextEditingController searchBarController = TextEditingController(); /// Currently selected game index - late int selectedGameIndex; + late String selectedGameId; @override void initState() { - selectedGameIndex = widget.initialGameIndex; + selectedGameId = widget.initialGameId; super.initState(); } @@ -49,7 +49,13 @@ class _ChooseGameViewState extends State { leading: IconButton( icon: const Icon(Icons.arrow_back_ios), onPressed: () { - Navigator.of(context).pop(selectedGameIndex); + Navigator.of(context).pop( + selectedGameId == '' + ? null + : widget.games.firstWhere( + (game) => game.id == selectedGameId, + ), + ); }, ), title: Text(loc.choose_game), @@ -62,7 +68,7 @@ class _ChooseGameViewState extends State { if (didPop) { return; } - Navigator.of(context).pop(selectedGameIndex); + Navigator.of(context).pop(widget.initialGameId); }, child: Column( children: [ @@ -79,19 +85,19 @@ class _ChooseGameViewState extends State { itemCount: widget.games.length, itemBuilder: (BuildContext context, int index) { return TitleDescriptionListTile( - title: widget.games[index].$1, - description: widget.games[index].$2, + title: widget.games[index].name, + description: widget.games[index].description, badgeText: translateRulesetToString( - widget.games[index].$3, + widget.games[index].ruleset, context, ), - isHighlighted: selectedGameIndex == index, + isHighlighted: selectedGameId == widget.games[index].id, onPressed: () async { setState(() { - if (selectedGameIndex == index) { - selectedGameIndex = -1; + if (selectedGameId != widget.games[index].id) { + selectedGameId = widget.games[index].id; } else { - selectedGameIndex = index; + selectedGameId = ''; } }); }, 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 83e5d0c..1a04c78 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 @@ -20,7 +20,9 @@ import 'package:tallee/presentation/widgets/tiles/choose_tile.dart'; class CreateMatchView extends StatefulWidget { /// A view that allows creating a new match - /// [onWinnerChanged]: Optional callback invoked when the winner is changed + /// - [onWinnerChanged]: Optional callback invoked when the winner is changed + /// - [matchToEdit]: An optional match to prefill the fields for editing. + /// - [onMatchUpdated]: Optional callback invoked when the match is updated (only in const CreateMatchView({ super.key, this.onWinnerChanged, @@ -28,13 +30,11 @@ class CreateMatchView extends StatefulWidget { this.onMatchUpdated, }); - /// Optional callback invoked when the winner is changed final VoidCallback? onWinnerChanged; - /// Optional callback invoked when the match is updated final void Function(Match)? onMatchUpdated; - /// An optional match to prefill the fields + /// An optional match to prefill the fields for editing. final Match? matchToEdit; @override @@ -50,20 +50,12 @@ class _CreateMatchViewState extends State { /// Hint text for the match name input field String? hintText; - /// List of all groups from the database List groupsList = []; - - /// List of all players from the database List playerList = []; + List gamesList = []; - /// The currently selected group Group? selectedGroup; - - /// The index of the currently selected game in [games] to mark it in - /// the [ChooseGameView] - int selectedGameIndex = -1; - - /// The currently selected players + Game? selectedGame; List selectedPlayers = []; /// GlobalKey for ScaffoldMessenger to show snackbars @@ -81,12 +73,14 @@ class _CreateMatchViewState extends State { Future.wait([ db.groupDao.getAllGroups(), db.playerDao.getAllPlayers(), + db.gameDao.getAllGames(), ]).then((result) async { groupsList = result[0] as List; playerList = result[1] as List; + gamesList = (result[2] as List); // If a match is provided, prefill the fields - if (widget.matchToEdit != null) { + if (isEditMode()) { prefillMatchDetails(); } }); @@ -105,20 +99,11 @@ class _CreateMatchViewState extends State { hintText ??= loc.match_name; } - List<(String, String, Ruleset)> games = [ - ('Example Game 1', 'This is a description', Ruleset.lowestScore), - ('Example Game 2', '', Ruleset.singleWinner), - ]; - @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); - final buttonText = widget.matchToEdit != null - ? loc.save_changes - : loc.create_match; - final viewTitle = widget.matchToEdit != null - ? loc.edit_match - : loc.create_new_match; + final buttonText = isEditMode() ? loc.save_changes : loc.create_match; + final viewTitle = isEditMode() ? loc.edit_match : loc.create_new_match; return ScaffoldMessenger( key: _scaffoldMessengerKey, @@ -140,21 +125,21 @@ class _CreateMatchViewState extends State { ), ChooseTile( title: loc.game, - trailingText: selectedGameIndex == -1 - ? loc.none - : games[selectedGameIndex].$1, + trailingText: selectedGame == null + ? loc.none_group + : selectedGame!.name, onPressed: () async { - selectedGameIndex = await Navigator.of(context).push( + selectedGame = await Navigator.of(context).push( adaptivePageRoute( builder: (context) => ChooseGameView( - games: games, - initialGameIndex: selectedGameIndex, + games: gamesList, + initialGameId: selectedGame?.id ?? '', ), ), ); setState(() { - if (selectedGameIndex != -1) { - hintText = games[selectedGameIndex].$1; + if (selectedGame != null) { + hintText = selectedGame!.name; } else { hintText = loc.match_name; } @@ -225,6 +210,10 @@ class _CreateMatchViewState extends State { ); } + bool isEditMode() { + return widget.matchToEdit != null; + } + /// Determines whether the "Create Match" button should be enabled. /// /// Returns `true` if: @@ -232,7 +221,7 @@ class _CreateMatchViewState extends State { /// - Either a group is selected OR at least 2 players are selected bool _enableCreateGameButton() { return (selectedGroup != null || - (selectedPlayers.length > 1) && selectedGameIndex != -1); + (selectedPlayers.length > 1) && selectedGame != null); } // If a match was provided to the view, it updates the match in the database @@ -240,7 +229,7 @@ class _CreateMatchViewState extends State { // If no match was provided, it creates a new match in the database and // navigates to the MatchResultView for the newly created match. void buttonNavigation(BuildContext context) async { - if (widget.matchToEdit != null) { + if (isEditMode()) { await updateMatch(); if (context.mounted) { Navigator.pop(context); @@ -266,9 +255,6 @@ class _CreateMatchViewState extends State { /// Updates attributes of the existing match in the database based on the /// changes made in the edit view. Future updateMatch() async { - //TODO: Remove when Games implemented - final tempGame = await getTemporaryGame(); - final updatedMatch = Match( id: widget.matchToEdit!.id, name: _matchNameController.text.isEmpty @@ -276,7 +262,7 @@ class _CreateMatchViewState extends State { : _matchNameController.text.trim(), group: selectedGroup, players: selectedPlayers, - game: tempGame, + game: widget.matchToEdit!.game, createdAt: widget.matchToEdit!.createdAt, endedAt: widget.matchToEdit!.endedAt, notes: widget.matchToEdit!.notes, @@ -322,8 +308,6 @@ class _CreateMatchViewState extends State { // Creates a new match and adds it to the database. // Returns the created match. Future createMatch() async { - final tempGame = await getTemporaryGame(); - Match match = Match( name: _matchNameController.text.isEmpty ? (hintText ?? '') @@ -331,35 +315,18 @@ class _CreateMatchViewState extends State { createdAt: DateTime.now(), group: selectedGroup, players: selectedPlayers, - game: tempGame, + game: selectedGame!, ); await db.matchDao.addMatch(match: match); return match; } - // TODO: Remove when games fully implemented - Future getTemporaryGame() async { - Game? game; - - final selectedGame = games[selectedGameIndex]; - game = Game( - name: selectedGame.$1, - description: selectedGame.$2, - ruleset: selectedGame.$3, - color: GameColor.blue, - icon: '', - ); - - await db.gameDao.addGame(game: game); - return game; - } - // If a match was provided to the view, this method prefills the input fields void prefillMatchDetails() { final match = widget.matchToEdit!; _matchNameController.text = match.name; selectedPlayers = match.players; - selectedGameIndex = 0; + selectedGame = match.game; if (match.group != null) { selectedGroup = match.group; From 0bad0862a7467c6fcd6babccfbb2c76ecd7069a8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 20:19:05 +0200 Subject: [PATCH 34/60] Adjusted colors --- lib/presentation/widgets/tiles/match_tile.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index eb82927..08c2fdc 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -79,6 +79,7 @@ class _MatchTileState extends State { ], ), + // Group Info if (group != null) ...[ Row( children: [ @@ -121,7 +122,7 @@ class _MatchTileState extends State { // Game Container( decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(200), + color: CustomTheme.primaryColor.withAlpha(230), borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), bottomLeft: Radius.circular(8), @@ -143,7 +144,7 @@ class _MatchTileState extends State { // Ruleset Container( decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(120), + color: CustomTheme.primaryColor.withAlpha(140), borderRadius: const BorderRadius.only( topRight: Radius.circular(8), bottomRight: Radius.circular(8), @@ -168,6 +169,7 @@ class _MatchTileState extends State { const SizedBox(height: 12), + // Winner / In Progress Info if (match.mvp.isNotEmpty) ...[ Container( padding: const EdgeInsets.symmetric( @@ -244,6 +246,7 @@ class _MatchTileState extends State { const SizedBox(height: 12), ], + // Players List if (players.isNotEmpty && widget.compact == false) ...[ Text( loc.players, From 2035e5b7d42046be8479b8fd9dd93cf9048a1dd4 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 20:24:10 +0200 Subject: [PATCH 35/60] Removed unnecessary declared attributes --- .../group_view/create_group_view.dart | 2 +- .../main_menu/group_view/group_view.dart | 2 +- .../views/main_menu/statistics_view.dart | 6 ++--- .../widgets/player_selection.dart | 4 ++-- test/db_tests/aggregates/group_test.dart | 8 +++---- test/db_tests/aggregates/match_test.dart | 22 +++++++---------- test/db_tests/aggregates/team_test.dart | 8 +++---- test/db_tests/entities/player_test.dart | 12 +++++----- .../relationships/player_group_test.dart | 8 +++---- .../relationships/player_match_test.dart | 24 ++++++++----------- test/db_tests/values/score_test.dart | 8 +++---- 11 files changed, 46 insertions(+), 58 deletions(-) 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 d5ac6a4..f88e2db 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 @@ -148,7 +148,7 @@ class _CreateGroupViewState extends State { final groupName = _groupNameController.text.trim(); final success = await db.groupDao.addGroup( - group: Group(name: groupName, description: '', members: selectedPlayers), + group: Group(name: groupName, members: selectedPlayers), ); return success; diff --git a/lib/presentation/views/main_menu/group_view/group_view.dart b/lib/presentation/views/main_menu/group_view/group_view.dart index b091541..c8a9398 100644 --- a/lib/presentation/views/main_menu/group_view/group_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_view.dart @@ -36,7 +36,7 @@ class _GroupViewState extends State { Group( name: 'Skeleton Group', description: '', - members: List.filled(6, Player(name: 'Skeleton Player', description: '')), + members: List.filled(6, Player(name: 'Skeleton Player')), ), ); diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index b0b9c22..3ffab7c 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -167,8 +167,7 @@ class _StatisticsViewState extends State { final playerId = winCounts[i].$1; final player = players.firstWhere( (p) => p.id == playerId, - orElse: () => - Player(id: playerId, name: loc.not_available, description: ''), + orElse: () => Player(id: playerId, name: loc.not_available), ); winCounts[i] = (player.name, winCounts[i].$2); } @@ -230,8 +229,7 @@ class _StatisticsViewState extends State { final playerId = matchCounts[i].$1; final player = players.firstWhere( (p) => p.id == playerId, - orElse: () => - Player(id: playerId, name: loc.not_available, description: ''), + orElse: () => Player(id: playerId, name: loc.not_available), ); matchCounts[i] = (player.name, matchCounts[i].$2); } diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 6d8769d..a1e7a95 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -62,7 +62,7 @@ class _PlayerSelectionState extends State { /// Skeleton data used while loading players. late final List skeletonData = List.filled( 7, - Player(name: 'Player 0', description: ''), + Player(name: 'Player 0'), ); @override @@ -282,7 +282,7 @@ class _PlayerSelectionState extends State { final loc = AppLocalizations.of(context); final playerName = _searchBarController.text.trim(); - final createdPlayer = Player(name: playerName, description: ''); + final createdPlayer = Player(name: playerName); final success = await db.playerDao.addPlayer(player: createdPlayer); if (!context.mounted) return; diff --git a/test/db_tests/aggregates/group_test.dart b/test/db_tests/aggregates/group_test.dart index 5e713c4..3d51a06 100644 --- a/test/db_tests/aggregates/group_test.dart +++ b/test/db_tests/aggregates/group_test.dart @@ -29,10 +29,10 @@ void main() { ); withClock(fakeClock, () { - testPlayer1 = Player(name: 'Alice', description: ''); - testPlayer2 = Player(name: 'Bob', description: ''); - testPlayer3 = Player(name: 'Charlie', description: ''); - testPlayer4 = Player(name: 'Diana', description: ''); + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); testGroup1 = Group( name: 'Test Group', description: '', diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 245cca1..14cdcad 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -36,11 +36,11 @@ void main() { ); withClock(fakeClock, () { - testPlayer1 = Player(name: 'Alice', description: ''); - testPlayer2 = Player(name: 'Bob', description: ''); - testPlayer3 = Player(name: 'Charlie', description: ''); - testPlayer4 = Player(name: 'Diana', description: ''); - testPlayer5 = Player(name: 'Eve', description: ''); + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testPlayer5 = Player(name: 'Eve'); testGroup1 = Group( name: 'Test Group 1', description: '', @@ -62,28 +62,24 @@ void main() { name: 'First Test Match', game: testGame, group: testGroup1, - players: [testPlayer4, testPlayer5], - notes: '', + players: [testPlayer4, testPlayer5] ); testMatch2 = Match( name: 'Second Test Match', game: testGame, group: testGroup2, - players: [testPlayer1, testPlayer2, testPlayer3], - notes: '', + players: [testPlayer1, testPlayer2, testPlayer3] ); testMatchOnlyPlayers = Match( name: 'Test Match with Players', game: testGame, - players: [testPlayer1, testPlayer2, testPlayer3], - notes: '', + players: [testPlayer1, testPlayer2, testPlayer3] ); testMatchOnlyGroup = Match( name: 'Test Match with Group', game: testGame, group: testGroup2, - players: testGroup2.members, - notes: '', + players: testGroup2.members ); }); await database.playerDao.addPlayersAsList( diff --git a/test/db_tests/aggregates/team_test.dart b/test/db_tests/aggregates/team_test.dart index 0850936..39c5be5 100644 --- a/test/db_tests/aggregates/team_test.dart +++ b/test/db_tests/aggregates/team_test.dart @@ -33,10 +33,10 @@ void main() { ); withClock(fakeClock, () { - testPlayer1 = Player(name: 'Alice', description: ''); - testPlayer2 = Player(name: 'Bob', description: ''); - testPlayer3 = Player(name: 'Charlie', description: ''); - testPlayer4 = Player(name: 'Diana', description: ''); + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]); testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]); testTeam3 = Team(name: 'Team Gamma', members: [testPlayer1, testPlayer3]); diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index 3042b33..fdb5e7b 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -24,10 +24,10 @@ void main() { ); withClock(fakeClock, () { - testPlayer1 = Player(name: 'Test Player', description: ''); - testPlayer2 = Player(name: 'Second Player', description: ''); - testPlayer3 = Player(name: 'Charlie', description: ''); - testPlayer4 = Player(name: 'Diana', description: ''); + testPlayer1 = Player(name: 'Test Player'); + testPlayer2 = Player(name: 'Second Player'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); }); }); tearDown(() async { @@ -348,7 +348,7 @@ void main() { // Verifies that a player with empty string name is stored correctly. test('Player with empty string name is stored correctly', () async { - final emptyNamePlayer = Player(name: '', description: ''); + final emptyNamePlayer = Player(name: ''); await database.playerDao.addPlayer(player: emptyNamePlayer); @@ -361,7 +361,7 @@ void main() { // Verifies that a player with very long name is stored correctly. test('Player with very long name is stored correctly', () async { final longName = 'A' * 1000; - final longNamePlayer = Player(name: longName, description: ''); + final longNamePlayer = Player(name: longName); await database.playerDao.addPlayer(player: longNamePlayer); diff --git a/test/db_tests/relationships/player_group_test.dart b/test/db_tests/relationships/player_group_test.dart index 7004e17..f687b1c 100644 --- a/test/db_tests/relationships/player_group_test.dart +++ b/test/db_tests/relationships/player_group_test.dart @@ -26,10 +26,10 @@ void main() { ); withClock(fakeClock, () { - testPlayer1 = Player(name: 'Alice', description: ''); - testPlayer2 = Player(name: 'Bob', description: ''); - testPlayer3 = Player(name: 'Charlie', description: ''); - testPlayer4 = Player(name: 'Diana', description: ''); + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); testGroup = Group( name: 'Test Group', description: '', diff --git a/test/db_tests/relationships/player_match_test.dart b/test/db_tests/relationships/player_match_test.dart index 3071edf..4f77bdb 100644 --- a/test/db_tests/relationships/player_match_test.dart +++ b/test/db_tests/relationships/player_match_test.dart @@ -37,12 +37,12 @@ void main() { ); withClock(fakeClock, () { - testPlayer1 = Player(name: 'Alice', description: ''); - testPlayer2 = Player(name: 'Bob', description: ''); - testPlayer3 = Player(name: 'Charlie', description: ''); - testPlayer4 = Player(name: 'Diana', description: ''); - testPlayer5 = Player(name: 'Eve', description: ''); - testPlayer6 = Player(name: 'Frank', description: ''); + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testPlayer5 = Player(name: 'Eve'); + testPlayer6 = Player(name: 'Frank'); testGroup = Group( name: 'Test Group', description: '', @@ -59,14 +59,12 @@ void main() { name: 'Test Match with Group', game: testGame, players: testGroup.members, - group: testGroup, - notes: '', + group: testGroup ); testMatchOnlyPlayers = Match( name: 'Test Match with Players', game: testGame, - players: [testPlayer4, testPlayer5, testPlayer6], - notes: '', + players: [testPlayer4, testPlayer5, testPlayer6] ); testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]); testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]); @@ -568,14 +566,12 @@ void main() { final match1 = Match( name: 'Match 1', game: testGame, - players: playersList, - notes: '', + players: playersList ); final match2 = Match( name: 'Match 2', game: testGame, - players: playersList, - notes: '', + players: playersList ); await Future.wait([ diff --git a/test/db_tests/values/score_test.dart b/test/db_tests/values/score_test.dart index c5e2209..d550995 100644 --- a/test/db_tests/values/score_test.dart +++ b/test/db_tests/values/score_test.dart @@ -30,9 +30,9 @@ void main() { ); withClock(fakeClock, () { - testPlayer1 = Player(name: 'Alice', description: ''); - testPlayer2 = Player(name: 'Bob', description: ''); - testPlayer3 = Player(name: 'Charlie', description: ''); + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); testGame = Game( name: 'Test Game', ruleset: Ruleset.singleWinner, @@ -44,13 +44,11 @@ void main() { name: 'Test Match 1', game: testGame, players: [testPlayer1, testPlayer2], - notes: '', ); testMatch2 = Match( name: 'Test Match 2', game: testGame, players: [testPlayer2, testPlayer3], - notes: '', ); }); From 31c6e03f4d58236253ce22932cbdc6ef8a1ff16a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 20:26:11 +0200 Subject: [PATCH 36/60] Fixed tests --- lib/data/dao/score_entry_dao.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index 016e2f9..566b9d1 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -250,7 +250,7 @@ class ScoreEntryDao extends DatabaseAccessor ); final result = await query.get(); - if (result.isNotEmpty) return null; + if (result.isEmpty) return null; final playerData = result.first.readTable(db.playerTable); return Player( @@ -317,7 +317,7 @@ class ScoreEntryDao extends DatabaseAccessor ); final result = await query.get(); - if (result.isNotEmpty) return null; + if (result.isEmpty) return null; final playerData = result.first.readTable(db.playerTable); return Player( From 571d32f07afae893dd4766453160b171c9ba4766 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 22:11:09 +0200 Subject: [PATCH 37/60] Added toString method + default values --- lib/data/models/score_entry.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/data/models/score_entry.dart b/lib/data/models/score_entry.dart index 0f8a8c3..f9c5ff0 100644 --- a/lib/data/models/score_entry.dart +++ b/lib/data/models/score_entry.dart @@ -1,13 +1,14 @@ class ScoreEntry { - int roundNumber = 0; + final int roundNumber; final int score; final int change; - ScoreEntry({ - required this.roundNumber, - required this.score, - required this.change, - }); + ScoreEntry({required this.score, this.roundNumber = 0, this.change = 0}); + + @override + String toString() { + return 'ScoreEntry{roundNumber: $roundNumber, score: $score, change: $change}'; + } ScoreEntry.fromJson(Map json) : roundNumber = json['roundNumber'], From 543b4d949e91d6c51ac3306760b82afa313a72b2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 22:11:45 +0200 Subject: [PATCH 38/60] Implemented correct score handling --- test/services/data_transfer_service_test.dart | 65 ++++++------------- 1 file changed, 19 insertions(+), 46 deletions(-) diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index 12eb4b1..e863629 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -296,46 +296,25 @@ void main() { final scoresJson = matchData['scores'] as Map; expect(scoresJson, isA>()); - final scores = scoresJson.map( - (playerId, scoreList) => MapEntry( - playerId, - (scoreList as List) - .map((s) => ScoreEntry.fromJson(s as Map)) - .toList(), - ), - ); + // Verify scores are properly structured (single score per player, not list) + expect(scoresJson[testPlayer1.id], isNotNull); + expect(scoresJson[testPlayer2.id], isNotNull); - expect(scores, isA>>()); + // Parse player 1 score + final player1ScoreJson = + scoresJson[testPlayer1.id] as Map; + final player1Score = ScoreEntry.fromJson(player1ScoreJson); + expect(player1Score.roundNumber, 1); + expect(player1Score.score, 10); + expect(player1Score.change, 10); - /* Player 1 scores */ - // General structure - expect(scores[testPlayer1.id], isNotNull); - expect(scores[testPlayer1.id]!.length, 2); - - // Round 1 - expect(scores[testPlayer1.id]![0].roundNumber, 1); - expect(scores[testPlayer1.id]![0].score, 10); - expect(scores[testPlayer1.id]![0].change, 10); - - // Round 2 - expect(scores[testPlayer1.id]![1].roundNumber, 2); - expect(scores[testPlayer1.id]![1].score, 20); - expect(scores[testPlayer1.id]![1].change, 10); - - /* Player 2 scores */ - // General structure - expect(scores[testPlayer2.id], isNotNull); - expect(scores[testPlayer2.id]!.length, 2); - - // Round 1 - expect(scores[testPlayer2.id]![0].roundNumber, 1); - expect(scores[testPlayer2.id]![0].score, 15); - expect(scores[testPlayer2.id]![0].change, 15); - - // Round 2 - expect(scores[testPlayer2.id]![1].roundNumber, 2); - expect(scores[testPlayer2.id]![1].score, 25); - expect(scores[testPlayer2.id]![1].change, 10); + // Parse player 2 score + final player2ScoreJson = + scoresJson[testPlayer2.id] as Map; + final player2Score = ScoreEntry.fromJson(player2ScoreJson); + expect(player2Score.roundNumber, 1); + expect(player2Score.score, 15); + expect(player2Score.change, 15); }); testWidgets('Match without group is handled correctly', (tester) async { @@ -898,14 +877,8 @@ void main() { 'playerIds': [testPlayer1.id, testPlayer2.id], 'notes': testMatch.notes, 'scores': { - testPlayer1.id: [ - {'roundNumber': 1, 'score': 10, 'change': 10}, - {'roundNumber': 2, 'score': 20, 'change': 10}, - ], - testPlayer2.id: [ - {'roundNumber': 1, 'score': 15, 'change': 15}, - {'roundNumber': 2, 'score': 25, 'change': 10}, - ], + testPlayer1.id: {'roundNumber': 1, 'score': 10, 'change': 10}, + testPlayer2.id: {'roundNumber': 1, 'score': 15, 'change': 15}, }, 'createdAt': testMatch.createdAt.toIso8601String(), 'endedAt': null, From f24aeff55a621c18e4711d76534beff35b59d509 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 22:12:13 +0200 Subject: [PATCH 39/60] Fixed bug --- lib/data/dao/player_match_dao.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/data/dao/player_match_dao.dart b/lib/data/dao/player_match_dao.dart index 36a7dbe..b467a1b 100644 --- a/lib/data/dao/player_match_dao.dart +++ b/lib/data/dao/player_match_dao.dart @@ -24,7 +24,7 @@ class PlayerMatchDao extends DatabaseAccessor matchId: matchId, teamId: Value(teamId), ), - mode: InsertMode.insertOrIgnore, + mode: InsertMode.insertOrReplace, ); } From a3ae8ef5b8bcf7566821c37c388b71d1a678b7df Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 22:13:15 +0200 Subject: [PATCH 40/60] Fixed json methods --- lib/data/models/match.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 8cfaf05..2ff02d6 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -55,7 +55,16 @@ class Match { ), group = null, players = [], - scores = json['scores'], + scores = json['scores'] != null + ? (json['scores'] as Map).map( + (key, value) => MapEntry( + key, + value != null + ? ScoreEntry.fromJson(value as Map) + : null, + ), + ) + : {}, notes = json['notes'] ?? ''; /// Converts the Match instance to a JSON object. Related objects are @@ -69,7 +78,7 @@ class Match { 'gameId': game.id, 'groupId': group?.id, 'playerIds': players.map((player) => player.id).toList(), - 'scores': scores, + 'scores': scores.map((key, value) => MapEntry(key, value?.toJson())), 'notes': notes, }; From b0a8529c1cfdf387be9b1d530c129d63530c37d9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 22:13:44 +0200 Subject: [PATCH 41/60] Formatting --- test/db_tests/aggregates/match_test.dart | 10 ++++++---- test/db_tests/relationships/player_match_test.dart | 10 +++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 14cdcad..3305b9a 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -8,6 +8,7 @@ import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/score_entry.dart'; void main() { late AppDatabase database; @@ -62,24 +63,25 @@ void main() { name: 'First Test Match', game: testGame, group: testGroup1, - players: [testPlayer4, testPlayer5] + players: [testPlayer4, testPlayer5], + scores: {testPlayer4.id: ScoreEntry(score: 1)}, ); testMatch2 = Match( name: 'Second Test Match', game: testGame, group: testGroup2, - players: [testPlayer1, testPlayer2, testPlayer3] + players: [testPlayer1, testPlayer2, testPlayer3], ); testMatchOnlyPlayers = Match( name: 'Test Match with Players', game: testGame, - players: [testPlayer1, testPlayer2, testPlayer3] + players: [testPlayer1, testPlayer2, testPlayer3], ); testMatchOnlyGroup = Match( name: 'Test Match with Group', game: testGame, group: testGroup2, - players: testGroup2.members + players: testGroup2.members, ); }); await database.playerDao.addPlayersAsList( diff --git a/test/db_tests/relationships/player_match_test.dart b/test/db_tests/relationships/player_match_test.dart index 4f77bdb..bb030f0 100644 --- a/test/db_tests/relationships/player_match_test.dart +++ b/test/db_tests/relationships/player_match_test.dart @@ -59,12 +59,12 @@ void main() { name: 'Test Match with Group', game: testGame, players: testGroup.members, - group: testGroup + group: testGroup, ); testMatchOnlyPlayers = Match( name: 'Test Match with Players', game: testGame, - players: [testPlayer4, testPlayer5, testPlayer6] + players: [testPlayer4, testPlayer5, testPlayer6], ); testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]); testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]); @@ -396,7 +396,6 @@ void main() { matchId: testMatchOnlyGroup.id, teamId: testTeam1.id, ); - expect(playersInTeam.length, 2); final playerIds = playersInTeam.map((p) => p.id).toSet(); expect(playerIds.contains(testPlayer1.id), true); @@ -545,6 +544,7 @@ void main() { matchId: testMatchOnlyGroup.id, teamId: testTeam1.id, ); + expect(playersInTeam1.length, 2); final team1Ids = playersInTeam1.map((p) => p.id).toSet(); expect(team1Ids.contains(testPlayer1.id), true); @@ -566,12 +566,12 @@ void main() { final match1 = Match( name: 'Match 1', game: testGame, - players: playersList + players: playersList, ); final match2 = Match( name: 'Match 2', game: testGame, - players: playersList + players: playersList, ); await Future.wait([ From b1abbf63766a232acd43154dae5ff73d97848ca4 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 22:13:55 +0200 Subject: [PATCH 42/60] Added missing score import --- lib/data/dao/match_dao.dart | 31 +++++++++++++++++++++---- lib/services/data_transfer_service.dart | 15 +++++++++++- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 0b9300e..93df7d7 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -130,6 +130,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { uniqueGames[match.game.id] = match.game; } + // Add games if (uniqueGames.isNotEmpty) { await db.batch( (b) => b.insertAll( @@ -152,7 +153,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); } - // Add all groups of the matches in batch + // Add groups await db.batch( (b) => b.insertAll( db.groupTable, @@ -171,7 +172,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ), ); - // Add all matches in batch + // Add matches await db.batch( (b) => b.insertAll( matchTable, @@ -192,7 +193,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ), ); - // Add all players of the matches in batch (unique) + // Add players final uniquePlayers = {}; for (final match in matches) { for (final p in match.players) { @@ -225,7 +226,27 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); } - // Add all player-match associations in batch + await db.batch((b) { + for (final match in matches) { + for (final entry in match.scores.entries) { + if (entry.value != null) { + b.insert( + db.scoreEntryTable, + ScoreEntryTableCompanion.insert( + matchId: match.id, + playerId: entry.key, + score: entry.value!.score, + roundNumber: entry.value!.roundNumber, + change: entry.value!.change, + ), + mode: InsertMode.insertOrReplace, + ); + } + } + } + }); + + // Add player-match associations await db.batch((b) { for (final match in matches) { for (final p in match.players) { @@ -241,7 +262,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { } }); - // Add all player-group associations in batch + // Add player-group associations await db.batch((b) { for (final match in matches) { if (match.group != null) { diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 05896ea..3203272 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -12,6 +12,7 @@ import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/data/models/team.dart'; class DataTransferService { @@ -71,7 +72,9 @@ class DataTransferService { 'gameId': m.game.id, 'groupId': m.group?.id, 'playerIds': m.players.map((p) => p.id).toList(), - 'scores': m.scores, + 'scores': m.scores.map( + (key, value) => MapEntry(key, value?.toJson()), + ), 'notes': m.notes, }, ) @@ -271,6 +274,15 @@ class DataTransferService { ? DateTime.parse(map['endedAt'] as String) : null; final notes = map['notes'] as String? ?? ''; + final scoresJson = map['scores'] as Map? ?? {}; + final scores = scoresJson.map( + (key, value) => MapEntry( + key, + value != null + ? ScoreEntry.fromJson(value as Map) + : null, + ), + ); // Link attributes to objects final game = gamesMap[gameId] ?? getFallbackGame(); @@ -292,6 +304,7 @@ class DataTransferService { createdAt: createdAt, endedAt: endedAt, notes: notes, + scores: scores, ); }).toList(); } From e910f1dcd341ec66b0c86dba1c21ed788bbbf92b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 22:17:17 +0200 Subject: [PATCH 43/60] Fixed tests --- test/db_tests/relationships/player_match_test.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/db_tests/relationships/player_match_test.dart b/test/db_tests/relationships/player_match_test.dart index bb030f0..92601f0 100644 --- a/test/db_tests/relationships/player_match_test.dart +++ b/test/db_tests/relationships/player_match_test.dart @@ -95,7 +95,7 @@ void main() { matchId: testMatchOnlyGroup.id, ); - expect(matchHasPlayers, false); + expect(matchHasPlayers, true); await database.playerMatchDao.addPlayerToMatch( matchId: testMatchOnlyGroup.id, @@ -424,18 +424,16 @@ void main() { playerId: testPlayer1.id, ); - // Try to add the same player again with different score await database.playerMatchDao.addPlayerToMatch( matchId: testMatchOnlyGroup.id, playerId: testPlayer1.id, ); - // Verify player count is still 1 final players = await database.playerMatchDao.getPlayersOfMatch( matchId: testMatchOnlyGroup.id, ); - expect(players?.length, 1); + expect(players?.length, 3); }); test( From dfc7788087c127eb5a8989007f1e1c41f496601e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 22:17:31 +0200 Subject: [PATCH 44/60] Updated skeleton data --- .../views/main_menu/home_view.dart | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index d6b9ee6..888acf8 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -53,14 +53,30 @@ class _HomeViewState extends State { name: 'Skeleton Group', description: 'This is a skeleton group description.', members: [ - Player(name: 'Skeleton Player 1', description: ''), - Player(name: 'Skeleton Player 2', description: ''), + Player( + name: + 'Skeleton Player 1' + '', + ), + Player( + name: + 'Skeleton Player 2' + '', + ), ], ), notes: 'These are skeleton notes.', players: [ - Player(name: 'Skeleton Player 1', description: ''), - Player(name: 'Skeleton Player 2', description: ''), + Player( + name: + 'Skeleton Player 1' + '', + ), + Player( + name: + 'Skeleton Player 2' + '', + ), ], ), ); @@ -231,7 +247,8 @@ class _HomeViewState extends State { /// Updates the winner information for a specific match in the recent matches list. Future updatedWinnerInRecentMatches(String matchId) async { final db = Provider.of(context, listen: false); - final winner = await db.scoreEntryDao.getWinner(matchId: matchId); + // TODO: fix + //final winner = await db.scoreEntryDao.getWinner(matchId: matchId); final matchIndex = recentMatches.indexWhere((match) => match.id == matchId); if (matchIndex != -1) { setState(() { From b4b598d1f5a5689e2c99642a3745bcfed819ac2a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 22:22:35 +0200 Subject: [PATCH 45/60] Fixed state bug --- .../views/main_menu/home_view.dart | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index 888acf8..321f12b 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -145,7 +145,11 @@ class _HomeViewState extends State { MatchResultView(match: match), ), ); - await updatedWinnerInRecentMatches(match.id); + await loadRecentMatches(); + + setState(() { + print('loaded'); + }); }, ), ) @@ -244,17 +248,12 @@ class _HomeViewState extends State { }); } - /// Updates the winner information for a specific match in the recent matches list. - Future updatedWinnerInRecentMatches(String matchId) async { + Future loadRecentMatches() async { final db = Provider.of(context, listen: false); - // TODO: fix - //final winner = await db.scoreEntryDao.getWinner(matchId: matchId); - final matchIndex = recentMatches.indexWhere((match) => match.id == matchId); - if (matchIndex != -1) { - setState(() { - // TODO: fix - //recentMatches[matchIndex].winner = winner; - }); - } + final matches = await db.matchDao.getAllMatches(); + recentMatches = + (matches..sort((a, b) => b.createdAt.compareTo(a.createdAt))) + .take(2) + .toList(); } } From 5cbcf626e8fa0fe02aa7f41e7b53db43b92cfc06 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 22:32:52 +0200 Subject: [PATCH 46/60] Removed game in compact mode --- .../widgets/tiles/match_tile.dart | 93 ++++++++++--------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 08c2fdc..1b94403 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -115,57 +115,58 @@ class _MatchTileState extends State { ], // Game + Ruleset Badge - 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), + 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, + ), ), ), - 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, + ), ), ), - ), - // 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, - ), - ), - ), - ], + ], + ), ), - ), const SizedBox(height: 12), From d381036849ce5ac6206bca0301852e27f7dc933f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 22:33:07 +0200 Subject: [PATCH 47/60] simplified json map --- lib/services/data_transfer_service.dart | 46 +++---------------------- 1 file changed, 5 insertions(+), 41 deletions(-) diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 3203272..37ee2e8 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -37,48 +37,12 @@ class DataTransferService { final games = await db.gameDao.getAllGames(); final teams = await db.teamDao.getAllTeams(); - // Construct a JSON representation of the data in normalized format final Map jsonMap = { - 'players': players.map((p) => p.toJson()).toList(), - 'games': games.map((g) => g.toJson()).toList(), - 'groups': groups - .map( - (g) => { - 'id': g.id, - 'name': g.name, - 'description': g.description, - 'createdAt': g.createdAt.toIso8601String(), - 'memberIds': (g.members).map((m) => m.id).toList(), - }, - ) - .toList(), - 'teams': teams - .map( - (t) => { - 'id': t.id, - 'name': t.name, - 'createdAt': t.createdAt.toIso8601String(), - 'memberIds': (t.members).map((m) => m.id).toList(), - }, - ) - .toList(), - 'matches': matches - .map( - (m) => { - 'id': m.id, - 'name': m.name, - 'createdAt': m.createdAt.toIso8601String(), - 'endedAt': m.endedAt?.toIso8601String(), - 'gameId': m.game.id, - 'groupId': m.group?.id, - 'playerIds': m.players.map((p) => p.id).toList(), - 'scores': m.scores.map( - (key, value) => MapEntry(key, value?.toJson()), - ), - 'notes': m.notes, - }, - ) - .toList(), + 'players': players.map((player) => player.toJson()).toList(), + 'games': games.map((game) => game.toJson()).toList(), + 'groups': groups.map((group) => group.toJson()).toList(), + 'teams': teams.map((team) => team.toJson()).toList(), + 'matches': matches.map((match) => match.toJson()).toList(), }; return json.encode(jsonMap); From bdb33f1ec8b7b4d47a01acc2430620bf3b962bc6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 23:19:46 +0200 Subject: [PATCH 48/60] Updated json schmema --- assets/schema.json | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/assets/schema.json b/assets/schema.json index f5e363b..6bcbe45 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -147,13 +147,19 @@ "type": "string" }, "endedAt": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "gameId": { "type": "string" }, "groupId": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "playerIds": { "type": "array", @@ -163,22 +169,28 @@ }, "scores": { "type": "object", - "items": { - "type": "array", - "items": { - "type": "string", - "properties": { - "roundNumber": { - "type": "number" + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "properties": { + "roundNumber": { + "type": "number" + }, + "score": { + "type": "number" + }, + "change": { + "type": "number" + } }, - "score": { - "type": "number" - }, - "change": { - "type": "number" - } + "required": ["roundNumber", "score", "change"], + "additionalProperties": false } - } + ] } }, "notes": { From 0b991026c7570722b6b678315a1cdf3f39dd62e6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 23:25:07 +0200 Subject: [PATCH 49/60] Updated packages --- lib/services/data_transfer_service.dart | 4 ++-- pubspec.yaml | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 37ee2e8..daf4768 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -59,7 +59,7 @@ class DataTransferService { ) async { try { final bytes = Uint8List.fromList(utf8.encode(jsonString)); - final path = await FilePicker.platform.saveFile( + final path = await FilePicker.saveFile( fileName: '$fileName.json', bytes: bytes, ); @@ -80,7 +80,7 @@ class DataTransferService { static Future importData(BuildContext context) async { final db = Provider.of(context, listen: false); - final path = await FilePicker.platform.pickFiles( + final path = await FilePicker.pickFiles( type: FileType.custom, allowedExtensions: ['json'], ); diff --git a/pubspec.yaml b/pubspec.yaml index b51c02a..d2522bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,24 +4,24 @@ publish_to: 'none' version: 0.0.21+255 environment: - sdk: ^3.8.1 + sdk: ^3.11.5 dependencies: + clock: ^1.1.2 + cupertino_icons: ^1.0.6 + drift: ^2.32.1 + drift_flutter: ^0.3.0 + file_picker: ^11.0.2 + file_saver: ^0.3.1 flutter: sdk: flutter flutter_localizations: sdk: flutter - clock: ^1.1.2 - cupertino_icons: ^1.0.6 - drift: ^2.27.0 - drift_flutter: ^0.2.4 - file_picker: ^10.3.6 - file_saver: ^0.3.1 fluttericon: ^2.0.0 - font_awesome_flutter: ^10.12.0 + font_awesome_flutter: ^11.0.0 intl: any json_schema: ^5.2.2 - package_info_plus: ^9.0.0 + package_info_plus: ^4.0.2 path_provider: ^2.1.5 provider: ^6.1.5 skeletonizer: ^2.1.0+1 @@ -33,7 +33,7 @@ dev_dependencies: sdk: flutter build_runner: ^2.7.0 dart_pubspec_licenses: ^3.0.14 - drift_dev: ^2.29.0 + drift_dev: ^2.32.1 flutter_lints: ^6.0.0 flutter: From 52518dc525cdada9e1ec35b8141aedda563c15bb Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 23:26:30 +0200 Subject: [PATCH 50/60] Downgraded flutter again --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d2522bf..02836c9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: 'none' version: 0.0.21+255 environment: - sdk: ^3.11.5 + sdk: ^3.10.7 dependencies: clock: ^1.1.2 From 1a2ad547be9d95fb9fb5265046455ea10ff2f239 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 23:27:35 +0200 Subject: [PATCH 51/60] Downgraded flutter again --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 02836c9..86c9ff2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,7 +32,7 @@ dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.7.0 - dart_pubspec_licenses: ^3.0.14 + dart_pubspec_licenses: ^3.0.12 drift_dev: ^2.32.1 flutter_lints: ^6.0.0 From db554557fedb350fa4d9c4bc95074fc319cbe45a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 23:36:06 +0200 Subject: [PATCH 52/60] Fixed versioning --- pubspec.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 86c9ff2..0e79310 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,13 +4,13 @@ publish_to: 'none' version: 0.0.21+255 environment: - sdk: ^3.10.7 + sdk: ^3.8.1 dependencies: clock: ^1.1.2 cupertino_icons: ^1.0.6 - drift: ^2.32.1 - drift_flutter: ^0.3.0 + drift: ^2.27.0 + drift_flutter: ^0.2.4 file_picker: ^11.0.2 file_saver: ^0.3.1 flutter: @@ -21,7 +21,7 @@ dependencies: font_awesome_flutter: ^11.0.0 intl: any json_schema: ^5.2.2 - package_info_plus: ^4.0.2 + package_info_plus: ^9.0.0 path_provider: ^2.1.5 provider: ^6.1.5 skeletonizer: ^2.1.0+1 @@ -32,8 +32,8 @@ dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.7.0 - dart_pubspec_licenses: ^3.0.12 - drift_dev: ^2.32.1 + dart_pubspec_licenses: ^3.0.14 + drift_dev: ^2.27.0 flutter_lints: ^6.0.0 flutter: From a7e234cf106495985a91c1d1b91d8db3e2ea2215 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 23:39:44 +0200 Subject: [PATCH 53/60] Removed unnecessary function --- lib/data/dao/game_dao.dart | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index 5632abd..7ad0f9f 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -45,25 +45,6 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { ); } - Future getGameByMatchId({required String matchId}) async { - final query = select(gameTable).join([ - innerJoin(matchTable, matchTable.gameId.equalsExp(gameTable.id)), - ])..where(matchTable.id.equals(matchId)); - - final result = await query.getSingle(); - final gameRow = result.readTable(gameTable); - - return Game( - id: gameRow.id, - name: gameRow.name, - ruleset: Ruleset.values.firstWhere((e) => e.name == gameRow.ruleset), - description: gameRow.description, - color: GameColor.values.firstWhere((e) => e.name == gameRow.color), - icon: gameRow.icon, - createdAt: gameRow.createdAt, - ); - } - /// Adds a new [game] to the database. /// If a game with the same ID already exists, no action is taken. /// Returns `true` if the game was added, `false` otherwise. From 2fe43a5ad1fd12510f6026ae4be9446222faaf76 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 23:40:55 +0200 Subject: [PATCH 54/60] Removed unnecessary table association --- lib/data/dao/game_dao.dart | 3 +-- lib/data/dao/game_dao.g.dart | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index 7ad0f9f..f07e2c7 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -2,12 +2,11 @@ import 'package:drift/drift.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/game_table.dart'; -import 'package:tallee/data/db/tables/match_table.dart'; import 'package:tallee/data/models/game.dart'; part 'game_dao.g.dart'; -@DriftAccessor(tables: [MatchTable, GameTable]) +@DriftAccessor(tables: [GameTable]) class GameDao extends DatabaseAccessor with _$GameDaoMixin { GameDao(super.db); diff --git a/lib/data/dao/game_dao.g.dart b/lib/data/dao/game_dao.g.dart index 7b86d21..a998fe7 100644 --- a/lib/data/dao/game_dao.g.dart +++ b/lib/data/dao/game_dao.g.dart @@ -5,8 +5,6 @@ part of 'game_dao.dart'; // ignore_for_file: type=lint mixin _$GameDaoMixin on DatabaseAccessor { $GameTableTable get gameTable => attachedDatabase.gameTable; - $GroupTableTable get groupTable => attachedDatabase.groupTable; - $MatchTableTable get matchTable => attachedDatabase.matchTable; GameDaoManager get managers => GameDaoManager(this); } @@ -15,8 +13,4 @@ class GameDaoManager { GameDaoManager(this._db); $$GameTableTableTableManager get gameTable => $$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable); - $$GroupTableTableTableManager get groupTable => - $$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable); - $$MatchTableTableTableManager get matchTable => - $$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable); } From 6f0147420a7fca7d80ab4eb5f8e2d1798b4c6d9b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 22 Apr 2026 00:02:36 +0200 Subject: [PATCH 55/60] Added fallback --- lib/data/models/game.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index 2eeee1e..607db0a 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -1,6 +1,6 @@ import 'package:clock/clock.dart'; -import 'package:uuid/uuid.dart'; import 'package:tallee/core/enums.dart'; +import 'package:uuid/uuid.dart'; class Game { final String id; @@ -33,7 +33,10 @@ class Game { : id = json['id'], createdAt = DateTime.parse(json['createdAt']), name = json['name'], - ruleset = Ruleset.values.firstWhere((e) => e.name == json['ruleset']), + ruleset = Ruleset.values.firstWhere( + (e) => e.name == json['ruleset'], + orElse: () => Ruleset.singleWinner, + ), description = json['description'], color = GameColor.values.firstWhere((e) => e.name == json['color']), icon = json['icon']; @@ -49,4 +52,3 @@ class Game { 'icon': icon, }; } - From 23b903e28a9fdb24ee61cc55bab14fe71dc5da65 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 22 Apr 2026 00:19:48 +0200 Subject: [PATCH 56/60] Added ruleset depending icons and adjusted mvp text --- .../widgets/tiles/match_tile.dart | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 1b94403..9256b76 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -187,15 +187,11 @@ class _MatchTileState extends State { ), child: Row( children: [ - const Icon( - Icons.emoji_events, - size: 20, - color: Colors.amber, - ), + getMvpIcon(), const SizedBox(width: 8), Expanded( child: Text( - getWinner(loc), + getMvpText(loc), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -290,13 +286,15 @@ class _MatchTileState extends State { } } - String getWinner(AppLocalizations loc) { + String getMvpText(AppLocalizations loc) { if (widget.match.mvp.isEmpty) return ''; final ruleset = widget.match.game.ruleset; - if (ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser) { + if (ruleset == Ruleset.singleWinner) { return '${loc.winner}: ${widget.match.mvp.first.name}'; - } else if (ruleset == Ruleset.lowestScore || + } else if (ruleset == Ruleset.singleLoser) { + return '${loc.loser}: ${widget.match.mvp.first.name}'; + } else if (ruleset == Ruleset.highestScore || ruleset == Ruleset.lowestScore) { final mvp = widget.match.mvp; final mvpScore = widget.match.scores[mvp.first.id]?.score ?? 0; @@ -306,4 +304,25 @@ class _MatchTileState extends State { } return '${loc.winner}: n.A.'; } + + Icon getMvpIcon() { + const Icon(Icons.emoji_events, size: 20, color: Colors.amber); + + switch (widget.match.game.ruleset) { + case Ruleset.singleWinner: + return const Icon(Icons.emoji_events, size: 20, color: Colors.amber); + case Ruleset.singleLoser: + return const Icon( + Icons.sentiment_dissatisfied_outlined, + size: 20, + color: Colors.blue, + ); + case Ruleset.lowestScore: + return const Icon(Icons.arrow_downward, size: 20, color: Colors.orange); + case Ruleset.highestScore: + return const Icon(Icons.arrow_upward, size: 20, color: Colors.green); + default: + return const Icon(Icons.emoji_events, size: 20, color: Colors.amber); + } + } } From daf1bc27d8c63d08dc2637e468927020b5515e39 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 24 Apr 2026 09:19:39 +0000 Subject: [PATCH 57/60] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index b51c02a..2de3b32 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.21+255 +version: 0.0.22+256 environment: sdk: ^3.8.1 From 39eba80e3ff2d897a9e6a7f76392f10b6bc69554 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 24 Apr 2026 09:20:20 +0000 Subject: [PATCH 58/60] Updated licenses [skip ci] --- .../settings_view/licenses/oss_licenses.dart | 101 +++++++++--------- 1 file changed, 51 insertions(+), 50 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 d712475..954b86e 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 @@ -30,7 +30,6 @@ const allDependencies = [ _cli_util, _clock, _code_assets, - _code_builder, _collection, _convert, _coverage, @@ -109,6 +108,7 @@ const allDependencies = [ _pubspec_parse, _quiver, _recase, + _record_use, _retry, _rfc_6901, _safe_url_check, @@ -567,17 +567,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// build_runner 2.13.1 +/// build_runner 2.14.0 const _build_runner = Package( name: 'build_runner', description: 'A build system for Dart code generation and modular compilation.', repository: 'https://github.com/dart-lang/build/tree/master/build_runner', authors: [], - version: '2.13.1', + version: '2.14.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('analyzer'), PackageRef('args'), PackageRef('async'), PackageRef('build'), PackageRef('build_config'), PackageRef('build_daemon'), PackageRef('built_collection'), PackageRef('built_value'), PackageRef('code_builder'), PackageRef('collection'), PackageRef('convert'), PackageRef('crypto'), PackageRef('dart_style'), PackageRef('glob'), PackageRef('graphs'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('json_annotation'), PackageRef('logging'), PackageRef('meta'), PackageRef('mime'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('pub_semver'), PackageRef('shelf'), PackageRef('shelf_web_socket'), PackageRef('stream_transform'), PackageRef('watcher'), PackageRef('web_socket_channel'), PackageRef('yaml')], + dependencies: [PackageRef('analyzer'), PackageRef('args'), PackageRef('async'), PackageRef('build'), PackageRef('build_config'), PackageRef('build_daemon'), PackageRef('built_collection'), PackageRef('built_value'), PackageRef('collection'), PackageRef('convert'), PackageRef('crypto'), PackageRef('dart_style'), PackageRef('glob'), PackageRef('graphs'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('json_annotation'), PackageRef('logging'), PackageRef('meta'), PackageRef('mime'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('pub_semver'), PackageRef('shelf'), PackageRef('shelf_web_socket'), PackageRef('stream_transform'), PackageRef('watcher'), PackageRef('web_socket_channel'), PackageRef('yaml')], devDependencies: [PackageRef('stream_channel'), PackageRef('test')], license: '''Copyright 2016, the Dart project authors. @@ -1153,47 +1153,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// code_builder 4.11.1 -const _code_builder = Package( - name: 'code_builder', - description: 'A fluent, builder-based library for generating valid Dart code.', - repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/code_builder', - authors: [], - version: '4.11.1', - spdxIdentifiers: ['BSD-3-Clause'], - isMarkdown: false, - isSdk: false, - dependencies: [PackageRef('built_collection'), PackageRef('built_value'), PackageRef('collection'), PackageRef('matcher'), PackageRef('meta')], - devDependencies: [PackageRef('build'), PackageRef('build_runner'), PackageRef('dart_style'), PackageRef('source_gen'), PackageRef('test')], - license: '''Copyright 2016, the Dart project authors. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google LLC nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', - ); - /// collection 1.19.1 const _collection = Package( name: 'collection', @@ -2892,17 +2851,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// hooks 1.0.2 +/// hooks 1.0.3 const _hooks = Package( name: 'hooks', description: 'A library that contains a Dart API for the JSON-based protocol for `hook/build.dart` and `hook/link.dart`.', repository: 'https://github.com/dart-lang/native/tree/main/pkgs/hooks', authors: [], - version: '1.0.2', + version: '1.0.3', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('collection'), PackageRef('crypto'), PackageRef('logging'), PackageRef('meta'), PackageRef('pub_semver'), PackageRef('yaml')], + dependencies: [PackageRef('collection'), PackageRef('crypto'), PackageRef('logging'), PackageRef('meta'), PackageRef('pub_semver'), PackageRef('record_use'), PackageRef('yaml')], devDependencies: [PackageRef('args'), PackageRef('code_assets'), PackageRef('glob'), PackageRef('json_schema'), PackageRef('path'), PackageRef('test')], license: '''Copyright 2025, the Dart project authors. @@ -5106,6 +5065,48 @@ Redistribution and use in source and binary forms, with or without modification, THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); +/// record_use 0.6.0 +const _record_use = Package( + name: 'record_use', + description: '''The serialization logic and API for the usage recording SDK feature. +''', + repository: 'https://github.com/dart-lang/native/tree/main/pkgs/record_use', + authors: [], + version: '0.6.0', + spdxIdentifiers: ['BSD-3-Clause'], + isMarkdown: false, + isSdk: false, + dependencies: [PackageRef('collection'), PackageRef('meta'), PackageRef('pub_semver')], + devDependencies: [PackageRef('json_schema'), PackageRef('test')], + license: '''Copyright 2024, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', + ); + /// retry 3.1.2 const _retry = Package( name: 'retry', @@ -37880,12 +37881,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.21+255 +/// tallee 0.0.22+256 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.21+255', + version: '0.0.22+256', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, From da99a6ec99c089764e5b4b585a140e7bb8c29120 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 24 Apr 2026 10:32:28 +0000 Subject: [PATCH 59/60] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d730b55..363ea7f 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.22+256 +version: 0.0.23+257 environment: sdk: ^3.8.1 From a5f00f16ab5225de4241092b4f483115159dad2f Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 24 Apr 2026 10:33:06 +0000 Subject: [PATCH 60/60] Updated licenses [skip ci] --- .../settings_view/licenses/oss_licenses.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 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 954b86e..8811411 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 @@ -159,14 +159,14 @@ const allDependencies = [ /// Direct `dependencies`. const dependencies = [ - _flutter, - _flutter_localizations, _clock, _cupertino_icons, _drift, _drift_flutter, _file_picker, _file_saver, + _flutter, + _flutter_localizations, _fluttericon, _font_awesome_flutter, _intl, @@ -2377,14 +2377,14 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// file_picker 10.3.10 +/// file_picker 11.0.2 const _file_picker = Package( name: 'file_picker', description: 'A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.', homepage: 'https://github.com/miguelpruivo/plugins_flutter_file_picker', repository: 'https://github.com/miguelpruivo/flutter_file_picker', authors: [], - version: '10.3.10', + version: '11.0.2', spdxIdentifiers: ['MIT'], isMarkdown: false, isSdk: false, @@ -2690,13 +2690,13 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// font_awesome_flutter 10.12.0 +/// font_awesome_flutter 11.0.0 const _font_awesome_flutter = Package( name: 'font_awesome_flutter', description: 'The Font Awesome Icon pack available as Flutter Icons. Provides 2000 additional icons to use in your apps.', repository: 'https://github.com/fluttercommunity/font_awesome_flutter', authors: [], - version: '10.12.0', + version: '11.0.0', spdxIdentifiers: ['MIT'], isMarkdown: false, isSdk: false, @@ -37881,16 +37881,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.22+256 +/// tallee 0.0.23+257 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.22+256', + version: '0.0.23+257', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('clock'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), 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('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')], 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