feat: add placement ruleset and related localization
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 45s
Pull Request Pipeline / lint (pull_request) Failing after 48s

This commit is contained in:
2026-05-09 02:08:40 +02:00
parent 044a6acbbe
commit bc997633eb
11 changed files with 176 additions and 6 deletions

View File

@@ -288,34 +288,39 @@ class _MatchDetailViewState extends State<MatchDetailView> {
}
}
/// Returns the result widget for scores
/// Returns the result widget for scores or placement
Widget getScoreResultWidget(AppLocalizations loc) {
List<(String, int)> playerScores = [];
for (var player in match.players) {
int score = match.scores[player.id]?.score ?? 0;
playerScores.add((player.name, score));
}
if (widget.match.game.ruleset == Ruleset.highestScore) {
final ruleset = match.game.ruleset;
if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) {
playerScores.sort((a, b) => b.$2.compareTo(a.$2));
} else if (widget.match.game.ruleset == Ruleset.lowestScore) {
} else if (ruleset == Ruleset.lowestScore) {
playerScores.sort((a, b) => a.$2.compareTo(b.$2));
}
return Column(
children: [
for (var score in playerScores)
for (var i = 0; i < playerScores.length; i++)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
score.$1,
playerScores[i].$1,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
Text(
getPointLabel(loc, score.$2),
ruleset == Ruleset.placement
? '#${i + 1}'
: getPointLabel(loc, playerScores[i].$2),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,

View File

@@ -10,6 +10,7 @@ import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_list_tile.dart';
class MatchResultView extends StatefulWidget {
/// A view that allows selecting and saving the winner of a match
@@ -68,6 +69,12 @@ class _MatchResultViewState extends State<MatchResultView> {
final score = scoreList?.score ?? 0;
controller[i].text = score.toString();
}
} else if (rulesetSupportsPlacement()) {
allPlayers.sort((a, b) {
final scoreA = widget.match.scores[a.id]?.score ?? 0;
final scoreB = widget.match.scores[b.id]?.score ?? 0;
return scoreB.compareTo(scoreA);
});
}
super.initState();
}
@@ -177,6 +184,70 @@ class _MatchResultViewState extends State<MatchResultView> {
},
),
),
if (rulesetSupportsPlacement())
Expanded(
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Column(
children: [
for (int i = 0; i < allPlayers.length; i++)
Container(
alignment: Alignment.center,
height: 60,
child: Container(
decoration:
CustomTheme.standardBoxDecoration,
alignment: Alignment.center,
height: 50,
width: 40,
child: Text(
" #${i + 1} ",
style: const TextStyle(
color: CustomTheme.primaryColor,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
],
),
),
Expanded(
child: ReorderableListView.builder(
padding: EdgeInsets.zero,
proxyDecorator: (child, index, animation) {
return Material(
color: Colors.transparent,
child: child,
);
},
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final Player item = allPlayers.removeAt(
oldIndex,
);
allPlayers.insert(newIndex, item);
});
},
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return TextIconListTile(
key: ValueKey(allPlayers[index].id),
text: allPlayers[index].name,
iconEnabled: false,
);
},
),
),
],
),
),
],
),
),
@@ -222,6 +293,8 @@ class _MatchResultViewState extends State<MatchResultView> {
} else if (ruleset == Ruleset.lowestScore ||
ruleset == Ruleset.highestScore) {
await _handleScores();
} else if (ruleset == Ruleset.placement) {
await _handlePlacement();
}
widget.onWinnerChanged?.call();
@@ -267,12 +340,29 @@ class _MatchResultViewState extends State<MatchResultView> {
}
}
/// Handles saving the placement for each player in the database.
Future<void> _handlePlacement() async {
for (int i = 0; i < allPlayers.length; i++) {
await db.scoreEntryDao.addScore(
matchId: widget.match.id,
playerId: allPlayers[i].id,
entry: ScoreEntry(
roundNumber: 0,
score: allPlayers.length - i,
change: 0,
),
);
}
}
String getTitleForRuleset(AppLocalizations loc) {
switch (ruleset) {
case Ruleset.singleWinner:
return loc.select_winner;
case Ruleset.singleLoser:
return loc.select_loser;
case Ruleset.placement:
return loc.drag_to_set_placement;
default:
return loc.enter_points;
}
@@ -285,4 +375,8 @@ class _MatchResultViewState extends State<MatchResultView> {
bool rulesetSupportsScoreEntry() {
return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore;
}
bool rulesetSupportsPlacement() {
return ruleset == Ruleset.placement;
}
}

View File

@@ -10,6 +10,7 @@ class TextIconListTile extends StatelessWidget {
super.key,
required this.text,
this.suffixText = '',
this.prefixText = '',
this.iconEnabled = true,
this.onPressed,
});
@@ -20,6 +21,9 @@ class TextIconListTile extends StatelessWidget {
/// An optional suffix text to display after the main text.
final String suffixText;
/// An optional prefix text to display before the main text.
final String prefixText;
/// A boolean to determine if the icon should be displayed.
final bool iconEnabled;
@@ -44,6 +48,14 @@ class TextIconListTile extends StatelessWidget {
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: prefixText,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
TextSpan(
text: text,
style: const TextStyle(