feat: new team member selection

This commit is contained in:
2026-05-21 17:27:34 +02:00
parent 63f050b34f
commit 1f9ba96401
9 changed files with 422 additions and 287 deletions

View File

@@ -1,5 +1,6 @@
{ {
"@@locale": "de", "@@locale": "de",
"add_team": "Team hinzufügen",
"all_players": "Alle Spieler:innen", "all_players": "Alle Spieler:innen",
"all_players_selected": "Alle Spieler:innen ausgewählt", "all_players_selected": "Alle Spieler:innen ausgewählt",
"amount_of_matches": "Anzahl der Spiele", "amount_of_matches": "Anzahl der Spiele",
@@ -50,7 +51,6 @@
"edit_game": "Spielvorlage bearbeiten", "edit_game": "Spielvorlage bearbeiten",
"edit_group": "Gruppe bearbeiten", "edit_group": "Gruppe bearbeiten",
"edit_match": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten",
"edit_members": "Mitglieder bearbeiten",
"enter_points": "Punkte eingeben", "enter_points": "Punkte eingeben",
"enter_results": "Ergebnisse eintragen", "enter_results": "Ergebnisse eintragen",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
@@ -81,6 +81,7 @@
"live_edit_mode": "Live-Bearbeitungsmodus", "live_edit_mode": "Live-Bearbeitungsmodus",
"loser": "Verlierer:in", "loser": "Verlierer:in",
"lowest_score": "Niedrigste Punkte", "lowest_score": "Niedrigste Punkte",
"manage_members": "Mitglieder bearbeiten",
"match_in_progress": "Spiel läuft...", "match_in_progress": "Spiel läuft...",
"match_name": "Spieltitel", "match_name": "Spieltitel",
"match_profile": "Spielprofil", "match_profile": "Spielprofil",

View File

@@ -1,5 +1,6 @@
{ {
"@@locale": "en", "@@locale": "en",
"add_team": "Add Team",
"all_players": "All players", "all_players": "All players",
"all_players_selected": "All players selected", "all_players_selected": "All players selected",
"amount_of_matches": "Amount of Matches", "amount_of_matches": "Amount of Matches",
@@ -50,7 +51,6 @@
"edit_game": "Edit Game", "edit_game": "Edit Game",
"edit_group": "Edit Group", "edit_group": "Edit Group",
"edit_match": "Edit Match", "edit_match": "Edit Match",
"edit_members": "Edit Members",
"enter_points": "Enter points", "enter_points": "Enter points",
"enter_results": "Enter Results", "enter_results": "Enter Results",
"error_creating_group": "Error while creating group, please try again", "error_creating_group": "Error while creating group, please try again",
@@ -81,6 +81,7 @@
"live_edit_mode": "Live Edit Mode", "live_edit_mode": "Live Edit Mode",
"loser": "Loser", "loser": "Loser",
"lowest_score": "Lowest Score", "lowest_score": "Lowest Score",
"manage_members": "Manage Members",
"match_in_progress": "Match in progress...", "match_in_progress": "Match in progress...",
"match_name": "Match name", "match_name": "Match name",
"match_profile": "Match Profile", "match_profile": "Match Profile",

View File

@@ -98,6 +98,12 @@ abstract class AppLocalizations {
Locale('en'), Locale('en'),
]; ];
/// No description provided for @add_team.
///
/// In en, this message translates to:
/// **'Add Team'**
String get add_team;
/// No description provided for @all_players. /// No description provided for @all_players.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -242,12 +248,6 @@ abstract class AppLocalizations {
/// **'Create new group'** /// **'Create new group'**
String get create_new_group; String get create_new_group;
/// No description provided for @created_on.
///
/// In en, this message translates to:
/// **'Created on'**
String get created_on;
/// No description provided for @create_new_match. /// No description provided for @create_new_match.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -260,6 +260,12 @@ abstract class AppLocalizations {
/// **'Create teams'** /// **'Create teams'**
String get create_teams; String get create_teams;
/// No description provided for @created_on.
///
/// In en, this message translates to:
/// **'Created on'**
String get created_on;
/// No description provided for @data. /// No description provided for @data.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -326,18 +332,18 @@ abstract class AppLocalizations {
/// **'Delete Match'** /// **'Delete Match'**
String get delete_match; String get delete_match;
/// No description provided for @drag_to_set_placement.
///
/// In en, this message translates to:
/// **'Drag to set placement'**
String get drag_to_set_placement;
/// No description provided for @description. /// No description provided for @description.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Description'** /// **'Description'**
String get description; String get description;
/// No description provided for @drag_to_set_placement.
///
/// In en, this message translates to:
/// **'Drag to set placement'**
String get drag_to_set_placement;
/// No description provided for @edit_game. /// No description provided for @edit_game.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -356,12 +362,6 @@ abstract class AppLocalizations {
/// **'Edit Match'** /// **'Edit Match'**
String get edit_match; String get edit_match;
/// No description provided for @edit_members.
///
/// In en, this message translates to:
/// **'Edit Members'**
String get edit_members;
/// No description provided for @enter_points. /// No description provided for @enter_points.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -464,6 +464,12 @@ abstract class AppLocalizations {
/// **'Groups'** /// **'Groups'**
String get groups; String get groups;
/// No description provided for @highest_score.
///
/// In en, this message translates to:
/// **'Highest Score'**
String get highest_score;
/// No description provided for @home. /// No description provided for @home.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -524,6 +530,24 @@ abstract class AppLocalizations {
/// **'Live Edit Mode'** /// **'Live Edit Mode'**
String get live_edit_mode; String get live_edit_mode;
/// No description provided for @loser.
///
/// In en, this message translates to:
/// **'Loser'**
String get loser;
/// No description provided for @lowest_score.
///
/// In en, this message translates to:
/// **'Lowest Score'**
String get lowest_score;
/// No description provided for @manage_members.
///
/// In en, this message translates to:
/// **'Manage Members'**
String get manage_members;
/// No description provided for @match_in_progress. /// No description provided for @match_in_progress.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -560,6 +584,12 @@ abstract class AppLocalizations {
/// **'Most Points'** /// **'Most Points'**
String get most_points; String get most_points;
/// No description provided for @multiple_winners.
///
/// In en, this message translates to:
/// **'Multiple Winners'**
String get multiple_winners;
/// No description provided for @no_data_available. /// No description provided for @no_data_available.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -578,18 +608,18 @@ abstract class AppLocalizations {
/// **'No groups created yet'** /// **'No groups created yet'**
String get no_groups_created_yet; String get no_groups_created_yet;
/// No description provided for @no_licenses_found.
///
/// In en, this message translates to:
/// **'No licenses found'**
String get no_licenses_found;
/// No description provided for @no_license_text_available. /// No description provided for @no_license_text_available.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'No license text available'** /// **'No license text available'**
String get no_license_text_available; String get no_license_text_available;
/// No description provided for @no_licenses_found.
///
/// In en, this message translates to:
/// **'No licenses found'**
String get no_licenses_found;
/// No description provided for @no_matches_created_yet. /// No description provided for @no_matches_created_yet.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -656,18 +686,18 @@ abstract class AppLocalizations {
/// **'Not available'** /// **'Not available'**
String get not_available; String get not_available;
/// No description provided for @placement.
///
/// In en, this message translates to:
/// **'Placement'**
String get placement;
/// No description provided for @place. /// No description provided for @place.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'place'** /// **'place'**
String get place; String get place;
/// No description provided for @placement.
///
/// In en, this message translates to:
/// **'Placement'**
String get placement;
/// No description provided for @played_matches. /// No description provided for @played_matches.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -782,6 +812,12 @@ abstract class AppLocalizations {
/// **'Search for players'** /// **'Search for players'**
String get search_for_players; String get search_for_players;
/// No description provided for @select_loser.
///
/// In en, this message translates to:
/// **'Select Loser'**
String get select_loser;
/// No description provided for @select_winner. /// No description provided for @select_winner.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -794,12 +830,6 @@ abstract class AppLocalizations {
/// **'Select Winners'** /// **'Select Winners'**
String get select_winners; String get select_winners;
/// No description provided for @select_loser.
///
/// In en, this message translates to:
/// **'Select Loser'**
String get select_loser;
/// No description provided for @selected_players. /// No description provided for @selected_players.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -824,30 +854,6 @@ abstract class AppLocalizations {
/// **'Single Winner'** /// **'Single Winner'**
String get single_winner; String get single_winner;
/// No description provided for @highest_score.
///
/// In en, this message translates to:
/// **'Highest Score'**
String get highest_score;
/// No description provided for @loser.
///
/// In en, this message translates to:
/// **'Loser'**
String get loser;
/// No description provided for @lowest_score.
///
/// In en, this message translates to:
/// **'Lowest Score'**
String get lowest_score;
/// No description provided for @multiple_winners.
///
/// In en, this message translates to:
/// **'Multiple Winners'**
String get multiple_winners;
/// No description provided for @statistics. /// No description provided for @statistics.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -8,6 +8,9 @@ import 'app_localizations.dart';
class AppLocalizationsDe extends AppLocalizations { class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale); AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get add_team => 'Team hinzufügen';
@override @override
String get all_players => 'Alle Spieler:innen'; String get all_players => 'Alle Spieler:innen';
@@ -82,15 +85,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get create_new_group => 'Neue Gruppe erstellen'; String get create_new_group => 'Neue Gruppe erstellen';
@override
String get created_on => 'Erstellt am';
@override @override
String get create_new_match => 'Neues Spiel erstellen'; String get create_new_match => 'Neues Spiel erstellen';
@override @override
String get create_teams => 'Teams erstellen'; String get create_teams => 'Teams erstellen';
@override
String get created_on => 'Erstellt am';
@override @override
String get data => 'Daten'; String get data => 'Daten';
@@ -135,10 +138,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get delete_match => 'Spiel löschen'; String get delete_match => 'Spiel löschen';
@override @override
String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen'; String get description => 'Beschreibung';
@override @override
String get description => 'Beschreibung'; String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen';
@override @override
String get edit_game => 'Spielvorlage bearbeiten'; String get edit_game => 'Spielvorlage bearbeiten';
@@ -149,9 +152,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get edit_match => 'Gruppe bearbeiten'; String get edit_match => 'Gruppe bearbeiten';
@override
String get edit_members => 'Mitglieder bearbeiten';
@override @override
String get enter_points => 'Punkte eingeben'; String get enter_points => 'Punkte eingeben';
@@ -207,6 +207,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get groups => 'Gruppen'; String get groups => 'Gruppen';
@override
String get highest_score => 'Höchste Punkte';
@override @override
String get home => 'Startseite'; String get home => 'Startseite';
@@ -237,6 +240,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get live_edit_mode => 'Live-Bearbeitungsmodus'; String get live_edit_mode => 'Live-Bearbeitungsmodus';
@override
String get loser => 'Verlierer:in';
@override
String get lowest_score => 'Niedrigste Punkte';
@override
String get manage_members => 'Mitglieder bearbeiten';
@override @override
String get match_in_progress => 'Spiel läuft...'; String get match_in_progress => 'Spiel läuft...';
@@ -255,6 +267,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get most_points => 'Höchste Punkte'; String get most_points => 'Höchste Punkte';
@override
String get multiple_winners => 'Mehrere Gewinner:innen';
@override @override
String get no_data_available => 'Keine Daten verfügbar'; String get no_data_available => 'Keine Daten verfügbar';
@@ -265,10 +280,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get no_groups_created_yet => 'Noch keine Gruppen erstellt'; String get no_groups_created_yet => 'Noch keine Gruppen erstellt';
@override @override
String get no_licenses_found => 'Keine Lizenzen gefunden'; String get no_license_text_available => 'Kein Lizenztext verfügbar';
@override @override
String get no_license_text_available => 'Kein Lizenztext verfügbar'; String get no_licenses_found => 'Keine Lizenzen gefunden';
@override @override
String get no_matches_created_yet => 'Noch keine Spiele erstellt'; String get no_matches_created_yet => 'Noch keine Spiele erstellt';
@@ -305,10 +320,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get not_available => 'Nicht verfügbar'; String get not_available => 'Nicht verfügbar';
@override @override
String get placement => 'Platzierung'; String get place => 'Platz';
@override @override
String get place => 'Platz'; String get placement => 'Platzierung';
@override @override
String get played_matches => 'Gespielte Spiele'; String get played_matches => 'Gespielte Spiele';
@@ -372,15 +387,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get search_for_players => 'Nach Spieler:innen suchen'; String get search_for_players => 'Nach Spieler:innen suchen';
@override
String get select_loser => 'Verlierer:in wählen';
@override @override
String get select_winner => 'Gewinner:in wählen'; String get select_winner => 'Gewinner:in wählen';
@override @override
String get select_winners => 'Gewinner:innen wählen'; String get select_winners => 'Gewinner:innen wählen';
@override
String get select_loser => 'Verlierer:in wählen';
@override @override
String get selected_players => 'Ausgewählte Spieler:innen'; String get selected_players => 'Ausgewählte Spieler:innen';
@@ -393,18 +408,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get single_winner => 'Ein:e Gewinner:in'; String get single_winner => 'Ein:e Gewinner:in';
@override
String get highest_score => 'Höchste Punkte';
@override
String get loser => 'Verlierer:in';
@override
String get lowest_score => 'Niedrigste Punkte';
@override
String get multiple_winners => 'Mehrere Gewinner:innen';
@override @override
String get statistics => 'Statistiken'; String get statistics => 'Statistiken';

View File

@@ -8,6 +8,9 @@ import 'app_localizations.dart';
class AppLocalizationsEn extends AppLocalizations { class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale); AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get add_team => 'Add Team';
@override @override
String get all_players => 'All players'; String get all_players => 'All players';
@@ -82,15 +85,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get create_new_group => 'Create new group'; String get create_new_group => 'Create new group';
@override
String get created_on => 'Created on';
@override @override
String get create_new_match => 'Create new match'; String get create_new_match => 'Create new match';
@override @override
String get create_teams => 'Create teams'; String get create_teams => 'Create teams';
@override
String get created_on => 'Created on';
@override @override
String get data => 'Data'; String get data => 'Data';
@@ -135,10 +138,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get delete_match => 'Delete Match'; String get delete_match => 'Delete Match';
@override @override
String get drag_to_set_placement => 'Drag to set placement'; String get description => 'Description';
@override @override
String get description => 'Description'; String get drag_to_set_placement => 'Drag to set placement';
@override @override
String get edit_game => 'Edit Game'; String get edit_game => 'Edit Game';
@@ -149,9 +152,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get edit_match => 'Edit Match'; String get edit_match => 'Edit Match';
@override
String get edit_members => 'Edit Members';
@override @override
String get enter_points => 'Enter points'; String get enter_points => 'Enter points';
@@ -207,6 +207,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get groups => 'Groups'; String get groups => 'Groups';
@override
String get highest_score => 'Highest Score';
@override @override
String get home => 'Home'; String get home => 'Home';
@@ -237,6 +240,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get live_edit_mode => 'Live Edit Mode'; String get live_edit_mode => 'Live Edit Mode';
@override
String get loser => 'Loser';
@override
String get lowest_score => 'Lowest Score';
@override
String get manage_members => 'Manage Members';
@override @override
String get match_in_progress => 'Match in progress...'; String get match_in_progress => 'Match in progress...';
@@ -255,6 +267,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get most_points => 'Most Points'; String get most_points => 'Most Points';
@override
String get multiple_winners => 'Multiple Winners';
@override @override
String get no_data_available => 'No data available'; String get no_data_available => 'No data available';
@@ -265,10 +280,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get no_groups_created_yet => 'No groups created yet'; String get no_groups_created_yet => 'No groups created yet';
@override @override
String get no_licenses_found => 'No licenses found'; String get no_license_text_available => 'No license text available';
@override @override
String get no_license_text_available => 'No license text available'; String get no_licenses_found => 'No licenses found';
@override @override
String get no_matches_created_yet => 'No matches created yet'; String get no_matches_created_yet => 'No matches created yet';
@@ -305,10 +320,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get not_available => 'Not available'; String get not_available => 'Not available';
@override @override
String get placement => 'Placement'; String get place => 'place';
@override @override
String get place => 'place'; String get placement => 'Placement';
@override @override
String get played_matches => 'Played Matches'; String get played_matches => 'Played Matches';
@@ -372,15 +387,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get search_for_players => 'Search for players'; String get search_for_players => 'Search for players';
@override
String get select_loser => 'Select Loser';
@override @override
String get select_winner => 'Select Winner'; String get select_winner => 'Select Winner';
@override @override
String get select_winners => 'Select Winners'; String get select_winners => 'Select Winners';
@override
String get select_loser => 'Select Loser';
@override @override
String get selected_players => 'Selected players'; String get selected_players => 'Selected players';
@@ -393,18 +408,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get single_winner => 'Single Winner'; String get single_winner => 'Single Winner';
@override
String get highest_score => 'Highest Score';
@override
String get loser => 'Loser';
@override
String get lowest_score => 'Lowest Score';
@override
String get multiple_winners => 'Multiple Winners';
@override @override
String get statistics => 'Statistics'; String get statistics => 'Statistics';

View File

@@ -10,8 +10,7 @@ import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/team.dart'; import 'package:tallee/data/models/team.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.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/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart'; import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart';
@@ -50,7 +49,6 @@ class _CreateTeamsViewState extends State<CreateTeamsView> {
// Init the controllers // Init the controllers
nameController = teams.map(getNewController).toList(); nameController = teams.map(getNewController).toList();
redistributePlayers();
} }
@override @override
@@ -71,36 +69,7 @@ class _CreateTeamsViewState extends State<CreateTeamsView> {
return TeamCreationTile( return TeamCreationTile(
color: teams[index].color, color: teams[index].color,
controller: nameController[index], controller: nameController[index],
players: teams[index].members,
hintText: '${loc.team} ${index + 1}', hintText: '${loc.team} ${index + 1}',
onEdit: () async {
final newPlayers = await Navigator.push(
context,
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) => EditMembersView(
matchPlayer: widget.match.players,
teamMember: teams[index].members,
),
),
);
setState(() {
// Remove the selected players from every team
for (final player in newPlayers) {
for (final team in teams) {
if (team.members.contains(player)) {
team.members.remove(player);
}
}
}
// Add the selected players to the current team
teams[index] = teams[index].copyWith(
members: newPlayers,
);
});
},
onDelete: teams.length <= 2 onDelete: teams.length <= 2
? null ? null
: () => _removeTeam(index), : () => _removeTeam(index),
@@ -118,19 +87,10 @@ class _CreateTeamsViewState extends State<CreateTeamsView> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Redistribute
MainMenuButton(
icon: Icons.cached,
text: loc.redistribute,
onPressed: () => setState(() {
redistributePlayers();
}),
),
const SizedBox(width: 15),
// Add new team // Add new team
MainMenuButton( MainMenuButton(
icon: Icons.add, icon: Icons.add,
text: loc.add_team,
onPressed: teams.length >= widget.match.players.length onPressed: teams.length >= widget.match.players.length
? null ? null
: addTeam, : addTeam,
@@ -139,21 +99,19 @@ class _CreateTeamsViewState extends State<CreateTeamsView> {
// Confirm teams and start match // Confirm teams and start match
MainMenuButton( MainMenuButton(
icon: Icons.check, icon: Icons.arrow_forward_sharp,
onPressed: teams.every((team) => team.members.isNotEmpty) onPressed: teams.length >= 2
? () async { ? () async {
final match = await createMatchWithTeams(); final match = await createMatchWithTeams();
if (context.mounted) { if (context.mounted) {
Navigator.pushAndRemoveUntil( Navigator.push(
context, context,
adaptivePageRoute( adaptivePageRoute(
fullscreenDialog: true, builder: (context) => ManageMembersView(
builder: (context) => MatchResultView(
match: match, match: match,
onWinnerChanged: widget.onWinnerChanged, onWinnerChanged: widget.onWinnerChanged,
), ),
), ),
(route) => route.isFirst,
); );
} }
} }
@@ -174,7 +132,6 @@ class _CreateTeamsViewState extends State<CreateTeamsView> {
final newTeam = getNewTeam(); final newTeam = getNewTeam();
teams.add(newTeam); teams.add(newTeam);
nameController.add(getNewController(newTeam)); nameController.add(getNewController(newTeam));
redistributePlayers();
}); });
} }
@@ -245,25 +202,6 @@ class _CreateTeamsViewState extends State<CreateTeamsView> {
} }
} }
// Iterates through all teams and redistributes players randomly and
// as evenly as possible.
void redistributePlayers() {
for (final team in teams) {
team.members.clear();
}
if (matchPlayers.isEmpty || teams.isEmpty) {
return;
}
final shuffledPlayers = [...matchPlayers]..shuffle(random);
for (int i = 0; i < shuffledPlayers.length; i++) {
final teamIndex = i % teams.length;
teams[teamIndex].members.add(shuffledPlayers[i]);
}
}
/// Saves the teams to the database and returns the updated match with the teams. /// Saves the teams to the database and returns the updated match with the teams.
Future<Match> createMatchWithTeams() async { Future<Match> createMatchWithTeams() async {
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);

View File

@@ -1,56 +0,0 @@
import 'package:flutter/material.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/player_selection.dart';
class EditMembersView extends StatefulWidget {
const EditMembersView({
super.key,
required this.matchPlayer,
required this.teamMember,
});
final List<Player> matchPlayer;
final List<Player> teamMember;
@override
State<EditMembersView> createState() => _EditMembersViewState();
}
class _EditMembersViewState extends State<EditMembersView> {
List<Player> selectedPlayers = [];
@override
void initState() {
selectedPlayers = [...widget.teamMember];
super.initState();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(loc.edit_members),
leading: HapticIconButton(
onPressed: selectedPlayers.isNotEmpty
? () => Navigator.pop(context, selectedPlayers)
: null,
icon: const Icon(Icons.arrow_back_ios_new_outlined),
),
),
body: PlayerSelection(
initialSelectedPlayers: widget.teamMember,
availablePlayers: widget.matchPlayer,
onChanged: (List<Player> newSelectedPlayers) {
setState(() {
selectedPlayers = newSelectedPlayers;
});
},
),
);
}
}

