Implementierung von multiple Winners #214

Merged
flixcoo merged 17 commits from feature/205-Implementierung-von-multipleWinners into development 2026-05-10 17:13:11 +00:00
16 changed files with 301 additions and 176 deletions

View File

@@ -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": { "matches": {
"type": "array", "type": "array",
"items": { "items": {
@@ -195,6 +165,9 @@
}, },
"notes": { "notes": {
"type": "string" "type": "string"
},
"teams": {
"type": ["array", "null"]
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -214,7 +187,6 @@
"players", "players",
"games", "games",
"groups", "groups",
"teams",
"matches" "matches"
] ]
} }

View File

@@ -34,21 +34,13 @@ enum ExportResult { success, canceled, unknownException }
/// - [Ruleset.multipleWinners]: Multiple players can be winners. /// - [Ruleset.multipleWinners]: Multiple players can be winners.
/// - [Ruleset.placement]: The player with the highest placement wins. /// - [Ruleset.placement]: The player with the highest placement wins.
enum Ruleset { enum Ruleset {
singleWinner,
multipleWinners,
highestScore, highestScore,
lowestScore, lowestScore,
singleWinner,
singleLoser,
multipleWinners,
placement, placement,
singleLoser,
} }
/// Different colors available for games /// Different colors for highlighting games
/// - [GameColor.red]: Red color enum GameColor { red, orange, yellow, green, teal, blue, purple, pink }
/// - [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 }

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 =
@@ -276,13 +276,42 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
/// Returns `true` if the winner was removed, `false` if there are multiple /// Returns `true` if the winner was removed, `false` if there are multiple
/// scores or if the winner cannot be removed. /// scores or if the winner cannot be removed.
Future<bool> removeWinner({required String matchId}) async { Future<bool> removeWinner({required String matchId}) async {
final scores = await getAllMatchScores(matchId: matchId); return await deleteAllScoresForMatch(matchId: matchId);
}
if (scores.length > 1) { /* multiple winners handling */
return false;
} else { /// Sets the winners for a match.
return await deleteAllScoresForMatch(matchId: matchId); ///
} /// 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;
} }
/* Loser handling */ /* Loser handling */
@@ -354,6 +383,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

@@ -120,6 +120,7 @@
"search_for_groups": "Nach Gruppen suchen", "search_for_groups": "Nach Gruppen suchen",
"search_for_players": "Nach Spieler:innen suchen", "search_for_players": "Nach Spieler:innen suchen",
"select_winner": "Gewinner:in wählen", "select_winner": "Gewinner:in wählen",
"select_winners": "Gewinner:innen wählen",
"select_loser": "Verlierer:in wählen", "select_loser": "Verlierer:in wählen",
"selected_players": "Ausgewählte Spieler:innen", "selected_players": "Ausgewählte Spieler:innen",
"settings": "Einstellungen", "settings": "Einstellungen",
@@ -140,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

@@ -120,6 +120,7 @@
"search_for_groups": "Search for groups", "search_for_groups": "Search for groups",
"search_for_players": "Search for players", "search_for_players": "Search for players",
"select_winner": "Select Winner", "select_winner": "Select Winner",
"select_winners": "Select Winners",
"select_loser": "Select Loser", "select_loser": "Select Loser",
"selected_players": "Selected players", "selected_players": "Selected players",
"settings": "Settings", "settings": "Settings",
@@ -149,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

