From 1f9ba964017ecf9860da5e7eca264c5e3d3a63e5 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 17:27:34 +0200 Subject: [PATCH] feat: new team member selection --- lib/l10n/arb/app_de.arb | 3 +- lib/l10n/arb/app_en.arb | 3 +- lib/l10n/generated/app_localizations.dart | 126 ++++---- lib/l10n/generated/app_localizations_de.dart | 57 ++-- lib/l10n/generated/app_localizations_en.dart | 57 ++-- .../create_teams/create_teams_view.dart | 74 +---- .../create_teams/edit_members_view.dart | 56 ---- .../create_teams/manage_members_view.dart | 286 ++++++++++++++++++ .../widgets/tiles/team_creation_tile.dart | 47 --- 9 files changed, 422 insertions(+), 287 deletions(-) delete mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart create mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 21a8d25..f97b827 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -1,5 +1,6 @@ { "@@locale": "de", + "add_team": "Team hinzufügen", "all_players": "Alle Spieler:innen", "all_players_selected": "Alle Spieler:innen ausgewählt", "amount_of_matches": "Anzahl der Spiele", @@ -50,7 +51,6 @@ "edit_game": "Spielvorlage bearbeiten", "edit_group": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten", - "edit_members": "Mitglieder bearbeiten", "enter_points": "Punkte eingeben", "enter_results": "Ergebnisse eintragen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", @@ -81,6 +81,7 @@ "live_edit_mode": "Live-Bearbeitungsmodus", "loser": "Verlierer:in", "lowest_score": "Niedrigste Punkte", + "manage_members": "Mitglieder bearbeiten", "match_in_progress": "Spiel läuft...", "match_name": "Spieltitel", "match_profile": "Spielprofil", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b80b909..79883fa 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,5 +1,6 @@ { "@@locale": "en", + "add_team": "Add Team", "all_players": "All players", "all_players_selected": "All players selected", "amount_of_matches": "Amount of Matches", @@ -50,7 +51,6 @@ "edit_game": "Edit Game", "edit_group": "Edit Group", "edit_match": "Edit Match", - "edit_members": "Edit Members", "enter_points": "Enter points", "enter_results": "Enter Results", "error_creating_group": "Error while creating group, please try again", @@ -81,6 +81,7 @@ "live_edit_mode": "Live Edit Mode", "loser": "Loser", "lowest_score": "Lowest Score", + "manage_members": "Manage Members", "match_in_progress": "Match in progress...", "match_name": "Match name", "match_profile": "Match Profile", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index e51aa32..2f7970d 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -98,6 +98,12 @@ abstract class AppLocalizations { 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. /// /// In en, this message translates to: @@ -242,12 +248,6 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: @@ -260,6 +260,12 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: @@ -326,18 +332,18 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: /// **'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. /// /// In en, this message translates to: @@ -356,12 +362,6 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: @@ -464,6 +464,12 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: @@ -524,6 +530,24 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: @@ -560,6 +584,12 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: @@ -578,18 +608,18 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: /// **'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. /// /// In en, this message translates to: @@ -656,18 +686,18 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: /// **'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. /// /// In en, this message translates to: @@ -782,6 +812,12 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: @@ -794,12 +830,6 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: @@ -824,30 +854,6 @@ abstract class AppLocalizations { /// **'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. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index ccdc989..ce42807 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -8,6 +8,9 @@ import 'app_localizations.dart'; class AppLocalizationsDe extends AppLocalizations { AppLocalizationsDe([String locale = 'de']) : super(locale); + @override + String get add_team => 'Team hinzufügen'; + @override String get all_players => 'Alle Spieler:innen'; @@ -82,15 +85,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get create_new_group => 'Neue Gruppe erstellen'; - @override - String get created_on => 'Erstellt am'; - @override String get create_new_match => 'Neues Spiel erstellen'; @override String get create_teams => 'Teams erstellen'; + @override + String get created_on => 'Erstellt am'; + @override String get data => 'Daten'; @@ -135,10 +138,10 @@ class AppLocalizationsDe extends AppLocalizations { String get delete_match => 'Spiel löschen'; @override - String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen'; + String get description => 'Beschreibung'; @override - String get description => 'Beschreibung'; + String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen'; @override String get edit_game => 'Spielvorlage bearbeiten'; @@ -149,9 +152,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get edit_match => 'Gruppe bearbeiten'; - @override - String get edit_members => 'Mitglieder bearbeiten'; - @override String get enter_points => 'Punkte eingeben'; @@ -207,6 +207,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get groups => 'Gruppen'; + @override + String get highest_score => 'Höchste Punkte'; + @override String get home => 'Startseite'; @@ -237,6 +240,15 @@ class AppLocalizationsDe extends AppLocalizations { @override 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 String get match_in_progress => 'Spiel läuft...'; @@ -255,6 +267,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get most_points => 'Höchste Punkte'; + @override + String get multiple_winners => 'Mehrere Gewinner:innen'; + @override 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'; @override - String get no_licenses_found => 'Keine Lizenzen gefunden'; + String get no_license_text_available => 'Kein Lizenztext verfügbar'; @override - String get no_license_text_available => 'Kein Lizenztext verfügbar'; + String get no_licenses_found => 'Keine Lizenzen gefunden'; @override 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'; @override - String get placement => 'Platzierung'; + String get place => 'Platz'; @override - String get place => 'Platz'; + String get placement => 'Platzierung'; @override String get played_matches => 'Gespielte Spiele'; @@ -372,15 +387,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get search_for_players => 'Nach Spieler:innen suchen'; + @override + String get select_loser => 'Verlierer:in wählen'; + @override String get select_winner => 'Gewinner:in wählen'; @override String get select_winners => 'Gewinner:innen wählen'; - @override - String get select_loser => 'Verlierer:in wählen'; - @override String get selected_players => 'Ausgewählte Spieler:innen'; @@ -393,18 +408,6 @@ class AppLocalizationsDe extends AppLocalizations { @override 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 String get statistics => 'Statistiken'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index feb085a..ea3234b 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -8,6 +8,9 @@ import 'app_localizations.dart'; class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); + @override + String get add_team => 'Add Team'; + @override String get all_players => 'All players'; @@ -82,15 +85,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get create_new_group => 'Create new group'; - @override - String get created_on => 'Created on'; - @override String get create_new_match => 'Create new match'; @override String get create_teams => 'Create teams'; + @override + String get created_on => 'Created on'; + @override String get data => 'Data'; @@ -135,10 +138,10 @@ class AppLocalizationsEn extends AppLocalizations { String get delete_match => 'Delete Match'; @override - String get drag_to_set_placement => 'Drag to set placement'; + String get description => 'Description'; @override - String get description => 'Description'; + String get drag_to_set_placement => 'Drag to set placement'; @override String get edit_game => 'Edit Game'; @@ -149,9 +152,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get edit_match => 'Edit Match'; - @override - String get edit_members => 'Edit Members'; - @override String get enter_points => 'Enter points'; @@ -207,6 +207,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get groups => 'Groups'; + @override + String get highest_score => 'Highest Score'; + @override String get home => 'Home'; @@ -237,6 +240,15 @@ class AppLocalizationsEn extends AppLocalizations { @override 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 String get match_in_progress => 'Match in progress...'; @@ -255,6 +267,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get most_points => 'Most Points'; + @override + String get multiple_winners => 'Multiple Winners'; + @override 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'; @override - String get no_licenses_found => 'No licenses found'; + String get no_license_text_available => 'No license text available'; @override - String get no_license_text_available => 'No license text available'; + String get no_licenses_found => 'No licenses found'; @override String get no_matches_created_yet => 'No matches created yet'; @@ -305,10 +320,10 @@ class AppLocalizationsEn extends AppLocalizations { String get not_available => 'Not available'; @override - String get placement => 'Placement'; + String get place => 'place'; @override - String get place => 'place'; + String get placement => 'Placement'; @override String get played_matches => 'Played Matches'; @@ -372,15 +387,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get search_for_players => 'Search for players'; + @override + String get select_loser => 'Select Loser'; + @override String get select_winner => 'Select Winner'; @override String get select_winners => 'Select Winners'; - @override - String get select_loser => 'Select Loser'; - @override String get selected_players => 'Selected players'; @@ -393,18 +408,6 @@ class AppLocalizationsEn extends AppLocalizations { @override 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 String get statistics => 'Statistics'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart index afee2fc..8799109 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart @@ -10,8 +10,7 @@ import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.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/create_match/create_teams/edit_members_view.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart'; @@ -50,7 +49,6 @@ class _CreateTeamsViewState extends State { // Init the controllers nameController = teams.map(getNewController).toList(); - redistributePlayers(); } @override @@ -71,36 +69,7 @@ class _CreateTeamsViewState extends State { return TeamCreationTile( color: teams[index].color, controller: nameController[index], - players: teams[index].members, 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 ? null : () => _removeTeam(index), @@ -118,19 +87,10 @@ class _CreateTeamsViewState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Redistribute - MainMenuButton( - icon: Icons.cached, - text: loc.redistribute, - onPressed: () => setState(() { - redistributePlayers(); - }), - ), - const SizedBox(width: 15), - // Add new team MainMenuButton( icon: Icons.add, + text: loc.add_team, onPressed: teams.length >= widget.match.players.length ? null : addTeam, @@ -139,21 +99,19 @@ class _CreateTeamsViewState extends State { // Confirm teams and start match MainMenuButton( - icon: Icons.check, - onPressed: teams.every((team) => team.members.isNotEmpty) + icon: Icons.arrow_forward_sharp, + onPressed: teams.length >= 2 ? () async { final match = await createMatchWithTeams(); if (context.mounted) { - Navigator.pushAndRemoveUntil( + Navigator.push( context, adaptivePageRoute( - fullscreenDialog: true, - builder: (context) => MatchResultView( + builder: (context) => ManageMembersView( match: match, onWinnerChanged: widget.onWinnerChanged, ), ), - (route) => route.isFirst, ); } } @@ -174,7 +132,6 @@ class _CreateTeamsViewState extends State { final newTeam = getNewTeam(); teams.add(newTeam); nameController.add(getNewController(newTeam)); - redistributePlayers(); }); } @@ -245,25 +202,6 @@ class _CreateTeamsViewState extends State { } } - // 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. Future createMatchWithTeams() async { final db = Provider.of(context, listen: false); diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart deleted file mode 100644 index 78eecef..0000000 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart +++ /dev/null @@ -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 matchPlayer; - - final List teamMember; - - @override - State createState() => _EditMembersViewState(); -} - -class _EditMembersViewState extends State { - List 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 newSelectedPlayers) { - setState(() { - selectedPlayers = newSelectedPlayers; - }); - }, - ), - ); - } -} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart new file mode 100644 index 0000000..befd38e --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart @@ -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 createState() => _ManageMembersViewState(); +} + +class _ManageMembersViewState extends State { + late AppDatabase db; + + late List teams; + + @override + void initState() { + super.initState(); + db = Provider.of(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, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/team_creation_tile.dart b/lib/presentation/widgets/tiles/team_creation_tile.dart index 8a10150..2439597 100644 --- a/lib/presentation/widgets/tiles/team_creation_tile.dart +++ b/lib/presentation/widgets/tiles/team_creation_tile.dart @@ -3,34 +3,26 @@ 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/l10n/generated/app_localizations.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/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.onEdit, this.onDelete, this.onColorSelection, }); final GameColor color; - final List players; - final TextEditingController controller; final String hintText; - final VoidCallback? onEdit; - final VoidCallback? onDelete; final ValueChanged? onColorSelection; @@ -112,45 +104,6 @@ class _TeamCreationTileState extends State { }).toList(), ), 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!, - ), - ), ], ), );