feat: implemented multiple winners with teams

This commit is contained in:
2026-05-18 22:25:49 +02:00
parent 0f621cd799
commit 0812f18d77
7 changed files with 182 additions and 84 deletions

View File

@@ -92,11 +92,10 @@ IconData getRulesetIcon(Ruleset ruleset) {
case Ruleset.lowestScore: case Ruleset.lowestScore:
return Icons.arrow_downward; return Icons.arrow_downward;
case Ruleset.singleWinner: case Ruleset.singleWinner:
case Ruleset.multipleWinners:
return Icons.emoji_events; return Icons.emoji_events;
case Ruleset.singleLoser: case Ruleset.singleLoser:
return Icons.sentiment_dissatisfied; return Icons.sentiment_dissatisfied;
case Ruleset.multipleWinners:
return Icons.group;
case Ruleset.placement: case Ruleset.placement:
return RpgAwesome.podium; return RpgAwesome.podium;
} }

View File

@@ -268,6 +268,21 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
return await updateTeamScore(teamId: teamId, matchId: matchId, score: 1); return await updateTeamScore(teamId: teamId, matchId: matchId, score: 1);
} }
Future<bool> setWinnerTeams({
required List<Team> winners,
required String matchId,
}) async {
List<bool?> success = List.generate(winners.length, (index) => null);
for (int i = 0; i < winners.length; i++) {
success[i] = await updateTeamScore(
teamId: winners[i].id,
matchId: matchId,
score: 1,
);
}
return success.every((result) => result == true);
}
Future<bool> removeWinnerTeam({ Future<bool> removeWinnerTeam({
required String teamId, required String teamId,
required String matchId, required String matchId,

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
@@ -103,20 +105,7 @@ class _MatchResultViewState extends State<MatchResultView> {
Expanded( Expanded(
child: isLiveEditMode child: isLiveEditMode
// Live Edit Mode // Live Edit Mode
? ListView.builder( ? buildLiveEditWidet(isTeamMatch)
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return LiveEditListTile(
title: allPlayers[index].name,
onChanged: (value) {
setState(() {
controller[index].text = value.toString();
});
},
value: int.tryParse(controller[index].text) ?? 0,
);
},
)
// Normal Container // Normal Container
: Container( : Container(
margin: const EdgeInsets.symmetric( margin: const EdgeInsets.symmetric(
@@ -150,35 +139,13 @@ class _MatchResultViewState extends State<MatchResultView> {
if (ruleset == Ruleset.multipleWinners) if (ruleset == Ruleset.multipleWinners)
// TODO: Implement view for teams // TODO: Implement view for teams
Expanded( Expanded(
child: ListView.builder( child: buildMultipleWinnerSelectionWidget(
physics: const NeverScrollableScrollPhysics(), isTeamMatch,
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],
);
}
});
},
);
},
), ),
) )
else else
Expanded( Expanded(
child: buildWinnerSelectionWidget(isTeamMatch), child: buildPlayerSelectionWidget(isTeamMatch),
), ),
// Show score entry // Show score entry
@@ -363,13 +330,24 @@ class _MatchResultViewState extends State<MatchResultView> {
/// Handles saving the (multiple) winners to the database. /// Handles saving the (multiple) winners to the database.
Future<bool> _handleWinners() async { Future<bool> _handleWinners() async {
if (_selectedPlayers.isEmpty) { if (isTeamMatch) {
return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); if (_selectedTeams.isEmpty) {
return await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
} else {
return await db.teamDao.setWinnerTeams(
matchId: widget.match.id,
winners: _selectedTeams.toList(),
);
}
} else { } else {
return await db.scoreEntryDao.setWinners( if (_selectedPlayers.isEmpty) {
matchId: widget.match.id, return await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
winners: allPlayers.where((p) => _selectedPlayers.contains(p)).toList(), } else {
); return await db.scoreEntryDao.setWinners(
matchId: widget.match.id,
winners: _selectedPlayers.toList(),
);
}
} }
} }
@@ -474,7 +452,11 @@ class _MatchResultViewState extends State<MatchResultView> {
return ruleset == Ruleset.placement; return ruleset == Ruleset.placement;
} }
Widget buildTeamTile({required Team team, double? width}) { Widget buildTeamTile({
required Team team,
double? width,
int showingPlayerAmount = 3,
}) {
return Container( return Container(
width: width, width: width,
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 2), margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 2),
@@ -498,7 +480,11 @@ class _MatchResultViewState extends State<MatchResultView> {
spacing: 4, spacing: 4,
runSpacing: 4, runSpacing: 4,
children: [ children: [
for (final member in team.members) for (
int i = 0;
i < min(team.members.length, showingPlayerAmount);
i++
)
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 4, vertical: 4,
@@ -509,7 +495,23 @@ class _MatchResultViewState extends State<MatchResultView> {
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text( child: Text(
member.name, team.members[i].name,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 13,
color: CustomTheme.textColor.withAlpha(180),
),
),
),
if (team.members.length > 4)
Container(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 4,
),
child: Text(
'+${team.members.length - showingPlayerAmount}',
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: TextStyle( style: TextStyle(
@@ -525,7 +527,7 @@ class _MatchResultViewState extends State<MatchResultView> {
); );
} }
Widget buildWinnerSelectionWidget(bool isTeamMatch) { Widget buildPlayerSelectionWidget(bool isTeamMatch) {
if (isTeamMatch) { if (isTeamMatch) {
return RadioGroup<Team>( return RadioGroup<Team>(
groupValue: _selectedTeam, groupValue: _selectedTeam,
@@ -604,7 +606,11 @@ class _MatchResultViewState extends State<MatchResultView> {
itemCount: allTeams.length, itemCount: allTeams.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return ScoreListTile( return ScoreListTile(
content: buildTeamTile(team: allTeams[index], width: 220), content: buildTeamTile(
team: allTeams[index],
width: 220,
showingPlayerAmount: 2,
),
horizontalPadding: 0, horizontalPadding: 0,
controller: controller[index], controller: controller[index],
); );
@@ -780,4 +786,86 @@ class _MatchResultViewState extends State<MatchResultView> {
return Row(children: [placementCol, valueCol]); return Row(children: [placementCol, valueCol]);
} }
Widget buildMultipleWinnerSelectionWidget(bool isTeamMatch) {
if (isTeamMatch) {
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: allTeams.length,
itemBuilder: (context, index) {
return CustomCheckboxListTile(
content: buildTeamTile(team: allTeams[index]),
value: _selectedTeams.contains(allTeams[index]),
onChanged: (bool value) {
setState(() {
if (value) {
_selectedTeams.add(allTeams[index]);
} else {
_selectedTeams.remove(allTeams[index]);
}
});
},
);
},
);
} else {
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return CustomCheckboxListTile(
content: Text(
allPlayers[index].name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
value: _selectedPlayers.contains(allPlayers[index]),
onChanged: (bool value) {
setState(() {
if (value) {
_selectedPlayers.add(allPlayers[index]);
} else {
_selectedPlayers.remove(allPlayers[index]);
}
});
},
);
},
);
}
}
Widget buildLiveEditWidet(bool isTeamMatch) {
if (isTeamMatch) {
return ListView.builder(
itemCount: allTeams.length,
itemBuilder: (context, index) {
return LiveEditListTile(
title: allTeams[index].name,
onChanged: (value) {
setState(() {
controller[index].text = value.toString();
});
},
value: int.tryParse(controller[index].text) ?? 0,
);
},
);
} else {
return ListView.builder(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return LiveEditListTile(
title: allPlayers[index].name,
onChanged: (value) {
setState(() {
controller[index].text = value.toString();
});
},
value: int.tryParse(controller[index].text) ?? 0,
);
},
);
}
}
} }

View File

@@ -56,6 +56,7 @@ class CustomWidthButton extends StatelessWidget {
onPressed!.call(); onPressed!.call();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor, foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor, disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor, backgroundColor: buttonBackgroundColor,
@@ -91,6 +92,7 @@ class CustomWidthButton extends StatelessWidget {
onPressed!.call(); onPressed!.call();
}, },
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor, foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor, disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor, backgroundColor: buttonBackgroundColor,
@@ -128,6 +130,7 @@ class CustomWidthButton extends StatelessWidget {
onPressed!.call(); onPressed!.call();
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor, foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor, disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor, backgroundColor: buttonBackgroundColor,

View File

@@ -5,12 +5,12 @@ import 'package:tallee/core/custom_theme.dart';
class CustomCheckboxListTile extends StatelessWidget { class CustomCheckboxListTile extends StatelessWidget {
const CustomCheckboxListTile({ const CustomCheckboxListTile({
super.key, super.key,
required this.text, required this.content,
required this.value, required this.value,
required this.onChanged, required this.onChanged,
}); });
final String text; final Widget content;
final bool value; final bool value;
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
@@ -39,16 +39,7 @@ class CustomCheckboxListTile extends StatelessWidget {
onChanged(v); onChanged(v);
}, },
), ),
Expanded( Expanded(child: content),
child: Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
], ],
), ),
), ),

View File

@@ -347,24 +347,27 @@ class _MatchTileState extends State<MatchTile> {
if (widget.match.mvt.isEmpty) return ''; if (widget.match.mvt.isEmpty) return '';
final ruleset = widget.match.game.ruleset; final ruleset = widget.match.game.ruleset;
if (ruleset == Ruleset.singleWinner) { switch (ruleset) {
return '${loc.winner}: ${widget.match.mvt.first.name}'; case Ruleset.singleWinner:
} else if (ruleset == Ruleset.singleLoser) { return '${loc.winner}: ${widget.match.mvt.first.name}';
return '${loc.loser}: ${widget.match.mvt.first.name}'; case Ruleset.singleLoser:
} else if (ruleset == Ruleset.highestScore || return '${loc.loser}: ${widget.match.mvt.first.name}';
ruleset == Ruleset.lowestScore) { case Ruleset.highestScore:
final mvt = widget.match.mvt; case Ruleset.lowestScore:
final mvtScore = final mvt = widget.match.mvt;
widget.match.teams! final mvtScore =
.firstWhere((team) => team.id == mvt.first.id) widget.match.teams!
.score ?? .firstWhere((team) => team.id == mvt.first.id)
0; .score ??
final mvtNames = mvt.map((team) => team.name).join(', '); 0;
return '${loc.winner}: $mvtNames (${getPointLabel(loc, mvtScore)})'; final mvtNames = mvt.map((team) => team.name).join(', ');
} else if (ruleset == Ruleset.placement) { return '${loc.winner}: $mvtNames (${getPointLabel(loc, mvtScore)})';
return '${loc.winner}: ${widget.match.mvt.first.name}'; case Ruleset.placement:
return '${loc.winner}: ${widget.match.mvt.first.name}';
case Ruleset.multipleWinners:
final mvtNames = widget.match.mvt.map((team) => team.name).join(', ');
return '${loc.winners}: $mvtNames';
} }
return '${loc.winner}: n.A.';
} }
Icon getMvpIcon() { Icon getMvpIcon() {
@@ -372,6 +375,7 @@ class _MatchTileState extends State<MatchTile> {
switch (widget.match.game.ruleset) { switch (widget.match.game.ruleset) {
case Ruleset.singleWinner: case Ruleset.singleWinner:
case Ruleset.multipleWinners:
return Icon(icon, size: 20, color: Colors.amber); return Icon(icon, size: 20, color: Colors.amber);
case Ruleset.singleLoser: case Ruleset.singleLoser:
return Icon(icon, size: 20, color: Colors.blue); return Icon(icon, size: 20, color: Colors.blue);
@@ -379,8 +383,6 @@ class _MatchTileState extends State<MatchTile> {
return Icon(icon, size: 20, color: Colors.orange); return Icon(icon, size: 20, color: Colors.orange);
case Ruleset.highestScore: case Ruleset.highestScore:
return Icon(icon, size: 20, color: Colors.green); return Icon(icon, size: 20, color: Colors.green);
case Ruleset.multipleWinners:
return Icon(icon, size: 20, color: Colors.amber);
case Ruleset.placement: case Ruleset.placement:
return Icon(icon, size: 20, color: Colors.deepOrangeAccent); return Icon(icon, size: 20, color: Colors.deepOrangeAccent);
} }

View File

@@ -1,7 +1,7 @@
name: tallee name: tallee
description: "Tracking App for Card Games" description: "Tracking App for Card Games"
publish_to: 'none' publish_to: 'none'
version: 0.0.30+316 version: 0.0.30+325
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1