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:
return Icons.arrow_downward;
case Ruleset.singleWinner:
case Ruleset.multipleWinners:
return Icons.emoji_events;
case Ruleset.singleLoser:
return Icons.sentiment_dissatisfied;
case Ruleset.multipleWinners:
return Icons.group;
case Ruleset.placement:
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);
}
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({
required String teamId,
required String matchId,

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart';
@@ -103,20 +105,7 @@ class _MatchResultViewState extends State<MatchResultView> {
Expanded(
child: isLiveEditMode
// Live Edit Mode
? 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,
);
},
)
? buildLiveEditWidet(isTeamMatch)
// Normal Container
: Container(
margin: const EdgeInsets.symmetric(
@@ -150,35 +139,13 @@ class _MatchResultViewState extends State<MatchResultView> {
if (ruleset == Ruleset.multipleWinners)
// TODO: Implement view for teams
Expanded(
child: 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],
);
}
});
},
);
},
child: buildMultipleWinnerSelectionWidget(
isTeamMatch,
),
)
else
Expanded(
child: buildWinnerSelectionWidget(isTeamMatch),
child: buildPlayerSelectionWidget(isTeamMatch),
),
// Show score entry
@@ -363,13 +330,24 @@ 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);
if (isTeamMatch) {
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 {
return await db.scoreEntryDao.setWinners(
matchId: widget.match.id,
winners: allPlayers.where((p) => _selectedPlayers.contains(p)).toList(),
);
if (_selectedPlayers.isEmpty) {
return await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
} 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;
}
Widget buildTeamTile({required Team team, double? width}) {
Widget buildTeamTile({
required Team team,
double? width,
int showingPlayerAmount = 3,
}) {
return Container(
width: width,
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 2),
@@ -498,7 +480,11 @@ class _MatchResultViewState extends State<MatchResultView> {
spacing: 4,
runSpacing: 4,
children: [
for (final member in team.members)
for (
int i = 0;
i < min(team.members.length, showingPlayerAmount);
i++
)
Container(
padding: const EdgeInsets.symmetric(
vertical: 4,
@@ -509,7 +495,23 @@ class _MatchResultViewState extends State<MatchResultView> {
borderRadius: BorderRadius.circular(4),
),
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,
textAlign: TextAlign.start,
style: TextStyle(
@@ -525,7 +527,7 @@ class _MatchResultViewState extends State<MatchResultView> {
);
}
Widget buildWinnerSelectionWidget(bool isTeamMatch) {
Widget buildPlayerSelectionWidget(bool isTeamMatch) {
if (isTeamMatch) {
return RadioGroup<Team>(
groupValue: _selectedTeam,
@@ -604,7 +606,11 @@ class _MatchResultViewState extends State<MatchResultView> {
itemCount: allTeams.length,
itemBuilder: (context, index) {
return ScoreListTile(
content: buildTeamTile(team: allTeams[index], width: 220),
content: buildTeamTile(
team: allTeams[index],
width: 220,
showingPlayerAmount: 2,
),
horizontalPadding: 0,
controller: controller[index],
);
@@ -780,4 +786,86 @@ class _MatchResultViewState extends State<MatchResultView> {
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();
},
style: ElevatedButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor,
@@ -91,6 +92,7 @@ class CustomWidthButton extends StatelessWidget {
onPressed!.call();
},
style: OutlinedButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor,
@@ -128,6 +130,7 @@ class CustomWidthButton extends StatelessWidget {
onPressed!.call();
},
style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor,

View File

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

View File

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

View File

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