Implemented different result view depending on ruleset
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 38s
Pull Request Pipeline / lint (pull_request) Failing after 45s

This commit is contained in:
2026-03-08 22:25:55 +01:00
parent d5a7bb320f
commit f0c575d2c9
2 changed files with 188 additions and 31 deletions

View File

@@ -6,7 +6,9 @@ import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/dto/player.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/tiles/custom_radio_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart';
class MatchResultView extends StatefulWidget { class MatchResultView extends StatefulWidget {
/// A view that allows selecting and saving the winner of a match /// A view that allows selecting and saving the winner of a match
@@ -22,6 +24,8 @@ class MatchResultView extends StatefulWidget {
/// The match for which the winner is to be selected /// The match for which the winner is to be selected
final Match match; final Match match;
/// The ruleset of the match, determines how the winner is selected or how
/// scores are entered
final Ruleset ruleset; final Ruleset ruleset;
/// Optional callback invoked when the winner is changed /// Optional callback invoked when the winner is changed
@@ -37,6 +41,9 @@ class _MatchResultViewState extends State<MatchResultView> {
/// List of all players who participated in the match /// List of all players who participated in the match
late final List<Player> allPlayers; late final List<Player> allPlayers;
/// List of text controllers for score entry, one for each player
late final List<TextEditingController> controller;
/// Currently selected winner player /// Currently selected winner player
Player? _selectedPlayer; Player? _selectedPlayer;
@@ -47,10 +54,19 @@ class _MatchResultViewState extends State<MatchResultView> {
allPlayers = widget.match.players; allPlayers = widget.match.players;
allPlayers.sort((a, b) => a.name.compareTo(b.name)); allPlayers.sort((a, b) => a.name.compareTo(b.name));
controller = List.generate(
allPlayers.length,
(index) => TextEditingController(),
);
if (widget.match.winner != null) { if (widget.match.winner != null) {
_selectedPlayer = allPlayers.firstWhere( if (rulesetSupportsWinnerSelection()) {
(p) => p.id == widget.match.winner!.id, _selectedPlayer = allPlayers.firstWhere(
); (p) => p.id == widget.match.winner!.id,
);
} else if (rulesetSupportsScoreEntry()) {
/// TODO: Update when score logic is overhauled
}
} }
super.initState(); super.initState();
} }
@@ -101,43 +117,70 @@ class _MatchResultViewState extends State<MatchResultView> {
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Expanded( if (rulesetSupportsWinnerSelection())
child: RadioGroup<Player>( Expanded(
groupValue: _selectedPlayer, child: RadioGroup<Player>(
onChanged: (Player? value) async { groupValue: _selectedPlayer,
setState(() { onChanged: (Player? value) async {
_selectedPlayer = value; setState(() {
}); _selectedPlayer = value;
await _handleSaving(); });
}, },
child: ListView.builder( child: ListView.builder(
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);
}
});
},
);
},
),
),
),
if (rulesetSupportsScoreEntry())
Expanded(
child: ListView.separated(
itemCount: allPlayers.length, itemCount: allPlayers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return CustomRadioListTile( print(allPlayers[index].name);
return ScoreListTile(
text: allPlayers[index].name, text: allPlayers[index].name,
value: allPlayers[index], controller: controller[index],
onContainerTap: (value) async { );
setState(() { },
// Check if the already selected player is the same as the newly tapped player. separatorBuilder: (BuildContext context, int index) {
if (_selectedPlayer == value) { return const Padding(
// If yes deselected the player by setting it to null. padding: EdgeInsets.symmetric(vertical: 8.0),
_selectedPlayer = null; child: Divider(indent: 20),
} else {
// If no assign the newly tapped player to the selected player.
(_selectedPlayer = value);
}
});
await _handleSaving();
},
); );
}, },
), ),
), ),
),
], ],
), ),
), ),
), ),
CustomWidthButton(
text: loc.save_changes,
sizeRelativeToWidth: 0.95,
onPressed: () async {
await _handleSaving();
if (!context.mounted) return;
Navigator.of(context).pop(_selectedPlayer);
},
),
], ],
), ),
), ),
@@ -172,15 +215,28 @@ class _MatchResultViewState extends State<MatchResultView> {
Future<bool> _handleLoser() async { Future<bool> _handleLoser() async {
if (_selectedPlayer == null) { if (_selectedPlayer == null) {
//TODO: removeLoser() method /// TODO: Update when score logic is overhauled
return false; return false;
} else { } else {
//TODO: setLoser() method /// TODO: Update when score logic is overhauled
return false; return false;
} }
} }
/// Handles saving the scores for each player in the database.
Future<bool> _handleScores() async { Future<bool> _handleScores() async {
for (int i = 0; i < allPlayers.length; i++) {
var text = controller[i].text;
if (text.isEmpty) {
text = '0';
}
final score = int.parse(text);
await db.playerMatchDao.updatePlayerScore(
matchId: widget.match.id,
playerId: allPlayers[i].id,
newScore: score,
);
}
return false; return false;
} }
@@ -194,4 +250,14 @@ class _MatchResultViewState extends State<MatchResultView> {
return loc.enter_points; return loc.enter_points;
} }
} }
bool rulesetSupportsWinnerSelection() {
return widget.ruleset == Ruleset.singleWinner ||
widget.ruleset == Ruleset.singleLoser;
}
bool rulesetSupportsScoreEntry() {
return widget.ruleset == Ruleset.lowestScore ||
widget.ruleset == Ruleset.highestScore;
}
} }

View File

@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
class ScoreListTile extends StatelessWidget {
/// A custom list tile widget that has a text field for inputting a score.
/// - [text]: The leading text to be displayed.
/// - [controller]: The controller for the text field to input the score.
const ScoreListTile({
super.key,
required this.text,
required this.controller,
/*
required this.onContainerTap,
*/
});
/// The text to display next to the radio button.
final String text;
final TextEditingController controller;
/// The callback invoked when the container is tapped.
/*
final ValueChanged<T> onContainerTap;
*/
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: const BoxDecoration(color: CustomTheme.boxColor),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
),
SizedBox(
width: 100,
height: 40,
child: TextField(
controller: controller,
keyboardType: TextInputType.number,
maxLength: 4,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: CustomTheme.textColor,
),
cursorColor: CustomTheme.textColor,
decoration: InputDecoration(
hintText: loc.points,
counterText: '',
filled: true,
fillColor: CustomTheme.onBoxColor,
contentPadding: const EdgeInsets.symmetric(
horizontal: 0,
vertical: 0,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: CustomTheme.textColor.withAlpha(100),
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: CustomTheme.primaryColor,
width: 2,
),
),
),
),
),
],
),
);
}
}