From 03ab2045b2f8924ea8cbe14e6f39679708acee94 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 14:54:00 +0200 Subject: [PATCH 01/15] Add support for selecting multiple winners and update 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 | 3 ++ lib/l10n/generated/app_localizations_en.dart | 3 ++ .../match_view/match_detail_view.dart | 1 + .../match_view/match_result_view.dart | 53 ++++++++++++++++++- .../custom_checkbox_list_tile.dart | 52 ++++++++++++++++++ 8 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 5492bab..622d9cb 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -120,6 +120,7 @@ "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", "select_winner": "Gewinner:in wählen", + "select_winners": "Gewinner:innen wählen", "select_loser": "Verlierer:in wählen", "selected_players": "Ausgewählte Spieler:innen", "settings": "Einstellungen", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7fb944b..b5d617e 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -120,6 +120,7 @@ "search_for_groups": "Search for groups", "search_for_players": "Search for players", "select_winner": "Select Winner", + "select_winners": "Select Winners", "select_loser": "Select Loser", "selected_players": "Selected players", "settings": "Settings", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index bfdb659..dd7617e 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -770,6 +770,12 @@ abstract class AppLocalizations { /// **'Select Winner'** String get select_winner; + /// No description provided for @select_winners. + /// + /// In en, this message translates to: + /// **'Select Winners'** + String get select_winners; + /// No description provided for @select_loser. /// /// 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 8567ba0..e9e6451 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -366,6 +366,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get select_winner => 'Gewinner:in wählen'; + @override + String get select_winners => 'Gewinner:innen wählen'; + @override String get select_loser => 'Verlierer:in wählen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 04e68b4..3b90592 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -366,6 +366,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get select_winner => 'Select Winner'; + @override + String get select_winners => 'Select Winners'; + @override String get select_loser => 'Select Loser'; 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 86c26c6..d8cd627 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 @@ -269,6 +269,7 @@ class _MatchDetailViewState extends State { /// Returns the widget to be displayed in the result [InfoTile] Widget getResultWidget(AppLocalizations loc) { + ///TODO: add support for multiple winners if (isSingleRowResult()) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, 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 61b2a55..fd2c35a 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 @@ -8,6 +8,7 @@ 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/match_result_view/custom_checkbox_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart'; @@ -45,9 +46,12 @@ class _MatchResultViewState extends State { /// Flag to indicate if the save button should be enabled late bool canSave; - /// Currently selected winner player + /// Currently selected winner player (single winner) Player? _selectedPlayer; + /// Currently selected winners (multiple winners) + Set _selectedWinners = {}; + @override void initState() { db = Provider.of(context, listen: false); @@ -80,7 +84,10 @@ class _MatchResultViewState extends State { final scoreB = widget.match.scores[b.id]?.score ?? 0; return scoreB.compareTo(scoreA); }); + } else if (rulesetSupportsMultipleWinners()) { + //TODO: Implement winners pre filling } + ; super.initState(); } } @@ -319,6 +326,36 @@ class _MatchResultViewState extends State { ], ), ), + + // Show multiple winner selection + if (rulesetSupportsMultipleWinners()) + Expanded( + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomCheckboxListTile( + text: allPlayers[index].name, + value: _selectedWinners.contains( + allPlayers[index].id, + ), + onChanged: (bool value) { + setState(() { + if (value) { + _selectedWinners.add( + allPlayers[index].id, + ); + } else { + _selectedWinners.remove( + allPlayers[index].id, + ); + } + }); + }, + ); + }, + ), + ), ], ), ), @@ -382,6 +419,8 @@ class _MatchResultViewState extends State { await _handleScores(); } else if (ruleset == Ruleset.placement) { await _handlePlacement(); + } else if (ruleset == Ruleset.multipleWinners) { + await _handleWinners(); } widget.onWinnerChanged?.call(); @@ -399,6 +438,12 @@ class _MatchResultViewState extends State { } } + /// Handles saving the winners to the database. + Future _handleWinners() async { + //TODO: Implement winner handling + return true; + } + /// Handles saving or removing the loser in the database. Future _handleLoser() async { if (_selectedPlayer == null) { @@ -443,6 +488,8 @@ class _MatchResultViewState extends State { return loc.select_loser; case Ruleset.placement: return loc.drag_to_set_placement; + case Ruleset.multipleWinners: + return loc.select_winners; default: return loc.enter_points; } @@ -459,4 +506,8 @@ class _MatchResultViewState extends State { bool rulesetSupportsPlacement() { return ruleset == Ruleset.placement; } + + bool rulesetSupportsMultipleWinners() { + return ruleset == Ruleset.multipleWinners; + } } diff --git a/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart new file mode 100644 index 0000000..77c9242 --- /dev/null +++ b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/custom_theme.dart'; + +class CustomCheckboxListTile extends StatelessWidget { + const CustomCheckboxListTile({ + super.key, + required this.text, + required this.value, + required this.onChanged, + }); + + final String text; + final bool value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onChanged(!value), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorderColor), + borderRadius: CustomTheme.standardBorderRadiusAll, + ), + child: Row( + children: [ + Checkbox( + value: value, + onChanged: (bool? v) { + if (v == null) return; + onChanged(v); + }, + ), + Expanded( + child: Text( + text, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ); + } +} From 3c5c0dbf2068896beb6fc9ffc448cd8e9319fc7f Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 16:26:51 +0200 Subject: [PATCH 02/15] Add support for multiple winners and update localization --- lib/data/dao/score_entry_dao.dart | 48 ++++++++++++++++++- lib/data/models/match.dart | 2 +- 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 | 3 ++ lib/l10n/generated/app_localizations_en.dart | 3 ++ .../match_view/match_detail_view.dart | 30 ++++++++++-- .../match_view/match_result_view.dart | 29 +++++++---- .../widgets/tiles/match_tile.dart | 3 ++ 10 files changed, 110 insertions(+), 16 deletions(-) diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index cf6a449..d59f40a 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -228,7 +228,7 @@ class ScoreEntryDao extends DatabaseAccessor required String playerId, }) async { // Clear previous winner if exists - deleteAllScoresForMatch(matchId: matchId); + await deleteAllScoresForMatch(matchId: matchId); // Set the winner's score to 1 final rowsAffected = await into(scoreEntryTable).insert( @@ -245,7 +245,7 @@ class ScoreEntryDao extends DatabaseAccessor return rowsAffected > 0; } - // Retrieves the winner of a match by looking for a score entry where 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 { final query = @@ -285,6 +285,48 @@ class ScoreEntryDao extends DatabaseAccessor } } + /* multiple winners handling */ + + /// Sets the winners for a match. + /// + /// Returns `true` if more than 0 rows were affected + Future setWinners({ + required List winners, + required String matchId, + }) async { + // Clear previous winners if exists + await deleteAllScoresForMatch(matchId: matchId); + + if (winners.isEmpty) return false; + + await batch((batch) { + batch.insertAll( + scoreEntryTable, + winners + .map( + (player) => ScoreEntryTableCompanion.insert( + playerId: player.id, + matchId: matchId, + roundNumber: 0, + score: 1, + change: 0, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ); + }); + + return true; + } + + /// Removes the winners of a match. + /// + /// Returns `true` if more than 0 rows were affected, `false` otherwise. + Future removeWinners({required String matchId}) async { + return await deleteAllScoresForMatch(matchId: matchId); + } + /* Loser handling */ Future hasLoser({required String matchId}) async { @@ -354,6 +396,8 @@ class ScoreEntryDao extends DatabaseAccessor } } + /* placement handling */ + /// Sets the placement for each player in a match. /// The highest score is assigned to the first player, the second highest to the second player, and so on. Future setPlacements({ diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 679f8a4..2c43fe3 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -155,7 +155,7 @@ class Match { return _getPlayersWithLowestScore().take(1).toList(); case Ruleset.multipleWinners: - return []; + return _getPlayersWithHighestScore().toList(); case Ruleset.placement: return _getPlayersWithHighestScore().take(1).toList(); diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 622d9cb..f9093a2 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -141,6 +141,7 @@ "undo": "Rückgängig", "unknown_exception": "Unbekannter Fehler (siehe Konsole)", "winner": "Gewinner:in", + "winners": "Gewinner:innen", "winrate": "Siegquote", "wins": "Siege", "yesterday_at": "Gestern um" diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b5d617e..b7da7f2 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -150,6 +150,7 @@ "undo": "Undo", "unknown_exception": "Unknown Exception (see console)", "winner": "Winner", + "winners": "Winners", "winrate": "Winrate", "wins": "Wins", "yesterday_at": "Yesterday at" diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index dd7617e..1bff731 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -896,6 +896,12 @@ abstract class AppLocalizations { /// **'Winner'** String get winner; + /// No description provided for @winners. + /// + /// In en, this message translates to: + /// **'Winners'** + String get winners; + /// No description provided for @winrate. /// /// 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 e9e6451..ea8e1f2 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -434,6 +434,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get winner => 'Gewinner:in'; + @override + String get winners => 'Gewinner:innen'; + @override String get winrate => 'Siegquote'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 3b90592..48f054b 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -433,6 +433,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get winner => 'Winner'; + @override + String get winners => 'Winners'; + @override String get winrate => 'Winrate'; 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 d8cd627..952cb60 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 @@ -269,9 +269,9 @@ class _MatchDetailViewState extends State { /// Returns the widget to be displayed in the result [InfoTile] Widget getResultWidget(AppLocalizations loc) { - ///TODO: add support for multiple winners if (isSingleRowResult()) { return Row( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: getSingleResultRow(loc), ); @@ -300,7 +300,8 @@ class _MatchDetailViewState extends State { ), ]; // Single Loser - } else if (match.game.ruleset == Ruleset.singleLoser) { + } else if (match.mvp.isNotEmpty && + match.game.ruleset == Ruleset.singleLoser) { return [ Text( loc.loser, @@ -315,6 +316,28 @@ class _MatchDetailViewState extends State { ), ), ]; + // Multiple Winners + } else if (match.mvp.isNotEmpty && + match.game.ruleset == Ruleset.multipleWinners) { + return [ + Text( + loc.winners, + style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), + ), + Flexible( + child: Container( + padding: EdgeInsets.only(left: 40), + child: Text( + match.mvp.map((player) => player.name).join(', '), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), + ), + ), + ), + ]; // No result entered yet } else { return [ @@ -402,7 +425,8 @@ class _MatchDetailViewState extends State { // Returns if the result can be displayed in a single row bool isSingleRowResult() { return match.game.ruleset == Ruleset.singleWinner || - match.game.ruleset == Ruleset.singleLoser; + match.game.ruleset == Ruleset.singleLoser || + match.game.ruleset == Ruleset.multipleWinners; } String getPlacementText(BuildContext context, int rank) { 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 fd2c35a..de31967 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 @@ -50,7 +50,7 @@ class _MatchResultViewState extends State { Player? _selectedPlayer; /// Currently selected winners (multiple winners) - Set _selectedWinners = {}; + Set _selectedWinners = {}; @override void initState() { @@ -85,9 +85,12 @@ class _MatchResultViewState extends State { return scoreB.compareTo(scoreA); }); } else if (rulesetSupportsMultipleWinners()) { - //TODO: Implement winners pre filling + for (int i = 0; i < allPlayers.length; i++) { + if (widget.match.scores[allPlayers[i].id]?.score == 1) { + _selectedWinners.add(allPlayers[i]); + } + } } - ; super.initState(); } } @@ -337,17 +340,17 @@ class _MatchResultViewState extends State { return CustomCheckboxListTile( text: allPlayers[index].name, value: _selectedWinners.contains( - allPlayers[index].id, + allPlayers[index], ), onChanged: (bool value) { setState(() { if (value) { _selectedWinners.add( - allPlayers[index].id, + allPlayers[index], ); } else { _selectedWinners.remove( - allPlayers[index].id, + allPlayers[index], ); } }); @@ -426,7 +429,7 @@ class _MatchResultViewState extends State { widget.onWinnerChanged?.call(); } - /// Handles saving or removing the winner in the database. + /// Handles saving or removing the (single) winner in the database. Future _handleWinner() async { if (_selectedPlayer == null) { return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); @@ -438,10 +441,16 @@ class _MatchResultViewState extends State { } } - /// Handles saving the winners to the database. + /// Handles saving the (multiple) winners to the database. Future _handleWinners() async { - //TODO: Implement winner handling - return true; + if (_selectedWinners.isEmpty) { + return await db.scoreEntryDao.removeWinners(matchId: widget.match.id); + } else { + return await db.scoreEntryDao.setWinners( + matchId: widget.match.id, + winners: allPlayers.where((p) => _selectedWinners.contains(p)).toList(), + ); + } } /// Handles saving or removing the loser in the database. diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index d034763..018c896 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -264,6 +264,9 @@ class _MatchTileState extends State { return '${loc.winner}: $mvpNames (${getPointLabel(loc, mvpScore)})'; } else if (ruleset == Ruleset.placement) { return '${loc.winner}: ${widget.match.mvp.first.name}'; + } else if (ruleset == Ruleset.multipleWinners) { + final mvpNames = widget.match.mvp.map((player) => player.name).join(', '); + return '${loc.winners}: $mvpNames'; } return '${loc.winner}: n.A.'; } From 8076f082bc7a7c09c5eb4dae73872db04739b01c Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 16:28:08 +0200 Subject: [PATCH 03/15] fix lint --- .../views/main_menu/match_view/match_detail_view.dart | 2 +- .../views/main_menu/match_view/match_result_view.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 952cb60..10fc324 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 @@ -326,7 +326,7 @@ class _MatchDetailViewState extends State { ), Flexible( child: Container( - padding: EdgeInsets.only(left: 40), + padding: const EdgeInsets.only(left: 40), child: Text( match.mvp.map((player) => player.name).join(', '), style: const TextStyle( 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 de31967..d14c259 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 @@ -50,7 +50,7 @@ class _MatchResultViewState extends State { Player? _selectedPlayer; /// Currently selected winners (multiple winners) - Set _selectedWinners = {}; + final Set _selectedWinners = {}; @override void initState() { From 009c53ad89fafe138b2678e1c82f88ca6e77958c Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 16:36:46 +0200 Subject: [PATCH 04/15] fix title and ruleset/color choose order in game creation --- .../match_view/create_match/create_game_view.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index e0f9d85..0e96508 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -98,6 +98,7 @@ class _CreateGameViewState extends State { Ruleset.multipleWinners, translateRulesetToString(Ruleset.multipleWinners, context), ), + (Ruleset.placement, translateRulesetToString(Ruleset.placement, context)), ]; _colors = [ (GameColor.green, translateGameColorToString(GameColor.green, context)), @@ -212,12 +213,12 @@ class _CreateGameViewState extends State { ), ), + // Choose color tile + ChooseTile(title: loc.ruleset, trailing: getRulesetDropdown(loc)), + // Choose ruleset tile if (!isEditMode()) - ChooseTile(title: loc.ruleset, trailing: getColorDropdown(loc)), - - // Choose color tile - ChooseTile(title: loc.color, trailing: getRulesetDropdown(loc)), + ChooseTile(title: loc.color, trailing: getColorDropdown(loc)), // Description input field Container( From 2a3c0fc98c32cef7b64115ff7c92f09ace15011b Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 17:50:16 +0200 Subject: [PATCH 05/15] fix schema --- assets/schema.json | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/assets/schema.json b/assets/schema.json index 6bcbe45..7f6aebd 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -102,36 +102,6 @@ ] } }, - "teams": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "memberIds": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "required": [ - "id", - "name", - "createdAt", - "memberIds" - ] - } - }, "matches": { "type": "array", "items": { @@ -195,6 +165,9 @@ }, "notes": { "type": "string" + }, + "teams": { + "type": ["array", "null"] } }, "additionalProperties": false, @@ -214,7 +187,6 @@ "players", "games", "groups", - "teams", "matches" ] } \ No newline at end of file From f98208b5089e08e89b0c1ed9ba229727b56422ed Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:17:01 +0200 Subject: [PATCH 06/15] fix: button enabled condition --- .../match_view/create_match/create_match_view.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 14908b6..fd98691 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 @@ -231,11 +231,11 @@ class _CreateMatchViewState extends State { /// Determines whether the "Create Match" button should be enabled. /// /// Returns `true` if: - /// - A ruleset is selected AND + /// - A game is selected AND /// - Either a group is selected OR at least 2 players are selected. bool _enableCreateGameButton() { - return (selectedGroup != null || - (selectedPlayers.length > 1) && selectedGame != null); + return ((selectedGroup != null || selectedPlayers.length > 1) && + selectedGame != null); } /// Handles navigation when the create or save button is pressed. From f5f97f676c34a78c7dec367a2ee4b60b561f042d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:22:56 +0200 Subject: [PATCH 07/15] fix: wrong tile hidden --- .../match_view/create_match/create_game_view.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 0e96508..d8dbe68 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -213,12 +213,15 @@ class _CreateGameViewState extends State { ), ), - // Choose color tile - ChooseTile(title: loc.ruleset, trailing: getRulesetDropdown(loc)), - // Choose ruleset tile if (!isEditMode()) - ChooseTile(title: loc.color, trailing: getColorDropdown(loc)), + ChooseTile( + title: loc.ruleset, + trailing: getRulesetDropdown(loc), + ), + + // Choose color tile + ChooseTile(title: loc.color, trailing: getColorDropdown(loc)), // Description input field Container( From 9ae562d92a8572979d49fc03903a42cc37aac487 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 18:23:39 +0200 Subject: [PATCH 08/15] update schema test --- test/services/data_transfer_service_test.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index fec70b7..586138a 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -883,14 +883,6 @@ void main() { 'createdAt': testGroup.createdAt.toIso8601String(), }, ], - 'teams': [ - { - 'id': testTeam.id, - 'name': testTeam.name, - 'memberIds': [testPlayer1.id, testPlayer2.id], - 'createdAt': testTeam.createdAt.toIso8601String(), - }, - ], 'matches': [ { 'id': testMatch.id, From 0c44c54bd7e7df1224b916dbf57197b4f17f6df1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:24:12 +0200 Subject: [PATCH 09/15] fix: schema test created json --- test/services/data_transfer_service_test.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index fec70b7..586138a 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -883,14 +883,6 @@ void main() { 'createdAt': testGroup.createdAt.toIso8601String(), }, ], - 'teams': [ - { - 'id': testTeam.id, - 'name': testTeam.name, - 'memberIds': [testPlayer1.id, testPlayer2.id], - 'createdAt': testTeam.createdAt.toIso8601String(), - }, - ], 'matches': [ { 'id': testMatch.id, From 6db265ea993550301356c5d3dfb75060f8cc63cd Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:39:11 +0200 Subject: [PATCH 10/15] feat: replaced variable content with generated lists --- lib/core/enums.dart | 18 ++----- .../create_match/create_game_view.dart | 47 ++++++------------- 2 files changed, 19 insertions(+), 46 deletions(-) diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 605d3aa..99141e4 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -34,21 +34,13 @@ enum ExportResult { success, canceled, unknownException } /// - [Ruleset.multipleWinners]: Multiple players can be winners. /// - [Ruleset.placement]: The player with the highest placement wins. enum Ruleset { + singleWinner, + multipleWinners, highestScore, lowestScore, - singleWinner, - singleLoser, - multipleWinners, placement, + singleLoser, } -/// Different colors available for games -/// - [GameColor.red]: Red color -/// - [GameColor.blue]: Blue color -/// - [GameColor.green]: Green color -/// - [GameColor.yellow]: Yellow color -/// - [GameColor.purple]: Purple color -/// - [GameColor.orange]: Orange color -/// - [GameColor.pink]: Pink color -/// - [GameColor.teal]: Teal color -enum GameColor { red, blue, green, yellow, purple, orange, pink, teal } +/// Different colors for highlighting games +enum GameColor { red, orange, yellow, green, teal, blue, purple, pink } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index d8dbe68..3156476 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -47,9 +47,9 @@ class _CreateGameViewState extends State { late final AppDatabase db; late List<(Ruleset, String)> _rulesets; - Ruleset? selectedRuleset = Ruleset.singleWinner; - late List<(GameColor, String)> _colors; + + Ruleset? selectedRuleset = Ruleset.singleWinner; GameColor? selectedColor = GameColor.orange; /// Controller for the game name input field. @@ -77,39 +77,20 @@ class _CreateGameViewState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - _rulesets = [ - ( - Ruleset.singleWinner, - translateRulesetToString(Ruleset.singleWinner, context), + _rulesets = List.generate( + Ruleset.values.length, + (index) => ( + Ruleset.values[index], + translateRulesetToString(Ruleset.values[index], context), ), - ( - Ruleset.singleLoser, - translateRulesetToString(Ruleset.singleLoser, context), + ); + _colors = List.generate( + GameColor.values.length, + (index) => ( + GameColor.values[index], + translateGameColorToString(GameColor.values[index], context), ), - ( - Ruleset.highestScore, - translateRulesetToString(Ruleset.highestScore, context), - ), - ( - Ruleset.lowestScore, - translateRulesetToString(Ruleset.lowestScore, context), - ), - ( - Ruleset.multipleWinners, - translateRulesetToString(Ruleset.multipleWinners, context), - ), - (Ruleset.placement, translateRulesetToString(Ruleset.placement, context)), - ]; - _colors = [ - (GameColor.green, translateGameColorToString(GameColor.green, context)), - (GameColor.teal, translateGameColorToString(GameColor.teal, context)), - (GameColor.blue, translateGameColorToString(GameColor.blue, context)), - (GameColor.purple, translateGameColorToString(GameColor.purple, context)), - (GameColor.pink, translateGameColorToString(GameColor.pink, context)), - (GameColor.red, translateGameColorToString(GameColor.red, context)), - (GameColor.orange, translateGameColorToString(GameColor.orange, context)), - (GameColor.yellow, translateGameColorToString(GameColor.yellow, context)), - ]; + ); if (widget.gameToEdit != null) { _gameNameController.text = widget.gameToEdit!.name; From 341b293151082944e58b4ebc8ac11bed21940a8d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:41:34 +0200 Subject: [PATCH 11/15] fix: text alignment --- .../views/main_menu/match_view/match_detail_view.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 10fc324..9c04590 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 @@ -326,9 +326,10 @@ class _MatchDetailViewState extends State { ), Flexible( child: Container( - padding: const EdgeInsets.only(left: 40), + padding: const EdgeInsets.only(left: 10), child: Text( match.mvp.map((player) => player.name).join(', '), + textAlign: TextAlign.end, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, From 50bf111f0368788af660cd8011c7f6d9b04c59e9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:47:48 +0200 Subject: [PATCH 12/15] fix: single result row --- .../match_view/match_detail_view.dart | 102 ++++++++---------- 1 file changed, 43 insertions(+), 59 deletions(-) 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 9c04590..73d534d 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 @@ -283,71 +283,55 @@ class _MatchDetailViewState extends State { /// Returns the result row for single winner/loser rulesets or a placeholder /// if no result is entered yet 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.mvp.first.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, + if (match.mvp.isNotEmpty) { + final ruleset = match.game.ruleset; + + if (ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser) { + return [ + Text( + ruleset == Ruleset.singleWinner ? loc.winner : loc.loser, + style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), ), - ), - ]; - // Single Loser - } else if (match.mvp.isNotEmpty && - match.game.ruleset == Ruleset.singleLoser) { - return [ - Text( - loc.loser, - style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), - ), - Text( - match.mvp.first.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, + Text( + match.mvp.first.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), ), - ), - ]; - // Multiple Winners - } else if (match.mvp.isNotEmpty && - match.game.ruleset == Ruleset.multipleWinners) { - return [ - Text( - loc.winners, - style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), - ), - Flexible( - child: Container( - padding: const EdgeInsets.only(left: 10), - child: Text( - match.mvp.map((player) => player.name).join(', '), - textAlign: TextAlign.end, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, + ]; + } else if (match.game.ruleset == Ruleset.multipleWinners) { + return [ + Text( + loc.winners, + style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), + ), + Flexible( + child: Container( + padding: const EdgeInsets.only(left: 10), + child: Text( + match.mvp.map((player) => player.name).join(', '), + textAlign: TextAlign.end, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), ), ), ), - ), - ]; - // No result entered yet - } else { - return [ - Text( - loc.no_results_entered_yet, - style: const TextStyle(fontSize: 14, color: CustomTheme.textColor), - ), - ]; + ]; + } } + + // No results yet + return [ + Text( + loc.no_results_entered_yet, + style: const TextStyle(fontSize: 14, color: CustomTheme.textColor), + ), + ]; } /// Returns the result widget for scores or placement From e503db1c1b28b91be8176288c7607d642547fe52 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:51:59 +0200 Subject: [PATCH 13/15] fix: removed unnecessary method --- lib/data/dao/score_entry_dao.dart | 15 +-------------- .../main_menu/match_view/match_result_view.dart | 2 +- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index d59f40a..830135d 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -276,13 +276,7 @@ class ScoreEntryDao extends DatabaseAccessor /// Returns `true` if the winner was removed, `false` if there are multiple /// scores or if the winner cannot be removed. Future removeWinner({required String matchId}) async { - final scores = await getAllMatchScores(matchId: matchId); - - if (scores.length > 1) { - return false; - } else { - return await deleteAllScoresForMatch(matchId: matchId); - } + return await deleteAllScoresForMatch(matchId: matchId); } /* multiple winners handling */ @@ -320,13 +314,6 @@ class ScoreEntryDao extends DatabaseAccessor return true; } - /// Removes the winners of a match. - /// - /// Returns `true` if more than 0 rows were affected, `false` otherwise. - Future removeWinners({required String matchId}) async { - return await deleteAllScoresForMatch(matchId: matchId); - } - /* Loser handling */ Future hasLoser({required String matchId}) async { 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 d14c259..ad41a09 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 @@ -444,7 +444,7 @@ class _MatchResultViewState extends State { /// Handles saving the (multiple) winners to the database. Future _handleWinners() async { if (_selectedWinners.isEmpty) { - return await db.scoreEntryDao.removeWinners(matchId: widget.match.id); + return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); } else { return await db.scoreEntryDao.setWinners( matchId: widget.match.id, From 60a92dafe1943ae6a238dd3f8b57f04c0340ce0d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 19:07:20 +0200 Subject: [PATCH 14/15] updated logic --- .../match_view/match_result_view.dart | 162 +++++++++--------- 1 file changed, 81 insertions(+), 81 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 ad41a09..a9bb08e 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 @@ -49,8 +49,8 @@ class _MatchResultViewState extends State { /// Currently selected winner player (single winner) Player? _selectedPlayer; - /// Currently selected winners (multiple winners) - final Set _selectedWinners = {}; + /// Currently selected players (multiple winners) + final Set _selectedPlayers = {}; @override void initState() { @@ -68,28 +68,30 @@ class _MatchResultViewState extends State { // Prefill fields if (widget.match.mvp.isNotEmpty) { - if (rulesetSupportsWinnerSelection()) { - _selectedPlayer = allPlayers.firstWhere( - (p) => p.id == widget.match.mvp.first.id, - ); + if (rulesetSupportsPlayerSelection()) { + if (ruleset == Ruleset.multipleWinners) { + for (int i = 0; i < allPlayers.length; i++) { + if (widget.match.scores[allPlayers[i].id]?.score == 1) { + _selectedPlayers.add(allPlayers[i]); + } + } + } else { + _selectedPlayer = allPlayers.firstWhere( + (p) => p.id == widget.match.mvp.first.id, + ); + } } else if (rulesetSupportsScoreEntry()) { 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(); } - } else if (rulesetSupportsPlacement()) { + } else if (rulesetSupportsDragBehaviour()) { allPlayers.sort((a, b) { final scoreA = widget.match.scores[a.id]?.score ?? 0; final scoreB = widget.match.scores[b.id]?.score ?? 0; return scoreB.compareTo(scoreA); }); - } else if (rulesetSupportsMultipleWinners()) { - for (int i = 0; i < allPlayers.length; i++) { - if (widget.match.scores[allPlayers[i].id]?.score == 1) { - _selectedWinners.add(allPlayers[i]); - } - } } super.initState(); } @@ -168,38 +170,68 @@ class _MatchResultViewState extends State { const SizedBox(height: 10), // Show player selection - if (rulesetSupportsWinnerSelection()) + if (rulesetSupportsPlayerSelection()) Expanded( - child: RadioGroup( - groupValue: _selectedPlayer, - onChanged: (Player? value) async { - setState(() { - _selectedPlayer = value; - }); - }, - child: ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return CustomRadioListTile( - text: allPlayers[index].name, - value: allPlayers[index], - onContainerTap: (value) async { + child: ruleset == Ruleset.multipleWinners + // Multiple winners + ? ListView.builder( + physics: + const NeverScrollableScrollPhysics(), + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomCheckboxListTile( + text: allPlayers[index].name, + value: _selectedPlayers.contains( + allPlayers[index], + ), + onChanged: (bool value) { + setState(() { + if (value) { + _selectedPlayers.add( + allPlayers[index], + ); + } else { + _selectedPlayers.remove( + allPlayers[index], + ); + } + }); + }, + ); + }, + ) + // Single winner / looser + : RadioGroup( + groupValue: _selectedPlayer, + onChanged: (Player? 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); - } + _selectedPlayer = value; }); }, - ); - }, - ), - ), + child: ListView.builder( + physics: + const NeverScrollableScrollPhysics(), + 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); + } + }); + }, + ); + }, + ), + ), ), // Show score entry @@ -226,7 +258,7 @@ class _MatchResultViewState extends State { ), // Show draggable placement list - if (rulesetSupportsPlacement()) + if (rulesetSupportsDragBehaviour()) Expanded( child: Row( children: [ @@ -329,36 +361,6 @@ class _MatchResultViewState extends State { ], ), ), - - // Show multiple winner selection - if (rulesetSupportsMultipleWinners()) - Expanded( - child: ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return CustomCheckboxListTile( - text: allPlayers[index].name, - value: _selectedWinners.contains( - allPlayers[index], - ), - onChanged: (bool value) { - setState(() { - if (value) { - _selectedWinners.add( - allPlayers[index], - ); - } else { - _selectedWinners.remove( - allPlayers[index], - ); - } - }); - }, - ); - }, - ), - ), ], ), ), @@ -443,12 +445,12 @@ class _MatchResultViewState extends State { /// Handles saving the (multiple) winners to the database. Future _handleWinners() async { - if (_selectedWinners.isEmpty) { + if (_selectedPlayers.isEmpty) { return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); } else { return await db.scoreEntryDao.setWinners( matchId: widget.match.id, - winners: allPlayers.where((p) => _selectedWinners.contains(p)).toList(), + winners: allPlayers.where((p) => _selectedPlayers.contains(p)).toList(), ); } } @@ -504,19 +506,17 @@ class _MatchResultViewState extends State { } } - bool rulesetSupportsWinnerSelection() { - return ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser; + bool rulesetSupportsPlayerSelection() { + return ruleset == Ruleset.singleWinner || + ruleset == Ruleset.singleLoser || + ruleset == Ruleset.multipleWinners; } bool rulesetSupportsScoreEntry() { return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore; } - bool rulesetSupportsPlacement() { + bool rulesetSupportsDragBehaviour() { return ruleset == Ruleset.placement; } - - bool rulesetSupportsMultipleWinners() { - return ruleset == Ruleset.multipleWinners; - } } From c75b3e4a6d8dea26ef865b424e891b0a5598b37b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 19:12:12 +0200 Subject: [PATCH 15/15] updated comment --- .../views/main_menu/match_view/match_result_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a9bb08e..ba138d6 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 @@ -46,7 +46,7 @@ class _MatchResultViewState extends State { /// Flag to indicate if the save button should be enabled late bool canSave; - /// Currently selected winner player (single winner) + /// Currently selected player (single winner / looser) Player? _selectedPlayer; /// Currently selected players (multiple winners)