feat: basic integration of teams

This commit is contained in:
2026-05-17 21:29:16 +02:00
parent badf5ea311
commit a957408c7e
20 changed files with 1325 additions and 325 deletions

View File

@@ -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;
});
}
}
}

View File

@@ -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();
}
}

View File

@@ -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]);
}
}