Add support for multiple winners and update localization
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 47s
Pull Request Pipeline / lint (pull_request) Failing after 51s

This commit is contained in:
2026-05-10 16:26:51 +02:00
parent 03ab2045b2
commit 3c5c0dbf20
10 changed files with 110 additions and 16 deletions

View File

@@ -228,7 +228,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
required String playerId, required String playerId,
}) async { }) async {
// Clear previous winner if exists // Clear previous winner if exists
deleteAllScoresForMatch(matchId: matchId); await deleteAllScoresForMatch(matchId: matchId);
// Set the winner's score to 1 // Set the winner's score to 1
final rowsAffected = await into(scoreEntryTable).insert( final rowsAffected = await into(scoreEntryTable).insert(
@@ -245,7 +245,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
return rowsAffected > 0; 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. /// is 1. Returns `null` if no player found, else the first with the score.
Future<Player?> getWinner({required String matchId}) async { Future<Player?> getWinner({required String matchId}) async {
final query = final query =
@@ -285,6 +285,48 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
} }
} }
/* multiple winners handling */
/// Sets the winners for a match.
///
/// Returns `true` if more than 0 rows were affected
Future<bool> setWinners({
required List<Player> 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<bool> removeWinners({required String matchId}) async {
return await deleteAllScoresForMatch(matchId: matchId);
}
/* Loser handling */ /* Loser handling */
Future<bool> hasLoser({required String matchId}) async { Future<bool> hasLoser({required String matchId}) async {
@@ -354,6 +396,8 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
} }
} }
/* placement handling */
/// Sets the placement for each player in a match. /// 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. /// The highest score is assigned to the first player, the second highest to the second player, and so on.
Future<void> setPlacements({ Future<void> setPlacements({

View File

@@ -155,7 +155,7 @@ class Match {
return _getPlayersWithLowestScore().take(1).toList(); return _getPlayersWithLowestScore().take(1).toList();
case Ruleset.multipleWinners: case Ruleset.multipleWinners:
return []; return _getPlayersWithHighestScore().toList();
case Ruleset.placement: case Ruleset.placement:
return _getPlayersWithHighestScore().take(1).toList(); return _getPlayersWithHighestScore().take(1).toList();

View File

@@ -141,6 +141,7 @@
"undo": "Rückgängig", "undo": "Rückgängig",
"unknown_exception": "Unbekannter Fehler (siehe Konsole)", "unknown_exception": "Unbekannter Fehler (siehe Konsole)",
"winner": "Gewinner:in", "winner": "Gewinner:in",
"winners": "Gewinner:innen",
"winrate": "Siegquote", "winrate": "Siegquote",
"wins": "Siege", "wins": "Siege",
"yesterday_at": "Gestern um" "yesterday_at": "Gestern um"

View File

@@ -150,6 +150,7 @@
"undo": "Undo", "undo": "Undo",
"unknown_exception": "Unknown Exception (see console)", "unknown_exception": "Unknown Exception (see console)",
"winner": "Winner", "winner": "Winner",
"winners": "Winners",
"winrate": "Winrate", "winrate": "Winrate",
"wins": "Wins", "wins": "Wins",
"yesterday_at": "Yesterday at" "yesterday_at": "Yesterday at"

View File

@@ -896,6 +896,12 @@ abstract class AppLocalizations {
/// **'Winner'** /// **'Winner'**
String get winner; String get winner;
/// No description provided for @winners.
///
/// In en, this message translates to:
/// **'Winners'**
String get winners;
/// No description provided for @winrate. /// No description provided for @winrate.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -434,6 +434,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get winner => 'Gewinner:in'; String get winner => 'Gewinner:in';
@override
String get winners => 'Gewinner:innen';
@override @override
String get winrate => 'Siegquote'; String get winrate => 'Siegquote';

View File

@@ -433,6 +433,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get winner => 'Winner'; String get winner => 'Winner';
@override
String get winners => 'Winners';
@override @override
String get winrate => 'Winrate'; String get winrate => 'Winrate';

View File

@@ -269,9 +269,9 @@ class _MatchDetailViewState extends State<MatchDetailView> {
/// Returns the widget to be displayed in the result [InfoTile] /// Returns the widget to be displayed in the result [InfoTile]
Widget getResultWidget(AppLocalizations loc) { Widget getResultWidget(AppLocalizations loc) {
///TODO: add support for multiple winners
if (isSingleRowResult()) { if (isSingleRowResult()) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: getSingleResultRow(loc), children: getSingleResultRow(loc),
); );
@@ -300,7 +300,8 @@ class _MatchDetailViewState extends State<MatchDetailView> {
), ),
]; ];
// Single Loser // Single Loser
} else if (match.game.ruleset == Ruleset.singleLoser) { } else if (match.mvp.isNotEmpty &&
match.game.ruleset == Ruleset.singleLoser) {
return [ return [
Text( Text(
loc.loser, loc.loser,
@@ -315,6 +316,28 @@ class _MatchDetailViewState extends State<MatchDetailView> {
), ),
), ),
]; ];
// 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 // No result entered yet
} else { } else {
return [ return [
@@ -402,7 +425,8 @@ class _MatchDetailViewState extends State<MatchDetailView> {
// Returns if the result can be displayed in a single row // Returns if the result can be displayed in a single row
bool isSingleRowResult() { bool isSingleRowResult() {
return match.game.ruleset == Ruleset.singleWinner || 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) { String getPlacementText(BuildContext context, int rank) {

View File

@@ -50,7 +50,7 @@ class _MatchResultViewState extends State<MatchResultView> {
Player? _selectedPlayer; Player? _selectedPlayer;
/// Currently selected winners (multiple winners) /// Currently selected winners (multiple winners)
Set<String> _selectedWinners = {}; Set<Player> _selectedWinners = {};
@override @override
void initState() { void initState() {
@@ -85,9 +85,12 @@ class _MatchResultViewState extends State<MatchResultView> {
return scoreB.compareTo(scoreA); return scoreB.compareTo(scoreA);
}); });
} else if (rulesetSupportsMultipleWinners()) { } 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(); super.initState();
} }
} }
@@ -337,17 +340,17 @@ class _MatchResultViewState extends State<MatchResultView> {
return CustomCheckboxListTile( return CustomCheckboxListTile(
text: allPlayers[index].name, text: allPlayers[index].name,
value: _selectedWinners.contains( value: _selectedWinners.contains(
allPlayers[index].id, allPlayers[index],
), ),
onChanged: (bool value) { onChanged: (bool value) {
setState(() { setState(() {
if (value) { if (value) {
_selectedWinners.add( _selectedWinners.add(
allPlayers[index].id, allPlayers[index],
); );
} else { } else {
_selectedWinners.remove( _selectedWinners.remove(
allPlayers[index].id, allPlayers[index],
); );
} }
}); });
@@ -426,7 +429,7 @@ class _MatchResultViewState extends State<MatchResultView> {
widget.onWinnerChanged?.call(); widget.onWinnerChanged?.call();
} }
/// Handles saving or removing the winner in the database. /// Handles saving or removing the (single) winner in the database.
Future<bool> _handleWinner() async { Future<bool> _handleWinner() async {
if (_selectedPlayer == null) { if (_selectedPlayer == null) {
return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); return await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
@@ -438,10 +441,16 @@ class _MatchResultViewState extends State<MatchResultView> {
} }
} }
/// Handles saving the winners to the database. /// Handles saving the (multiple) winners to the database.
Future<bool> _handleWinners() async { Future<bool> _handleWinners() async {
//TODO: Implement winner handling if (_selectedWinners.isEmpty) {
return true; 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. /// Handles saving or removing the loser in the database.

View File

@@ -264,6 +264,9 @@ class _MatchTileState extends State<MatchTile> {
return '${loc.winner}: $mvpNames (${getPointLabel(loc, mvpScore)})'; return '${loc.winner}: $mvpNames (${getPointLabel(loc, mvpScore)})';
} else if (ruleset == Ruleset.placement) { } else if (ruleset == Ruleset.placement) {
return '${loc.winner}: ${widget.match.mvp.first.name}'; 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.'; return '${loc.winner}: n.A.';
} }