View File

@@ -0,0 +1,286 @@
import 'dart:core' hide Match;
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fluttericon/rpg_awesome_icons.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/l10n/generated/app_localizations.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/text_icon_list_tile.dart';
/// Displays the given [teams] as a flat reorderable list where every team is
/// preceded by a header row and followed by its members. Members can be
/// dragged across team boundaries to be reassigned to another team.
class ManageMembersView extends StatefulWidget {
const ManageMembersView({
super.key,
required this.match,
required this.onWinnerChanged,
});
final Match match;
final VoidCallback? onWinnerChanged;
@override
State<ManageMembersView> createState() => _ManageMembersViewState();
}
class _ManageMembersViewState extends State<ManageMembersView> {
late AppDatabase db;
late List<Team> teams;
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
teams = widget.match.teams!;
redistributePlayers();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.manage_members)),
body: SafeArea(
child: Stack(
children: [
Expanded(
child: ReorderableListView.builder(
padding: const EdgeInsets.symmetric(vertical: 12),
buildDefaultDragHandles: false,
itemCount: allItemsCount,
onReorder: onReorder,
proxyDecorator: (child, index, animation) =>
Material(type: MaterialType.transparency, child: child),
itemBuilder: (context, index) {
final teamIndex = teamIndexForFlat(index);
final memberIndex = memberIndexForFlat(index, teamIndex);
final team = teams[teamIndex];
if (memberIndex == -1) {
return buildTeamTile(team: team);
}
final player = team.members[memberIndex];
return ReorderableDelayedDragStartListener(
key: ValueKey('player_${player.id}'),
index: index,
child: TextIconListTile(
text: player.name,
suffixText: getNameCountText(player),
icon: Icons.drag_handle,
),
);
},
),
),
Positioned(
bottom: MediaQuery.of(context).padding.bottom,
left: 0,
right: 0,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MainMenuButton(
onPressed: () => setState(() {
redistributePlayers();
}),
icon: Icons.cached,
),
const SizedBox(width: 16),
MainMenuButton(
onPressed: allTeamsHaveMembers
? () async => submitMatch()
: null,
text: loc.create_match,
icon: RpgAwesome.clovers_card,
),
],
),
),
),
],
),
),
);
}
void submitMatch() async {
await db.matchDao.addMatch(match: widget.match);
if (mounted) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (_) => MatchResultView(
match: widget.match,
onWinnerChanged: widget.onWinnerChanged,
),
),
(route) => route.isFirst,
);
}
}
bool get allTeamsHaveMembers {
return teams.every((team) => team.members.isNotEmpty);
}
// Iterates through all teams and redistributes players randomly and
// as evenly as possible.
void redistributePlayers() {
for (final team in teams) {
team.members.clear();
}
var matchPlayers = widget.match.players;
Random random = Random();
if (matchPlayers.isEmpty || teams.isEmpty) {
return;
}
final shuffledPlayers = [...matchPlayers]..shuffle(random);
for (int i = 0; i < shuffledPlayers.length; i++) {
final teamIndex = i % teams.length;
teams[teamIndex].members.add(shuffledPlayers[i]);
}
}
/// Total players + teams length
int get allItemsCount {
var count = 0;
for (final team in teams) {
count += 1 + team.members.length;
}
return count;
}
/// Returns the index of the team that owns the flat-list item at [flatIndex].
int teamIndexForFlat(int flatIndex) {
var remaining = flatIndex;
for (var i = 0; i < teams.length; i++) {
final size = 1 + teams[i].members.length;
if (remaining < size) return i;
remaining -= size;
}
return teams.length - 1;
}
/// Returns the member index within its team, or `-1` if the item at
/// [flatIndex] is the team header.
int memberIndexForFlat(int flatIndex, int teamIndex) {
var offset = 0;
for (var i = 0; i < teamIndex; i++) {
offset += 1 + teams[i].members.length;
}
// offset now points to the header of [teamIndex]. Anything beyond is a
// member of that team.
final localIndex = flatIndex - offset;
return localIndex == 0 ? -1 : localIndex - 1;
}
void onReorder(int oldIndex, int newIndex) {
final sourceTeamIndex = teamIndexForFlat(oldIndex);
final sourceMemberIndex = memberIndexForFlat(oldIndex, sourceTeamIndex);
// Headers themselves can't be reordered.
if (sourceMemberIndex == -1) return;
// Flutter convention: when moving down, the target index is shifted by 1
// because the item is removed first.
var targetIndex = newIndex;
if (newIndex > oldIndex) targetIndex -= 1;
targetIndex = targetIndex.clamp(0, allItemsCount - 1);
// Resolve target location based on the item currently at [targetIndex]
// *before* the move.
int destTeamIndex;
int insertPositionInTeam;
if (targetIndex >= allItemsCount - 1 && newIndex >= allItemsCount) {
// Dropped at the very end -> append to the last team.
destTeamIndex = teams.length - 1;
insertPositionInTeam = teams[destTeamIndex].members.length;
} else {
destTeamIndex = teamIndexForFlat(targetIndex);
final anchorMemberIndex = memberIndexForFlat(targetIndex, destTeamIndex);
if (anchorMemberIndex == -1) {
// Dropped right before a header -> append to the previous team.
destTeamIndex = destTeamIndex - 1;
if (destTeamIndex < 0) {
// Dropped above the very first header -> stay in team 0 at top.
destTeamIndex = 0;
insertPositionInTeam = 0;
} else {
insertPositionInTeam = teams[destTeamIndex].members.length;
}
} else {
insertPositionInTeam = anchorMemberIndex;
}
}
setState(() {
final sourceMembers = teams[sourceTeamIndex].members;
final player = sourceMembers.removeAt(sourceMemberIndex);
// Adjust insert index if we removed from before the insert point in the
// same team.
if (sourceTeamIndex == destTeamIndex &&
insertPositionInTeam > sourceMembers.length) {
insertPositionInTeam = sourceMembers.length;
}
teams[destTeamIndex].members.insert(insertPositionInTeam, player);
});
}
Widget buildTeamTile({required Team team}) {
final color = getColorFromGameColor(team.color);
return Padding(
key: ValueKey('header_${team.id}'),
padding: const EdgeInsets.fromLTRB(12, 16, 12, 8),
child: Row(
children: [
Container(
width: 14,
height: 14,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 10),
Expanded(
child: Text(
team.name,
style: const TextStyle(
color: CustomTheme.textColor,
fontSize: 17,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Text(
'${team.members.length}',
style: const TextStyle(
color: CustomTheme.hintColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}

View File

@@ -3,34 +3,26 @@ import 'package:tallee/core/common.dart';
import 'package:tallee/core/constants.dart'; import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.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 { class TeamCreationTile extends StatefulWidget {
const TeamCreationTile({ const TeamCreationTile({
super.key, super.key,
required this.color, required this.color,
required this.controller, required this.controller,
required this.players,
required this.hintText, required this.hintText,
this.onEdit,
this.onDelete, this.onDelete,
this.onColorSelection, this.onColorSelection,
}); });
final GameColor color; final GameColor color;
final List<Player> players;
final TextEditingController controller; final TextEditingController controller;
final String hintText; final String hintText;
final VoidCallback? onEdit;
final VoidCallback? onDelete; final VoidCallback? onDelete;
final ValueChanged<GameColor>? onColorSelection; final ValueChanged<GameColor>? onColorSelection;
@@ -112,45 +104,6 @@ class _TeamCreationTileState extends State<TeamCreationTile> {
}).toList(), }).toList(),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text(
loc.players,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
),
const SizedBox(height: 8),
if (widget.players.isEmpty)
Text(
loc.no_players_selected,
style: const TextStyle(color: CustomTheme.hintColor),
)
else
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.players
.map(
(player) => TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
iconEnabled: false,
),
)
.toList(),
),
if (widget.onEdit != null)
Padding(
padding: const EdgeInsets.only(top: 12),
child: AnimatedDialogButton(
buttonConstraints: const BoxConstraints(
minWidth: double.infinity,
),
buttonText: loc.edit_members,
onPressed: widget.onEdit!,
),
),
], ],
), ),
); );