feat: basic integration of teams
This commit is contained in:
@@ -12,6 +12,7 @@ import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_game_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
|
||||
import 'package:tallee/presentation/widgets/player_selection.dart';
|
||||
@@ -59,6 +60,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
|
||||
Group? selectedGroup;
|
||||
Game? selectedGame;
|
||||
bool isTeamMatch = false;
|
||||
List<Player> selectedPlayers = [];
|
||||
|
||||
/// GlobalKey for ScaffoldMessenger to show snackbars
|
||||
@@ -135,24 +137,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
trailing: selectedGame == null
|
||||
? Text(loc.none_group)
|
||||
: Text(selectedGame!.name),
|
||||
onPressed: () async {
|
||||
selectedGame = await Navigator.of(context).push(
|
||||
adaptivePageRoute(
|
||||
builder: (context) => ChooseGameView(
|
||||
games: gamesList,
|
||||
initialGameId: selectedGame?.id ?? '',
|
||||
onGamesUpdated: widget.onMatchesUpdated,
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
if (selectedGame != null) {
|
||||
hintText = selectedGame!.name;
|
||||
} else {
|
||||
hintText = loc.match_name;
|
||||
}
|
||||
});
|
||||
},
|
||||
onPressed: () async => await onChoosingGame(),
|
||||
),
|
||||
|
||||
// Group selection tile.
|
||||
@@ -161,37 +146,20 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
trailing: selectedGroup == null
|
||||
? Text(loc.none_group)
|
||||
: Text(selectedGroup!.name),
|
||||
onPressed: () async {
|
||||
// Remove all players from the previously selected group from
|
||||
// the selected players list, in case the user deselects the
|
||||
// group or selects a different group.
|
||||
selectedPlayers.removeWhere(
|
||||
(player) =>
|
||||
selectedGroup?.members.any(
|
||||
(member) => member.id == player.id,
|
||||
) ??
|
||||
false,
|
||||
);
|
||||
|
||||
selectedGroup = await Navigator.of(context).push(
|
||||
adaptivePageRoute(
|
||||
builder: (context) => ChooseGroupView(
|
||||
groups: groupsList,
|
||||
initialGroupId: selectedGroup?.id ?? '',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
if (selectedGroup != null) {
|
||||
setState(() {
|
||||
selectedPlayers += [...selectedGroup!.members];
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onPressed: () async => onChoosingGroup(),
|
||||
),
|
||||
|
||||
if (!isEditMode())
|
||||
ChooseTile(
|
||||
title: 'Team Match',
|
||||
trailing: Switch.adaptive(
|
||||
activeTrackColor: CustomTheme.primaryColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: -15),
|
||||
value: isTeamMatch,
|
||||
onChanged: (value) => setState(() => isTeamMatch = value),
|
||||
),
|
||||
),
|
||||
|
||||
// Player selection widget.
|
||||
Expanded(
|
||||
child: PlayerSelection(
|
||||
@@ -211,9 +179,9 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
text: buttonText,
|
||||
sizeRelativeToWidth: 0.95,
|
||||
buttonType: ButtonType.primary,
|
||||
onPressed: _enableCreateGameButton()
|
||||
onPressed: isSubmitButtonEnabled()
|
||||
? () {
|
||||
buttonNavigation(context);
|
||||
submitButtonNavigation(context);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
@@ -228,12 +196,86 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
return widget.matchToEdit != null;
|
||||
}
|
||||
|
||||
// If a match was provided to the view, this method prefills the input fields
|
||||
void prefillMatchDetails() {
|
||||
final match = widget.matchToEdit!;
|
||||
_matchNameController.text = match.name;
|
||||
selectedPlayers = match.players;
|
||||
selectedGame = match.game;
|
||||
|
||||
if (match.group != null) {
|
||||
selectedGroup = match.group;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onChoosingGame() async {
|
||||
selectedGame = await Navigator.of(context).push(
|
||||
adaptivePageRoute(
|
||||
builder: (context) => ChooseGameView(
|
||||
games: gamesList,
|
||||
initialGameId: selectedGame?.id ?? '',
|
||||
onGamesUpdated: widget.onMatchesUpdated,
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
if (selectedGame != null) {
|
||||
hintText = selectedGame!.name;
|
||||
} else {
|
||||
hintText = AppLocalizations.of(context).match_name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> onChoosingGroup() async {
|
||||
// Remove all players from the previously selected group from
|
||||
// the selected players list, in case the user deselects the
|
||||
// group or selects a different group.
|
||||
selectedPlayers.removeWhere(
|
||||
(player) =>
|
||||
selectedGroup?.members.any((member) => member.id == player.id) ??
|
||||
false,
|
||||
);
|
||||
|
||||
selectedGroup = await Navigator.of(context).push(
|
||||
adaptivePageRoute(
|
||||
builder: (context) => ChooseGroupView(
|
||||
groups: groupsList,
|
||||
initialGroupId: selectedGroup?.id ?? '',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
if (selectedGroup != null) {
|
||||
setState(() {
|
||||
selectedPlayers += [...selectedGroup!.members];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If none of the selected players are from the currently selected group,
|
||||
// the group is also deselected.
|
||||
Future<void> removeGroupWhenNoMemberLeft() async {
|
||||
if (selectedGroup == null) return;
|
||||
|
||||
if (!selectedPlayers.any(
|
||||
(player) =>
|
||||
selectedGroup!.members.any((member) => member.id == player.id),
|
||||
)) {
|
||||
setState(() {
|
||||
selectedGroup = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines whether the "Create Match" button should be enabled.
|
||||
///
|
||||
/// Returns `true` if:
|
||||
/// - A ruleset is selected AND
|
||||
/// - Either a group is selected OR at least 2 players are selected.
|
||||
bool _enableCreateGameButton() {
|
||||
bool isSubmitButtonEnabled() {
|
||||
return (selectedGroup != null ||
|
||||
(selectedPlayers.length > 1) && selectedGame != null);
|
||||
}
|
||||
@@ -242,20 +284,32 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
///
|
||||
/// If a match is being edited, updates the match in the database.
|
||||
/// Otherwise, creates a new match and navigates to the MatchResultView.
|
||||
void buttonNavigation(BuildContext context) async {
|
||||
void submitButtonNavigation(BuildContext context) async {
|
||||
if (isEditMode()) {
|
||||
await updateMatch();
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} else {
|
||||
final match = await createMatch();
|
||||
}
|
||||
|
||||
final match = await createMatch();
|
||||
|
||||
if (isTeamMatch) {
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: true,
|
||||
fullscreenDialog: !isTeamMatch,
|
||||
builder: (context) => OrganizeTeamsView(match: match),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: !isTeamMatch,
|
||||
builder: (context) => MatchResultView(
|
||||
match: match,
|
||||
onWinnerChanged: widget.onWinnerChanged,
|
||||
@@ -328,36 +382,10 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
createdAt: DateTime.now(),
|
||||
group: selectedGroup,
|
||||
players: selectedPlayers,
|
||||
isTeamMatch: isTeamMatch,
|
||||
game: selectedGame!,
|
||||
);
|
||||
await db.matchDao.addMatch(match: match);
|
||||
return match;
|
||||
}
|
||||
|
||||
// If a match was provided to the view, this method prefills the input fields
|
||||
void prefillMatchDetails() {
|
||||
final match = widget.matchToEdit!;
|
||||
_matchNameController.text = match.name;
|
||||
selectedPlayers = match.players;
|
||||
selectedGame = match.game;
|
||||
|
||||
if (match.group != null) {
|
||||
selectedGroup = match.group;
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the selected players are from the currently selected group,
|
||||
// the group is also deselected.
|
||||
Future<void> removeGroupWhenNoMemberLeft() async {
|
||||
if (selectedGroup == null) return;
|
||||
|
||||
if (!selectedPlayers.any(
|
||||
(player) =>
|
||||
selectedGroup!.members.any((member) => member.id == player.id),
|
||||
)) {
|
||||
setState(() {
|
||||
selectedGroup = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/adaptive_page_route.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/team.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart';
|
||||
|
||||
class OrganizeTeamsView extends StatefulWidget {
|
||||
const OrganizeTeamsView({super.key, required this.match});
|
||||
|
||||
final Match match;
|
||||
|
||||
@override
|
||||
State<OrganizeTeamsView> createState() => _OrganizeTeamsViewState();
|
||||
}
|
||||
|
||||
class _OrganizeTeamsViewState extends State<OrganizeTeamsView> {
|
||||
final Random _random = Random();
|
||||
late final List<_TeamDraft> _teams;
|
||||
|
||||
List<Player> get _players => widget.match.players;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_teams = List.generate(2, _createTeamDraft);
|
||||
_redistributePlayers();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final team in _teams) {
|
||||
team.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(title: const Text('Teams organisieren')),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 12),
|
||||
itemCount: _teams.length,
|
||||
itemBuilder: (context, index) {
|
||||
return TeamCreationTile(
|
||||
color: _teams[index].color,
|
||||
controller: _teams[index].nameController,
|
||||
players: _teams[index].members,
|
||||
hintText: 'Team ${index + 1}',
|
||||
onDelete: () => _removeTeam(index),
|
||||
onColorSelection: (color) {
|
||||
setState(() {
|
||||
_teams[index].color = color;
|
||||
});
|
||||
},
|
||||
onPlayerTap: (player) =>
|
||||
_showMovePlayerSheet(player, index),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MainMenuButton(
|
||||
icon: Icons.cached,
|
||||
onPressed: () => setState(() {
|
||||
_redistributePlayers();
|
||||
}),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
MainMenuButton(
|
||||
text: 'Add team',
|
||||
icon: Icons.emoji_events,
|
||||
onPressed: _teams.length >= widget.match.players.length
|
||||
? null
|
||||
: _addTeam,
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
MainMenuButton(
|
||||
icon: Icons.check,
|
||||
onPressed: () async {
|
||||
final match = await createMatchWithTeams();
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => MatchResultView(match: match),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Match> createMatchWithTeams() async {
|
||||
final teams = _teams
|
||||
.map(
|
||||
(team) => Team(
|
||||
name: team.nameController.text.trim().isNotEmpty
|
||||
? team.nameController.text.trim()
|
||||
: 'Team ${_teams.indexOf(team) + 1}',
|
||||
color: team.color,
|
||||
members: team.members,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
await db.teamDao.addTeamsAsList(teams: teams, matchId: widget.match.id);
|
||||
return widget.match.copyWith(teams: teams);
|
||||
}
|
||||
|
||||
_TeamDraft _createTeamDraft(int index) {
|
||||
return _TeamDraft(
|
||||
nameController: TextEditingController(text: 'Team ${index + 1}'),
|
||||
color: getTeamColor(index),
|
||||
);
|
||||
}
|
||||
|
||||
void _addTeam() {
|
||||
setState(() {
|
||||
_teams.add(_createTeamDraft(_teams.length));
|
||||
_redistributePlayers();
|
||||
});
|
||||
}
|
||||
|
||||
void _removeTeam(int index) {
|
||||
setState(() {
|
||||
final removedTeam = _teams.removeAt(index);
|
||||
removedTeam.dispose();
|
||||
|
||||
if (_teams.isEmpty) {
|
||||
_teams.add(_createTeamDraft(0));
|
||||
}
|
||||
|
||||
_redistributePlayers();
|
||||
});
|
||||
}
|
||||
|
||||
void _movePlayer(Player player, int fromTeamIndex, int toTeamIndex) {
|
||||
setState(() {
|
||||
_teams[fromTeamIndex].members.remove(player);
|
||||
_teams[toTeamIndex].members.add(player);
|
||||
});
|
||||
}
|
||||
|
||||
void _showMovePlayerSheet(Player player, int fromTeamIndex) {
|
||||
final otherTeams = [
|
||||
for (int i = 0; i < _teams.length; i++)
|
||||
if (i != fromTeamIndex) (index: i, team: _teams[i]),
|
||||
];
|
||||
|
||||
if (otherTeams.isEmpty) return;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(
|
||||
'${player.name} verschieben in …',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
...otherTeams.map((entry) {
|
||||
final teamName =
|
||||
entry.team.nameController.text.trim().isNotEmpty
|
||||
? entry.team.nameController.text.trim()
|
||||
: 'Team ${entry.index + 1}';
|
||||
final teamColor = getColorFromGameColor(entry.team.color);
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundColor: teamColor,
|
||||
),
|
||||
title: Text(
|
||||
teamName,
|
||||
style: const TextStyle(color: CustomTheme.textColor),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_movePlayer(player, fromTeamIndex, entry.index);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _redistributePlayers() {
|
||||
for (final team in _teams) {
|
||||
team.members.clear();
|
||||
}
|
||||
|
||||
if (_players.isEmpty || _teams.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final shuffledPlayers = [..._players]..shuffle(_random);
|
||||
|
||||
for (int i = 0; i < shuffledPlayers.length; i++) {
|
||||
final teamIndex = i % _teams.length;
|
||||
_teams[teamIndex].members.add(shuffledPlayers[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _TeamDraft {
|
||||
_TeamDraft({required this.nameController, required this.color});
|
||||
|
||||
final TextEditingController nameController;
|
||||
GameColor color;
|
||||
final List<Player> members = [];
|
||||
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/score_entry.dart';
|
||||
import 'package:tallee/data/models/team.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/match_result_view/custom_radio_list_tile.dart';
|
||||
@@ -36,8 +38,8 @@ class _MatchResultViewState extends State<MatchResultView> {
|
||||
|
||||
late final Ruleset ruleset;
|
||||
|
||||
/// List of all players who participated in the match
|
||||
late final List<Player> allPlayers;
|
||||
late final List<Team> allTeams;
|
||||
|
||||
/// List of text controllers for score entry, one for each player
|
||||
late final List<TextEditingController> controller;
|
||||
@@ -45,44 +47,27 @@ class _MatchResultViewState extends State<MatchResultView> {
|
||||
/// Flag to indicate if the save button should be enabled
|
||||
late bool canSave;
|
||||
|
||||
late bool isTeamMatch;
|
||||
|
||||
/// Currently selected winner player
|
||||
Player? _selectedPlayer;
|
||||
Team? _selectedTeam;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
ruleset = widget.match.game.ruleset;
|
||||
canSave = !rulesetSupportsScoreEntry();
|
||||
isTeamMatch = widget.match.isTeamMatch;
|
||||
print(widget.match.teams);
|
||||
|
||||
allPlayers = widget.match.players;
|
||||
allPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||
|
||||
controller = List.generate(
|
||||
allPlayers.length,
|
||||
(index) => TextEditingController()..addListener(() => onTextEnter()),
|
||||
);
|
||||
|
||||
// Prefill fields
|
||||
if (widget.match.mvp.isNotEmpty) {
|
||||
if (rulesetSupportsWinnerSelection()) {
|
||||
_selectedPlayer = allPlayers.firstWhere(
|
||||
(p) => p.id == widget.match.mvp.first.id,
|
||||
);
|
||||
} else if (rulesetSupportsScoreEntry()) {
|
||||
for (int i = 0; i < allPlayers.length; i++) {
|
||||
final scoreList = widget.match.scores[allPlayers[i].id];
|
||||
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();
|
||||
if (isTeamMatch) {
|
||||
initializeAsTeamMatch();
|
||||
} else {
|
||||
inizializeAsNormalMatch();
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -160,165 +145,16 @@ class _MatchResultViewState extends State<MatchResultView> {
|
||||
// Show player selection
|
||||
if (rulesetSupportsWinnerSelection())
|
||||
Expanded(
|
||||
child: RadioGroup<Player>(
|
||||
groupValue: _selectedPlayer,
|
||||
onChanged: (Player? value) async {
|
||||
setState(() {
|
||||
_selectedPlayer = value;
|
||||
});
|
||||
},
|
||||
child: ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
child: buildWinnerSelectionWidget(isTeamMatch),
|
||||
),
|
||||
|
||||
// Show score entry
|
||||
if (rulesetSupportsScoreEntry())
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: allPlayers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ScoreListTile(
|
||||
text: allPlayers[index].name,
|
||||
controller: controller[index],
|
||||
);
|
||||
},
|
||||
separatorBuilder:
|
||||
(BuildContext context, int index) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Divider(indent: 20),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(child: buildScoreEntryWidget(isTeamMatch)),
|
||||
|
||||
// Show draggable placement list
|
||||
if (rulesetSupportsPlacement())
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// Placement indicators
|
||||
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: BoxDecoration(
|
||||
color:
|
||||
CustomTheme.boxBorderColor,
|
||||
borderRadius: CustomTheme
|
||||
.standardBorderRadiusAll,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
height: 50,
|
||||
width: 50,
|
||||
child: Text(
|
||||
' #${i + 1} ',
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Drag list
|
||||
Expanded(
|
||||
child: ReorderableListView.builder(
|
||||
physics:
|
||||
const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
proxyDecorator: (child, index, animation) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
child: child,
|
||||
builder: (context, child) {
|
||||
final alpha =
|
||||
(Curves.easeInOut.transform(
|
||||
animation.value,
|
||||
) *
|
||||
40)
|
||||
.toInt();
|
||||
return Stack(
|
||||
children: [
|
||||
child!,
|
||||
Positioned.fill(
|
||||
left: 4,
|
||||
top: 4,
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white
|
||||
.withAlpha(alpha),
|
||||
borderRadius: CustomTheme
|
||||
.standardBorderRadiusAll,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
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,
|
||||
icon: Icons.drag_handle,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: buildPlacementWidget(isTeamMatch)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -361,6 +197,63 @@ class _MatchResultViewState extends State<MatchResultView> {
|
||||
);
|
||||
}
|
||||
|
||||
void initializeAsTeamMatch() {
|
||||
allTeams =
|
||||
widget.match.teams ??
|
||||
List.generate(
|
||||
4,
|
||||
(index) => Team(
|
||||
name: 'Team ${index + 1}',
|
||||
members: [
|
||||
Player(name: 'Player ${index + 1}'),
|
||||
Player(name: 'Player ${index + 2}'),
|
||||
Player(name: 'Player ${index + 3}'),
|
||||
Player(name: 'Player ${index + 4}'),
|
||||
],
|
||||
),
|
||||
);
|
||||
allTeams.sort((a, b) => a.name.compareTo(b.name));
|
||||
|
||||
controller = List.generate(
|
||||
allTeams.length,
|
||||
(index) => TextEditingController()..addListener(() => onTextEnter()),
|
||||
);
|
||||
|
||||
// Prefill fields
|
||||
//TODO
|
||||
}
|
||||
|
||||
void inizializeAsNormalMatch() {
|
||||
allPlayers = widget.match.players;
|
||||
allPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||
|
||||
controller = List.generate(
|
||||
allPlayers.length,
|
||||
(index) => TextEditingController()..addListener(() => onTextEnter()),
|
||||
);
|
||||
|
||||
// Prefill fields
|
||||
if (widget.match.mvp.isNotEmpty) {
|
||||
if (rulesetSupportsWinnerSelection()) {
|
||||
_selectedPlayer = allPlayers.firstWhere(
|
||||
(p) => p.id == widget.match.mvp.first.id,
|
||||
);
|
||||
} else if (rulesetSupportsScoreEntry()) {
|
||||
for (int i = 0; i < allPlayers.length; i++) {
|
||||
final scoreList = widget.match.scores[allPlayers[i].id];
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updated [canSave] everytime a text is entered in one of the score entry fields.
|
||||
void onTextEnter() {
|
||||
if (rulesetSupportsScoreEntry()) {
|
||||
@@ -459,4 +352,311 @@ class _MatchResultViewState extends State<MatchResultView> {
|
||||
bool rulesetSupportsPlacement() {
|
||||
return ruleset == Ruleset.placement;
|
||||
}
|
||||
|
||||
Widget buildTeamTile({required Team team, double? width}) {
|
||||
return Container(
|
||||
width: width,
|
||||
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 2),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: getColorFromGameColor(team.color).withAlpha(30),
|
||||
border: Border.all(color: getColorFromGameColor(team.color), width: 2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
team.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (final member in team.members)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.onBoxColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
member.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: CustomTheme.textColor.withAlpha(180),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildWinnerSelectionWidget(bool isTeamMatch) {
|
||||
if (isTeamMatch) {
|
||||
return RadioGroup<Team>(
|
||||
groupValue: _selectedTeam,
|
||||
onChanged: (Team? team) async {
|
||||
setState(() {
|
||||
_selectedTeam = team;
|
||||
});
|
||||
},
|
||||
child: ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: allTeams.length,
|
||||
itemBuilder: (context, index) {
|
||||
return CustomRadioListTile(
|
||||
content: buildTeamTile(team: allTeams[index]),
|
||||
value: allTeams[index],
|
||||
onContainerTap: (team) async {
|
||||
setState(() {
|
||||
// Check if the already selected player is the same as the newly tapped player.
|
||||
if (_selectedTeam == team) {
|
||||
// If yes deselected the player by setting it to null.
|
||||
_selectedTeam = null;
|
||||
} else {
|
||||
// If no assign the newly tapped player to the selected player.
|
||||
(_selectedTeam = team);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return RadioGroup<Player>(
|
||||
groupValue: _selectedPlayer,
|
||||
onChanged: (Player? value) async {
|
||||
setState(() {
|
||||
_selectedPlayer = value;
|
||||
});
|
||||
},
|
||||
child: ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: allPlayers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return CustomRadioListTile(
|
||||
content: Text(
|
||||
allPlayers[index].name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildScoreEntryWidget(bool isTeamMatch) {
|
||||
if (isTeamMatch) {
|
||||
return ListView.separated(
|
||||
itemCount: allTeams.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ScoreListTile(
|
||||
content: buildTeamTile(team: allTeams[index], width: 220),
|
||||
horizontalPadding: 0,
|
||||
controller: controller[index],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Divider(indent: 20),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return ListView.separated(
|
||||
itemCount: allPlayers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ScoreListTile(
|
||||
content: Text(
|
||||
allPlayers[index].name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
|
||||
),
|
||||
controller: controller[index],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Divider(indent: 20),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildPlacementWidget(bool isTeamMatch) {
|
||||
final placementCol = Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
for (
|
||||
int i = 0;
|
||||
i < (isTeamMatch ? allTeams.length : allPlayers.length);
|
||||
i++
|
||||
)
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
height: 60,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.boxBorderColor,
|
||||
borderRadius: CustomTheme.standardBorderRadiusAll,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
height: 50,
|
||||
width: 50,
|
||||
child: Text(
|
||||
' #${i + 1} ',
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
final valueCol = isTeamMatch
|
||||
? Expanded(
|
||||
child: ReorderableListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
proxyDecorator: (child, index, animation) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
child: child,
|
||||
builder: (context, child) {
|
||||
final alpha =
|
||||
(Curves.easeInOut.transform(animation.value) * 40)
|
||||
.toInt();
|
||||
return Stack(
|
||||
children: [
|
||||
child!,
|
||||
Positioned.fill(
|
||||
left: 4,
|
||||
top: 4,
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withAlpha(alpha),
|
||||
borderRadius: CustomTheme.standardBorderRadiusAll,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final Team team = allTeams.removeAt(oldIndex);
|
||||
allTeams.insert(newIndex, team);
|
||||
});
|
||||
},
|
||||
itemCount: allTeams.length,
|
||||
itemBuilder: (context, index) {
|
||||
return TextIconListTile(
|
||||
key: ValueKey(allTeams[index].id),
|
||||
text: allTeams[index].name,
|
||||
icon: Icons.drag_handle,
|
||||
color: getColorFromGameColor(allTeams[index].color),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: Expanded(
|
||||
child: ReorderableListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
proxyDecorator: (child, index, animation) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
child: child,
|
||||
builder: (context, child) {
|
||||
final alpha =
|
||||
(Curves.easeInOut.transform(animation.value) * 40)
|
||||
.toInt();
|
||||
return Stack(
|
||||
children: [
|
||||
child!,
|
||||
Positioned.fill(
|
||||
left: 4,
|
||||
top: 4,
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withAlpha(alpha),
|
||||
borderRadius: CustomTheme.standardBorderRadiusAll,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
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,
|
||||
icon: Icons.drag_handle,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return Row(children: [placementCol, valueCol]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user