16 Commits

Author SHA1 Message Date
13b3d2cad9 Merge branch 'development' into bug/195-datenbank-onDelete-ueberpruefen
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 45s
Pull Request Pipeline / test (pull_request) Successful in 47s
Pull Request Pipeline / localizations (pull_request) Successful in 25s
2026-05-23 16:15:00 +02:00
4c77eaa8c9 Merge branch 'development' into bug/195-datenbank-onDelete-ueberpruefen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 45s
Pull Request Pipeline / lint (pull_request) Successful in 53s
2026-05-21 19:55:41 +00:00
gelbeinhalb
e6e065ed44 deleted is not required anymore
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 47s
Pull Request Pipeline / lint (pull_request) Successful in 55s
2026-05-12 21:43:36 +02:00
gelbeinhalb
c674c8e248 regenerate database.g.dart 2026-05-12 21:43:09 +02:00
gelbeinhalb
a6e0971208 deleted teams get ignored when includeDeleted is false
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 46s
Pull Request Pipeline / lint (pull_request) Successful in 54s
2026-05-12 20:45:12 +02:00
gelbeinhalb
b1934e64bd deleted players get ignored when includeDeleted is false 2026-05-12 20:43:28 +02:00
gelbeinhalb
0870f418d5 deleted matches get ignored when includeDeleted is false
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 46s
Pull Request Pipeline / lint (pull_request) Successful in 54s
2026-05-12 20:35:09 +02:00
gelbeinhalb
8f4254748b deleted groups get ignored when includeDeleted is false 2026-05-12 20:33:02 +02:00
gelbeinhalb
36ad2ab446 deleted games get ignored when includeDeleted is false 2026-05-12 20:30:46 +02:00
gelbeinhalb
b3b3def07a add deleted to schema 2026-05-12 20:23:41 +02:00
gelbeinhalb
d3c9827529 remove useless deleted attributes 2026-05-12 20:20:40 +02:00
gelbeinhalb
81b73beeef Merge remote-tracking branch 'origin/development' into bug/195-datenbank-onDelete-ueberpruefen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 53s
# Conflicts:
#	assets/schema.json
#	lib/data/db/tables/player_match_table.dart
#	lib/data/models/game.dart
2026-05-12 20:19:50 +02:00
gelbeinhalb
421fe1a2bf add deleted to schema
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 43s
Pull Request Pipeline / lint (pull_request) Successful in 50s
2026-05-05 10:35:04 +02:00
gelbeinhalb
66e657235a add deleted attribute
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 45s
Pull Request Pipeline / lint (pull_request) Successful in 46s
2026-04-30 11:56:15 +02:00
gelbeinhalb
1ec1df3514 Merge remote-tracking branch 'origin/development' into bug/195-datenbank-onDelete-ueberpruefen
# Conflicts:
#	lib/data/db/tables/player_table.dart
2026-04-30 11:20:47 +02:00
gelbeinhalb
8bd53a69c3 add soft delete parameter
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 44s
Pull Request Pipeline / lint (pull_request) Successful in 47s
2026-04-19 12:39:14 +02:00
38 changed files with 772 additions and 1154 deletions

View File

