From 0812f18d77cef116d3ea2a2df69caa46ba1c4979 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 22:25:49 +0200 Subject: [PATCH] feat: implemented multiple winners with teams --- lib/core/common.dart | 3 +- lib/data/dao/team_dao.dart | 15 ++ .../match_view/match_result_view.dart | 188 +++++++++++++----- .../widgets/buttons/custom_width_button.dart | 3 + .../custom_checkbox_list_tile.dart | 15 +- .../widgets/tiles/match_tile.dart | 40 ++-- pubspec.yaml | 2 +- 7 files changed, 182 insertions(+), 84 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 29d2f57..c581858 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -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; } diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index a6f03f0..4efcc74 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -268,6 +268,21 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { return await updateTeamScore(teamId: teamId, matchId: matchId, score: 1); } + Future setWinnerTeams({ + required List winners, + required String matchId, + }) async { + List 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 removeWinnerTeam({ required String teamId, required String matchId, 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 63f3346..a0cbc2a 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 @@ -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 { 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 { 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 { /// Handles saving the (multiple) winners to the database. Future _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 { 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 { 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 { 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 { ); } - Widget buildWinnerSelectionWidget(bool isTeamMatch) { + Widget buildPlayerSelectionWidget(bool isTeamMatch) { if (isTeamMatch) { return RadioGroup( groupValue: _selectedTeam, @@ -604,7 +606,11 @@ class _MatchResultViewState extends State { 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 { 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, + ); + }, + ); + } + } } diff --git a/lib/presentation/widgets/buttons/custom_width_button.dart b/lib/presentation/widgets/buttons/custom_width_button.dart index 556b784..46e7193 100644 --- a/lib/presentation/widgets/buttons/custom_width_button.dart +++ b/lib/presentation/widgets/buttons/custom_width_button.dart @@ -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, diff --git a/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart index bb6c933..23d7be6 100644 --- a/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart @@ -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 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), ], ), ), diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 77c3f12..76cc86e 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -347,24 +347,27 @@ class _MatchTileState extends State { 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 { 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 { 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); } diff --git a/pubspec.yaml b/pubspec.yaml index 95a31f7..1310ab9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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