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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class MainMenuButton extends StatefulWidget {
|
||||
});
|
||||
|
||||
/// The callback to be invoked when the button is pressed.
|
||||
final void Function() onPressed;
|
||||
final void Function()? onPressed;
|
||||
|
||||
/// The icon of the button.
|
||||
final IconData icon;
|
||||
@@ -31,9 +31,11 @@ class MainMenuButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MainMenuButtonState extends State<MainMenuButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late AnimationController _disabledAnimationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _disabledScaleAnimation;
|
||||
|
||||
/// How long the button needs to be pressed to register it as long press
|
||||
Timer? _longPressTimer;
|
||||
@@ -52,37 +54,59 @@ class _MainMenuButtonState extends State<MainMenuButton>
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_disabledAnimationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_disabledScaleAnimation = Tween<double>(begin: 1.0, end: 0.98).animate(
|
||||
CurvedAnimation(
|
||||
parent: _disabledAnimationController,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
scale: widget.onPressed == null
|
||||
? _disabledScaleAnimation
|
||||
: _scaleAnimation,
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) {
|
||||
_animationController.forward();
|
||||
if (widget.onLongPressed != null) {
|
||||
_longPressTimer = Timer(const Duration(milliseconds: 400), () {
|
||||
_isLongPressing = true;
|
||||
widget.onLongPressed?.call();
|
||||
_repeatTimer = Timer.periodic(
|
||||
const Duration(milliseconds: 250),
|
||||
(_) => widget.onLongPressed?.call(),
|
||||
);
|
||||
});
|
||||
if (widget.onPressed == null) {
|
||||
_disabledAnimationController.forward();
|
||||
} else {
|
||||
_animationController.forward();
|
||||
if (widget.onLongPressed != null) {
|
||||
_longPressTimer = Timer(const Duration(milliseconds: 400), () {
|
||||
_isLongPressing = true;
|
||||
widget.onLongPressed?.call();
|
||||
_repeatTimer = Timer.periodic(
|
||||
const Duration(milliseconds: 250),
|
||||
(_) => widget.onLongPressed?.call(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
onTapUp: (_) async {
|
||||
_cancelTimers();
|
||||
if (mounted && !_isLongPressing) {
|
||||
widget.onPressed();
|
||||
if (widget.onPressed == null) {
|
||||
_disabledAnimationController.reverse();
|
||||
} else {
|
||||
_cancelTimers();
|
||||
if (mounted && !_isLongPressing) {
|
||||
widget.onPressed?.call();
|
||||
}
|
||||
_isLongPressing = false;
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
await _animationController.reverse();
|
||||
}
|
||||
_isLongPressing = false;
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
await _animationController.reverse();
|
||||
},
|
||||
onTapCancel: () {
|
||||
_isLongPressing = false;
|
||||
@@ -91,7 +115,7 @@ class _MainMenuButtonState extends State<MainMenuButton>
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: widget.onPressed == null ? Colors.grey : Colors.white,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
@@ -122,6 +146,7 @@ class _MainMenuButtonState extends State<MainMenuButton>
|
||||
void dispose() {
|
||||
_cancelTimers();
|
||||
_animationController.dispose();
|
||||
_disabledAnimationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ class TextInputField extends StatelessWidget {
|
||||
filled: true,
|
||||
fillColor: CustomTheme.boxColor,
|
||||
hintText: hintText,
|
||||
hintStyle: const TextStyle(fontSize: 18),
|
||||
counterText: showCounterText ? null : '',
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
|
||||
@@ -8,13 +8,13 @@ class CustomRadioListTile<T> extends StatelessWidget {
|
||||
/// - [onContainerTap]: The callback invoked when the container is tapped.
|
||||
const CustomRadioListTile({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.content,
|
||||
required this.value,
|
||||
required this.onContainerTap,
|
||||
});
|
||||
|
||||
/// The text to display next to the radio button.
|
||||
final String text;
|
||||
final Widget content;
|
||||
|
||||
/// The value associated with the radio button.
|
||||
final T value;
|
||||
@@ -37,16 +37,7 @@ class CustomRadioListTile<T> extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Radio<T>(value: value, toggleable: true),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: content),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,36 +5,34 @@ 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.
|
||||
/// - [content]: The leading Widget to be displayed.
|
||||
/// - [controller]: The controller for the text field to input the score.
|
||||
const ScoreListTile({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.content,
|
||||
required this.controller,
|
||||
this.horizontalPadding = 20,
|
||||
});
|
||||
|
||||
/// The text to display next to the radio button.
|
||||
final String text;
|
||||
final Widget content;
|
||||
|
||||
final TextEditingController controller;
|
||||
|
||||
final double horizontalPadding;
|
||||
|
||||
@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),
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||
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),
|
||||
),
|
||||
content,
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 40,
|
||||
|
||||
@@ -128,6 +128,12 @@ class _MatchTileState extends State<MatchTile> {
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Text(
|
||||
'team match: ${match.isTeamMatch}',
|
||||
style: const TextStyle(fontSize: 14, color: Colors.white),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
// Winner / In Progress Info
|
||||
if (match.mvp.isNotEmpty) ...[
|
||||
Container(
|
||||
|
||||
141
lib/presentation/widgets/tiles/team_creation_tile.dart
Normal file
141
lib/presentation/widgets/tiles/team_creation_tile.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/constants.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
class TeamCreationTile extends StatefulWidget {
|
||||
const TeamCreationTile({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.controller,
|
||||
required this.players,
|
||||
required this.hintText,
|
||||
this.onDelete,
|
||||
this.onColorSelection,
|
||||
this.onPlayerTap,
|
||||
});
|
||||
|
||||
final GameColor color;
|
||||
|
||||
final List<Player> players;
|
||||
|
||||
final TextEditingController controller;
|
||||
|
||||
final String hintText;
|
||||
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
final ValueChanged<GameColor>? onColorSelection;
|
||||
|
||||
final void Function(Player player)? onPlayerTap;
|
||||
|
||||
@override
|
||||
State<TeamCreationTile> createState() => _TeamCreationTileState();
|
||||
}
|
||||
|
||||
class _TeamCreationTileState extends State<TeamCreationTile> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: CustomTheme.standardMargin,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: CustomTheme.standardBoxDecoration,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextInputField(
|
||||
controller: widget.controller,
|
||||
hintText: widget.hintText,
|
||||
maxLength: Constants.MAX_TEAM_NAME_LENGTH,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
onPressed: () => widget.onDelete?.call(),
|
||||
icon: const Icon(Icons.delete, size: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Color',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: GameColor.values.map((color) {
|
||||
final isSelected = widget.color == color;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onColorSelection?.call(color);
|
||||
},
|
||||
child: Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: getColorFromGameColor(color),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected ? Colors.white : Colors.transparent,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(Icons.check, size: 18, color: Colors.white)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Players',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.players.isEmpty)
|
||||
const Text(
|
||||
'Keine Spieler:innen zugewiesen',
|
||||
style: TextStyle(color: CustomTheme.hintColor),
|
||||
)
|
||||
else
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: widget.players
|
||||
.map(
|
||||
(player) => GestureDetector(
|
||||
onTap: () => widget.onPlayerTap?.call(player),
|
||||
child: TextIconTile(
|
||||
text: player.name,
|
||||
suffixText: getNameCountText(player),
|
||||
iconEnabled: widget.onPlayerTap != null,
|
||||
onIconTap: () => widget.onPlayerTap?.call(player),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ class TextIconListTile extends StatelessWidget {
|
||||
required this.text,
|
||||
this.suffixText = '',
|
||||
this.icon,
|
||||
this.color,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
@@ -23,6 +24,8 @@ class TextIconListTile extends StatelessWidget {
|
||||
/// The icon to display in the tile.
|
||||
final IconData? icon;
|
||||
|
||||
final Color? color;
|
||||
|
||||
/// The callback to be invoked when the icon is pressed.
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@@ -31,7 +34,17 @@ class TextIconListTile extends StatelessWidget {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
decoration: CustomTheme.standardBoxDecoration,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Color.lerp(CustomTheme.onBoxColor, color?.withAlpha(10), 0.1) ??
|
||||
CustomTheme.boxColor,
|
||||
border: Border.all(
|
||||
color: color ?? CustomTheme.boxBorderColor,
|
||||
width: color != null ? 2 : 1,
|
||||
strokeAlign: BorderSide.strokeAlignCenter,
|
||||
),
|
||||
borderRadius: CustomTheme.standardBorderRadiusAll,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
||||
@@ -6,12 +6,14 @@ class TextIconTile extends StatelessWidget {
|
||||
/// - [text]: The text to display in the tile.
|
||||
/// - [iconEnabled]: A boolean to determine if the icon should be displayed.
|
||||
/// - [onIconTap]: The callback to be invoked when the icon is tapped.
|
||||
/// - [icon]: Optional custom icon. Defaults to [Icons.close].
|
||||
const TextIconTile({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.suffixText = '',
|
||||
this.iconEnabled = true,
|
||||
this.onIconTap,
|
||||
this.icon = Icons.close,
|
||||
});
|
||||
|
||||
/// The text to display in the tile.
|
||||
@@ -25,6 +27,9 @@ class TextIconTile extends StatelessWidget {
|
||||
/// The callback to be invoked when the icon is tapped.
|
||||
final VoidCallback? onIconTap;
|
||||
|
||||
/// The icon to display. Defaults to [Icons.close].
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -65,10 +70,7 @@ class TextIconTile extends StatelessWidget {
|
||||
),
|
||||
if (iconEnabled) ...<Widget>[
|
||||
const SizedBox(width: 3),
|
||||
GestureDetector(
|
||||
onTap: onIconTap,
|
||||
child: const Icon(Icons.close, size: 20),
|
||||
),
|
||||
GestureDetector(onTap: onIconTap, child: Icon(icon, size: 20)),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user