From 3c5c0dbf2068896beb6fc9ffc448cd8e9319fc7f Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 16:26:51 +0200 Subject: [PATCH] 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.'; }