From 975679b048fab162d10fdadf5eeca57308c3efb9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 8 Mar 2026 08:24:35 +0100 Subject: [PATCH 01/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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 1363c1fb06bee6628877b1fc94bae911bd1ccf48 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 17:19:01 +0200 Subject: [PATCH 09/42] 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 10/42] 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 11/42] 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 12/42] 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 13/42] 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 14/42] 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 15/42] 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 16/42] 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 17/42] 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 18/42] 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 19/42] 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 20/42] 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 21/42] 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 22/42] 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 23/42] 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 24/42] 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 25/42] 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 26/42] 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 27/42] 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 28/42] 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 29/42] 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 30/42] 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 31/42] 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 32/42] 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 33/42] 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 34/42] 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 35/42] 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 36/42] 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 37/42] 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 38/42] 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 39/42] 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 40/42] 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 41/42] 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 42/42] 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); + } + } }