@@ -770,6 +770,12 @@ abstract class AppLocalizations {
/// **'Select Winner'** /// **'Select Winner'**
String get 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. /// No description provided for @select_loser.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -890,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

@@ -366,6 +366,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get select_winner => 'Gewinner:in wählen'; String get select_winner => 'Gewinner:in wählen';
@override
String get select_winners => 'Gewinner:innen wählen';
@override @override
String get select_loser => 'Verlierer:in wählen'; String get select_loser => 'Verlierer:in wählen';
@@ -431,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

@@ -366,6 +366,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get select_winner => 'Select Winner'; String get select_winner => 'Select Winner';
@override
String get select_winners => 'Select Winners';
@override @override
String get select_loser => 'Select Loser'; String get select_loser => 'Select Loser';
@@ -430,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

@@ -47,9 +47,9 @@ class _CreateGameViewState extends State<CreateGameView> {
late final AppDatabase db; late final AppDatabase db;
late List<(Ruleset, String)> _rulesets; late List<(Ruleset, String)> _rulesets;
Ruleset? selectedRuleset = Ruleset.singleWinner;
late List<(GameColor, String)> _colors; late List<(GameColor, String)> _colors;
Ruleset? selectedRuleset = Ruleset.singleWinner;
GameColor? selectedColor = GameColor.orange; GameColor? selectedColor = GameColor.orange;
/// Controller for the game name input field. /// Controller for the game name input field.
@@ -77,38 +77,20 @@ class _CreateGameViewState extends State<CreateGameView> {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_rulesets = [ _rulesets = List.generate(
( Ruleset.values.length,
Ruleset.singleWinner, (index) => (
translateRulesetToString(Ruleset.singleWinner, context), Ruleset.values[index],
translateRulesetToString(Ruleset.values[index], context),
), ),
( );
Ruleset.singleLoser, _colors = List.generate(
translateRulesetToString(Ruleset.singleLoser, context), 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),
),
];
_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) { if (widget.gameToEdit != null) {
_gameNameController.text = widget.gameToEdit!.name; _gameNameController.text = widget.gameToEdit!.name;
@@ -214,10 +196,13 @@ class _CreateGameViewState extends State<CreateGameView> {
// Choose ruleset tile // Choose ruleset tile
if (!isEditMode()) if (!isEditMode())
ChooseTile(title: loc.ruleset, trailing: getColorDropdown(loc)), ChooseTile(
title: loc.ruleset,
trailing: getRulesetDropdown(loc),
),
// Choose color tile // Choose color tile
ChooseTile(title: loc.color, trailing: getRulesetDropdown(loc)), ChooseTile(title: loc.color, trailing: getColorDropdown(loc)),
// Description input field // Description input field
Container( Container(

View File

@@ -231,11 +231,11 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// Determines whether the "Create Match" button should be enabled. /// Determines whether the "Create Match" button should be enabled.
/// ///
/// Returns `true` if: /// 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. /// - Either a group is selected OR at least 2 players are selected.
bool _enableCreateGameButton() { bool _enableCreateGameButton() {
return (selectedGroup != null || return ((selectedGroup != null || selectedPlayers.length > 1) &&
(selectedPlayers.length > 1) && selectedGame != null); selectedGame != null);
} }
/// Handles navigation when the create or save button is pressed. /// Handles navigation when the create or save button is pressed.

View File

@@ -271,6 +271,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
Widget getResultWidget(AppLocalizations loc) { Widget getResultWidget(AppLocalizations loc) {
if (isSingleRowResult()) { if (isSingleRowResult()) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: getSingleResultRow(loc), children: getSingleResultRow(loc),
); );
@@ -282,47 +283,55 @@ class _MatchDetailViewState extends State<MatchDetailView> {
/// Returns the result row for single winner/loser rulesets or a placeholder /// Returns the result row for single winner/loser rulesets or a placeholder
/// if no result is entered yet /// if no result is entered yet
List<Widget> getSingleResultRow(AppLocalizations loc) { List<Widget> getSingleResultRow(AppLocalizations loc) {
// Single Winner if (match.mvp.isNotEmpty) {
if (match.mvp.isNotEmpty && match.game.ruleset == Ruleset.singleWinner) { final ruleset = match.game.ruleset;
return [
Text( if (ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser) {
loc.winner, return [
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), Text(
), ruleset == Ruleset.singleWinner ? loc.winner : loc.loser,
Text( style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
match.mvp.first.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
), ),
), Text(
]; match.mvp.first.name,
// Single Loser style: const TextStyle(
} else if (match.game.ruleset == Ruleset.singleLoser) { fontSize: 16,
return [ fontWeight: FontWeight.bold,
Text( color: CustomTheme.primaryColor,
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,
), ),
), ];
]; } else if (match.game.ruleset == Ruleset.multipleWinners) {
// No result entered yet return [
} else { Text(
return [ loc.winners,
Text( style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
loc.no_results_entered_yet, ),
style: const TextStyle(fontSize: 14, 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 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 /// Returns the result widget for scores or placement
@@ -401,7 +410,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

@@ -8,6 +8,7 @@ import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/data/models/score_entry.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.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/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/live_edit_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart';
@@ -45,9 +46,12 @@ class _MatchResultViewState extends State<MatchResultView> {
/// Flag to indicate if the save button should be enabled /// Flag to indicate if the save button should be enabled
late bool canSave; late bool canSave;
/// Currently selected winner player /// Currently selected player (single winner / looser)
Player? _selectedPlayer; Player? _selectedPlayer;
/// Currently selected players (multiple winners)
final Set<Player> _selectedPlayers = {};
@override @override
void initState() { void initState() {
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
@@ -64,17 +68,25 @@ class _MatchResultViewState extends State<MatchResultView> {
// Prefill fields // Prefill fields
if (widget.match.mvp.isNotEmpty) { if (widget.match.mvp.isNotEmpty) {
if (rulesetSupportsWinnerSelection()) { if (rulesetSupportsPlayerSelection()) {
_selectedPlayer = allPlayers.firstWhere( if (ruleset == Ruleset.multipleWinners) {
(p) => p.id == widget.match.mvp.first.id, 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()) { } else if (rulesetSupportsScoreEntry()) {
for (int i = 0; i < allPlayers.length; i++) { for (int i = 0; i < allPlayers.length; i++) {
final scoreList = widget.match.scores[allPlayers[i].id]; final scoreList = widget.match.scores[allPlayers[i].id];
final score = scoreList?.score ?? 0; final score = scoreList?.score ?? 0;
controller[i].text = score.toString(); controller[i].text = score.toString();
} }
} else if (rulesetSupportsPlacement()) { } else if (rulesetSupportsDragBehaviour()) {
allPlayers.sort((a, b) { allPlayers.sort((a, b) {
final scoreA = widget.match.scores[a.id]?.score ?? 0; final scoreA = widget.match.scores[a.id]?.score ?? 0;
final scoreB = widget.match.scores[b.id]?.score ?? 0; final scoreB = widget.match.scores[b.id]?.score ?? 0;
@@ -158,38 +170,68 @@ class _MatchResultViewState extends State<MatchResultView> {
const SizedBox(height: 10), const SizedBox(height: 10),
// Show player selection // Show player selection
if (rulesetSupportsWinnerSelection()) if (rulesetSupportsPlayerSelection())
Expanded( Expanded(
child: RadioGroup<Player>( child: ruleset == Ruleset.multipleWinners
groupValue: _selectedPlayer, // Multiple winners
onChanged: (Player? value) async { ? ListView.builder(
setState(() { physics:
_selectedPlayer = value; const NeverScrollableScrollPhysics(),
}); itemCount: allPlayers.length,
}, itemBuilder: (context, index) {
child: ListView.builder( return CustomCheckboxListTile(
physics: const NeverScrollableScrollPhysics(), text: allPlayers[index].name,
itemCount: allPlayers.length, value: _selectedPlayers.contains(
itemBuilder: (context, index) { allPlayers[index],
return CustomRadioListTile( ),
text: allPlayers[index].name, onChanged: (bool value) {
value: allPlayers[index], setState(() {
onContainerTap: (value) async { if (value) {
_selectedPlayers.add(
allPlayers[index],
);
} else {
_selectedPlayers.remove(
allPlayers[index],
);
}
});
},
);
},
)
// Single winner / looser
: RadioGroup<Player>(
groupValue: _selectedPlayer,
onChanged: (Player? value) async {
setState(() { setState(() {
// Check if the already selected player is the same as the newly tapped player. _selectedPlayer = value;
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);
}
}); });
}, },
); 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 // Show score entry
@@ -216,7 +258,7 @@ class _MatchResultViewState extends State<MatchResultView> {
), ),
// Show draggable placement list // Show draggable placement list
if (rulesetSupportsPlacement()) if (rulesetSupportsDragBehaviour())
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
@@ -382,12 +424,14 @@ class _MatchResultViewState extends State<MatchResultView> {
await _handleScores(); await _handleScores();
} else if (ruleset == Ruleset.placement) { } else if (ruleset == Ruleset.placement) {
await _handlePlacement(); await _handlePlacement();
} else if (ruleset == Ruleset.multipleWinners) {
await _handleWinners();
} }
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);
@@ -399,6 +443,18 @@ class _MatchResultViewState extends State<MatchResultView> {
} }
} }
/// Handles saving the (multiple) winners to the database.
Future<bool> _handleWinners() async {
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) => _selectedPlayers.contains(p)).toList(),
);
}
}
/// Handles saving or removing the loser in the database. /// Handles saving or removing the loser in the database.
Future<bool> _handleLoser() async { Future<bool> _handleLoser() async {
if (_selectedPlayer == null) { if (_selectedPlayer == null) {
@@ -443,20 +499,24 @@ class _MatchResultViewState extends State<MatchResultView> {
return loc.select_loser; return loc.select_loser;
case Ruleset.placement: case Ruleset.placement:
return loc.drag_to_set_placement; return loc.drag_to_set_placement;
case Ruleset.multipleWinners:
return loc.select_winners;
default: default:
return loc.enter_points; return loc.enter_points;
} }
} }
bool rulesetSupportsWinnerSelection() { bool rulesetSupportsPlayerSelection() {
return ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser; return ruleset == Ruleset.singleWinner ||
ruleset == Ruleset.singleLoser ||
ruleset == Ruleset.multipleWinners;
} }
bool rulesetSupportsScoreEntry() { bool rulesetSupportsScoreEntry() {
return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore; return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore;
} }
bool rulesetSupportsPlacement() { bool rulesetSupportsDragBehaviour() {
return ruleset == Ruleset.placement; return ruleset == Ruleset.placement;
} }
} }

View File

@@ -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<bool> 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,
),
),
),
],
),
),
);
}
}

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.';
} }

View File

@@ -883,14 +883,6 @@ void main() {
'createdAt': testGroup.createdAt.toIso8601String(), 'createdAt': testGroup.createdAt.toIso8601String(),
}, },
], ],
'teams': [
{
'id': testTeam.id,
'name': testTeam.name,
'memberIds': [testPlayer1.id, testPlayer2.id],
'createdAt': testTeam.createdAt.toIso8601String(),
},
],
'matches': [ 'matches': [
{ {
'id': testMatch.id, 'id': testMatch.id,