Merge branch 'feature/180-Spielerprofile-implementieren' into feature/193-statisticsview-rework
This commit is contained in:
@@ -185,6 +185,38 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
/// Retrieves all groups a specific player belongs to.
|
||||
/// Returns an empty list if the player is not part of any group.
|
||||
Future<List<Group>> getGroupsByPlayer({required String playerId}) async {
|
||||
final playerGroups = await (select(
|
||||
playerGroupTable,
|
||||
)..where((pg) => pg.playerId.equals(playerId))).get();
|
||||
|
||||
if (playerGroups.isEmpty) return [];
|
||||
|
||||
final groupIds = playerGroups.map((pg) => pg.groupId).toSet().toList();
|
||||
final rows =
|
||||
await (select(groupTable)
|
||||
..where((g) => g.id.isIn(groupIds))
|
||||
..orderBy([(g) => OrderingTerm.desc(g.createdAt)]))
|
||||
.get();
|
||||
|
||||
return Future.wait(
|
||||
rows.map((groupData) async {
|
||||
final members = await db.playerGroupDao.getPlayersOfGroup(
|
||||
groupId: groupData.id,
|
||||
);
|
||||
return Group(
|
||||
id: groupData.id,
|
||||
name: groupData.name,
|
||||
description: groupData.description,
|
||||
members: members,
|
||||
createdAt: groupData.createdAt,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Checks if a group with the given [groupId] exists in the database.
|
||||
/// Returns `true` if the group exists, `false` otherwise.
|
||||
Future<bool> groupExists({required String groupId}) async {
|
||||
|
||||
@@ -352,6 +352,53 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
Future<List<Match>> getMatchesByPlayer({required String playerId}) async {
|
||||
final playerMatches = await (select(
|
||||
playerMatchTable,
|
||||
)..where((pm) => pm.playerId.equals(playerId))).get();
|
||||
|
||||
if (playerMatches.isEmpty) return [];
|
||||
|
||||
final matchIds = playerMatches.map((pm) => pm.matchId).toSet().toList();
|
||||
final rows =
|
||||
await (select(matchTable)
|
||||
..where((m) => m.id.isIn(matchIds))
|
||||
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
||||
.get();
|
||||
|
||||
return Future.wait(
|
||||
rows.map((row) async {
|
||||
final game = await db.gameDao.getGameById(gameId: row.gameId);
|
||||
|
||||
Group? group;
|
||||
if (row.groupId != null) {
|
||||
group = await db.groupDao.getGroupById(groupId: row.groupId!);
|
||||
}
|
||||
|
||||
final players = await db.playerMatchDao.getPlayersOfMatch(
|
||||
matchId: row.id,
|
||||
);
|
||||
final scores = await db.scoreEntryDao.getAllMatchScores(
|
||||
matchId: row.id,
|
||||
);
|
||||
final teams = await _getMatchTeams(matchId: row.id);
|
||||
|
||||
return Match(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
game: game,
|
||||
group: group,
|
||||
players: players,
|
||||
teams: teams.isEmpty ? null : teams,
|
||||
notes: row.notes,
|
||||
createdAt: row.createdAt,
|
||||
endedAt: row.endedAt,
|
||||
scores: scores,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Retrieves all matches associated with the given [groupId].
|
||||
/// Queries the database directly, filtering by [groupId].
|
||||
Future<List<Match>> getMatchesByGroup({required String groupId}) async {
|
||||
|
||||
@@ -17,7 +17,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
/// the new one.
|
||||
Future<bool> addPlayer({required Player player}) async {
|
||||
if (!await playerExists(playerId: player.id)) {
|
||||
final int nameCount = await calculateNameCount(name: player.name);
|
||||
final int nameCount = await _processNameCount(name: player.name);
|
||||
|
||||
await into(playerTable).insert(
|
||||
PlayerTableCompanion.insert(
|
||||
@@ -64,7 +64,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
final playersWithName = entry.value;
|
||||
|
||||
// Get the current nameCount
|
||||
var nameCount = await calculateNameCount(name: name);
|
||||
var nameCount = await _processNameCount(name: name);
|
||||
|
||||
// One player with the same name
|
||||
if (playersWithName.length == 1) {
|
||||
@@ -159,44 +159,63 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
/* Update */
|
||||
|
||||
/// Updates the name of the player with the given [playerId] to [name].
|
||||
///
|
||||
/// Keeps the `nameCount` values of the affected name groups consistent:
|
||||
/// - The renamed player gets a fresh `nameCount` for the new name group.
|
||||
/// - All players in the previous name group whose `nameCount` was greater
|
||||
/// than the removed one get decremented by 1, so the numbering stays
|
||||
/// contiguous (1..N) in `createdAt` order.
|
||||
/// - If only one player remains in the previous name group, their
|
||||
/// `nameCount` is reset to 0.
|
||||
Future<bool> updatePlayerName({
|
||||
required String playerId,
|
||||
required String name,
|
||||
}) async {
|
||||
// Get previous name and name count for the player before updating
|
||||
final previousPlayerName =
|
||||
await (select(playerTable)..where((p) => p.id.equals(playerId)))
|
||||
.map((row) => row.name)
|
||||
.getSingleOrNull() ??
|
||||
'';
|
||||
final previousNameCount = await getNameCount(name: previousPlayerName);
|
||||
return transaction(() async {
|
||||
final previousPlayer = await (select(
|
||||
playerTable,
|
||||
)..where((p) => p.id.equals(playerId))).getSingleOrNull();
|
||||
if (previousPlayer == null) return false;
|
||||
|
||||
final previousName = previousPlayer.name;
|
||||
final previousCount = previousPlayer.nameCount;
|
||||
|
||||
// Determine the nameCount for the renamed player in the new group.
|
||||
final newNameCount = await _processNameCount(name: name);
|
||||
|
||||
final rowsAffected =
|
||||
await (update(playerTable)..where((p) => p.id.equals(playerId))).write(
|
||||
PlayerTableCompanion(name: Value(name)),
|
||||
await (update(
|
||||
playerTable,
|
||||
)..where((p) => p.id.equals(playerId))).write(
|
||||
PlayerTableCompanion(
|
||||
name: Value(name),
|
||||
nameCount: Value(newNameCount),
|
||||
),
|
||||
);
|
||||
|
||||
// Update name count for the new name
|
||||
final count = await calculateNameCount(name: name);
|
||||
if (count > 0) {
|
||||
await (update(playerTable)..where((p) => p.name.equals(name))).write(
|
||||
PlayerTableCompanion(nameCount: Value(count)),
|
||||
// Consolidate the previous name group.
|
||||
final remainingCount = await getNameCount(name: previousName);
|
||||
|
||||
if (remainingCount == 1) {
|
||||
// Only one player left
|
||||
await (update(playerTable)..where((p) => p.name.equals(previousName)))
|
||||
.write(const PlayerTableCompanion(nameCount: Value(0)));
|
||||
} else if (remainingCount > 1 && previousCount > 0) {
|
||||
// Shift every player above the gap down by one to keep numbering in order.
|
||||
await (update(playerTable)..where(
|
||||
(p) =>
|
||||
p.name.equals(previousName) &
|
||||
p.nameCount.isBiggerThanValue(previousCount),
|
||||
))
|
||||
.write(
|
||||
PlayerTableCompanion.custom(
|
||||
nameCount: playerTable.nameCount - const Constant(1),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (previousNameCount > 0) {
|
||||
// Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount
|
||||
final player = await getPlayerWithHighestNameCount(
|
||||
name: previousPlayerName,
|
||||
);
|
||||
if (player != null) {
|
||||
await updateNameCount(
|
||||
playerId: player.id,
|
||||
nameCount: previousNameCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
return rowsAffected > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// Updates the description of the player with the given [playerId] to
|
||||
@@ -226,6 +245,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
/* Name count management */
|
||||
|
||||
/// Retrieves the count of players with the given [name].
|
||||
/// Returns the highest name count if players with the same name exist,
|
||||
/// otherwise `null`.
|
||||
Future<int> getNameCount({required String name}) async {
|
||||
final query = select(playerTable)..where((p) => p.name.equals(name));
|
||||
final result = await query.get();
|
||||
@@ -264,25 +285,39 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Processes the name count for a new player with the given [name].
|
||||
///- 0 Player: returning 0
|
||||
///- 1 Player: returning 2, and initializes the nameCount for the existing player to 1
|
||||
///- Other: returning the existing count + 1
|
||||
Future<int> _processNameCount({required String name}) async {
|
||||
final nameCount = await calculateNameCount(name: name);
|
||||
if (nameCount == 2) {
|
||||
// If one other player exists with the same name, initialize the nameCount
|
||||
await initializeNameCount(name: name);
|
||||
}
|
||||
return nameCount;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
/// Calculates the name count for a new player with the given [name].
|
||||
/// - 0 Players: Name count is 0
|
||||
/// - 1 Player: Name count is 2 (since the existing player will be 1)
|
||||
/// - Other: Name count is the existing count + 1
|
||||
Future<int> calculateNameCount({required String name}) async {
|
||||
final count = await getNameCount(name: name);
|
||||
final int nameCount;
|
||||
|
||||
if (count == 1) {
|
||||
// If one other player exists with the same name, initialize the nameCount
|
||||
await initializeNameCount(name: name);
|
||||
// And for the new player, set nameCount to 2
|
||||
if (count == 0) {
|
||||
// If no other players exist with the same name, the returned nameCount is 0
|
||||
nameCount = 0;
|
||||
} else if (count == 1) {
|
||||
// If one other player with the name count exists, the returned name count is 2
|
||||
nameCount = 2;
|
||||
} else if (count > 1) {
|
||||
} else {
|
||||
// If more than one player exists with the same name, just increment
|
||||
// the nameCount for the new player
|
||||
nameCount = count + 1;
|
||||
} else {
|
||||
// If no other players exist with the same name, set nameCount to 0
|
||||
nameCount = 0;
|
||||
}
|
||||
|
||||
return nameCount;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"color_red": "Rot",
|
||||
"color_teal": "Türkis",
|
||||
"color_yellow": "Gelb",
|
||||
"confirm": "Bestätigen",
|
||||
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
|
||||
"create_game": "Spielvorlage erstellen",
|
||||
"create_group": "Gruppe erstellen",
|
||||
@@ -44,11 +45,14 @@
|
||||
},
|
||||
"delete_group": "Gruppe löschen",
|
||||
"delete_match": "Spiel löschen",
|
||||
"delete_player": "Spieler:in löschen",
|
||||
"description": "Beschreibung",
|
||||
"drag_to_set_placement": "Ziehen um Platzierung zu setzen",
|
||||
"edit_game": "Spielvorlage bearbeiten",
|
||||
"edit_group": "Gruppe bearbeiten",
|
||||
"edit_match": "Gruppe bearbeiten",
|
||||
"edit_name": "Name ändern",
|
||||
"edit_player": "Spieler bearbeiten",
|
||||
"enter_points": "Punkte eingeben",
|
||||
"enter_results": "Ergebnisse eintragen",
|
||||
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
|
||||
@@ -66,6 +70,7 @@
|
||||
"group_name": "Gruppenname",
|
||||
"group_profile": "Gruppenprofil",
|
||||
"groups": "Gruppen",
|
||||
"groups_part_of": "Gruppen Teil von",
|
||||
"highest_score": "Höchste Punkte",
|
||||
"home": "Startseite",
|
||||
"import_canceled": "Import abgebrochen",
|
||||
@@ -83,6 +88,9 @@
|
||||
"match_name": "Spieltitel",
|
||||
"match_profile": "Spielprofil",
|
||||
"matches": "Spiele",
|
||||
"matches_part_of": "Spiele Teil von",
|
||||
"matches_played": "Spiele gespielt",
|
||||
"matches_won": "Spiele gewonnen",
|
||||
"members": "Mitglieder",
|
||||
"most_points": "Höchste Punkte",
|
||||
"multiple_winners": "Mehrere Gewinner:innen",
|
||||
@@ -92,6 +100,7 @@
|
||||
"no_license_text_available": "Kein Lizenztext verfügbar",
|
||||
"no_licenses_found": "Keine Lizenzen gefunden",
|
||||
"no_matches_created_yet": "Noch keine Spiele erstellt",
|
||||
"no_matches_played_yet": "Noch kein Spiel gespielt",
|
||||
"no_players_created_yet": "Noch keine Spieler:in erstellt",
|
||||
"no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
|
||||
"no_players_selected": "Keine Spieler:innen ausgewählt",
|
||||
@@ -102,10 +111,12 @@
|
||||
"none": "Kein",
|
||||
"none_group": "Keine",
|
||||
"not_available": "Nicht verfügbar",
|
||||
"not_part_of_any_group": "Noch keiner Gruppe hinzugefügt",
|
||||
"place": "Platz",
|
||||
"placement": "Platzierung",
|
||||
"played_matches": "Gespielte Spiele",
|
||||
"player_name": "Spieler:innenname",
|
||||
"player_profile": "Spieler:in-Profil",
|
||||
"players": "Spieler:innen",
|
||||
"point": "Punkt",
|
||||
"points": "Punkte",
|
||||
@@ -127,6 +138,7 @@
|
||||
"select_winner": "Gewinner:in wählen",
|
||||
"select_winners": "Gewinner:innen wählen",
|
||||
"selected_players": "Ausgewählte Spieler:innen",
|
||||
"set_name": "Name setzen",
|
||||
"settings": "Einstellungen",
|
||||
"single_loser": "Ein:e Verlierer:in",
|
||||
"single_winner": "Ein:e Gewinner:in",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"color_red": "Red",
|
||||
"color_teal": "Teal",
|
||||
"color_yellow": "Yellow",
|
||||
"confirm": "Confirm",
|
||||
"could_not_add_player": "Could not add player",
|
||||
"create_game": "Create Game",
|
||||
"create_group": "Create Group",
|
||||
@@ -44,11 +45,14 @@
|
||||
},
|
||||
"delete_group": "Delete Group",
|
||||
"delete_match": "Delete Match",
|
||||
"delete_player": "Delete player?",
|
||||
"description": "Description",
|
||||
"drag_to_set_placement": "Drag to set placement",
|
||||
"edit_game": "Edit Game",
|
||||
"edit_group": "Edit Group",
|
||||
"edit_match": "Edit Match",
|
||||
"edit_name": "Edit name",
|
||||
"edit_player": "Edit player",
|
||||
"enter_points": "Enter points",
|
||||
"enter_results": "Enter Results",
|
||||
"error_creating_group": "Error while creating group, please try again",
|
||||
@@ -66,6 +70,7 @@
|
||||
"group_name": "Group name",
|
||||
"group_profile": "Group Profile",
|
||||
"groups": "Groups",
|
||||
"groups_part_of": "Groups part of",
|
||||
"highest_score": "Highest Score",
|
||||
"home": "Home",
|
||||
"import_canceled": "Import canceled",
|
||||
@@ -83,6 +88,9 @@
|
||||
"match_name": "Match name",
|
||||
"match_profile": "Match Profile",
|
||||
"matches": "Matches",
|
||||
"matches_part_of": "Matches part of",
|
||||
"matches_played": "Matches played",
|
||||
"matches_won": "Matches won",
|
||||
"members": "Members",
|
||||
"most_points": "Most Points",
|
||||
"multiple_winners": "Multiple Winners",
|
||||
@@ -92,6 +100,7 @@
|
||||
"no_license_text_available": "No license text available",
|
||||
"no_licenses_found": "No licenses found",
|
||||
"no_matches_created_yet": "No matches created yet",
|
||||
"no_matches_played_yet": "No games played yet",
|
||||
"no_players_created_yet": "No players created yet",
|
||||
"no_players_found_with_that_name": "No players found with that name",
|
||||
"no_players_selected": "No players selected",
|
||||
@@ -102,10 +111,12 @@
|
||||
"none": "None",
|
||||
"none_group": "None",
|
||||
"not_available": "Not available",
|
||||
"not_part_of_any_group": "Not part of any group yet",
|
||||
"place": "place",
|
||||
"placement": "Placement",
|
||||
"played_matches": "Played Matches",
|
||||
"player_name": "Player name",
|
||||
"player_profile": "Player Profile",
|
||||
"players": "Players",
|
||||
"point": "Point",
|
||||
"points": "Points",
|
||||
@@ -126,6 +137,7 @@
|
||||
"select_winner": "Select Winner",
|
||||
"select_winners": "Select Winners",
|
||||
"selected_players": "Selected players",
|
||||
"set_name": "Set name",
|
||||
"settings": "Settings",
|
||||
"single_loser": "Single Loser",
|
||||
"single_winner": "Single Winner",
|
||||
|
||||
@@ -212,6 +212,12 @@ abstract class AppLocalizations {
|
||||
/// **'Yellow'**
|
||||
String get color_yellow;
|
||||
|
||||
/// No description provided for @confirm.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Confirm'**
|
||||
String get confirm;
|
||||
|
||||
/// No description provided for @could_not_add_player.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -320,6 +326,12 @@ abstract class AppLocalizations {
|
||||
/// **'Delete Match'**
|
||||
String get delete_match;
|
||||
|
||||
/// No description provided for @delete_player.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete player?'**
|
||||
String get delete_player;
|
||||
|
||||
/// No description provided for @description.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -350,6 +362,18 @@ abstract class AppLocalizations {
|
||||
/// **'Edit Match'**
|
||||
String get edit_match;
|
||||
|
||||
/// No description provided for @edit_name.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit name'**
|
||||
String get edit_name;
|
||||
|
||||
/// No description provided for @edit_player.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit player'**
|
||||
String get edit_player;
|
||||
|
||||
/// No description provided for @enter_points.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -452,6 +476,12 @@ abstract class AppLocalizations {
|
||||
/// **'Groups'**
|
||||
String get groups;
|
||||
|
||||
/// No description provided for @groups_part_of.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Groups part of'**
|
||||
String get groups_part_of;
|
||||
|
||||
/// No description provided for @highest_score.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -554,6 +584,24 @@ abstract class AppLocalizations {
|
||||
/// **'Matches'**
|
||||
String get matches;
|
||||
|
||||
/// No description provided for @matches_part_of.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Matches part of'**
|
||||
String get matches_part_of;
|
||||
|
||||
/// No description provided for @matches_played.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Matches played'**
|
||||
String get matches_played;
|
||||
|
||||
/// No description provided for @matches_won.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Matches won'**
|
||||
String get matches_won;
|
||||
|
||||
/// No description provided for @members.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -608,6 +656,12 @@ abstract class AppLocalizations {
|
||||
/// **'No matches created yet'**
|
||||
String get no_matches_created_yet;
|
||||
|
||||
/// No description provided for @no_matches_played_yet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No games played yet'**
|
||||
String get no_matches_played_yet;
|
||||
|
||||
/// No description provided for @no_players_created_yet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -668,6 +722,12 @@ abstract class AppLocalizations {
|
||||
/// **'Not available'**
|
||||
String get not_available;
|
||||
|
||||
/// No description provided for @not_part_of_any_group.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not part of any group yet'**
|
||||
String get not_part_of_any_group;
|
||||
|
||||
/// No description provided for @place.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -692,6 +752,12 @@ abstract class AppLocalizations {
|
||||
/// **'Player name'**
|
||||
String get player_name;
|
||||
|
||||
/// No description provided for @player_profile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Player Profile'**
|
||||
String get player_profile;
|
||||
|
||||
/// No description provided for @players.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -812,6 +878,12 @@ abstract class AppLocalizations {
|
||||
/// **'Selected players'**
|
||||
String get selected_players;
|
||||
|
||||
/// No description provided for @set_name.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set name'**
|
||||
String get set_name;
|
||||
|
||||
/// No description provided for @settings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -65,6 +65,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get color_yellow => 'Gelb';
|
||||
|
||||
@override
|
||||
String get confirm => 'Bestätigen';
|
||||
|
||||
@override
|
||||
String could_not_add_player(Object playerName) {
|
||||
return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
|
||||
@@ -131,6 +134,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get delete_match => 'Spiel löschen';
|
||||
|
||||
@override
|
||||
String get delete_player => 'Spieler:in löschen';
|
||||
|
||||
@override
|
||||
String get description => 'Beschreibung';
|
||||
|
||||
@@ -146,6 +152,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get edit_match => 'Gruppe bearbeiten';
|
||||
|
||||
@override
|
||||
String get edit_name => 'Name ändern';
|
||||
|
||||
@override
|
||||
String get edit_player => 'Spieler bearbeiten';
|
||||
|
||||
@override
|
||||
String get enter_points => 'Punkte eingeben';
|
||||
|
||||
@@ -201,6 +213,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get groups => 'Gruppen';
|
||||
|
||||
@override
|
||||
String get groups_part_of => 'Gruppen Teil von';
|
||||
|
||||
@override
|
||||
String get highest_score => 'Höchste Punkte';
|
||||
|
||||
@@ -252,6 +267,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get matches => 'Spiele';
|
||||
|
||||
@override
|
||||
String get matches_part_of => 'Spiele Teil von';
|
||||
|
||||
@override
|
||||
String get matches_played => 'Spiele gespielt';
|
||||
|
||||
@override
|
||||
String get matches_won => 'Spiele gewonnen';
|
||||
|
||||
@override
|
||||
String get members => 'Mitglieder';
|
||||
|
||||
@@ -279,6 +303,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get no_matches_created_yet => 'Noch keine Spiele erstellt';
|
||||
|
||||
@override
|
||||
String get no_matches_played_yet => 'Noch kein Spiel gespielt';
|
||||
|
||||
@override
|
||||
String get no_players_created_yet => 'Noch keine Spieler:in erstellt';
|
||||
|
||||
@@ -310,6 +337,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get not_available => 'Nicht verfügbar';
|
||||
|
||||
@override
|
||||
String get not_part_of_any_group => 'Noch keiner Gruppe hinzugefügt';
|
||||
|
||||
@override
|
||||
String get place => 'Platz';
|
||||
|
||||
@@ -322,6 +352,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get player_name => 'Spieler:innenname';
|
||||
|
||||
@override
|
||||
String get player_profile => 'Spieler:in-Profil';
|
||||
|
||||
@override
|
||||
String get players => 'Spieler:innen';
|
||||
|
||||
@@ -387,6 +420,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get selected_players => 'Ausgewählte Spieler:innen';
|
||||
|
||||
@override
|
||||
String get set_name => 'Name setzen';
|
||||
|
||||
@override
|
||||
String get settings => 'Einstellungen';
|
||||
|
||||
|
||||
@@ -65,6 +65,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get color_yellow => 'Yellow';
|
||||
|
||||
@override
|
||||
String get confirm => 'Confirm';
|
||||
|
||||
@override
|
||||
String could_not_add_player(Object playerName) {
|
||||
return 'Could not add player';
|
||||
@@ -131,6 +134,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get delete_match => 'Delete Match';
|
||||
|
||||
@override
|
||||
String get delete_player => 'Delete player?';
|
||||
|
||||
@override
|
||||
String get description => 'Description';
|
||||
|
||||
@@ -146,6 +152,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get edit_match => 'Edit Match';
|
||||
|
||||
@override
|
||||
String get edit_name => 'Edit name';
|
||||
|
||||
@override
|
||||
String get edit_player => 'Edit player';
|
||||
|
||||
@override
|
||||
String get enter_points => 'Enter points';
|
||||
|
||||
@@ -201,6 +213,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get groups => 'Groups';
|
||||
|
||||
@override
|
||||
String get groups_part_of => 'Groups part of';
|
||||
|
||||
@override
|
||||
String get highest_score => 'Highest Score';
|
||||
|
||||
@@ -252,6 +267,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get matches => 'Matches';
|
||||
|
||||
@override
|
||||
String get matches_part_of => 'Matches part of';
|
||||
|
||||
@override
|
||||
String get matches_played => 'Matches played';
|
||||
|
||||
@override
|
||||
String get matches_won => 'Matches won';
|
||||
|
||||
@override
|
||||
String get members => 'Members';
|
||||
|
||||
@@ -279,6 +303,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get no_matches_created_yet => 'No matches created yet';
|
||||
|
||||
@override
|
||||
String get no_matches_played_yet => 'No games played yet';
|
||||
|
||||
@override
|
||||
String get no_players_created_yet => 'No players created yet';
|
||||
|
||||
@@ -310,6 +337,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get not_available => 'Not available';
|
||||
|
||||
@override
|
||||
String get not_part_of_any_group => 'Not part of any group yet';
|
||||
|
||||
@override
|
||||
String get place => 'place';
|
||||
|
||||
@@ -322,6 +352,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get player_name => 'Player name';
|
||||
|
||||
@override
|
||||
String get player_profile => 'Player Profile';
|
||||
|
||||
@override
|
||||
String get players => 'Players';
|
||||
|
||||
@@ -387,6 +420,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get selected_players => 'Selected players';
|
||||
|
||||
@override
|
||||
String get set_name => 'Set name';
|
||||
|
||||
@override
|
||||
String get settings => 'Settings';
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
Expanded(
|
||||
child: PlayerSelection(
|
||||
initialSelectedPlayers: initialSelectedPlayers,
|
||||
onPlayerCreated: () => widget.onMembersChanged?.call(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedPlayers = [...value];
|
||||
@@ -134,6 +135,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
if (!mounted) return;
|
||||
|
||||
if (success) {
|
||||
widget.onMembersChanged?.call();
|
||||
await HapticFeedback.successNotification();
|
||||
if (mounted) {
|
||||
Navigator.pop(context, updatedGroup);
|
||||
@@ -157,7 +159,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
final success = await db.groupDao.addGroup(
|
||||
group: Group(name: groupName, members: selectedPlayers),
|
||||
);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ class _GroupViewState extends State<GroupView> {
|
||||
);
|
||||
}
|
||||
return GroupTile(
|
||||
onPlayerChanged: loadGroups,
|
||||
group: groups[index],
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
@@ -106,13 +107,10 @@ class _GroupViewState extends State<GroupView> {
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) {
|
||||
return const CreateGroupView();
|
||||
return CreateGroupView(onMembersChanged: loadGroups);
|
||||
},
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
loadGroups();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -196,6 +196,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
child: PlayerSelection(
|
||||
key: ValueKey(selectedGroup?.id ?? 'no_group'),
|
||||
initialSelectedPlayers: selectedPlayers,
|
||||
onPlayerCreated: () => widget.onMatchesUpdated?.call(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedPlayers = value;
|
||||
|
||||
@@ -97,6 +97,7 @@ class _MatchViewState extends State<MatchView> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: MatchTile(
|
||||
onPlayerEdited: loadMatches,
|
||||
width: MediaQuery.sizeOf(context).width * 0.95,
|
||||
onTap: () async {
|
||||
Navigator.push(
|
||||
|
||||
394
lib/presentation/views/main_menu/player_detail_view.dart
Normal file
394
lib/presentation/views/main_menu/player_detail_view.dart
Normal file
@@ -0,0 +1,394 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.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/game.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:tallee/presentation/widgets/colored_icon_container.dart';
|
||||
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
|
||||
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
|
||||
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
class PlayerDetailView extends StatefulWidget {
|
||||
const PlayerDetailView({
|
||||
super.key,
|
||||
required this.player,
|
||||
required this.callback,
|
||||
});
|
||||
|
||||
/// The player to display
|
||||
final Player player;
|
||||
|
||||
final VoidCallback callback;
|
||||
|
||||
@override
|
||||
State<PlayerDetailView> createState() => _PlayerDetailViewState();
|
||||
}
|
||||
|
||||
class _PlayerDetailViewState extends State<PlayerDetailView> {
|
||||
late final AppDatabase db;
|
||||
late Player _player;
|
||||
late String playerNameCount;
|
||||
bool isLoading = true;
|
||||
|
||||
/// Total matches played by this player
|
||||
int totalMatches = 0;
|
||||
|
||||
/// Total matches won by this player
|
||||
int matchesWon = 0;
|
||||
|
||||
/// Total groups this player belongs to
|
||||
int totalGroups = 0;
|
||||
|
||||
/// Full list of groups this player belongs to
|
||||
List<Group> playerGroups = List.filled(
|
||||
4,
|
||||
Group(name: 'Skeleton group', members: []),
|
||||
);
|
||||
|
||||
/// Full list of matches this player played in
|
||||
List<Match> playerMatches = List.filled(
|
||||
4,
|
||||
Match(
|
||||
name: 'Skeleton match',
|
||||
game: Game(name: 'Game name', ruleset: Ruleset.singleWinner),
|
||||
players: [],
|
||||
),
|
||||
);
|
||||
|
||||
TextEditingController nameController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_player = widget.player;
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
playerNameCount = getNameCountText(_player);
|
||||
_loadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(loc.player_profile),
|
||||
actions: [
|
||||
HapticIconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () async {
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => CustomAlertDialog(
|
||||
title: loc.delete_player,
|
||||
content: Text(loc.this_cannot_be_undone),
|
||||
actions: [
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
text: loc.delete,
|
||||
),
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
buttonType: ButtonType.secondary,
|
||||
text: loc.cancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((confirmed) async {
|
||||
if (confirmed! && context.mounted) {
|
||||
//TODO: implement player deletion in db
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
widget.callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ListView(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 20,
|
||||
bottom: 100,
|
||||
),
|
||||
children: [
|
||||
const Center(
|
||||
child: ColoredIconContainer(
|
||||
icon: Icons.person,
|
||||
containerSize: 55,
|
||||
iconSize: 38,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_player.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
playerNameCount,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor.withAlpha(120),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(_player.createdAt)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
InfoTile(
|
||||
title: '${loc.matches_part_of} ($totalMatches)',
|
||||
icon: Icons.sports_esports,
|
||||
horizontalAlignment: CrossAxisAlignment.start,
|
||||
content: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
fixLayoutBuilder: true,
|
||||
alignment: Alignment.topLeft,
|
||||
child: playerMatches.isNotEmpty
|
||||
? Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: playerMatches.map((match) {
|
||||
return TextIconTile(
|
||||
text: match.name,
|
||||
iconEnabled: false,
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
: Text(
|
||||
loc.no_matches_played_yet,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
InfoTile(
|
||||
title: '${loc.groups_part_of} ($totalGroups)',
|
||||
icon: Icons.people,
|
||||
horizontalAlignment: CrossAxisAlignment.start,
|
||||
content: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
fixLayoutBuilder: true,
|
||||
alignment: Alignment.topLeft,
|
||||
child: playerGroups.isNotEmpty
|
||||
? Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: playerGroups.map((group) {
|
||||
return TextIconTile(
|
||||
text: group.name,
|
||||
iconEnabled: false,
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
: Text(
|
||||
loc.not_part_of_any_group,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
InfoTile(
|
||||
title: loc.statistics,
|
||||
icon: Icons.bar_chart,
|
||||
content: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
fixLayoutBuilder: true,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildStatRow(
|
||||
loc.matches_played,
|
||||
totalMatches.toString(),
|
||||
),
|
||||
_buildStatRow(loc.matches_won, matchesWon.toString()),
|
||||
_buildStatRow(
|
||||
loc.winrate,
|
||||
'${totalMatches == 0 ? 0 : ((matchesWon / totalMatches) * 100).round()}%',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
child: MainMenuButton(
|
||||
text: loc.edit_player,
|
||||
icon: Icons.edit,
|
||||
onPressed: () async {
|
||||
nameController.text = _player.name;
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
return CustomAlertDialog(
|
||||
title: loc.edit_name,
|
||||
content: TextInputField(
|
||||
controller: nameController,
|
||||
hintText: loc.set_name,
|
||||
onChanged: (_) => setDialogState(() {}),
|
||||
),
|
||||
actions: [
|
||||
CustomDialogAction(
|
||||
onPressed: isConfirmButtonEnabled()
|
||||
? () => Navigator.of(context).pop(true)
|
||||
: null,
|
||||
text: loc.confirm,
|
||||
),
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
buttonType: ButtonType.secondary,
|
||||
text: loc.cancel,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
).then((confirmed) async {
|
||||
if (confirmed! && context.mounted) {
|
||||
final newName = nameController.text.trim();
|
||||
|
||||
if (newName != _player.name) {
|
||||
final fetchedPlayerNameCount = await db.playerDao
|
||||
.getNameCount(name: newName);
|
||||
await db.playerDao.updatePlayerName(
|
||||
playerId: _player.id,
|
||||
name: newName,
|
||||
);
|
||||
widget.callback.call();
|
||||
setState(() {
|
||||
_player = Player(
|
||||
name: newName,
|
||||
createdAt: _player.createdAt,
|
||||
id: _player.id,
|
||||
nameCount: _player.nameCount,
|
||||
description: _player.description,
|
||||
);
|
||||
|
||||
// If there is already a player with the same name,
|
||||
// the count of that player is 0, so we start counting from 2 to get the correct count for this player. If there are no players with the same name, we just show the name without a count.
|
||||
playerNameCount = fetchedPlayerNameCount == 0
|
||||
? ''
|
||||
: ' #${fetchedPlayerNameCount + 1}';
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads statistics for this player
|
||||
Future<void> _loadData() async {
|
||||
isLoading = true;
|
||||
final fetchedMatches = await db.matchDao.getMatchesByPlayer(
|
||||
playerId: _player.id,
|
||||
);
|
||||
final fetchedGroups = await db.groupDao.getGroupsByPlayer(
|
||||
playerId: _player.id,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
playerMatches = fetchedMatches;
|
||||
totalMatches = fetchedMatches.length;
|
||||
matchesWon = fetchedMatches
|
||||
.where((match) => match.mvp.any((mvp) => mvp.id == _player.id))
|
||||
.length;
|
||||
playerGroups = fetchedGroups;
|
||||
totalGroups = fetchedGroups.length;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// Builds a single statistic row with a label and value
|
||||
/// - [label]: The label of the statistic
|
||||
/// - [value]: The value of the statistic
|
||||
Widget _buildStatRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool isConfirmButtonEnabled() {
|
||||
return nameController.text.trim().isNotEmpty;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,13 @@ class AppSkeleton extends StatefulWidget {
|
||||
/// - [child]: The widget tree to apply the skeleton effect to.
|
||||
/// - [enabled]: A boolean to enable or disable the skeleton effect.
|
||||
/// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher.
|
||||
/// - [alignment]: The alignment used for the custom layout builder and optional Align wrapper. Defaults to [Alignment.center].
|
||||
const AppSkeleton({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.enabled = true,
|
||||
this.fixLayoutBuilder = false,
|
||||
this.alignment = Alignment.center,
|
||||
});
|
||||
|
||||
/// The widget tree to apply the skeleton effect to.
|
||||
@@ -22,6 +24,9 @@ class AppSkeleton extends StatefulWidget {
|
||||
/// A boolean to fix the layout builder for AnimatedSwitcher.
|
||||
final bool fixLayoutBuilder;
|
||||
|
||||
/// The alignment used for the custom layout builder and optional Align wrapper
|
||||
final Alignment alignment;
|
||||
|
||||
@override
|
||||
State<AppSkeleton> createState() => _AppSkeletonState();
|
||||
}
|
||||
@@ -45,13 +50,14 @@ class _AppSkeletonState extends State<AppSkeleton> {
|
||||
layoutBuilder: !widget.fixLayoutBuilder
|
||||
? AnimatedSwitcher.defaultLayoutBuilder
|
||||
: (Widget? currentChild, List<Widget> previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [...previousChildren, ?currentChild],
|
||||
);
|
||||
final children = <Widget>[...previousChildren];
|
||||
if (currentChild != null) children.add(currentChild);
|
||||
return Stack(alignment: widget.alignment, children: children);
|
||||
},
|
||||
),
|
||||
child: widget.child,
|
||||
child: widget.fixLayoutBuilder
|
||||
? Align(alignment: widget.alignment, child: widget.child)
|
||||
: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class AnimatedDialogButton extends StatefulWidget {
|
||||
const AnimatedDialogButton({
|
||||
super.key,
|
||||
required this.buttonText,
|
||||
required this.onPressed,
|
||||
this.onPressed,
|
||||
this.buttonConstraints,
|
||||
this.buttonType = ButtonType.primary,
|
||||
this.isDescructive = false,
|
||||
@@ -19,7 +19,7 @@ class AnimatedDialogButton extends StatefulWidget {
|
||||
|
||||
final String buttonText;
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
final BoxConstraints? buttonConstraints;
|
||||
|
||||
@@ -38,8 +38,13 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
|
||||
Widget build(BuildContext context) {
|
||||
final textStyling = _getTextStyling();
|
||||
final buttonDecoration = _getButtonDecoration();
|
||||
bool isDisabled = widget.onPressed == null;
|
||||
|
||||
return GestureDetector(
|
||||
return IgnorePointer(
|
||||
ignoring: isDisabled,
|
||||
child: Opacity(
|
||||
opacity: isDisabled ? 0.5 : 1.0,
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => setState(() => _isPressed = true),
|
||||
onTapUp: (_) => setState(() => _isPressed = false),
|
||||
onTapCancel: () => setState(() => _isPressed = false),
|
||||
@@ -54,7 +59,10 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
|
||||
child: Container(
|
||||
constraints: widget.buttonConstraints,
|
||||
decoration: buttonDecoration,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
widget.buttonText,
|
||||
@@ -65,6 +73,8 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget content;
|
||||
final List<CustomDialogAction> actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
|
||||
@@ -10,7 +10,7 @@ class CustomDialogAction extends StatelessWidget {
|
||||
/// - [onPressed]: Callback function that is triggered when the button is pressed.
|
||||
const CustomDialogAction({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
this.onPressed,
|
||||
required this.text,
|
||||
this.buttonType = ButtonType.primary,
|
||||
this.isDestructive = false,
|
||||
@@ -20,17 +20,18 @@ class CustomDialogAction extends StatelessWidget {
|
||||
|
||||
final ButtonType buttonType;
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
final bool isDestructive;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogButton(
|
||||
onPressed: () async {
|
||||
onPressed: onPressed != null
|
||||
? () async {
|
||||
await HapticFeedback.selectionClick();
|
||||
onPressed.call();
|
||||
},
|
||||
onPressed?.call();
|
||||
}
|
||||
: null,
|
||||
buttonText: text,
|
||||
buttonType: buttonType,
|
||||
isDescructive: isDestructive,
|
||||
|
||||
@@ -26,6 +26,7 @@ class PlayerSelection extends StatefulWidget {
|
||||
this.availablePlayers,
|
||||
this.initialSelectedPlayers,
|
||||
required this.onChanged,
|
||||
this.onPlayerCreated,
|
||||
});
|
||||
|
||||
/// An optional list of players to choose from. If null, all players from the database are used.
|
||||
@@ -37,6 +38,9 @@ class PlayerSelection extends StatefulWidget {
|
||||
/// A callback function that is invoked whenever the selection changes,
|
||||
final Function(List<Player> value) onChanged;
|
||||
|
||||
/// A callback function that is invoked when a player was created in this widget
|
||||
final VoidCallback? onPlayerCreated;
|
||||
|
||||
@override
|
||||
State<PlayerSelection> createState() => _PlayerSelectionState();
|
||||
}
|
||||
@@ -323,6 +327,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
|
||||
/// Updates the state after successfully adding a new player.
|
||||
void _handleSuccessfulPlayerCreation(Player player) {
|
||||
widget.onPlayerCreated?.call();
|
||||
selectedPlayers.insert(0, player);
|
||||
widget.onChanged([...selectedPlayers]);
|
||||
allPlayers.add(player);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/data/models/group.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
class GroupTile extends StatefulWidget {
|
||||
@@ -15,6 +17,7 @@ class GroupTile extends StatefulWidget {
|
||||
required this.group,
|
||||
this.isHighlighted = false,
|
||||
this.onTap,
|
||||
this.onPlayerChanged,
|
||||
});
|
||||
|
||||
/// The group data to be displayed.
|
||||
@@ -26,6 +29,9 @@ class GroupTile extends StatefulWidget {
|
||||
/// Callback function to be executed when the tile is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Callback function to be executed when the players in the group are changed.
|
||||
final VoidCallback? onPlayerChanged;
|
||||
|
||||
@override
|
||||
State<GroupTile> createState() => _GroupTileState();
|
||||
}
|
||||
@@ -92,6 +98,19 @@ class _GroupTileState extends State<GroupTile> {
|
||||
text: member.name,
|
||||
suffixText: getNameCountText(member),
|
||||
iconEnabled: false,
|
||||
onTileTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) => PlayerDetailView(
|
||||
player: member,
|
||||
callback: () {
|
||||
widget.onPlayerChanged?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,11 +3,13 @@ import 'dart:core' hide Match;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.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/models/match.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
|
||||
import 'package:tallee/presentation/widgets/game_label.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
@@ -24,6 +26,7 @@ class MatchTile extends StatefulWidget {
|
||||
required this.onTap,
|
||||
this.width,
|
||||
this.compact = false,
|
||||
this.onPlayerEdited,
|
||||
});
|
||||
|
||||
/// The match data to be displayed.
|
||||
@@ -32,6 +35,9 @@ class MatchTile extends StatefulWidget {
|
||||
/// The callback invoked when the tile is tapped.
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// The callback invoked when the players are edited
|
||||
final VoidCallback? onPlayerEdited;
|
||||
|
||||
/// Optional width for the tile.
|
||||
final double? width;
|
||||
|
||||
@@ -224,6 +230,19 @@ class _MatchTileState extends State<MatchTile> {
|
||||
text: player.name,
|
||||
suffixText: getNameCountText(player),
|
||||
iconEnabled: false,
|
||||
onTileTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) => PlayerDetailView(
|
||||
player: player,
|
||||
callback: () {
|
||||
widget.onPlayerEdited?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
@@ -12,6 +12,7 @@ class TextIconTile extends StatelessWidget {
|
||||
this.suffixText = '',
|
||||
this.iconEnabled = true,
|
||||
this.onIconTap,
|
||||
this.onTileTap,
|
||||
});
|
||||
|
||||
/// The text to display in the tile.
|
||||
@@ -25,9 +26,14 @@ class TextIconTile extends StatelessWidget {
|
||||
/// The callback to be invoked when the icon is tapped.
|
||||
final VoidCallback? onIconTap;
|
||||
|
||||
/// The callback to be invoked when the tile is tapped.
|
||||
final VoidCallback? onTileTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
return GestureDetector(
|
||||
onTap: onTileTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.onBoxColor,
|
||||
@@ -72,6 +78,7 @@ class TextIconTile extends StatelessWidget {
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +194,31 @@ void main() {
|
||||
expect(allGroups, isEmpty);
|
||||
});
|
||||
|
||||
test('getGroupsByPlayer() works correctly', () async {
|
||||
await database.groupDao.addGroupsAsList(
|
||||
groups: [testGroup1, testGroup2],
|
||||
);
|
||||
|
||||
final groups = await database.groupDao.getGroupsByPlayer(
|
||||
playerId: testPlayer2.id,
|
||||
);
|
||||
|
||||
expect(groups, hasLength(2));
|
||||
expect(groups.any((group) => group.id == testGroup1.id), isTrue);
|
||||
expect(groups.any((group) => group.id == testGroup2.id), isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'getGroupsByPlayer() returns empty list for non-existent player',
|
||||
() async {
|
||||
final groups = await database.groupDao.getGroupsByPlayer(
|
||||
playerId: 'non-existent-player-id',
|
||||
);
|
||||
|
||||
expect(groups, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('addGroupsAsList() with duplicate groups only adds once', () async {
|
||||
await database.groupDao.addGroupsAsList(
|
||||
groups: [testGroup1, testGroup1, testGroup1],
|
||||
|
||||
@@ -260,6 +260,34 @@ void main() {
|
||||
expect(match.group!.id, testGroup1.id);
|
||||
});
|
||||
|
||||
test('getMatchesByPlayer() works correctly', () async {
|
||||
await database.matchDao.addMatchesAsList(
|
||||
matches: [testMatch1, testMatch2],
|
||||
);
|
||||
|
||||
final matches = await database.matchDao.getMatchesByPlayer(
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
|
||||
expect(matches, hasLength(1));
|
||||
expect(matches.first.id, testMatch2.id);
|
||||
expect(
|
||||
matches.first.players.any((p) => p.id == testPlayer1.id),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'getMatchesByPlayer() returns empty list for non-existent player',
|
||||
() async {
|
||||
final matches = await database.matchDao.getMatchesByPlayer(
|
||||
playerId: 'non-existing-player-id',
|
||||
);
|
||||
|
||||
expect(matches, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('getMatchCount() works correctly', () async {
|
||||
var count = await database.matchDao.getMatchCount();
|
||||
expect(count, 0);
|
||||
|
||||
@@ -233,6 +233,95 @@ void main() {
|
||||
expect(allPlayers, isEmpty);
|
||||
});
|
||||
|
||||
test('updatePlayerName() sets correct nameCount with 2 player', () async {
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
await database.playerDao.addPlayer(player: testPlayer2);
|
||||
|
||||
final newName = testPlayer1.name;
|
||||
await database.playerDao.updatePlayerName(
|
||||
playerId: testPlayer2.id,
|
||||
name: newName,
|
||||
);
|
||||
|
||||
var player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
expect(player.nameCount, 1);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer2.id,
|
||||
);
|
||||
expect(player.nameCount, 2);
|
||||
|
||||
await database.playerDao.updatePlayerName(
|
||||
playerId: testPlayer1.id,
|
||||
name: 'different name',
|
||||
);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
expect(player.nameCount, 0);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer2.id,
|
||||
);
|
||||
expect(player.nameCount, 0);
|
||||
});
|
||||
|
||||
test('updatePlayerName() sets correct nameCount with 3 player', () async {
|
||||
await database.playerDao.addPlayersAsList(
|
||||
players: [testPlayer1, testPlayer2, testPlayer3],
|
||||
);
|
||||
|
||||
// Changing both names to player 1's name
|
||||
final newName = testPlayer1.name;
|
||||
await database.playerDao.updatePlayerName(
|
||||
playerId: testPlayer2.id,
|
||||
name: newName,
|
||||
);
|
||||
await database.playerDao.updatePlayerName(
|
||||
playerId: testPlayer3.id,
|
||||
name: newName,
|
||||
);
|
||||
|
||||
var player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
expect(player.nameCount, 1);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer2.id,
|
||||
);
|
||||
expect(player.nameCount, 2);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer3.id,
|
||||
);
|
||||
expect(player.nameCount, 3);
|
||||
|
||||
// Changing the middle players name
|
||||
await database.playerDao.updatePlayerName(
|
||||
playerId: testPlayer2.id,
|
||||
name: 'different name',
|
||||
);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
expect(player.nameCount, 1);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer2.id,
|
||||
);
|
||||
expect(player.nameCount, 0);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer3.id,
|
||||
);
|
||||
expect(player.nameCount, 2);
|
||||
});
|
||||
|
||||
test('updatePlayerDescription() works correctly', () async {
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
|
||||
@@ -372,14 +461,22 @@ void main() {
|
||||
final player1 = Player(name: testPlayer1.name, description: '');
|
||||
await database.playerDao.addPlayer(player: player1);
|
||||
|
||||
final player2 = Player(name: testPlayer1.name, description: '');
|
||||
await database.playerDao.addPlayer(player: player2);
|
||||
|
||||
var players = await database.playerDao.getAllPlayers();
|
||||
|
||||
expect(players.length, 2);
|
||||
expect(players.length, 3);
|
||||
players.sort((a, b) => a.nameCount.compareTo(b.nameCount));
|
||||
|
||||
for (int i = 0; i < players.length - 1; i++) {
|
||||
expect(players[i].nameCount, i + 1);
|
||||
}
|
||||
|
||||
// ids are correct in the right order
|
||||
expect(players[0].id, testPlayer1.id);
|
||||
expect(players[1].id, player1.id);
|
||||
expect(players[2].id, player2.id);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -404,24 +501,62 @@ void main() {
|
||||
for (int i = 0; i < players.length - 1; i++) {
|
||||
expect(players[i].nameCount, i + 1);
|
||||
}
|
||||
|
||||
// ids are correct in the right order
|
||||
expect(players[0].id, testPlayer1.id);
|
||||
expect(players[1].id, player1.id);
|
||||
expect(players[2].id, player2.id);
|
||||
expect(players[3].id, player3.id);
|
||||
},
|
||||
);
|
||||
|
||||
test('getNameCount works correctly', () async {
|
||||
final player1 = Player(name: testPlayer1.name);
|
||||
final player2 = Player(name: testPlayer1.name);
|
||||
final player3 = Player(name: testPlayer1.name);
|
||||
|
||||
await database.playerDao.addPlayersAsList(
|
||||
players: [testPlayer1, player2, player3],
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
|
||||
var nameCount = await database.playerDao.getNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
|
||||
final nameCount = await database.playerDao.getNameCount(
|
||||
expect(nameCount, 1);
|
||||
|
||||
await database.playerDao.addPlayersAsList(players: [player1, player2]);
|
||||
|
||||
nameCount = await database.playerDao.getNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
|
||||
expect(nameCount, 3);
|
||||
});
|
||||
|
||||
test('calculateNameCount works correctly', () async {
|
||||
final player1 = Player(name: testPlayer1.name);
|
||||
final player2 = Player(name: testPlayer1.name);
|
||||
|
||||
// Case 1: No existing players with the name
|
||||
var nameCount = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(nameCount, 0);
|
||||
|
||||
// Case 2: One existing player with the name. Should return 2 for
|
||||
// the new player
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
nameCount = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(nameCount, 2);
|
||||
|
||||
// Case 3: Multiple existing players with the name. Should return count + 1
|
||||
await database.playerDao.addPlayersAsList(players: [player1, player2]);
|
||||
nameCount = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(nameCount, 4);
|
||||
});
|
||||
|
||||
test('updateNameCount works correctly', () async {
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
|
||||
@@ -441,14 +576,24 @@ void main() {
|
||||
final player2 = Player(name: testPlayer1.name, description: '');
|
||||
final player3 = Player(name: testPlayer1.name, description: '');
|
||||
|
||||
await database.playerDao.addPlayersAsList(
|
||||
players: [testPlayer1, player2, player3],
|
||||
);
|
||||
|
||||
final player = await database.playerDao.getPlayerWithHighestNameCount(
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
var player = await database.playerDao.getPlayerWithHighestNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(player, isNotNull);
|
||||
expect(player!.nameCount, 0);
|
||||
|
||||
await database.playerDao.addPlayer(player: player2);
|
||||
player = await database.playerDao.getPlayerWithHighestNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(player, isNotNull);
|
||||
expect(player!.nameCount, 2);
|
||||
|
||||
await database.playerDao.addPlayer(player: player3);
|
||||
player = await database.playerDao.getPlayerWithHighestNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(player, isNotNull);
|
||||
expect(player!.nameCount, 3);
|
||||
});
|
||||
@@ -460,32 +605,6 @@ void main() {
|
||||
expect(player, isNull);
|
||||
});
|
||||
|
||||
test('calculateNameCount works correctly', () async {
|
||||
// Case 1: No existing players with the name
|
||||
var count = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(count, 0);
|
||||
|
||||
// Case 2: One existing player with the name. Should update that
|
||||
// player's nameCount to 1 and return 2 for the new player
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
|
||||
count = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(count, 2);
|
||||
|
||||
// Case 3: Multiple existing players with the name.
|
||||
final player2 = Player(name: testPlayer1.name, nameCount: count);
|
||||
await database.playerDao.addPlayer(player: player2);
|
||||
|
||||
count = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(count, 3);
|
||||
});
|
||||
|
||||
test('getPlayerWithHighestNameCount with non existing player', () async {
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
await database.playerDao.initializeNameCount(name: testPlayer1.name);
|
||||
|
||||
Reference in New Issue
Block a user