@@ -18,6 +18,9 @@
}, },
"description": { "description": {
"type": "string" "type": "string"
},
"deleted": {
"type": "boolean"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -54,6 +57,9 @@
}, },
"icon": { "icon": {
"type": "string" "type": "string"
},
"deleted": {
"type": "boolean"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -90,6 +96,9 @@
"items": { "items": {
"type": "string" "type": "string"
} }
},
"deleted": {
"type": "boolean"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -168,6 +177,9 @@
}, },
"teams": { "teams": {
"type": ["array", "null"] "type": ["array", "null"]
},
"deleted": {
"type": "boolean"
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View File

@@ -65,25 +65,40 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/* Read */ /* Read */
/// Retrieves the total count of games in the database. /// Retrieves the total count of games in the database.
Future<int> getGameCount() async { /// By default, only returns non-deleted games.
final count = Future<int> getGameCount({bool includeDeleted = false}) async {
await (selectOnly(gameTable)..addColumns([gameTable.id.count()])) final query = selectOnly(gameTable)..addColumns([gameTable.id.count()]);
if (!includeDeleted) {
query.where(gameTable.deleted.equals(false));
}
final count = await query
.map((row) => row.read(gameTable.id.count())) .map((row) => row.read(gameTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
/// Checks if a game with the given [gameId] exists in the database. /// Checks if a game with the given [gameId] exists in the database.
/// By default, only returns non-deleted games.
/// Returns `true` if the game exists, `false` otherwise. /// Returns `true` if the game exists, `false` otherwise.
Future<bool> gameExists({required String gameId}) async { Future<bool> gameExists({
required String gameId,
bool includeDeleted = false,
}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId)); final query = select(gameTable)..where((g) => g.id.equals(gameId));
if (!includeDeleted) {
query.where((g) => g.deleted.equals(false));
}
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return result != null; return result != null;
} }
/// Retrieves all games from the database. /// Retrieves all games from the database.
Future<List<Game>> getAllGames() async { /// By default, only returns non-deleted games.
Future<List<Game>> getAllGames({bool includeDeleted = false}) async {
final query = select(gameTable); final query = select(gameTable);
if (!includeDeleted) {
query.where((g) => g.deleted.equals(false));
}
final result = await query.get(); final result = await query.get();
return result return result
.map( .map(
@@ -101,8 +116,15 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
} }
/// Retrieves a [Game] by its [gameId]. /// Retrieves a [Game] by its [gameId].
Future<Game> getGameById({required String gameId}) async { /// By default, only returns non-deleted games.
Future<Game> getGameById({
required String gameId,
bool includeDeleted = false,
}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId)); final query = select(gameTable)..where((g) => g.id.equals(gameId));
if (!includeDeleted) {
query.where((g) => g.deleted.equals(false));
}
final result = await query.getSingle(); final result = await query.getSingle();
return Game( return Game(
id: result.id, id: result.id,
@@ -196,9 +218,10 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
} }
/// Retrieves all games with their respective match counts. /// Retrieves all games with their respective match counts.
/// By default, only returns non-deleted games.
/// Returns a list of tuples (Game, matchCount). /// Returns a list of tuples (Game, matchCount).
Future<List<(Game, int)>> getGameUsage() async { Future<List<(Game, int)>> getGameUsage({bool includeDeleted = false}) async {
final games = await getAllGames(); final games = await getAllGames(includeDeleted: includeDeleted);
final results = <(Game, int)>[]; final results = <(Game, int)>[];

View File

@@ -139,8 +139,12 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
/* Read */ /* Read */
/// Retrieves all groups from the database. /// Retrieves all groups from the database.
Future<List<Group>> getAllGroups() async { /// By default, only returns non-deleted groups.
Future<List<Group>> getAllGroups({bool includeDeleted = false}) async {
final query = select(groupTable); final query = select(groupTable);
if (!includeDeleted) {
query.where((g) => g.deleted.equals(false));
}
final result = await query.get(); final result = await query.get();
return Future.wait( return Future.wait(
result.map((groupData) async { result.map((groupData) async {
@@ -159,8 +163,15 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
} }
/// Retrieves a [Group] by its [groupId], including its members. /// Retrieves a [Group] by its [groupId], including its members.
Future<Group> getGroupById({required String groupId}) async { /// By default, only returns non-deleted groups.
Future<Group> getGroupById({
required String groupId,
bool includeDeleted = false,
}) async {
final query = select(groupTable)..where((g) => g.id.equals(groupId)); final query = select(groupTable)..where((g) => g.id.equals(groupId));
if (!includeDeleted) {
query.where((g) => g.deleted.equals(false));
}
final result = await query.getSingle(); final result = await query.getSingle();
List<Player> members = await db.playerGroupDao.getPlayersOfGroup( List<Player> members = await db.playerGroupDao.getPlayersOfGroup(
@@ -177,50 +188,29 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
} }
/// Retrieves the number of groups in the database. /// Retrieves the number of groups in the database.
Future<int> getGroupCount() async { /// By default, only returns non-deleted groups.
final count = Future<int> getGroupCount({bool includeDeleted = false}) async {
await (selectOnly(groupTable)..addColumns([groupTable.id.count()])) final query = selectOnly(groupTable)..addColumns([groupTable.id.count()]);
if (!includeDeleted) {
query.where(groupTable.deleted.equals(false));
}
final count = await query
.map((row) => row.read(groupTable.id.count())) .map((row) => row.read(groupTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; 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. /// Checks if a group with the given [groupId] exists in the database.
/// By default, only returns non-deleted groups.
/// Returns `true` if the group exists, `false` otherwise. /// Returns `true` if the group exists, `false` otherwise.
Future<bool> groupExists({required String groupId}) async { Future<bool> groupExists({
required String groupId,
bool includeDeleted = false,
}) async {
final query = select(groupTable)..where((g) => g.id.equals(groupId)); final query = select(groupTable)..where((g) => g.id.equals(groupId));
if (!includeDeleted) {
query.where((g) => g.deleted.equals(false));
}
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return result != null; return result != null;
} }

View File

@@ -255,25 +255,40 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/* Read */ /* Read */
/// Checks if a match with the given [matchId] exists in the database. /// Checks if a match with the given [matchId] exists in the database.
/// By default, only returns non-deleted matches.
/// Returns `true` if the match exists, otherwise `false`. /// Returns `true` if the match exists, otherwise `false`.
Future<bool> matchExists({required String matchId}) async { Future<bool> matchExists({
required String matchId,
bool includeDeleted = false,
}) async {
final query = select(matchTable)..where((g) => g.id.equals(matchId)); final query = select(matchTable)..where((g) => g.id.equals(matchId));
if (!includeDeleted) {
query.where((g) => g.deleted.equals(false));
}
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return result != null; return result != null;
} }
/// Retrieves the number of matches in the database. /// Retrieves the number of matches in the database.
Future<int> getMatchCount() async { /// By default, only returns non-deleted matches.
final count = Future<int> getMatchCount({bool includeDeleted = false}) async {
await (selectOnly(matchTable)..addColumns([matchTable.id.count()])) final query = selectOnly(matchTable)..addColumns([matchTable.id.count()]);
if (!includeDeleted) {
query.where(matchTable.deleted.equals(false));
}
final count = await query
.map((row) => row.read(matchTable.id.count())) .map((row) => row.read(matchTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
/// Retrieves all matches from the database. /// Retrieves all matches from the database.
Future<List<Match>> getAllMatches() async { /// By default, only returns non-deleted matches.
Future<List<Match>> getAllMatches({bool includeDeleted = false}) async {
final query = select(matchTable); final query = select(matchTable);
if (!includeDeleted) {
query.where((m) => m.deleted.equals(false));
}
final result = await query.get(); final result = await query.get();
return Future.wait( return Future.wait(
@@ -310,8 +325,15 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
} }
/// Retrieves a [Match] by its [matchId]. /// Retrieves a [Match] by its [matchId].
Future<Match> getMatchById({required String matchId}) async { /// By default, only returns non-deleted matches.
Future<Match> getMatchById({
required String matchId,
bool includeDeleted = false,
}) async {
final query = select(matchTable)..where((g) => g.id.equals(matchId)); final query = select(matchTable)..where((g) => g.id.equals(matchId));
if (!includeDeleted) {
query.where((g) => g.deleted.equals(false));
}
final result = await query.getSingle(); final result = await query.getSingle();
final game = await db.gameDao.getGameById(gameId: result.gameId); final game = await db.gameDao.getGameById(gameId: result.gameId);
@@ -342,67 +364,34 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
} }
/// Retrieves the number of matches associated with a specific game. /// Retrieves the number of matches associated with a specific game.
Future<int> getMatchCountByGame({required String gameId}) async { /// By default, only returns non-deleted matches.
final count = Future<int> getMatchCountByGame({
await (selectOnly(matchTable) required String gameId,
..where(matchTable.gameId.equals(gameId)) bool includeDeleted = false,
..addColumns([matchTable.id.count()])) }) async {
final query = selectOnly(matchTable)
..where(matchTable.gameId.equals(gameId));
if (!includeDeleted) {
query.where(matchTable.deleted.equals(false));
}
query.addColumns([matchTable.id.count()]);
final count = await query
.map((row) => row.read(matchTable.id.count())) .map((row) => row.read(matchTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; 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]. /// Retrieves all matches associated with the given [groupId].
/// By default, only returns non-deleted matches.
/// Queries the database directly, filtering by [groupId]. /// Queries the database directly, filtering by [groupId].
Future<List<Match>> getMatchesByGroup({required String groupId}) async { Future<List<Match>> getMatchesByGroup({
required String groupId,
bool includeDeleted = false,
}) async {
final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); final query = select(matchTable)..where((m) => m.groupId.equals(groupId));
if (!includeDeleted) {
query.where((m) => m.deleted.equals(false));
}
final rows = await query.get(); final rows = await query.get();
return Future.wait( return Future.wait(

View File

@@ -17,7 +17,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// the new one. /// the new one.
Future<bool> addPlayer({required Player player}) async { Future<bool> addPlayer({required Player player}) async {
if (!await playerExists(playerId: player.id)) { if (!await playerExists(playerId: player.id)) {
final int nameCount = await _processNameCount(name: player.name); final int nameCount = await calculateNameCount(name: player.name);
await into(playerTable).insert( await into(playerTable).insert(
PlayerTableCompanion.insert( PlayerTableCompanion.insert(
@@ -64,7 +64,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
final playersWithName = entry.value; final playersWithName = entry.value;
// Get the current nameCount // Get the current nameCount
var nameCount = await _processNameCount(name: name); var nameCount = await calculateNameCount(name: name);
// One player with the same name // One player with the same name
if (playersWithName.length == 1) { if (playersWithName.length == 1) {
@@ -110,25 +110,40 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/* Read */ /* Read */
/// Retrieves the total count of players in the database. /// Retrieves the total count of players in the database.
Future<int> getPlayerCount() async { /// By default, only returns non-deleted players.
final count = Future<int> getPlayerCount({bool includeDeleted = false}) async {
await (selectOnly(playerTable)..addColumns([playerTable.id.count()])) final query = selectOnly(playerTable)..addColumns([playerTable.id.count()]);
if (!includeDeleted) {
query.where(playerTable.deleted.equals(false));
}
final count = await query
.map((row) => row.read(playerTable.id.count())) .map((row) => row.read(playerTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
/// Checks if a player with the given [playerId] exists in the database. /// Checks if a player with the given [playerId] exists in the database.
/// By default, only returns non-deleted players.
/// Returns `true` if the player exists, `false` otherwise. /// Returns `true` if the player exists, `false` otherwise.
Future<bool> playerExists({required String playerId}) async { Future<bool> playerExists({
required String playerId,
bool includeDeleted = false,
}) async {
final query = select(playerTable)..where((p) => p.id.equals(playerId)); final query = select(playerTable)..where((p) => p.id.equals(playerId));
if (!includeDeleted) {
query.where((p) => p.deleted.equals(false));
}
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return result != null; return result != null;
} }
/// Retrieves all players from the database. /// Retrieves all players from the database.
Future<List<Player>> getAllPlayers() async { /// By default, only returns non-deleted players.
Future<List<Player>> getAllPlayers({bool includeDeleted = false}) async {
final query = select(playerTable); final query = select(playerTable);
if (!includeDeleted) {
query.where((p) => p.deleted.equals(false));
}
final result = await query.get(); final result = await query.get();
return result return result
.map( .map(
@@ -144,8 +159,15 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
} }
/// Retrieves a [Player] by their [id]. /// Retrieves a [Player] by their [id].
Future<Player> getPlayerById({required String playerId}) async { /// By default, only returns non-deleted players.
Future<Player> getPlayerById({
required String playerId,
bool includeDeleted = false,
}) async {
final query = select(playerTable)..where((p) => p.id.equals(playerId)); final query = select(playerTable)..where((p) => p.id.equals(playerId));
if (!includeDeleted) {
query.where((p) => p.deleted.equals(false));
}
final result = await query.getSingle(); final result = await query.getSingle();
return Player( return Player(
id: result.id, id: result.id,
@@ -159,63 +181,44 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/* Update */ /* Update */
/// Updates the name of the player with the given [playerId] to [name]. /// 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({ Future<bool> updatePlayerName({
required String playerId, required String playerId,
required String name, required String name,
}) async { }) async {
return transaction(() async { // Get previous name and name count for the player before updating
final previousPlayer = await (select( final previousPlayerName =
playerTable, await (select(playerTable)..where((p) => p.id.equals(playerId)))
)..where((p) => p.id.equals(playerId))).getSingleOrNull(); .map((row) => row.name)
if (previousPlayer == null) return false; .getSingleOrNull() ??
'';
final previousName = previousPlayer.name; final previousNameCount = await getNameCount(name: previousPlayerName);
final previousCount = previousPlayer.nameCount;
// Determine the nameCount for the renamed player in the new group.
final newNameCount = await _processNameCount(name: name);
final rowsAffected = final rowsAffected =
await (update( await (update(playerTable)..where((p) => p.id.equals(playerId))).write(
playerTable, PlayerTableCompanion(name: Value(name)),
)..where((p) => p.id.equals(playerId))).write(
PlayerTableCompanion(
name: Value(name),
nameCount: Value(newNameCount),
),
); );
// Consolidate the previous name group. // Update name count for the new name
final remainingCount = await getNameCount(name: previousName); final count = await calculateNameCount(name: name);
if (count > 0) {
if (remainingCount == 1) { await (update(playerTable)..where((p) => p.name.equals(name))).write(
// Only one player left PlayerTableCompanion(nameCount: Value(count)),
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; return rowsAffected > 0;
});
} }
/// Updates the description of the player with the given [playerId] to /// Updates the description of the player with the given [playerId] to
@@ -245,10 +248,15 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/* Name count management */ /* Name count management */
/// Retrieves the count of players with the given [name]. /// Retrieves the count of players with the given [name].
/// Returns the highest name count if players with the same name exist, /// By default, only returns non-deleted players.
/// otherwise `null`. Future<int> getNameCount({
Future<int> getNameCount({required String name}) async { required String name,
bool includeDeleted = false,
}) async {
final query = select(playerTable)..where((p) => p.name.equals(name)); final query = select(playerTable)..where((p) => p.name.equals(name));
if (!includeDeleted) {
query.where((p) => p.deleted.equals(false));
}
final result = await query.get(); final result = await query.get();
return result.length; return result.length;
} }
@@ -267,11 +275,17 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
} }
@visibleForTesting @visibleForTesting
Future<Player?> getPlayerWithHighestNameCount({required String name}) async { Future<Player?> getPlayerWithHighestNameCount({
required String name,
bool includeDeleted = false,
}) async {
final query = select(playerTable) final query = select(playerTable)
..where((p) => p.name.equals(name)) ..where((p) => p.name.equals(name))
..orderBy([(p) => OrderingTerm.desc(p.nameCount)]) ..orderBy([(p) => OrderingTerm.desc(p.nameCount)])
..limit(1); ..limit(1);
if (!includeDeleted) {
query.where((p) => p.deleted.equals(false));
}
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
if (result != null) { if (result != null) {
return Player( return Player(
@@ -285,39 +299,25 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
return null; 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 @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 { Future<int> calculateNameCount({required String name}) async {
final count = await getNameCount(name: name); final count = await getNameCount(name: name);
final int nameCount; final int nameCount;
if (count == 0) { if (count == 1) {
// If no other players exist with the same name, the returned nameCount is 0 // If one other player exists with the same name, initialize the nameCount
nameCount = 0; await initializeNameCount(name: name);
} else if (count == 1) { // And for the new player, set nameCount to 2
// If one other player with the name count exists, the returned name count is 2
nameCount = 2; nameCount = 2;
} else { } else if (count > 1) {
// If more than one player exists with the same name, just increment // If more than one player exists with the same name, just increment
// the nameCount for the new player // the nameCount for the new player
nameCount = count + 1; nameCount = count + 1;
} else {
// If no other players exist with the same name, set nameCount to 0
nameCount = 0;
} }
return nameCount; return nameCount;
} }

View File

@@ -83,25 +83,40 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
/* Read */ /* Read */
/// Retrieves the total count of teams in the database. /// Retrieves the total count of teams in the database.
Future<int> getTeamCount() async { /// By default, only returns non-deleted players.
final count = Future<int> getTeamCount({bool includeDeleted = false}) async {
await (selectOnly(teamTable)..addColumns([teamTable.id.count()])) final query = selectOnly(teamTable)..addColumns([teamTable.id.count()]);
if (!includeDeleted) {
query.where(teamTable.deleted.equals(false));
}
final count = await query
.map((row) => row.read(teamTable.id.count())) .map((row) => row.read(teamTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
/// Checks if a team with the given [teamId] exists in the database. /// Checks if a team with the given [teamId] exists in the database.
/// By default, only returns non-deleted players.
/// Returns `true` if the team exists, `false` otherwise. /// Returns `true` if the team exists, `false` otherwise.
Future<bool> teamExists({required String teamId}) async { Future<bool> teamExists({
required String teamId,
bool includeDeleted = false,
}) async {
final query = select(teamTable)..where((t) => t.id.equals(teamId)); final query = select(teamTable)..where((t) => t.id.equals(teamId));
if (!includeDeleted) {
query.where((t) => t.deleted.equals(false));
}
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return result != null; return result != null;
} }
/// Retrieves all teams from the database. /// Retrieves all teams from the database.
Future<List<Team>> getAllTeams() async { /// By default, only returns non-deleted players.
Future<List<Team>> getAllTeams({bool includeDeleted = false}) async {
final query = select(teamTable); final query = select(teamTable);
if (!includeDeleted) {
query.where((t) => t.deleted.equals(false));
}
final result = await query.get(); final result = await query.get();
return Future.wait( return Future.wait(
result.map((row) async { result.map((row) async {
@@ -117,8 +132,15 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
} }
/// Retrieves a [Team] by its [teamId], including its members. /// Retrieves a [Team] by its [teamId], including its members.
Future<Team> getTeamById({required String teamId}) async { /// By default, only returns non-deleted players.
Future<Team> getTeamById({
required String teamId,
bool includeDeleted = false,
}) async {
final query = select(teamTable)..where((t) => t.id.equals(teamId)); final query = select(teamTable)..where((t) => t.id.equals(teamId));
if (!includeDeleted) {
query.where((t) => t.deleted.equals(false));
}
final result = await query.getSingle(); final result = await query.getSingle();
final members = await _getTeamMembers(teamId: teamId); final members = await _getTeamMembers(teamId: teamId);
return Team( return Team(

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ class GameTable extends Table {
TextColumn get color => text()(); TextColumn get color => text()();
TextColumn get icon => text()(); TextColumn get icon => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
@override @override
Set<Column<Object>> get primaryKey => {id}; Set<Column<Object>> get primaryKey => {id};

View File

@@ -5,6 +5,7 @@ class GroupTable extends Table {
TextColumn get name => text()(); TextColumn get name => text()();
TextColumn get description => text()(); TextColumn get description => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
@override @override
Set<Column<Object>> get primaryKey => {id}; Set<Column<Object>> get primaryKey => {id};

View File

@@ -15,6 +15,7 @@ class MatchTable extends Table {
TextColumn get notes => text()(); TextColumn get notes => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get endedAt => dateTime().nullable()(); DateTimeColumn get endedAt => dateTime().nullable()();
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
@override @override
Set<Column<Object>> get primaryKey => {id}; Set<Column<Object>> get primaryKey => {id};

View File

@@ -6,6 +6,7 @@ class PlayerTable extends Table {
TextColumn get name => text()(); TextColumn get name => text()();
IntColumn get nameCount => integer().withDefault(const Constant(0))(); IntColumn get nameCount => integer().withDefault(const Constant(0))();
TextColumn get description => text()(); TextColumn get description => text()();
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
@override @override
Set<Column<Object>> get primaryKey => {id}; Set<Column<Object>> get primaryKey => {id};

View File

@@ -4,6 +4,7 @@ class TeamTable extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get name => text()(); TextColumn get name => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
@override @override
Set<Column<Object>> get primaryKey => {id}; Set<Column<Object>> get primaryKey => {id};

View File

@@ -9,6 +9,7 @@ class Group {
final String description; final String description;
final DateTime createdAt; final DateTime createdAt;
final List<Player> members; final List<Player> members;
final bool deleted;
Group({ Group({
String? id, String? id,
@@ -16,6 +17,7 @@ class Group {
required this.name, required this.name,
String? description, String? description,
required this.members, required this.members,
this.deleted = false,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(), createdAt = createdAt ?? clock.now(),
description = description ?? ''; description = description ?? '';
@@ -68,7 +70,8 @@ class Group {
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
name = json['name'], name = json['name'],
description = json['description'], description = json['description'],
members = []; members = [],
deleted = json['deleted'] ?? false;
/// Converts the Group instance to a JSON object. Related [Player] objects are /// Converts the Group instance to a JSON object. Related [Player] objects are
/// represented by their IDs. /// represented by their IDs.
@@ -78,5 +81,6 @@ class Group {
'name': name, 'name': name,
'description': description, 'description': description,
'memberIds': members.map((member) => member.id).toList(), 'memberIds': members.map((member) => member.id).toList(),
'deleted': deleted,
}; };
} }

View File

@@ -19,6 +19,7 @@ class Match {
final List<Team>? teams; final List<Team>? teams;
final String notes; final String notes;
Map<String, ScoreEntry?> scores; Map<String, ScoreEntry?> scores;
final bool deleted;
Match({ Match({
required this.name, required this.name,
@@ -31,6 +32,7 @@ class Match {
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
Map<String, ScoreEntry?>? scores, Map<String, ScoreEntry?>? scores,
this.deleted = false,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(), createdAt = createdAt ?? clock.now(),
scores = scores ?? {for (Player p in players) p.id: null}; scores = scores ?? {for (Player p in players) p.id: null};
@@ -123,7 +125,8 @@ class Match {
), ),
) )
: {}, : {},
notes = json['notes'] ?? ''; notes = json['notes'] ?? '',
deleted = json['deleted'] ?? false;
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
@@ -136,6 +139,7 @@ class Match {
'teams': teams?.map((team) => team.toJson()).toList(), 'teams': teams?.map((team) => team.toJson()).toList(),
'scores': scores.map((key, value) => MapEntry(key, value?.toJson())), 'scores': scores.map((key, value) => MapEntry(key, value?.toJson())),
'notes': notes, 'notes': notes,
'deleted': deleted,
}; };
List<Player> get mvp { List<Player> get mvp {

View File

@@ -7,6 +7,7 @@ class Player {
final String name; final String name;
int nameCount; int nameCount;
final String description; final String description;
final bool deleted;
Player({ Player({
String? id, String? id,
@@ -14,6 +15,7 @@ class Player {
required this.name, required this.name,
this.nameCount = 0, this.nameCount = 0,
String? description, String? description,
this.deleted = false,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(), createdAt = createdAt ?? clock.now(),
description = description ?? ''; description = description ?? '';
@@ -58,12 +60,14 @@ class Player {
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
name = json['name'], name = json['name'],
nameCount = 0, nameCount = 0,
description = json['description']; description = json['description'],
deleted = json['deleted'] ?? false;
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'name': name, 'name': name,
'description': description, 'description': description,
'deleted': deleted,
}; };
} }

View File

@@ -2,8 +2,9 @@ class ScoreEntry {
final int roundNumber; final int roundNumber;
final int score; final int score;
final int change; final int change;
final bool deleted;
ScoreEntry({required this.score, this.roundNumber = 0, this.change = 0}); ScoreEntry({required this.score, this.roundNumber = 0, this.change = 0, this.deleted = false});
@override @override
String toString() { String toString() {
@@ -33,11 +34,13 @@ class ScoreEntry {
ScoreEntry.fromJson(Map<String, dynamic> json) ScoreEntry.fromJson(Map<String, dynamic> json)
: roundNumber = json['roundNumber'], : roundNumber = json['roundNumber'],
score = json['score'], score = json['score'],
change = json['change']; change = json['change'],
deleted = json['deleted'] ?? false;
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'roundNumber': roundNumber, 'roundNumber': roundNumber,
'score': score, 'score': score,
'change': change, 'change': change,
'deleted': deleted,
}; };
} }

View File

@@ -8,12 +8,14 @@ class Team {
final String name; final String name;
final DateTime createdAt; final DateTime createdAt;
final List<Player> members; final List<Player> members;
final bool deleted;
Team({ Team({
String? id, String? id,
required this.name, required this.name,
DateTime? createdAt, DateTime? createdAt,
required this.members, required this.members,
this.deleted = false,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(); createdAt = createdAt ?? clock.now();
@@ -58,12 +60,14 @@ class Team {
: id = json['id'], : id = json['id'],
name = json['name'], name = json['name'],
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
members = []; // Populated during import via DataTransferService members = [],
deleted = json['deleted'] ?? false;
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'name': name, 'name': name,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'memberIds': members.map((member) => member.id).toList(), 'memberIds': members.map((member) => member.id).toList(),
'deleted': deleted,
}; };
} }

View File

@@ -19,7 +19,6 @@
"color_red": "Rot", "color_red": "Rot",
"color_teal": "Türkis", "color_teal": "Türkis",
"color_yellow": "Gelb", "color_yellow": "Gelb",
"confirm": "Bestätigen",
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
"create_game": "Spielvorlage erstellen", "create_game": "Spielvorlage erstellen",
"create_group": "Gruppe erstellen", "create_group": "Gruppe erstellen",
@@ -45,14 +44,11 @@
}, },
"delete_group": "Gruppe löschen", "delete_group": "Gruppe löschen",
"delete_match": "Spiel löschen", "delete_match": "Spiel löschen",
"delete_player": "Spieler:in löschen",
"description": "Beschreibung", "description": "Beschreibung",
"drag_to_set_placement": "Ziehen um Platzierung zu setzen", "drag_to_set_placement": "Ziehen um Platzierung zu setzen",
"edit_game": "Spielvorlage bearbeiten", "edit_game": "Spielvorlage bearbeiten",
"edit_group": "Gruppe bearbeiten", "edit_group": "Gruppe bearbeiten",
"edit_match": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten",
"edit_name": "Name ändern",
"edit_player": "Spieler bearbeiten",
"enter_points": "Punkte eingeben", "enter_points": "Punkte eingeben",
"enter_results": "Ergebnisse eintragen", "enter_results": "Ergebnisse eintragen",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
@@ -70,7 +66,6 @@
"group_name": "Gruppenname", "group_name": "Gruppenname",
"group_profile": "Gruppenprofil", "group_profile": "Gruppenprofil",
"groups": "Gruppen", "groups": "Gruppen",
"groups_part_of": "Gruppen Teil von",
"highest_score": "Höchste Punkte", "highest_score": "Höchste Punkte",
"home": "Startseite", "home": "Startseite",
"import_canceled": "Import abgebrochen", "import_canceled": "Import abgebrochen",
@@ -88,9 +83,6 @@
"match_name": "Spieltitel", "match_name": "Spieltitel",
"match_profile": "Spielprofil", "match_profile": "Spielprofil",
"matches": "Spiele", "matches": "Spiele",
"matches_part_of": "Spiele Teil von",
"matches_played": "Spiele gespielt",
"matches_won": "Spiele gewonnen",
"members": "Mitglieder", "members": "Mitglieder",
"most_points": "Höchste Punkte", "most_points": "Höchste Punkte",
"multiple_winners": "Mehrere Gewinner:innen", "multiple_winners": "Mehrere Gewinner:innen",
@@ -100,7 +92,6 @@
"no_license_text_available": "Kein Lizenztext verfügbar", "no_license_text_available": "Kein Lizenztext verfügbar",
"no_licenses_found": "Keine Lizenzen gefunden", "no_licenses_found": "Keine Lizenzen gefunden",
"no_matches_created_yet": "Noch keine Spiele erstellt", "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_created_yet": "Noch keine Spieler:in erstellt",
"no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden", "no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
"no_players_selected": "Keine Spieler:innen ausgewählt", "no_players_selected": "Keine Spieler:innen ausgewählt",
@@ -111,12 +102,10 @@
"none": "Kein", "none": "Kein",
"none_group": "Keine", "none_group": "Keine",
"not_available": "Nicht verfügbar", "not_available": "Nicht verfügbar",
"not_part_of_any_group": "Noch keiner Gruppe hinzugefügt",
"place": "Platz", "place": "Platz",
"placement": "Platzierung", "placement": "Platzierung",
"played_matches": "Gespielte Spiele", "played_matches": "Gespielte Spiele",
"player_name": "Spieler:innenname", "player_name": "Spieler:innenname",
"player_profile": "Spieler:in-Profil",
"players": "Spieler:innen", "players": "Spieler:innen",
"point": "Punkt", "point": "Punkt",
"points": "Punkte", "points": "Punkte",
@@ -138,7 +127,6 @@
"select_winner": "Gewinner:in wählen", "select_winner": "Gewinner:in wählen",
"select_winners": "Gewinner:innen wählen", "select_winners": "Gewinner:innen wählen",
"selected_players": "Ausgewählte Spieler:innen", "selected_players": "Ausgewählte Spieler:innen",
"set_name": "Name setzen",
"settings": "Einstellungen", "settings": "Einstellungen",
"single_loser": "Ein:e Verlierer:in", "single_loser": "Ein:e Verlierer:in",
"single_winner": "Ein:e Gewinner:in", "single_winner": "Ein:e Gewinner:in",

View File

@@ -19,7 +19,6 @@
"color_red": "Red", "color_red": "Red",
"color_teal": "Teal", "color_teal": "Teal",
"color_yellow": "Yellow", "color_yellow": "Yellow",
"confirm": "Confirm",
"could_not_add_player": "Could not add player", "could_not_add_player": "Could not add player",
"create_game": "Create Game", "create_game": "Create Game",
"create_group": "Create Group", "create_group": "Create Group",
@@ -45,14 +44,11 @@
}, },
"delete_group": "Delete Group", "delete_group": "Delete Group",
"delete_match": "Delete Match", "delete_match": "Delete Match",
"delete_player": "Delete player?",
"description": "Description", "description": "Description",
"drag_to_set_placement": "Drag to set placement", "drag_to_set_placement": "Drag to set placement",
"edit_game": "Edit Game", "edit_game": "Edit Game",
"edit_group": "Edit Group", "edit_group": "Edit Group",
"edit_match": "Edit Match", "edit_match": "Edit Match",
"edit_name": "Edit name",
"edit_player": "Edit player",
"enter_points": "Enter points", "enter_points": "Enter points",
"enter_results": "Enter Results", "enter_results": "Enter Results",
"error_creating_group": "Error while creating group, please try again", "error_creating_group": "Error while creating group, please try again",
@@ -70,7 +66,6 @@
"group_name": "Group name", "group_name": "Group name",
"group_profile": "Group Profile", "group_profile": "Group Profile",
"groups": "Groups", "groups": "Groups",
"groups_part_of": "Groups part of",
"highest_score": "Highest Score", "highest_score": "Highest Score",
"home": "Home", "home": "Home",
"import_canceled": "Import canceled", "import_canceled": "Import canceled",
@@ -88,9 +83,6 @@
"match_name": "Match name", "match_name": "Match name",
"match_profile": "Match Profile", "match_profile": "Match Profile",
"matches": "Matches", "matches": "Matches",
"matches_part_of": "Matches part of",
"matches_played": "Matches played",
"matches_won": "Matches won",
"members": "Members", "members": "Members",
"most_points": "Most Points", "most_points": "Most Points",
"multiple_winners": "Multiple Winners", "multiple_winners": "Multiple Winners",
@@ -100,7 +92,6 @@
"no_license_text_available": "No license text available", "no_license_text_available": "No license text available",
"no_licenses_found": "No licenses found", "no_licenses_found": "No licenses found",
"no_matches_created_yet": "No matches created yet", "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_created_yet": "No players created yet",
"no_players_found_with_that_name": "No players found with that name", "no_players_found_with_that_name": "No players found with that name",
"no_players_selected": "No players selected", "no_players_selected": "No players selected",
@@ -111,12 +102,10 @@
"none": "None", "none": "None",
"none_group": "None", "none_group": "None",
"not_available": "Not available", "not_available": "Not available",
"not_part_of_any_group": "Not part of any group yet",
"place": "place", "place": "place",
"placement": "Placement", "placement": "Placement",
"played_matches": "Played Matches", "played_matches": "Played Matches",
"player_name": "Player name", "player_name": "Player name",
"player_profile": "Player Profile",
"players": "Players", "players": "Players",
"point": "Point", "point": "Point",
"points": "Points", "points": "Points",
@@ -137,7 +126,6 @@
"select_winner": "Select Winner", "select_winner": "Select Winner",
"select_winners": "Select Winners", "select_winners": "Select Winners",
"selected_players": "Selected players", "selected_players": "Selected players",
"set_name": "Set name",
"settings": "Settings", "settings": "Settings",
"single_loser": "Single Loser", "single_loser": "Single Loser",
"single_winner": "Single Winner", "single_winner": "Single Winner",

View File

@@ -212,12 +212,6 @@ abstract class AppLocalizations {
/// **'Yellow'** /// **'Yellow'**
String get color_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. /// No description provided for @could_not_add_player.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -326,12 +320,6 @@ abstract class AppLocalizations {
/// **'Delete Match'** /// **'Delete Match'**
String get 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. /// No description provided for @description.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -362,18 +350,6 @@ abstract class AppLocalizations {
/// **'Edit Match'** /// **'Edit Match'**
String get 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. /// No description provided for @enter_points.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -476,12 +452,6 @@ abstract class AppLocalizations {
/// **'Groups'** /// **'Groups'**
String get 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. /// No description provided for @highest_score.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -584,24 +554,6 @@ abstract class AppLocalizations {
/// **'Matches'** /// **'Matches'**
String get 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. /// No description provided for @members.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -656,12 +608,6 @@ abstract class AppLocalizations {
/// **'No matches created yet'** /// **'No matches created yet'**
String get 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. /// No description provided for @no_players_created_yet.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -722,12 +668,6 @@ abstract class AppLocalizations {
/// **'Not available'** /// **'Not available'**
String get 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. /// No description provided for @place.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -752,12 +692,6 @@ abstract class AppLocalizations {
/// **'Player name'** /// **'Player name'**
String get 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. /// No description provided for @players.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -878,12 +812,6 @@ abstract class AppLocalizations {
/// **'Selected players'** /// **'Selected players'**
String get 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. /// No description provided for @settings.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -65,9 +65,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get color_yellow => 'Gelb'; String get color_yellow => 'Gelb';
@override
String get confirm => 'Bestätigen';
@override @override
String could_not_add_player(Object playerName) { String could_not_add_player(Object playerName) {
return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
@@ -134,9 +131,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get delete_match => 'Spiel löschen'; String get delete_match => 'Spiel löschen';
@override
String get delete_player => 'Spieler:in löschen';
@override @override
String get description => 'Beschreibung'; String get description => 'Beschreibung';
@@ -152,12 +146,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get edit_match => 'Gruppe bearbeiten'; String get edit_match => 'Gruppe bearbeiten';
@override
String get edit_name => 'Name ändern';
@override
String get edit_player => 'Spieler bearbeiten';
@override @override
String get enter_points => 'Punkte eingeben'; String get enter_points => 'Punkte eingeben';
@@ -213,9 +201,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get groups => 'Gruppen'; String get groups => 'Gruppen';
@override
String get groups_part_of => 'Gruppen Teil von';
@override @override
String get highest_score => 'Höchste Punkte'; String get highest_score => 'Höchste Punkte';
@@ -267,15 +252,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get matches => 'Spiele'; 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 @override
String get members => 'Mitglieder'; String get members => 'Mitglieder';
@@ -303,9 +279,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get no_matches_created_yet => 'Noch keine Spiele erstellt'; String get no_matches_created_yet => 'Noch keine Spiele erstellt';
@override
String get no_matches_played_yet => 'Noch kein Spiel gespielt';
@override @override
String get no_players_created_yet => 'Noch keine Spieler:in erstellt'; String get no_players_created_yet => 'Noch keine Spieler:in erstellt';
@@ -337,9 +310,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get not_available => 'Nicht verfügbar'; String get not_available => 'Nicht verfügbar';
@override
String get not_part_of_any_group => 'Noch keiner Gruppe hinzugefügt';
@override @override
String get place => 'Platz'; String get place => 'Platz';
@@ -352,9 +322,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get player_name => 'Spieler:innenname'; String get player_name => 'Spieler:innenname';
@override
String get player_profile => 'Spieler:in-Profil';
@override @override
String get players => 'Spieler:innen'; String get players => 'Spieler:innen';
@@ -420,9 +387,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get selected_players => 'Ausgewählte Spieler:innen'; String get selected_players => 'Ausgewählte Spieler:innen';
@override
String get set_name => 'Name setzen';
@override @override
String get settings => 'Einstellungen'; String get settings => 'Einstellungen';

View File

@@ -65,9 +65,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get color_yellow => 'Yellow'; String get color_yellow => 'Yellow';
@override
String get confirm => 'Confirm';
@override @override
String could_not_add_player(Object playerName) { String could_not_add_player(Object playerName) {
return 'Could not add player'; return 'Could not add player';
@@ -134,9 +131,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get delete_match => 'Delete Match'; String get delete_match => 'Delete Match';
@override
String get delete_player => 'Delete player?';
@override @override
String get description => 'Description'; String get description => 'Description';
@@ -152,12 +146,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get edit_match => 'Edit Match'; String get edit_match => 'Edit Match';
@override
String get edit_name => 'Edit name';
@override
String get edit_player => 'Edit player';
@override @override
String get enter_points => 'Enter points'; String get enter_points => 'Enter points';
@@ -213,9 +201,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get groups => 'Groups'; String get groups => 'Groups';
@override
String get groups_part_of => 'Groups part of';
@override @override
String get highest_score => 'Highest Score'; String get highest_score => 'Highest Score';
@@ -267,15 +252,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get matches => 'Matches'; 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 @override
String get members => 'Members'; String get members => 'Members';
@@ -303,9 +279,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get no_matches_created_yet => 'No matches created yet'; String get no_matches_created_yet => 'No matches created yet';
@override
String get no_matches_played_yet => 'No games played yet';
@override @override
String get no_players_created_yet => 'No players created yet'; String get no_players_created_yet => 'No players created yet';
@@ -337,9 +310,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get not_available => 'Not available'; String get not_available => 'Not available';
@override
String get not_part_of_any_group => 'Not part of any group yet';
@override @override
String get place => 'place'; String get place => 'place';
@@ -352,9 +322,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get player_name => 'Player name'; String get player_name => 'Player name';
@override
String get player_profile => 'Player Profile';
@override @override
String get players => 'Players'; String get players => 'Players';
@@ -420,9 +387,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get selected_players => 'Selected players'; String get selected_players => 'Selected players';
@override
String get set_name => 'Set name';
@override @override
String get settings => 'Settings'; String get settings => 'Settings';

View File

@@ -89,7 +89,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
initialSelectedPlayers: initialSelectedPlayers, initialSelectedPlayers: initialSelectedPlayers,
onPlayerCreated: () => widget.onMembersChanged?.call(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectedPlayers = [...value]; selectedPlayers = [...value];
@@ -135,7 +134,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
if (!mounted) return; if (!mounted) return;
if (success) { if (success) {
widget.onMembersChanged?.call();
await HapticFeedback.successNotification(); await HapticFeedback.successNotification();
if (mounted) { if (mounted) {
Navigator.pop(context, updatedGroup); Navigator.pop(context, updatedGroup);
@@ -159,6 +157,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
final success = await db.groupDao.addGroup( final success = await db.groupDao.addGroup(
group: Group(name: groupName, members: selectedPlayers), group: Group(name: groupName, members: selectedPlayers),
); );
return success; return success;
} }

View File

@@ -77,7 +77,6 @@ class _GroupViewState extends State<GroupView> {
); );
} }
return GroupTile( return GroupTile(
onPlayerChanged: loadGroups,
group: groups[index], group: groups[index],
onTap: () async { onTap: () async {
await Navigator.push( await Navigator.push(
@@ -107,10 +106,13 @@ class _GroupViewState extends State<GroupView> {
context, context,
adaptivePageRoute( adaptivePageRoute(
builder: (context) { builder: (context) {
return CreateGroupView(onMembersChanged: loadGroups); return const CreateGroupView();
}, },
), ),
); );
setState(() {
loadGroups();
});
}, },
), ),
), ),

View File

@@ -196,7 +196,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
child: PlayerSelection( child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'), key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialSelectedPlayers: selectedPlayers, initialSelectedPlayers: selectedPlayers,
onPlayerCreated: () => widget.onMatchesUpdated?.call(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectedPlayers = value; selectedPlayers = value;

View File

@@ -97,7 +97,6 @@ class _MatchViewState extends State<MatchView> {
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 12.0), padding: const EdgeInsets.only(bottom: 12.0),
child: MatchTile( child: MatchTile(
onPlayerEdited: loadMatches,
width: MediaQuery.sizeOf(context).width * 0.95, width: MediaQuery.sizeOf(context).width * 0.95,
onTap: () async { onTap: () async {
Navigator.push( Navigator.push(

View File

@@ -1,394 +0,0 @@
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;
}
}

View File

@@ -6,13 +6,11 @@ class AppSkeleton extends StatefulWidget {
/// - [child]: The widget tree to apply the skeleton effect to. /// - [child]: The widget tree to apply the skeleton effect to.
/// - [enabled]: A boolean to enable or disable the skeleton effect. /// - [enabled]: A boolean to enable or disable the skeleton effect.
/// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher. /// - [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({ const AppSkeleton({
super.key, super.key,
required this.child, required this.child,
this.enabled = true, this.enabled = true,
this.fixLayoutBuilder = false, this.fixLayoutBuilder = false,
this.alignment = Alignment.center,
}); });
/// The widget tree to apply the skeleton effect to. /// The widget tree to apply the skeleton effect to.
@@ -24,9 +22,6 @@ class AppSkeleton extends StatefulWidget {
/// A boolean to fix the layout builder for AnimatedSwitcher. /// A boolean to fix the layout builder for AnimatedSwitcher.
final bool fixLayoutBuilder; final bool fixLayoutBuilder;
/// The alignment used for the custom layout builder and optional Align wrapper
final Alignment alignment;
@override @override
State<AppSkeleton> createState() => _AppSkeletonState(); State<AppSkeleton> createState() => _AppSkeletonState();
} }
@@ -50,14 +45,13 @@ class _AppSkeletonState extends State<AppSkeleton> {
layoutBuilder: !widget.fixLayoutBuilder layoutBuilder: !widget.fixLayoutBuilder
? AnimatedSwitcher.defaultLayoutBuilder ? AnimatedSwitcher.defaultLayoutBuilder
: (Widget? currentChild, List<Widget> previousChildren) { : (Widget? currentChild, List<Widget> previousChildren) {
final children = <Widget>[...previousChildren]; return Stack(
if (currentChild != null) children.add(currentChild); alignment: Alignment.topCenter,
return Stack(alignment: widget.alignment, children: children); children: [...previousChildren, ?currentChild],
);
}, },
), ),
child: widget.fixLayoutBuilder child: widget.child,
? Align(alignment: widget.alignment, child: widget.child)
: widget.child,
); );
} }
} }

View File

@@ -11,7 +11,7 @@ class AnimatedDialogButton extends StatefulWidget {
const AnimatedDialogButton({ const AnimatedDialogButton({
super.key, super.key,
required this.buttonText, required this.buttonText,
this.onPressed, required this.onPressed,
this.buttonConstraints, this.buttonConstraints,
this.buttonType = ButtonType.primary, this.buttonType = ButtonType.primary,
this.isDescructive = false, this.isDescructive = false,
@@ -19,7 +19,7 @@ class AnimatedDialogButton extends StatefulWidget {
final String buttonText; final String buttonText;
final VoidCallback? onPressed; final VoidCallback onPressed;
final BoxConstraints? buttonConstraints; final BoxConstraints? buttonConstraints;
@@ -38,13 +38,8 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textStyling = _getTextStyling(); final textStyling = _getTextStyling();
final buttonDecoration = _getButtonDecoration(); final buttonDecoration = _getButtonDecoration();
bool isDisabled = widget.onPressed == null;
return IgnorePointer( return GestureDetector(
ignoring: isDisabled,
child: Opacity(
opacity: isDisabled ? 0.5 : 1.0,
child: GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true), onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false), onTapUp: (_) => setState(() => _isPressed = false),
onTapCancel: () => setState(() => _isPressed = false), onTapCancel: () => setState(() => _isPressed = false),
@@ -59,10 +54,7 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
child: Container( child: Container(
constraints: widget.buttonConstraints, constraints: widget.buttonConstraints,
decoration: buttonDecoration, decoration: buttonDecoration,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
horizontal: 16,
vertical: 12,
),
margin: const EdgeInsets.symmetric(vertical: 8), margin: const EdgeInsets.symmetric(vertical: 8),
child: Text( child: Text(
widget.buttonText, widget.buttonText,
@@ -73,8 +65,6 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
), ),
), ),
), ),
),
),
); );
} }

View File

@@ -19,6 +19,7 @@ class CustomAlertDialog extends StatelessWidget {
final String title; final String title;
final Widget content; final Widget content;
final List<CustomDialogAction> actions; final List<CustomDialogAction> actions;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(

View File

@@ -10,7 +10,7 @@ class CustomDialogAction extends StatelessWidget {
/// - [onPressed]: Callback function that is triggered when the button is pressed. /// - [onPressed]: Callback function that is triggered when the button is pressed.
const CustomDialogAction({ const CustomDialogAction({
super.key, super.key,
this.onPressed, required this.onPressed,
required this.text, required this.text,
this.buttonType = ButtonType.primary, this.buttonType = ButtonType.primary,
this.isDestructive = false, this.isDestructive = false,
@@ -20,18 +20,17 @@ class CustomDialogAction extends StatelessWidget {
final ButtonType buttonType; final ButtonType buttonType;
final VoidCallback? onPressed; final VoidCallback onPressed;
final bool isDestructive; final bool isDestructive;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedDialogButton( return AnimatedDialogButton(
onPressed: onPressed != null onPressed: () async {
? () async {
await HapticFeedback.selectionClick(); await HapticFeedback.selectionClick();
onPressed?.call(); onPressed.call();
} },
: null,
buttonText: text, buttonText: text,
buttonType: buttonType, buttonType: buttonType,
isDescructive: isDestructive, isDescructive: isDestructive,

View File

@@ -26,7 +26,6 @@ class PlayerSelection extends StatefulWidget {
this.availablePlayers, this.availablePlayers,
this.initialSelectedPlayers, this.initialSelectedPlayers,
required this.onChanged, required this.onChanged,
this.onPlayerCreated,
}); });
/// An optional list of players to choose from. If null, all players from the database are used. /// An optional list of players to choose from. If null, all players from the database are used.
@@ -38,9 +37,6 @@ class PlayerSelection extends StatefulWidget {
/// A callback function that is invoked whenever the selection changes, /// A callback function that is invoked whenever the selection changes,
final Function(List<Player> value) onChanged; final Function(List<Player> value) onChanged;
/// A callback function that is invoked when a player was created in this widget
final VoidCallback? onPlayerCreated;
@override @override
State<PlayerSelection> createState() => _PlayerSelectionState(); State<PlayerSelection> createState() => _PlayerSelectionState();
} }
@@ -327,7 +323,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
/// Updates the state after successfully adding a new player. /// Updates the state after successfully adding a new player.
void _handleSuccessfulPlayerCreation(Player player) { void _handleSuccessfulPlayerCreation(Player player) {
widget.onPlayerCreated?.call();
selectedPlayers.insert(0, player); selectedPlayers.insert(0, player);
widget.onChanged([...selectedPlayers]); widget.onChanged([...selectedPlayers]);
allPlayers.add(player); allPlayers.add(player);

View File

@@ -1,10 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/group.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'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class GroupTile extends StatefulWidget { class GroupTile extends StatefulWidget {
@@ -17,7 +15,6 @@ class GroupTile extends StatefulWidget {
required this.group, required this.group,
this.isHighlighted = false, this.isHighlighted = false,
this.onTap, this.onTap,
this.onPlayerChanged,
}); });
/// The group data to be displayed. /// The group data to be displayed.
@@ -29,9 +26,6 @@ class GroupTile extends StatefulWidget {
/// Callback function to be executed when the tile is tapped. /// Callback function to be executed when the tile is tapped.
final VoidCallback? onTap; final VoidCallback? onTap;
/// Callback function to be executed when the players in the group are changed.
final VoidCallback? onPlayerChanged;
@override @override
State<GroupTile> createState() => _GroupTileState(); State<GroupTile> createState() => _GroupTileState();
} }
@@ -98,19 +92,6 @@ class _GroupTileState extends State<GroupTile> {
text: member.name, text: member.name,
suffixText: getNameCountText(member), suffixText: getNameCountText(member),
iconEnabled: false, iconEnabled: false,
onTileTap: () {
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => PlayerDetailView(
player: member,
callback: () {
widget.onPlayerChanged?.call();
},
),
),
);
},
), ),
], ],
), ),

View File

@@ -3,13 +3,11 @@ import 'dart:core' hide Match;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.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/game_label.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -26,7 +24,6 @@ class MatchTile extends StatefulWidget {
required this.onTap, required this.onTap,
this.width, this.width,
this.compact = false, this.compact = false,
this.onPlayerEdited,
}); });
/// The match data to be displayed. /// The match data to be displayed.
@@ -35,9 +32,6 @@ class MatchTile extends StatefulWidget {
/// The callback invoked when the tile is tapped. /// The callback invoked when the tile is tapped.
final VoidCallback onTap; final VoidCallback onTap;
/// The callback invoked when the players are edited
final VoidCallback? onPlayerEdited;
/// Optional width for the tile. /// Optional width for the tile.
final double? width; final double? width;
@@ -230,19 +224,6 @@ class _MatchTileState extends State<MatchTile> {
text: player.name, text: player.name,
suffixText: getNameCountText(player), suffixText: getNameCountText(player),
iconEnabled: false, iconEnabled: false,
onTileTap: () {
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => PlayerDetailView(
player: player,
callback: () {
widget.onPlayerEdited?.call();
},
),
),
);
},
); );
}).toList(), }).toList(),
), ),

View File

@@ -12,7 +12,6 @@ class TextIconTile extends StatelessWidget {
this.suffixText = '', this.suffixText = '',
this.iconEnabled = true, this.iconEnabled = true,
this.onIconTap, this.onIconTap,
this.onTileTap,
}); });
/// The text to display in the tile. /// The text to display in the tile.
@@ -26,14 +25,9 @@ class TextIconTile extends StatelessWidget {
/// The callback to be invoked when the icon is tapped. /// The callback to be invoked when the icon is tapped.
final VoidCallback? onIconTap; final VoidCallback? onIconTap;
/// The callback to be invoked when the tile is tapped.
final VoidCallback? onTileTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return Container(
onTap: onTileTap,
child: Container(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: CustomTheme.onBoxColor, color: CustomTheme.onBoxColor,
@@ -78,7 +72,6 @@ class TextIconTile extends StatelessWidget {
], ],
], ],
), ),
),
); );
} }
} }

View File

@@ -194,31 +194,6 @@ void main() {
expect(allGroups, isEmpty); 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 { test('addGroupsAsList() with duplicate groups only adds once', () async {
await database.groupDao.addGroupsAsList( await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup1, testGroup1], groups: [testGroup1, testGroup1, testGroup1],

View File

@@ -260,34 +260,6 @@ void main() {
expect(match.group!.id, testGroup1.id); 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 { test('getMatchCount() works correctly', () async {
var count = await database.matchDao.getMatchCount(); var count = await database.matchDao.getMatchCount();
expect(count, 0); expect(count, 0);

View File

@@ -233,95 +233,6 @@ void main() {
expect(allPlayers, isEmpty); 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 { test('updatePlayerDescription() works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
@@ -461,22 +372,14 @@ void main() {
final player1 = Player(name: testPlayer1.name, description: ''); final player1 = Player(name: testPlayer1.name, description: '');
await database.playerDao.addPlayer(player: player1); 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(); var players = await database.playerDao.getAllPlayers();
expect(players.length, 3); expect(players.length, 2);
players.sort((a, b) => a.nameCount.compareTo(b.nameCount)); players.sort((a, b) => a.nameCount.compareTo(b.nameCount));
for (int i = 0; i < players.length - 1; i++) { for (int i = 0; i < players.length - 1; i++) {
expect(players[i].nameCount, i + 1); 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);
}, },
); );
@@ -501,62 +404,24 @@ void main() {
for (int i = 0; i < players.length - 1; i++) { for (int i = 0; i < players.length - 1; i++) {
expect(players[i].nameCount, i + 1); 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 { test('getNameCount works correctly', () async {
final player1 = Player(name: testPlayer1.name);
final player2 = Player(name: testPlayer1.name); final player2 = Player(name: testPlayer1.name);
final player3 = Player(name: testPlayer1.name);
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayersAsList(
players: [testPlayer1, player2, player3],
var nameCount = await database.playerDao.getNameCount(
name: testPlayer1.name,
); );
expect(nameCount, 1); final nameCount = await database.playerDao.getNameCount(
await database.playerDao.addPlayersAsList(players: [player1, player2]);
nameCount = await database.playerDao.getNameCount(
name: testPlayer1.name, name: testPlayer1.name,
); );
expect(nameCount, 3); 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 { test('updateNameCount works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
@@ -576,24 +441,14 @@ void main() {
final player2 = Player(name: testPlayer1.name, description: ''); final player2 = Player(name: testPlayer1.name, description: '');
final player3 = Player(name: testPlayer1.name, description: ''); final player3 = Player(name: testPlayer1.name, description: '');
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayersAsList(
var player = await database.playerDao.getPlayerWithHighestNameCount( players: [testPlayer1, player2, player3],
name: testPlayer1.name,
); );
expect(player, isNotNull);
expect(player!.nameCount, 0);
await database.playerDao.addPlayer(player: player2); final player = await database.playerDao.getPlayerWithHighestNameCount(
player = await database.playerDao.getPlayerWithHighestNameCount(
name: testPlayer1.name, 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, isNotNull);
expect(player!.nameCount, 3); expect(player!.nameCount, 3);
}); });
@@ -605,6 +460,32 @@ void main() {
expect(player, isNull); 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 { test('getPlayerWithHighestNameCount with non existing player', () async {
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.initializeNameCount(name: testPlayer1.name); await database.playerDao.initializeNameCount(name: testPlayer1.name);