Merge branch 'development' into feature/132-verschiedene-regelsaetze-implementieren

# Conflicts:
#	lib/presentation/views/main_menu/match_view/match_result_view.dart
This commit is contained in:
2026-04-14 23:05:45 +02:00
60 changed files with 3471 additions and 2221 deletions

View File

@@ -20,6 +20,7 @@
"type": "string"
}
},
"additionalProperties": false,
"required": [
"id",
"createdAt",
@@ -55,6 +56,7 @@
"type": "string"
}
},
"additionalProperties": false,
"required": [
"id",
"createdAt",
@@ -90,6 +92,7 @@
}
}
},
"additionalProperties": false,
"required": [
"id",
"name",
@@ -120,6 +123,7 @@
}
}
},
"additionalProperties": false,
"required": [
"id",
"name",
@@ -157,10 +161,31 @@
"type": "string"
}
},
"scores": {
"type": "object",
"items": {
"type": "array",
"items": {
"type": "string",
"properties": {
"roundNumber": {
"type": "number"
},
"score": {
"type": "number"
},
"change": {
"type": "number"
}
}
}
}
},
"notes": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"id",
"name",
@@ -172,6 +197,7 @@
}
}
},
"additionalProperties": false,
"required": [
"players",
"games",

View File

@@ -1,6 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
/// Translates a [Ruleset] enum value to its corresponding localized string.

View File

@@ -1,8 +1,8 @@
import 'package:drift/drift.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/game_table.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/game.dart';
part 'game_dao.g.dart';
@@ -111,14 +111,20 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
}
/// Updates the name of the game with the given [gameId] to [newName].
Future<void> updateGameName({required String gameId, required String newName}) async {
await (update(
gameTable,
)..where((g) => g.id.equals(gameId))).write(GameTableCompanion(name: Value(newName)));
Future<void> updateGameName({
required String gameId,
required String newName,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(name: Value(newName)),
);
}
/// Updates the ruleset of the game with the given [gameId].
Future<void> updateGameRuleset({required String gameId, required Ruleset newRuleset}) async {
Future<void> updateGameRuleset({
required String gameId,
required Ruleset newRuleset,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(ruleset: Value(newRuleset.name)),
);
@@ -135,24 +141,31 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
}
/// Updates the color of the game with the given [gameId].
Future<void> updateGameColor({required String gameId, required GameColor newColor}) async {
await (update(
gameTable,
)..where((g) => g.id.equals(gameId))).write(GameTableCompanion(color: Value(newColor.name)));
Future<void> updateGameColor({
required String gameId,
required GameColor newColor,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(color: Value(newColor.name)),
);
}
/// Updates the icon of the game with the given [gameId].
Future<void> updateGameIcon({required String gameId, required String newIcon}) async {
await (update(
gameTable,
)..where((g) => g.id.equals(gameId))).write(GameTableCompanion(icon: Value(newIcon)));
Future<void> updateGameIcon({
required String gameId,
required String newIcon,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(icon: Value(newIcon)),
);
}
/// Retrieves the total count of games in the database.
Future<int> getGameCount() async {
final count = await (selectOnly(
gameTable,
)..addColumns([gameTable.id.count()])).map((row) => row.read(gameTable.id.count())).getSingle();
final count =
await (selectOnly(gameTable)..addColumns([gameTable.id.count()]))
.map((row) => row.read(gameTable.id.count()))
.getSingle();
return count ?? 0;
}

View File

@@ -5,4 +5,12 @@ part of 'game_dao.dart';
// ignore_for_file: type=lint
mixin _$GameDaoMixin on DatabaseAccessor<AppDatabase> {
$GameTableTable get gameTable => attachedDatabase.gameTable;
GameDaoManager get managers => GameDaoManager(this);
}
class GameDaoManager {
final _$GameDaoMixin _db;
GameDaoManager(this._db);
$$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
}

View File

@@ -1,13 +1,14 @@
import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/group_table.dart';
import 'package:tallee/data/db/tables/match_table.dart';
import 'package:tallee/data/db/tables/player_group_table.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
part 'group_dao.g.dart';
@DriftAccessor(tables: [GroupTable, PlayerGroupTable])
@DriftAccessor(tables: [GroupTable, PlayerGroupTable, MatchTable])
class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
GroupDao(super.db);
@@ -205,8 +206,6 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
return rowsAffected > 0;
}
/// Retrieves the number of groups in the database.
Future<int> getGroupCount() async {
final count =
@@ -235,10 +234,13 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
/// Replaces all players in a group with the provided list of players.
/// Removes all existing players from the group and adds the new players.
/// Also adds any new players to the player table if they don't exist.
Future<void> replaceGroupPlayers({
/// Returns `true` if the group exists and players were replaced, `false` otherwise.
Future<bool> replaceGroupPlayers({
required String groupId,
required List<Player> newPlayers,
}) async {
if (!await groupExists(groupId: groupId)) return false;
await db.transaction(() async {
// Remove all existing players from the group
final deleteQuery = delete(db.playerGroupTable)
@@ -270,5 +272,6 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
),
);
});
return true;
}
}

View File

@@ -8,4 +8,25 @@ mixin _$GroupDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$PlayerGroupTableTable get playerGroupTable =>
attachedDatabase.playerGroupTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
GroupDaoManager get managers => GroupDaoManager(this);
}
class GroupDaoManager {
final _$GroupDaoMixin _db;
GroupDaoManager(this._db);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
$$PlayerGroupTableTableTableManager get playerGroupTable =>
$$PlayerGroupTableTableTableManager(
_db.attachedDatabase,
_db.playerGroupTable,
);
$$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
$$MatchTableTableTableManager get matchTable =>
$$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable);
}

View File

@@ -4,10 +4,10 @@ import 'package:tallee/data/db/tables/game_table.dart';
import 'package:tallee/data/db/tables/group_table.dart';
import 'package:tallee/data/db/tables/match_table.dart';
import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.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';
part 'match_dao.g.dart';
@@ -29,16 +29,22 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
}
final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? [];
final winner = await getWinner(matchId: row.id);
final scores = await db.scoreEntryDao.getAllMatchScores(
matchId: row.id,
);
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
return Match(
id: row.id,
name: row.name ?? '',
name: row.name,
game: game,
group: group,
players: players,
notes: row.notes ?? '',
createdAt: row.createdAt,
endedAt: row.endedAt,
scores: scores,
winner: winner,
);
}),
@@ -60,17 +66,20 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
final winner = await getWinner(matchId: matchId);
final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId);
final winner = await db.scoreEntryDao.getWinner(matchId: matchId);
return Match(
id: result.id,
name: result.name ?? '',
name: result.name,
game: game,
group: group,
players: players,
notes: result.notes ?? '',
createdAt: result.createdAt,
endedAt: result.endedAt,
scores: scores,
winner: winner,
);
}
@@ -85,7 +94,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
id: match.id,
gameId: match.game.id,
groupId: Value(match.group?.id),
name: Value(match.name),
name: match.name,
notes: Value(match.notes),
createdAt: match.createdAt,
endedAt: Value(match.endedAt),
@@ -100,8 +109,20 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
);
}
for (final pid in match.scores.keys) {
final playerScores = match.scores[pid]!;
await db.scoreEntryDao.addScoresAsList(
entrys: playerScores,
playerId: pid,
matchId: match.id,
);
}
if (match.winner != null) {
await setWinner(matchId: match.id, winnerId: match.winner!.id);
await db.scoreEntryDao.setWinner(
matchId: match.id,
playerId: match.winner!.id,
);
}
});
}
@@ -170,7 +191,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
id: match.id,
gameId: match.game.id,
groupId: Value(match.group?.id),
name: Value(match.name),
name: match.name,
notes: Value(match.notes),
createdAt: match.createdAt,
endedAt: Value(match.endedAt),
@@ -223,7 +244,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
PlayerMatchTableCompanion.insert(
matchId: match.id,
playerId: p.id,
score: 0,
),
mode: InsertMode.insertOrIgnore,
);
@@ -268,6 +288,34 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return count ?? 0;
}
/// Retrieves all matches associated with the given [groupId].
/// Queries the database directly, filtering by [groupId].
Future<List<Match>> getGroupMatches({required String groupId}) async {
final query = select(matchTable)..where((m) => m.groupId.equals(groupId));
final rows = await query.get();
return Future.wait(
rows.map((row) async {
final game = await db.gameDao.getGameById(gameId: row.gameId);
final group = await db.groupDao.getGroupById(groupId: groupId);
final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? [];
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
return Match(
id: row.id,
name: row.name,
game: game,
group: group,
players: players,
notes: row.notes ?? '',
createdAt: row.createdAt,
endedAt: row.endedAt,
winner: winner,
);
}),
);
}
/// Checks if a match with the given [matchId] exists in the database.
/// Returns `true` if the match exists, otherwise `false`.
Future<bool> matchExists({required String matchId}) async {
@@ -338,6 +386,17 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return rowsAffected > 0;
}
/// Removes the group association of the match with the given [matchId].
/// Sets the groupId to null.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeMatchGroup({required String matchId}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
const MatchTableCompanion(groupId: Value(null)),
);
return rowsAffected > 0;
}
/// Updates the createdAt timestamp of the match with the given [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchCreatedAt({
@@ -398,91 +457,4 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
);
});
}
// ============================================================
// Winner methods - handle winner logic via player scores
// ============================================================
/// Checks if a match has a winner.
/// Returns true if any player in the match has their score set to 1.
Future<bool> hasWinner({required String matchId}) async {
final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
for (final player in players) {
final score = await db.playerMatchDao.getPlayerScore(
matchId: matchId,
playerId: player.id,
);
if (score == 1) {
return true;
}
}
return false;
}
/// Gets the winner of a match.
/// Returns the player with score 1, or null if no winner is set.
Future<Player?> getWinner({required String matchId}) async {
final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
for (final player in players) {
final score = await db.playerMatchDao.getPlayerScore(
matchId: matchId,
playerId: player.id,
);
if (score == 1) {
return player;
}
}
return null;
}
/// Sets the winner of a match.
/// Sets all players' scores to 0, then sets the specified player's score to 1.
/// Returns `true` if the operation was successful, otherwise `false`.
Future<bool> setWinner({
required String matchId,
required String winnerId,
}) async {
await db.transaction(() async {
final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
// Set all players' scores to 0
for (final player in players) {
await db.playerMatchDao.updatePlayerScore(
matchId: matchId,
playerId: player.id,
newScore: 0,
);
}
// Set the winner's score to 1
await db.playerMatchDao.updatePlayerScore(
matchId: matchId,
playerId: winnerId,
newScore: 1,
);
});
return true;
}
/// Removes the winner of a match.
/// Sets the current winner's score to 0 (no winner).
/// Returns `true` if a winner was removed, otherwise `false`.
Future<bool> removeWinner({required String matchId}) async {
final winner = await getWinner(matchId: matchId);
if (winner == null) {
return false;
}
final success = await db.playerMatchDao.updatePlayerScore(
matchId: matchId,
playerId: winner.id,
newScore: 0,
);
return success;
}
}

View File

@@ -11,4 +11,25 @@ mixin _$MatchDaoMixin on DatabaseAccessor<AppDatabase> {
$TeamTableTable get teamTable => attachedDatabase.teamTable;
$PlayerMatchTableTable get playerMatchTable =>
attachedDatabase.playerMatchTable;
MatchDaoManager get managers => MatchDaoManager(this);
}
class MatchDaoManager {
final _$MatchDaoMixin _db;
MatchDaoManager(this._db);
$$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$MatchTableTableTableManager get matchTable =>
$$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
$$TeamTableTableTableManager get teamTable =>
$$TeamTableTableTableManager(_db.attachedDatabase, _db.teamTable);
$$PlayerMatchTableTableTableManager get playerMatchTable =>
$$PlayerMatchTableTableTableManager(
_db.attachedDatabase,
_db.playerMatchTable,
);
}

View File

@@ -1,7 +1,7 @@
import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/player_table.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/player.dart';
part 'player_dao.g.dart';

View File

@@ -5,4 +5,12 @@ part of 'player_dao.dart';
// ignore_for_file: type=lint
mixin _$PlayerDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
PlayerDaoManager get managers => PlayerDaoManager(this);
}
class PlayerDaoManager {
final _$PlayerDaoMixin _db;
PlayerDaoManager(this._db);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
}

View File

@@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/player_group_table.dart';
import 'package:tallee/data/db/tables/player_table.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/player.dart';
part 'player_group_dao.g.dart';

View File

@@ -8,4 +8,19 @@ mixin _$PlayerGroupDaoMixin on DatabaseAccessor<AppDatabase> {
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$PlayerGroupTableTable get playerGroupTable =>
attachedDatabase.playerGroupTable;
PlayerGroupDaoManager get managers => PlayerGroupDaoManager(this);
}
class PlayerGroupDaoManager {
final _$PlayerGroupDaoMixin _db;
PlayerGroupDaoManager(this._db);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$PlayerGroupTableTableTableManager get playerGroupTable =>
$$PlayerGroupTableTableTableManager(
_db.attachedDatabase,
_db.playerGroupTable,
);
}

View File

@@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:tallee/data/db/tables/team_table.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/player.dart';
part 'player_match_dao.g.dart';
@@ -17,14 +17,12 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
required String matchId,
required String playerId,
String? teamId,
int score = 0,
}) async {
await into(playerMatchTable).insert(
PlayerMatchTableCompanion.insert(
playerId: playerId,
matchId: matchId,
teamId: Value(teamId),
score: score,
),
mode: InsertMode.insertOrIgnore,
);
@@ -40,41 +38,12 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
if (result.isEmpty) return null;
final futures = result.map(
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
);
final players = await Future.wait(futures);
return players;
}
/// Retrieves a player's score for a specific match.
/// Returns null if the player is not in the match.
Future<int?> getPlayerScore({
required String matchId,
required String playerId,
}) async {
final result = await (select(playerMatchTable)
..where(
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
))
.getSingleOrNull();
return result?.score;
}
/// Updates the score for a player in a match.
/// Returns `true` if the update was successful, otherwise `false`.
Future<bool> updatePlayerScore({
required String matchId,
required String playerId,
required int newScore,
}) async {
final rowsAffected = await (update(playerMatchTable)
..where(
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
))
.write(PlayerMatchTableCompanion(score: Value(newScore)));
return rowsAffected > 0;
}
/// Updates the team for a player in a match.
/// Returns `true` if the update was successful, otherwise `false`.
Future<bool> updatePlayerTeam({
@@ -82,11 +51,11 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
required String playerId,
required String? teamId,
}) async {
final rowsAffected = await (update(playerMatchTable)
..where(
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
))
.write(PlayerMatchTableCompanion(teamId: Value(teamId)));
final rowsAffected =
await (update(playerMatchTable)..where(
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
))
.write(PlayerMatchTableCompanion(teamId: Value(teamId)));
return rowsAffected > 0;
}
@@ -94,11 +63,11 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
/// Returns `true` if there are players, otherwise `false`.
Future<bool> matchHasPlayers({required String matchId}) async {
final count =
await (selectOnly(playerMatchTable)
..where(playerMatchTable.matchId.equals(matchId))
..addColumns([playerMatchTable.playerId.count()]))
.map((row) => row.read(playerMatchTable.playerId.count()))
.getSingle();
await (selectOnly(playerMatchTable)
..where(playerMatchTable.matchId.equals(matchId))
..addColumns([playerMatchTable.playerId.count()]))
.map((row) => row.read(playerMatchTable.playerId.count()))
.getSingle();
return (count ?? 0) > 0;
}
@@ -109,12 +78,12 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
required String playerId,
}) async {
final count =
await (selectOnly(playerMatchTable)
..where(playerMatchTable.matchId.equals(matchId))
..where(playerMatchTable.playerId.equals(playerId))
..addColumns([playerMatchTable.playerId.count()]))
.map((row) => row.read(playerMatchTable.playerId.count()))
.getSingle();
await (selectOnly(playerMatchTable)
..where(playerMatchTable.matchId.equals(matchId))
..where(playerMatchTable.playerId.equals(playerId))
..addColumns([playerMatchTable.playerId.count()]))
.map((row) => row.read(playerMatchTable.playerId.count()))
.getSingle();
return (count ?? 0) > 0;
}
@@ -153,9 +122,9 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
if (playersToRemove.isNotEmpty) {
await (delete(playerMatchTable)..where(
(pg) =>
pg.matchId.equals(matchId) &
pg.playerId.isIn(playersToRemove.toList()),
))
pg.matchId.equals(matchId) &
pg.playerId.isIn(playersToRemove.toList()),
))
.go();
}
@@ -164,15 +133,14 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
final inserts = playersToAdd
.map(
(id) => PlayerMatchTableCompanion.insert(
playerId: id,
matchId: matchId,
score: 0,
),
)
playerId: id,
matchId: matchId,
),
)
.toList();
await Future.wait(
inserts.map(
(c) => into(
(c) => into(
playerMatchTable,
).insert(c, mode: InsertMode.insertOrIgnore),
),
@@ -186,16 +154,14 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
required String matchId,
required String teamId,
}) async {
final result = await (select(playerMatchTable)
..where(
(p) => p.matchId.equals(matchId) & p.teamId.equals(teamId),
))
.get();
final result = await (select(
playerMatchTable,
)..where((p) => p.matchId.equals(matchId) & p.teamId.equals(teamId))).get();
if (result.isEmpty) return [];
final futures = result.map(
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
);
return Future.wait(futures);
}

View File

@@ -11,4 +11,25 @@ mixin _$PlayerMatchDaoMixin on DatabaseAccessor<AppDatabase> {
$TeamTableTable get teamTable => attachedDatabase.teamTable;
$PlayerMatchTableTable get playerMatchTable =>
attachedDatabase.playerMatchTable;
PlayerMatchDaoManager get managers => PlayerMatchDaoManager(this);
}
class PlayerMatchDaoManager {
final _$PlayerMatchDaoMixin _db;
PlayerMatchDaoManager(this._db);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
$$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$MatchTableTableTableManager get matchTable =>
$$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable);
$$TeamTableTableTableManager get teamTable =>
$$TeamTableTableTableManager(_db.attachedDatabase, _db.teamTable);
$$PlayerMatchTableTableTableManager get playerMatchTable =>
$$PlayerMatchTableTableTableManager(
_db.attachedDatabase,
_db.playerMatchTable,
);
}

View File

@@ -1,191 +0,0 @@
import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/score_table.dart';
part 'score_dao.g.dart';
/// A data class representing a score entry.
class ScoreEntry {
final String playerId;
final String matchId;
final int roundNumber;
final int score;
final int change;
ScoreEntry({
required this.playerId,
required this.matchId,
required this.roundNumber,
required this.score,
required this.change,
});
}
@DriftAccessor(tables: [ScoreTable])
class ScoreDao extends DatabaseAccessor<AppDatabase> with _$ScoreDaoMixin {
ScoreDao(super.db);
/// Adds a score entry to the database.
Future<void> addScore({
required String playerId,
required String matchId,
required int roundNumber,
required int score,
required int change,
}) async {
await into(scoreTable).insert(
ScoreTableCompanion.insert(
playerId: playerId,
matchId: matchId,
roundNumber: roundNumber,
score: score,
change: change,
),
mode: InsertMode.insertOrReplace,
);
}
/// Retrieves all scores for a specific match.
Future<List<ScoreEntry>> getScoresForMatch({required String matchId}) async {
final query = select(scoreTable)..where((s) => s.matchId.equals(matchId));
final result = await query.get();
return result
.map(
(row) => ScoreEntry(
playerId: row.playerId,
matchId: row.matchId,
roundNumber: row.roundNumber,
score: row.score,
change: row.change,
),
)
.toList();
}
/// Retrieves all scores for a specific player in a match.
Future<List<ScoreEntry>> getPlayerScoresInMatch({
required String playerId,
required String matchId,
}) async {
final query = select(scoreTable)
..where(
(s) => s.playerId.equals(playerId) & s.matchId.equals(matchId),
)
..orderBy([(s) => OrderingTerm.asc(s.roundNumber)]);
final result = await query.get();
return result
.map(
(row) => ScoreEntry(
playerId: row.playerId,
matchId: row.matchId,
roundNumber: row.roundNumber,
score: row.score,
change: row.change,
),
)
.toList();
}
/// Retrieves the score for a specific round.
Future<ScoreEntry?> getScoreForRound({
required String playerId,
required String matchId,
required int roundNumber,
}) async {
final query = select(scoreTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
);
final result = await query.getSingleOrNull();
if (result == null) return null;
return ScoreEntry(
playerId: result.playerId,
matchId: result.matchId,
roundNumber: result.roundNumber,
score: result.score,
change: result.change,
);
}
/// Updates a score entry.
Future<bool> updateScore({
required String playerId,
required String matchId,
required int roundNumber,
required int newScore,
required int newChange,
}) async {
final rowsAffected = await (update(scoreTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
))
.write(
ScoreTableCompanion(
score: Value(newScore),
change: Value(newChange),
),
);
return rowsAffected > 0;
}
/// Deletes a score entry.
Future<bool> deleteScore({
required String playerId,
required String matchId,
required int roundNumber,
}) async {
final query = delete(scoreTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Deletes all scores for a specific match.
Future<bool> deleteScoresForMatch({required String matchId}) async {
final query = delete(scoreTable)..where((s) => s.matchId.equals(matchId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Deletes all scores for a specific player.
Future<bool> deleteScoresForPlayer({required String playerId}) async {
final query = delete(scoreTable)..where((s) => s.playerId.equals(playerId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Gets the latest round number for a match.
Future<int> getLatestRoundNumber({required String matchId}) async {
final query = selectOnly(scoreTable)
..where(scoreTable.matchId.equals(matchId))
..addColumns([scoreTable.roundNumber.max()]);
final result = await query.getSingle();
return result.read(scoreTable.roundNumber.max()) ?? 0;
}
/// Gets the total score for a player in a match (sum of all changes).
Future<int> getTotalScoreForPlayer({
required String playerId,
required String matchId,
}) async {
final scores = await getPlayerScoresInMatch(
playerId: playerId,
matchId: matchId,
);
if (scores.isEmpty) return 0;
// Return the score from the latest round
return scores.last.score;
}
}

View File

@@ -1,12 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'score_dao.dart';
// ignore_for_file: type=lint
mixin _$ScoreDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$ScoreTableTable get scoreTable => attachedDatabase.scoreTable;
}

View File

@@ -0,0 +1,328 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/score_entry_table.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
part 'score_entry_dao.g.dart';
@DriftAccessor(tables: [ScoreEntryTable])
class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
with _$ScoreEntryDaoMixin {
ScoreEntryDao(super.db);
/// Adds a score entry to the database.
Future<void> addScore({
required String playerId,
required String matchId,
required ScoreEntry entry,
}) async {
await into(scoreEntryTable).insert(
ScoreEntryTableCompanion.insert(
playerId: playerId,
matchId: matchId,
roundNumber: entry.roundNumber,
score: entry.score,
change: entry.change,
),
mode: InsertMode.insertOrReplace,
);
}
Future<void> addScoresAsList({
required List<ScoreEntry> entrys,
required String playerId,
required String matchId,
}) async {
if (entrys.isEmpty) return;
final entries = entrys
.map(
(score) => ScoreEntryTableCompanion.insert(
playerId: playerId,
matchId: matchId,
roundNumber: score.roundNumber,
score: score.score,
change: score.change,
),
)
.toList();
await batch((batch) {
batch.insertAll(
scoreEntryTable,
entries,
mode: InsertMode.insertOrReplace,
);
});
}
/// Retrieves the score for a specific round.
Future<ScoreEntry?> getScore({
required String playerId,
required String matchId,
int roundNumber = 0,
}) async {
final query = select(scoreEntryTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
);
final result = await query.getSingleOrNull();
if (result == null) return null;
return ScoreEntry(
roundNumber: result.roundNumber,
score: result.score,
change: result.change,
);
}
/// Retrieves all scores for a specific match.
Future<Map<String, List<ScoreEntry>>> getAllMatchScores({
required String matchId,
}) async {
final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId));
final result = await query.get();
final Map<String, List<ScoreEntry>> scoresByPlayer = {};
for (final row in result) {
final score = ScoreEntry(
roundNumber: row.roundNumber,
score: row.score,
change: row.change,
);
scoresByPlayer.putIfAbsent(row.playerId, () => []).add(score);
}
return scoresByPlayer;
}
/// Retrieves all scores for a specific player in a match.
Future<List<ScoreEntry>> getAllPlayerScoresInMatch({
required String playerId,
required String matchId,
}) async {
final query = select(scoreEntryTable)
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId))
..orderBy([(s) => OrderingTerm.asc(s.roundNumber)]);
final result = await query.get();
return result
.map(
(row) => ScoreEntry(
roundNumber: row.roundNumber,
score: row.score,
change: row.change,
),
)
.toList()
..sort(
(scoreA, scoreB) => scoreA.roundNumber.compareTo(scoreB.roundNumber),
);
}
/// Updates a score entry.
Future<bool> updateScore({
required String playerId,
required String matchId,
required ScoreEntry newEntry,
}) async {
final rowsAffected =
await (update(scoreEntryTable)..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(newEntry.roundNumber),
))
.write(
ScoreEntryTableCompanion(
score: Value(newEntry.score),
change: Value(newEntry.change),
),
);
return rowsAffected > 0;
}
/// Deletes a score entry.
Future<bool> deleteScore({
required String playerId,
required String matchId,
int roundNumber = 0,
}) async {
final query = delete(scoreEntryTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
Future<bool> deleteAllScoresForMatch({required String matchId}) async {
final query = delete(scoreEntryTable)
..where((s) => s.matchId.equals(matchId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
Future<bool> deleteAllScoresForPlayerInMatch({
required String matchId,
required String playerId,
}) async {
final query = delete(scoreEntryTable)
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Gets the highest (latest) round number for a match.
/// Returns `null` if there are no scores for the match.
Future<int?> getLatestRoundNumber({required String matchId}) async {
final query = selectOnly(scoreEntryTable)
..where(scoreEntryTable.matchId.equals(matchId))
..addColumns([scoreEntryTable.roundNumber.max()]);
final result = await query.getSingle();
return result.read(scoreEntryTable.roundNumber.max());
}
/// Aggregates the total score for a player in a match by summing all their
/// score entry changes. Returns `0` if there are no scores for the player
/// in the match.
Future<int> getTotalScoreForPlayer({
required String playerId,
required String matchId,
}) async {
final scores = await getAllPlayerScoresInMatch(
playerId: playerId,
matchId: matchId,
);
if (scores.isEmpty) return 0;
// Return the sum of all score changes
return scores.fold<int>(0, (sum, element) => sum + element.change);
}
Future<bool> hasWinner({required String matchId}) async {
return await getWinner(matchId: matchId) != null;
}
// Setting the winner for a game and clearing previous winner if exists.
Future<bool> setWinner({
required String matchId,
required String playerId,
}) async {
// Clear previous winner if exists
deleteAllScoresForMatch(matchId: matchId);
// Set the winner's score to 1
final rowsAffected = await into(scoreEntryTable).insert(
ScoreEntryTableCompanion.insert(
playerId: playerId,
matchId: matchId,
roundNumber: 0,
score: 1,
change: 0,
),
mode: InsertMode.insertOrReplace,
);
return rowsAffected > 0;
}
// Retrieves the winner of a match based on the highest score.
Future<Player?> getWinner({required String matchId}) async {
final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId))
..orderBy([(s) => OrderingTerm.desc(s.score)])
..limit(1);
final result = await query.getSingleOrNull();
if (result == null) return null;
final player = await db.playerDao.getPlayerById(playerId: result.playerId);
return Player(
id: player.id,
name: player.name,
createdAt: player.createdAt,
description: player.description,
);
}
/// Removes the winner of a match.
///
/// Returns `true` if the winner was removed, `false` if there are multiple
/// scores or if the winner cannot be removed.
Future<bool> removeWinner({required String matchId}) async {
final scores = await getAllMatchScores(matchId: matchId);
if (scores.length > 1) {
return false;
} else {
return await deleteAllScoresForMatch(matchId: matchId);
}
}
Future<bool> hasLooser({required String matchId}) async {
return await getLooser(matchId: matchId) != null;
}
// Setting the looser for a game and clearing previous looser if exists.
Future<bool> setLooser({
required String matchId,
required String playerId,
}) async {
// Clear previous loosers if exists
deleteAllScoresForMatch(matchId: matchId);
// Set the loosers score to 0
final rowsAffected = await into(scoreEntryTable).insert(
ScoreEntryTableCompanion.insert(
playerId: playerId,
matchId: matchId,
roundNumber: 0,
score: 0,
change: 0,
),
mode: InsertMode.insertOrReplace,
);
return rowsAffected > 0;
}
/// Retrieves the looser of a match based on the score 0.
Future<Player?> getLooser({required String matchId}) async {
final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId) & s.score.equals(0));
final result = await query.getSingleOrNull();
if (result == null) return null;
final player = await db.playerDao.getPlayerById(playerId: result.playerId);
return Player(
id: player.id,
name: player.name,
createdAt: player.createdAt,
description: player.description,
);
}
/// Removes the looser of a match.
///
/// Returns `true` if the looser was removed, `false` if there are multiple
/// scores or if the looser cannot be removed.
Future<bool> removeLooser({required String matchId}) async {
final scores = await getAllMatchScores(matchId: matchId);
if (scores.length > 1) {
return false;
} else {
return await deleteAllScoresForMatch(matchId: matchId);
}
}
}

View File

@@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'score_entry_dao.dart';
// ignore_for_file: type=lint
mixin _$ScoreEntryDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$ScoreEntryTableTable get scoreEntryTable => attachedDatabase.scoreEntryTable;
ScoreEntryDaoManager get managers => ScoreEntryDaoManager(this);
}
class ScoreEntryDaoManager {
final _$ScoreEntryDaoMixin _db;
ScoreEntryDaoManager(this._db);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
$$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$MatchTableTableTableManager get matchTable =>
$$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable);
$$ScoreEntryTableTableTableManager get scoreEntryTable =>
$$ScoreEntryTableTableTableManager(
_db.attachedDatabase,
_db.scoreEntryTable,
);
}

View File

@@ -1,8 +1,8 @@
import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/team_table.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/dto/team.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/team.dart';
part 'team_dao.g.dart';
@@ -144,4 +144,3 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
return rowsAffected > 0;
}
}

View File

@@ -5,4 +5,12 @@ part of 'team_dao.dart';
// ignore_for_file: type=lint
mixin _$TeamDaoMixin on DatabaseAccessor<AppDatabase> {
$TeamTableTable get teamTable => attachedDatabase.teamTable;
TeamDaoManager get managers => TeamDaoManager(this);
}
class TeamDaoManager {
final _$TeamDaoMixin _db;
TeamDaoManager(this._db);
$$TeamTableTableTableManager get teamTable =>
$$TeamTableTableTableManager(_db.attachedDatabase, _db.teamTable);
}

View File

@@ -7,7 +7,7 @@ import 'package:tallee/data/dao/match_dao.dart';
import 'package:tallee/data/dao/player_dao.dart';
import 'package:tallee/data/dao/player_group_dao.dart';
import 'package:tallee/data/dao/player_match_dao.dart';
import 'package:tallee/data/dao/score_dao.dart';
import 'package:tallee/data/dao/score_entry_dao.dart';
import 'package:tallee/data/dao/team_dao.dart';
import 'package:tallee/data/db/tables/game_table.dart';
import 'package:tallee/data/db/tables/group_table.dart';
@@ -15,7 +15,7 @@ import 'package:tallee/data/db/tables/match_table.dart';
import 'package:tallee/data/db/tables/player_group_table.dart';
import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:tallee/data/db/tables/player_table.dart';
import 'package:tallee/data/db/tables/score_table.dart';
import 'package:tallee/data/db/tables/score_entry_table.dart';
import 'package:tallee/data/db/tables/team_table.dart';
part 'database.g.dart';
@@ -29,7 +29,7 @@ part 'database.g.dart';
PlayerMatchTable,
GameTable,
TeamTable,
ScoreTable,
ScoreEntryTable,
],
daos: [
PlayerDao,
@@ -38,8 +38,8 @@ part 'database.g.dart';
PlayerGroupDao,
PlayerMatchDao,
GameDao,
ScoreDao,
TeamDao
ScoreEntryDao,
TeamDao,
],
)
class AppDatabase extends _$AppDatabase {
@@ -60,7 +60,9 @@ class AppDatabase extends _$AppDatabase {
static QueryExecutor _openConnection() {
return driftDatabase(
name: 'gametracker_db',
native: const DriftNativeOptions(databaseDirectory: getApplicationSupportDirectory),
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,11 @@ class MatchTable extends Table {
TextColumn get gameId =>
text().references(GameTable, #id, onDelete: KeyAction.cascade)();
// Nullable if there is no group associated with the match
TextColumn get groupId =>
text().references(GroupTable, #id, onDelete: KeyAction.cascade).nullable()();
TextColumn get name => text().nullable()();
// onDelete: If a group gets deleted, groupId in the match gets set to null
TextColumn get groupId => text()
.references(GroupTable, #id, onDelete: KeyAction.setNull)
.nullable()();
TextColumn get name => text()();
TextColumn get notes => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get endedAt => dateTime().nullable()();

View File

@@ -8,9 +8,7 @@ class PlayerMatchTable extends Table {
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
TextColumn get teamId =>
text().references(TeamTable, #id).nullable()();
IntColumn get score => integer()();
TextColumn get teamId => text().references(TeamTable, #id).nullable()();
@override
Set<Column<Object>> get primaryKey => {playerId, matchId};

View File

@@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
import 'package:tallee/data/db/tables/match_table.dart';
import 'package:tallee/data/db/tables/player_table.dart';
class ScoreTable extends Table {
class ScoreEntryTable extends Table {
TextColumn get playerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>

View File

@@ -1,61 +0,0 @@
import 'package:clock/clock.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:uuid/uuid.dart';
class Match {
final String id;
final DateTime createdAt;
final DateTime? endedAt;
final String name;
final Game game;
final Group? group;
final List<Player> players;
final String notes;
Player? winner;
Match({
String? id,
DateTime? createdAt,
this.endedAt,
required this.name,
required this.game,
this.group,
this.players = const [],
String? notes,
this.winner,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(),
notes = notes ?? '';
@override
String toString() {
return 'Match{id: $id, name: $name, game: $game, group: $group, players: $players, notes: $notes, endedAt: $endedAt}';
}
/// Creates a Match instance from a JSON object (ID references format).
/// Related objects are reconstructed from IDs by the DataTransferService.
Match.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
endedAt = json['endedAt'] != null ? DateTime.parse(json['endedAt']) : null,
name = json['name'],
game = Game(name: '', ruleset: Ruleset.singleWinner, description: '', color: GameColor.blue, icon: ''), // Populated during import via DataTransferService
group = null, // Populated during import via DataTransferService
players = [], // Populated during import via DataTransferService
notes = json['notes'] ?? '';
/// Converts the Match instance to a JSON object using normalized format (ID references only).
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'endedAt': endedAt?.toIso8601String(),
'name': name,
'gameId': game.id,
'groupId': group?.id,
'playerIds': players.map((player) => player.id).toList(),
'notes': notes,
};
}

View File

@@ -1,5 +1,5 @@
import 'package:clock/clock.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/player.dart';
import 'package:uuid/uuid.dart';
class Group {
@@ -24,16 +24,17 @@ class Group {
return 'Group{id: $id, name: $name, description: $description, members: $members}';
}
/// Creates a Group instance from a JSON object (memberIds format).
/// Player objects are reconstructed from memberIds by the DataTransferService.
/// Creates a Group instance from a JSON object where the related [Player]
/// objects are represented by their IDs.
Group.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'],
description = json['description'],
members = []; // Populated during import via DataTransferService
members = [];
/// Converts the Group instance to a JSON object using normalized format (memberIds only).
/// Converts the Group instance to a JSON object. Related [Player] objects are
/// represented by their IDs.
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),

View File

@@ -0,0 +1,80 @@
import 'package:clock/clock.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
import 'package:uuid/uuid.dart';
class Match {
final String id;
final DateTime createdAt;
final DateTime? endedAt;
final String name;
final Game game;
final Group? group;
final List<Player> players;
final String notes;
Map<String, List<ScoreEntry>> scores;
Player? winner;
Match({
String? id,
DateTime? createdAt,
this.endedAt,
required this.name,
required this.game,
this.group,
this.players = const [],
this.notes = '',
Map<String, List<ScoreEntry>>? scores,
this.winner,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(),
scores = scores ?? {for (var player in players) player.id: []};
@override
String toString() {
return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, winner: $winner}';
}
/// Creates a Match instance from a JSON object where related objects are
/// represented by their IDs. Therefore, the game, group, and players are not
/// fully constructed here.
Match.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
endedAt = json['endedAt'] != null
? DateTime.parse(json['endedAt'])
: null,
name = json['name'],
game = Game(
name: '',
ruleset: Ruleset.singleWinner,
description: '',
color: GameColor.blue,
icon: '',
),
group = null,
players = [],
scores = json['scores'],
notes = json['notes'] ?? '';
/// Converts the Match instance to a JSON object. Related objects are
/// represented by their IDs, so the game, group, and players are not fully
/// serialized here.
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'endedAt': endedAt?.toIso8601String(),
'name': name,
'gameId': game.id,
'groupId': group?.id,
'playerIds': players.map((player) => player.id).toList(),
'scores': scores.map(
(playerId, scoreList) =>
MapEntry(playerId, scoreList.map((score) => score.toJson()).toList()),
),
'notes': notes,
};
}

View File

@@ -0,0 +1,22 @@
class ScoreEntry {
int roundNumber = 0;
final int score;
final int change;
ScoreEntry({
required this.roundNumber,
required this.score,
required this.change,
});
ScoreEntry.fromJson(Map<String, dynamic> json)
: roundNumber = json['roundNumber'],
score = json['score'],
change = json['change'];
Map<String, dynamic> toJson() => {
'roundNumber': roundNumber,
'score': score,
'change': change,
};
}

View File

@@ -1,5 +1,5 @@
import 'package:clock/clock.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/player.dart';
import 'package:uuid/uuid.dart';
class Team {
@@ -29,7 +29,8 @@ class Team {
createdAt = DateTime.parse(json['createdAt']),
members = []; // Populated during import via DataTransferService
/// Converts the Team instance to a JSON object using normalized format (memberIds only).
/// Converts the Team instance to a JSON object. Related objects are
/// represented by their IDs.
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
@@ -37,4 +38,3 @@ class Team {
'memberIds': members.map((member) => member.id).toList(),
};
}

View File

@@ -62,8 +62,7 @@ import 'app_localizations_en.dart';
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
@@ -71,8 +70,7 @@ abstract class AppLocalizations {
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
@@ -84,18 +82,17 @@ abstract class AppLocalizations {
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('de'),
Locale('en'),
Locale('en')
];
/// Label for all players list
@@ -759,8 +756,7 @@ abstract class AppLocalizations {
String get yesterday_at;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
@@ -769,26 +765,25 @@ class _AppLocalizationsDelegate
}
@override
bool isSupported(Locale locale) =>
<String>['de', 'en'].contains(locale.languageCode);
bool isSupported(Locale locale) => <String>['de', 'en'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'de':
return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
case 'de': return AppLocalizationsDe();
case 'en': return AppLocalizationsEn();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
'that was used.'
);
}

View File

@@ -97,16 +97,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get enter_results => 'Ergebnisse eintragen';
@override
String get error_creating_group =>
'Fehler beim Erstellen der Gruppe, bitte erneut versuchen';
String get error_creating_group => 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen';
@override
String get error_deleting_group =>
'Fehler beim Löschen der Gruppe, bitte erneut versuchen';
String get error_deleting_group => 'Fehler beim Löschen der Gruppe, bitte erneut versuchen';
@override
String get error_editing_group =>
'Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen';
String get error_editing_group => 'Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen';
@override
String get error_reading_file => 'Fehler beim Lesen der Datei';
@@ -202,8 +199,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get no_players_created_yet => 'Noch keine Spieler:in erstellt';
@override
String get no_players_found_with_that_name =>
'Keine Spieler:in mit diesem Namen gefunden';
String get no_players_found_with_that_name => 'Keine Spieler:in mit diesem Namen gefunden';
@override
String get no_players_selected => 'Keine Spieler:innen ausgewählt';
@@ -262,20 +258,16 @@ class AppLocalizationsDe extends AppLocalizations {
String get ruleset => 'Regelwerk';
@override
String get ruleset_least_points =>
'Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.';
String get ruleset_least_points => 'Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.';
@override
String get ruleset_most_points =>
'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.';
String get ruleset_most_points => 'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.';
@override
String get ruleset_single_loser =>
'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.';
String get ruleset_single_loser => 'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.';
@override
String get ruleset_single_winner =>
'Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.';
String get ruleset_single_winner => 'Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.';
@override
String get save_changes => 'Änderungen speichern';
@@ -328,12 +320,10 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get there_is_no_group_matching_your_search =>
'Es gibt keine Gruppe, die deiner Suche entspricht';
String get there_is_no_group_matching_your_search => 'Es gibt keine Gruppe, die deiner Suche entspricht';
@override
String get this_cannot_be_undone =>
'Dies kann nicht rückgängig gemacht werden.';
String get this_cannot_be_undone => 'Dies kann nicht rückgängig gemacht werden.';
@override
String get today_at => 'Heute um';

View File

@@ -97,16 +97,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get enter_results => 'Enter Results';
@override
String get error_creating_group =>
'Error while creating group, please try again';
String get error_creating_group => 'Error while creating group, please try again';
@override
String get error_deleting_group =>
'Error while deleting group, please try again';
String get error_deleting_group => 'Error while deleting group, please try again';
@override
String get error_editing_group =>
'Error while editing group, please try again';
String get error_editing_group => 'Error while editing group, please try again';
@override
String get error_reading_file => 'Error reading file';
@@ -202,8 +199,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get no_players_created_yet => 'No players created yet';
@override
String get no_players_found_with_that_name =>
'No players found with that name';
String get no_players_found_with_that_name => 'No players found with that name';
@override
String get no_players_selected => 'No players selected';
@@ -262,20 +258,16 @@ class AppLocalizationsEn extends AppLocalizations {
String get ruleset => 'Ruleset';
@override
String get ruleset_least_points =>
'Inverse scoring: the player with the fewest points wins.';
String get ruleset_least_points => 'Inverse scoring: the player with the fewest points wins.';
@override
String get ruleset_most_points =>
'Traditional ruleset: the player with the most points wins.';
String get ruleset_most_points => 'Traditional ruleset: the player with the most points wins.';
@override
String get ruleset_single_loser =>
'Exactly one loser is determined; last place receives the penalty or consequence.';
String get ruleset_single_loser => 'Exactly one loser is determined; last place receives the penalty or consequence.';
@override
String get ruleset_single_winner =>
'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.';
String get ruleset_single_winner => 'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.';
@override
String get save_changes => 'Save Changes';
@@ -328,8 +320,7 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get there_is_no_group_matching_your_search =>
'There is no group matching your search';
String get there_is_no_group_matching_your_search => 'There is no group matching your search';
@override
String get this_cannot_be_undone => 'This can\'t be undone.';

View File

@@ -1,21 +1,24 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/constants.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/dto/group.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/player_selection.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
class CreateGroupView extends StatefulWidget {
const CreateGroupView({super.key, this.groupToEdit});
const CreateGroupView({super.key, this.groupToEdit, this.onMembersChanged});
/// The group to edit, if any
final Group? groupToEdit;
final VoidCallback? onMembersChanged;
@override
State<CreateGroupView> createState() => _CreateGroupViewState();
}
@@ -69,49 +72,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
title: Text(
widget.groupToEdit == null ? loc.create_new_group : loc.edit_group,
),
actions: widget.groupToEdit == null
? []
: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
if (widget.groupToEdit != null) {
showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(loc.delete_group),
content: Text(loc.this_cannot_be_undone),
actions: [
TextButton(
onPressed: () =>
Navigator.of(context).pop(false),
child: Text(loc.cancel),
),
TextButton(
onPressed: () =>
Navigator.of(context).pop(true),
child: Text(loc.delete),
),
],
),
).then((confirmed) async {
if (confirmed == true && context.mounted) {
bool success = await db.groupDao.deleteGroup(
groupId: widget.groupToEdit!.id,
);
if (!context.mounted) return;
if (success) {
Navigator.pop(context);
} else {
if (!mounted) return;
showSnackbar(message: loc.error_deleting_group);
}
}
});
}
},
),
],
),
body: SafeArea(
child: Column(
@@ -122,6 +82,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
child: TextInputField(
controller: _groupNameController,
hintText: loc.group_name,
maxLength: Constants.MAX_GROUP_NAME_LENGTH,
),
),
Expanded(
@@ -144,42 +105,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
(_groupNameController.text.isEmpty ||
(selectedPlayers.length < 2))
? null
: () async {
late Group? updatedGroup;
late bool success;
if (widget.groupToEdit == null) {
success = await db.groupDao.addGroup(
group: Group(
name: _groupNameController.text.trim(),
members: selectedPlayers,
),
);
} else {
updatedGroup = Group(
id: widget.groupToEdit!.id,
name: _groupNameController.text.trim(),
description: '',
members: selectedPlayers,
);
//TODO: Implement group editing in database
/*
success = await db.groupDao.updateGroup(
group: updatedGroup,
);
*/
success = true;
}
if (!context.mounted) return;
if (success) {
Navigator.pop(context, updatedGroup);
} else {
showSnackbar(
message: widget.groupToEdit == null
? loc.error_creating_group
: loc.error_editing_group,
);
}
},
: _saveGroup,
),
const SizedBox(height: 20),
],
@@ -189,6 +115,104 @@ class _CreateGroupViewState extends State<CreateGroupView> {
);
}
/// Saves the group by creating a new one or updating the existing one,
/// depending on whether the widget is in edit mode.
Future<void> _saveGroup() async {
final loc = AppLocalizations.of(context);
late bool success;
Group? updatedGroup;
if (widget.groupToEdit == null) {
success = await _createGroup();
} else {
final result = await _editGroup();
success = result.$1;
updatedGroup = result.$2;
}
if (!mounted) return;
if (success) {
Navigator.pop(context, updatedGroup);
} else {
showSnackbar(
message: widget.groupToEdit == null
? loc.error_creating_group
: loc.error_editing_group,
);
}
}
/// Handles creating a new group and returns whether the operation was successful.
Future<bool> _createGroup() async {
final groupName = _groupNameController.text.trim();
final success = await db.groupDao.addGroup(
group: Group(name: groupName, description: '', members: selectedPlayers),
);
return success;
}
/// Handles editing an existing group and returns a tuple of
/// (success, updatedGroup).
Future<(bool, Group?)> _editGroup() async {
final groupName = _groupNameController.text.trim();
Group? updatedGroup = Group(
id: widget.groupToEdit!.id,
name: groupName,
description: '',
members: selectedPlayers,
);
bool successfullNameChange = true;
bool successfullMemberChange = true;
if (widget.groupToEdit!.name != groupName) {
successfullNameChange = await db.groupDao.updateGroupName(
groupId: widget.groupToEdit!.id,
newName: groupName,
);
}
if (widget.groupToEdit!.members != selectedPlayers) {
successfullMemberChange = await db.groupDao.replaceGroupPlayers(
groupId: widget.groupToEdit!.id,
newPlayers: selectedPlayers,
);
await deleteObsoleteMatchGroupRelations();
widget.onMembersChanged?.call();
}
final success = successfullNameChange && successfullMemberChange;
return (success, updatedGroup);
}
/// Removes the group association from matches that no longer belong to the edited group.
///
/// After updating the group's members, matches that were previously linked to
/// this group but don't have any of the newly selected players are considered
/// obsolete. For each such match, the group association is removed by setting
/// its [groupId] to null.
Future<void> deleteObsoleteMatchGroupRelations() async {
final groupMatches = await db.matchDao.getGroupMatches(
groupId: widget.groupToEdit!.id,
);
final selectedPlayerIds = selectedPlayers.map((p) => p.id).toSet();
final relationshipsToDelete = groupMatches.where((match) {
return !match.players.any(
(player) => selectedPlayerIds.contains(player.id),
);
}).toList();
for (var match in relationshipsToDelete) {
await db.matchDao.removeMatchGroup(matchId: match.id);
}
}
/// Displays a snackbar with the given message and optional action.
///
/// [message] The message to display in the snackbar.

View File

@@ -4,9 +4,9 @@ import 'package:provider/provider.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.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/views/main_menu/group_view/create_group_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart';
@@ -191,7 +191,12 @@ class _GroupDetailViewState extends State<GroupDetailView> {
context,
adaptivePageRoute(
builder: (context) {
return CreateGroupView(groupToEdit: _group);
return CreateGroupView(
groupToEdit: _group,
onMembersChanged: () {
_loadStatistics();
},
);
},
),
);
@@ -242,10 +247,8 @@ class _GroupDetailViewState extends State<GroupDetailView> {
/// Loads statistics for this group
Future<void> _loadStatistics() async {
final matches = await db.matchDao.getAllMatches();
final groupMatches = matches
.where((match) => match.group?.id == _group.id)
.toList();
isLoading = true;
final groupMatches = await db.matchDao.getGroupMatches(groupId: _group.id);
setState(() {
totalMatches = groupMatches.length;
@@ -260,7 +263,9 @@ class _GroupDetailViewState extends State<GroupDetailView> {
// Count wins for each player
for (var match in matches) {
if (match.winner != null) {
if (match.winner != null &&
_group.members.any((m) => m.id == match.winner?.id)) {
print(match.winner);
bestPlayerCounts.update(
match.winner!,
(value) => value + 1,

View File

@@ -4,8 +4,8 @@ import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart';
import 'package:tallee/presentation/views/main_menu/group_view/group_detail_view.dart';

View File

@@ -4,10 +4,10 @@ import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.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/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart';
@@ -42,7 +42,13 @@ class _HomeViewState extends State<HomeView> {
2,
Match(
name: 'Skeleton Match',
game: Game(name: '', ruleset: Ruleset.singleWinner, description: '', color: GameColor.blue, icon: ''),
game: Game(
name: '',
ruleset: Ruleset.singleWinner,
description: '',
color: GameColor.blue,
icon: '',
),
group: Group(
name: 'Skeleton Group',
description: '',
@@ -104,7 +110,9 @@ class _HomeViewState extends State<HomeView> {
if (recentMatches.isNotEmpty)
for (Match match in recentMatches)
Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
padding: const EdgeInsets.symmetric(
vertical: 6.0,
),
child: MatchTile(
compact: true,
width: constraints.maxWidth * 0.9,
@@ -113,7 +121,8 @@ class _HomeViewState extends State<HomeView> {
await Navigator.of(context).push(
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) => MatchResultView(match: match),
builder: (context) =>
MatchResultView(match: match),
),
);
await updatedWinnerInRecentMatches(match.id);
@@ -121,7 +130,10 @@ class _HomeViewState extends State<HomeView> {
),
)
else
Center(heightFactor: 5, child: Text(loc.no_recent_matches_available)),
Center(
heightFactor: 5,
child: Text(loc.no_recent_matches_available),
),
],
),
),
@@ -137,22 +149,40 @@ class _HomeViewState extends State<HomeView> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
QuickCreateButton(text: 'Category 1', onPressed: () {}),
QuickCreateButton(text: 'Category 2', onPressed: () {}),
QuickCreateButton(
text: 'Category 1',
onPressed: () {},
),
QuickCreateButton(
text: 'Category 2',
onPressed: () {},
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
QuickCreateButton(text: 'Category 3', onPressed: () {}),
QuickCreateButton(text: 'Category 4', onPressed: () {}),
QuickCreateButton(
text: 'Category 3',
onPressed: () {},
),
QuickCreateButton(
text: 'Category 4',
onPressed: () {},
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
QuickCreateButton(text: 'Category 5', onPressed: () {}),
QuickCreateButton(text: 'Category 6', onPressed: () {}),
QuickCreateButton(
text: 'Category 5',
onPressed: () {},
),
QuickCreateButton(
text: 'Category 6',
onPressed: () {},
),
],
),
],
@@ -181,9 +211,11 @@ class _HomeViewState extends State<HomeView> {
matchCount = results[0] as int;
groupCount = results[1] as int;
loadedRecentMatches = results[2] as List<Match>;
recentMatches = (loadedRecentMatches..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2)
.toList();
recentMatches =
(loadedRecentMatches
..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2)
.toList();
if (mounted) {
setState(() {
isLoading = false;
@@ -195,7 +227,7 @@ class _HomeViewState extends State<HomeView> {
/// Updates the winner information for a specific match in the recent matches list.
Future<void> updatedWinnerInRecentMatches(String matchId) async {
final db = Provider.of<AppDatabase>(context, listen: false);
final winner = await db.matchDao.getWinner(matchId: matchId);
final winner = await db.scoreEntryDao.getWinner(matchId: matchId);
final matchIndex = recentMatches.indexWhere((match) => match.id == matchId);
if (matchIndex != -1) {
setState(() {

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:tallee/presentation/widgets/tiles/group_tile.dart';

View File

@@ -5,10 +5,10 @@ import 'package:tallee/core/constants.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/dto/game.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.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/views/main_menu/match_view/create_match/choose_game_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart';

View File

@@ -6,7 +6,7 @@ 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/dto/match.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';

View File

@@ -3,8 +3,8 @@ import 'package:provider/provider.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/dto/match.dart';
import 'package:tallee/data/dto/player.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/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart';
@@ -204,11 +204,11 @@ class _MatchResultViewState extends State<MatchResultView> {
Future<bool> _handleWinner() async {
if (_selectedPlayer == null) {
return await db.matchDao.removeWinner(matchId: widget.match.id);
await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
} else {
return await db.matchDao.setWinner(
await db.scoreEntryDao.setWinner(
matchId: widget.match.id,
winnerId: _selectedPlayer!.id,
playerId: _selectedPlayer!.id,
);
}
}

View File

@@ -6,10 +6,10 @@ import 'package:tallee/core/constants.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/dto/game.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.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/views/main_menu/match_view/create_match/create_match_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_detail_view.dart';

View File

@@ -70,6 +70,8 @@ const allDependencies = <Package>[
_http_parser,
_intl,
_io,
_jni,
_jni_flutter,
_js,
_json_annotation,
_json_schema,
@@ -360,13 +362,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// async 2.13.0
/// async 2.13.1
const _async = Package(
name: 'async',
description: "Utility functions and classes related to the 'dart:async' library.",
repository: 'https://github.com/dart-lang/core/tree/main/pkgs/async',
authors: [],
version: '2.13.0',
version: '2.13.1',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -442,13 +444,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// build 4.0.4
/// build 4.0.5
const _build = Package(
name: 'build',
description: 'A package for authoring build_runner compatible code generators.',
repository: 'https://github.com/dart-lang/build/tree/master/build',
authors: [],
version: '4.0.4',
version: '4.0.5',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -565,13 +567,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// build_runner 2.12.2
/// build_runner 2.13.1
const _build_runner = Package(
name: 'build_runner',
description: 'A build system for Dart code generation and modular compilation.',
repository: 'https://github.com/dart-lang/build/tree/master/build_runner',
authors: [],
version: '2.12.2',
version: '2.13.1',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -649,14 +651,14 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// built_value 8.12.4
/// built_value 8.12.5
const _built_value = Package(
name: 'built_value',
description: '''Value types with builders, Dart classes as enums, and serialization. This library is the runtime dependency.
''',
repository: 'https://github.com/google/built_value.dart/tree/master/built_value',
authors: [],
version: '8.12.4',
version: '8.12.5',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -1436,18 +1438,18 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// cupertino_icons 1.0.8
/// cupertino_icons 1.0.9
const _cupertino_icons = Package(
name: 'cupertino_icons',
description: 'Default icons asset for Cupertino widgets based on Apple styled icons',
repository: 'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons',
authors: [],
version: '1.0.8',
version: '1.0.9',
spdxIdentifiers: ['MIT'],
isMarkdown: false,
isSdk: false,
dependencies: [],
devDependencies: [PackageRef('flutter'), PackageRef('flutter_test')],
devDependencies: [PackageRef('collection'), PackageRef('flutter'), PackageRef('flutter_test'), PackageRef('path')],
license: '''The MIT License (MIT)
Copyright (c) 2016 Vladimir Kharlampidi
@@ -2627,13 +2629,13 @@ const _flutter_localizations = Package(
devDependencies: [PackageRef('flutter_test')],
);
/// flutter_plugin_android_lifecycle 2.0.33
/// flutter_plugin_android_lifecycle 2.0.34
const _flutter_plugin_android_lifecycle = Package(
name: 'flutter_plugin_android_lifecycle',
description: 'Flutter plugin for accessing an Android Lifecycle within other plugins.',
repository: 'https://github.com/flutter/packages/tree/main/packages/flutter_plugin_android_lifecycle',
authors: [],
version: '2.0.33',
version: '2.0.34',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -3173,6 +3175,88 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// jni 1.0.0
const _jni = Package(
name: 'jni',
description: 'A library to access JNI from Dart and Flutter that acts as a support library for package:jnigen.',
repository: 'https://github.com/dart-lang/native/tree/main/pkgs/jni',
authors: [],
version: '1.0.0',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('args'), PackageRef('collection'), PackageRef('ffi'), PackageRef('meta'), PackageRef('package_config'), PackageRef('path'), PackageRef('plugin_platform_interface')],
devDependencies: [PackageRef('dart_style'), PackageRef('logging'), PackageRef('test')],
license: '''Copyright 2022, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// jni_flutter 1.0.1
const _jni_flutter = Package(
name: 'jni_flutter',
description: 'A library to access Flutter Android specific APIs from Dart.',
repository: 'https://github.com/dart-lang/native/tree/main/pkgs/jni_flutter',
authors: [],
version: '1.0.1',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('flutter'), PackageRef('jni')],
devDependencies: [PackageRef('flutter_test')],
license: '''Copyright 2026, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// js 0.7.2
const _js = Package(
name: 'js',
@@ -3511,13 +3595,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// markdown 7.3.0
/// markdown 7.3.1
const _markdown = Package(
name: 'markdown',
description: 'A portable Markdown library written in Dart that can parse Markdown into HTML.',
repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/markdown',
authors: [],
version: '7.3.0',
version: '7.3.1',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -3890,13 +3974,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// native_toolchain_c 0.17.5
/// native_toolchain_c 0.17.6
const _native_toolchain_c = Package(
name: 'native_toolchain_c',
description: 'A library to invoke the native C compiler installed on the host machine.',
repository: 'https://github.com/dart-lang/native/tree/main/pkgs/native_toolchain_c',
authors: [],
version: '0.17.5',
version: '0.17.6',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -4110,14 +4194,14 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// package_info_plus 9.0.0
/// package_info_plus 9.0.1
const _package_info_plus = Package(
name: 'package_info_plus',
description: 'Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android.',
homepage: 'https://github.com/fluttercommunity/plus_plugins',
repository: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus/package_info_plus',
authors: [],
version: '9.0.0',
version: '9.0.1',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -4194,13 +4278,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// pana 0.23.10
/// pana 0.23.11
const _pana = Package(
name: 'pana',
description: 'PAckage aNAlyzer - produce a report summarizing the health and quality of a Dart package.',
repository: 'https://github.com/dart-lang/pana',
authors: [],
version: '0.23.10',
version: '0.23.11',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -4315,17 +4399,17 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// path_provider_android 2.2.22
/// path_provider_android 2.3.1
const _path_provider_android = Package(
name: 'path_provider_android',
description: 'Android implementation of the path_provider plugin.',
repository: 'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_android',
authors: [],
version: '2.2.22',
version: '2.3.1',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('flutter'), PackageRef('path_provider_platform_interface')],
dependencies: [PackageRef('flutter'), PackageRef('jni'), PackageRef('jni_flutter'), PackageRef('path_provider_platform_interface')],
devDependencies: [PackageRef('flutter_test'), PackageRef('test')],
license: '''Copyright 2013 The Flutter Authors
@@ -36119,13 +36203,13 @@ Copyright (C) 2009-2017, International Business Machines Corporation,
Google, and others. All Rights Reserved.''',
);
/// source_gen 4.2.0
/// source_gen 4.2.2
const _source_gen = Package(
name: 'source_gen',
description: 'Source code generation builders and utilities for the Dart build system',
repository: 'https://github.com/dart-lang/source_gen/tree/master/source_gen',
authors: [],
version: '4.2.0',
version: '4.2.2',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -36318,13 +36402,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''',
);
/// sqlite3_flutter_libs 0.5.41
/// sqlite3_flutter_libs 0.5.42
const _sqlite3_flutter_libs = Package(
name: 'sqlite3_flutter_libs',
description: 'Flutter plugin to include native sqlite3 libraries with your app',
homepage: 'https://github.com/simolus3/sqlite3.dart/tree/v2/sqlite3_flutter_libs',
authors: [],
version: '0.5.41',
version: '0.5.42',
spdxIdentifiers: ['MIT'],
isMarkdown: false,
isSdk: false,
@@ -36835,13 +36919,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// url_launcher_android 6.3.28
/// url_launcher_android 6.3.29
const _url_launcher_android = Package(
name: 'url_launcher_android',
description: 'Android implementation of the url_launcher plugin.',
repository: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_android',
authors: [],
version: '6.3.28',
version: '6.3.29',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -37796,12 +37880,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''',
);
/// tallee 0.0.18+252
/// tallee 0.0.20+254
const _tallee = Package(
name: 'tallee',
description: 'Tracking App for Card Games',
authors: [],
version: '0.0.18+252',
version: '0.0.20+254',
spdxIdentifiers: ['LGPL-3.0'],
isMarkdown: false,
isSdk: false,

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.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/tiles/statistics_tile.dart';
@@ -167,7 +167,8 @@ class _StatisticsViewState extends State<StatisticsView> {
final playerId = winCounts[i].$1;
final player = players.firstWhere(
(p) => p.id == playerId,
orElse: () => Player(id: playerId, name: loc.not_available, description: ''),
orElse: () =>
Player(id: playerId, name: loc.not_available, description: ''),
);
winCounts[i] = (player.name, winCounts[i].$2);
}
@@ -208,11 +209,11 @@ class _StatisticsViewState extends State<StatisticsView> {
// -1 means player not found in matchCounts
if (index != -1) {
final current = matchCounts[index].$2;
matchCounts[index] = (playerId, current + 1);
} else {
matchCounts.add((playerId, 1));
}
matchCounts[index] = (playerId, current + 1);
} else {
matchCounts.add((playerId, 1));
}
}
}
// Adding all players with zero matches
@@ -229,7 +230,8 @@ class _StatisticsViewState extends State<StatisticsView> {
final playerId = matchCounts[i].$1;
final player = players.firstWhere(
(p) => p.id == playerId,
orElse: () => Player(id: playerId, name: loc.not_available, description: ''),
orElse: () =>
Player(id: playerId, name: loc.not_available, description: ''),
);
matchCounts[i] = (player.name, matchCounts[i].$2);
}

View File

@@ -3,7 +3,7 @@ import 'package:provider/provider.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/player.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/text_input/custom_search_bar.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class GroupTile extends StatefulWidget {

View File

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';

View File

@@ -8,16 +8,17 @@ import 'package:json_schema/json_schema.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/dto/team.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/data/models/team.dart';
class DataTransferService {
/// Deletes all data from the database.
static Future<void> deleteAllData(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false);
await db.matchDao.deleteAllMatches();
await db.teamDao.deleteAllTeams();
await db.groupDao.deleteAllGroups();
@@ -40,33 +41,53 @@ class DataTransferService {
'players': players.map((p) => p.toJson()).toList(),
'games': games.map((g) => g.toJson()).toList(),
'groups': groups
.map((g) => {
'id': g.id,
'name': g.name,
'description': g.description,
'createdAt': g.createdAt.toIso8601String(),
'memberIds': (g.members).map((m) => m.id).toList(),
})
.map(
(g) => {
'id': g.id,
'name': g.name,
'description': g.description,
'createdAt': g.createdAt.toIso8601String(),
'memberIds': (g.members).map((m) => m.id).toList(),
},
)
.toList(),
'teams': teams
.map((t) => {
'id': t.id,
'name': t.name,
'createdAt': t.createdAt.toIso8601String(),
'memberIds': (t.members).map((m) => m.id).toList(),
})
.map(
(t) => {
'id': t.id,
'name': t.name,
'createdAt': t.createdAt.toIso8601String(),
'memberIds': (t.members).map((m) => m.id).toList(),
},
)
.toList(),
'matches': matches
.map((m) => {
'id': m.id,
'name': m.name,
'createdAt': m.createdAt.toIso8601String(),
'endedAt': m.endedAt?.toIso8601String(),
'gameId': m.game.id,
'groupId': m.group?.id,
'playerIds': m.players.map((p) => p.id).toList(),
'notes': m.notes,
})
.map(
(m) => {
'id': m.id,
'name': m.name,
'createdAt': m.createdAt.toIso8601String(),
'endedAt': m.endedAt?.toIso8601String(),
'gameId': m.game.id,
'groupId': m.group?.id,
'playerIds': m.players.map((p) => p.id).toList(),
'scores': m.scores.map(
(playerId, scores) => MapEntry(
playerId,
scores
.map(
(s) => {
'roundNumber': s.roundNumber,
'score': s.score,
'change': s.change,
},
)
.toList(),
),
),
'notes': m.notes,
},
)
.toList(),
};
@@ -76,12 +97,12 @@ class DataTransferService {
/// Exports the given JSON string to a file with the specified name.
/// Returns an [ExportResult] indicating the outcome.
///
/// [jsonString] The JSON string to be exported.
/// [fileName] The desired name for the exported file (without extension).
/// - [jsonString]: The JSON string to be exported.
/// - [fileName]: The desired name for the exported file (without extension).
static Future<ExportResult> exportData(
String jsonString,
String fileName
) async {
String jsonString,
String fileName,
) async {
try {
final bytes = Uint8List.fromList(utf8.encode(jsonString));
final path = await FilePicker.platform.saveFile(
@@ -94,7 +115,6 @@ class DataTransferService {
} else {
return ExportResult.success;
}
} catch (e, stack) {
print('[exportData] $e');
print(stack);
@@ -119,110 +139,12 @@ class DataTransferService {
final jsonString = await _readFileContent(path.files.single);
if (jsonString == null) return ImportResult.fileReadError;
final isValid = await _validateJsonSchema(jsonString);
final isValid = await validateJsonSchema(jsonString);
if (!isValid) return ImportResult.invalidSchema;
final Map<String, dynamic> decoded = json.decode(jsonString) as Map<String, dynamic>;
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final List<dynamic> playersJson = (decoded['players'] as List<dynamic>?) ?? [];
final List<dynamic> gamesJson = (decoded['games'] as List<dynamic>?) ?? [];
final List<dynamic> groupsJson = (decoded['groups'] as List<dynamic>?) ?? [];
final List<dynamic> teamsJson = (decoded['teams'] as List<dynamic>?) ?? [];
final List<dynamic> matchesJson = (decoded['matches'] as List<dynamic>?) ?? [];
// Import Players
final List<Player> importedPlayers = playersJson
.map((p) => Player.fromJson(p as Map<String, dynamic>))
.toList();
final Map<String, Player> playerById = {
for (final p in importedPlayers) p.id: p,
};
// Import Games
final List<Game> importedGames = gamesJson
.map((g) => Game.fromJson(g as Map<String, dynamic>))
.toList();
final Map<String, Game> gameById = {
for (final g in importedGames) g.id: g,
};
// Import Groups
final List<Group> importedGroups = groupsJson.map((g) {
final map = g as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? []).cast<String>();
final members = memberIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Group(
id: map['id'] as String,
name: map['name'] as String,
description: map['description'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
final Map<String, Group> groupById = {
for (final g in importedGroups) g.id: g,
};
// Import Teams
final List<Team> importedTeams = teamsJson.map((t) {
final map = t as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? []).cast<String>();
final members = memberIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Team(
id: map['id'] as String,
name: map['name'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
// Import Matches
final List<Match> importedMatches = matchesJson.map((m) {
final map = m as Map<String, dynamic>;
final String gameId = map['gameId'] as String;
final String? groupId = map['groupId'] as String?;
final List<String> playerIds = (map['playerIds'] as List<dynamic>? ?? []).cast<String>();
final DateTime? endedAt = map['endedAt'] != null ? DateTime.parse(map['endedAt'] as String) : null;
final game = gameById[gameId];
final group = (groupId == null) ? null : groupById[groupId];
final players = playerIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Match(
id: map['id'] as String,
name: map['name'] as String,
game: game ?? Game(name: 'Unknown', ruleset: Ruleset.singleWinner, description: '', color: GameColor.blue, icon: ''),
group: group,
players: players,
createdAt: DateTime.parse(map['createdAt'] as String),
endedAt: endedAt,
notes: map['notes'] as String? ?? '',
);
}).toList();
// Import all data into the database
await db.playerDao.addPlayersAsList(players: importedPlayers);
await db.gameDao.addGamesAsList(games: importedGames);
await db.groupDao.addGroupsAsList(groups: importedGroups);
await db.teamDao.addTeamsAsList(teams: importedTeams);
await db.matchDao.addMatchAsList(matches: importedMatches);
await importDataToDatabase(db, decoded);
return ImportResult.success;
} on FormatException catch (e, stack) {
@@ -238,6 +160,167 @@ class DataTransferService {
}
}
/// Imports parsed JSON data into the database.
@visibleForTesting
static Future<void> importDataToDatabase(
AppDatabase db,
Map<String, dynamic> decodedJson,
) async {
// Fetch all entities first to create lookup maps for relationships
final importedPlayers = parsePlayersFromJson(decodedJson);
final playerById = {for (final p in importedPlayers) p.id: p};
final importedGames = parseGamesFromJson(decodedJson);
final gameById = {for (final g in importedGames) g.id: g};
final importedGroups = parseGroupsFromJson(decodedJson, playerById);
final groupById = {for (final g in importedGroups) g.id: g};
final importedTeams = parseTeamsFromJson(decodedJson, playerById);
final importedMatches = parseMatchesFromJson(
decodedJson,
gameById,
groupById,
playerById,
);
await db.playerDao.addPlayersAsList(players: importedPlayers);
await db.gameDao.addGamesAsList(games: importedGames);
await db.groupDao.addGroupsAsList(groups: importedGroups);
await db.teamDao.addTeamsAsList(teams: importedTeams);
await db.matchDao.addMatchAsList(matches: importedMatches);
}
/* Parsing Methods */
@visibleForTesting
static List<Player> parsePlayersFromJson(Map<String, dynamic> decodedJson) {
final playersJson = (decodedJson['players'] as List<dynamic>?) ?? [];
return playersJson
.map((p) => Player.fromJson(p as Map<String, dynamic>))
.toList();
}
@visibleForTesting
static List<Game> parseGamesFromJson(Map<String, dynamic> decodedJson) {
final gamesJson = (decodedJson['games'] as List<dynamic>?) ?? [];
return gamesJson
.map((g) => Game.fromJson(g as Map<String, dynamic>))
.toList();
}
@visibleForTesting
static List<Group> parseGroupsFromJson(
Map<String, dynamic> decodedJson,
Map<String, Player> playerById,
) {
final groupsJson = (decodedJson['groups'] as List<dynamic>?) ?? [];
return groupsJson.map((g) {
final map = g as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? [])
.cast<String>();
final members = memberIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Group(
id: map['id'] as String,
name: map['name'] as String,
description: map['description'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
}
/// Parses teams from JSON data.
@visibleForTesting
static List<Team> parseTeamsFromJson(
Map<String, dynamic> decodedJson,
Map<String, Player> playerById,
) {
final teamsJson = (decodedJson['teams'] as List<dynamic>?) ?? [];
return teamsJson.map((t) {
final map = t as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? [])
.cast<String>();
final members = memberIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Team(
id: map['id'] as String,
name: map['name'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
}
/// Parses matches from JSON data.
@visibleForTesting
static List<Match> parseMatchesFromJson(
Map<String, dynamic> decodedJson,
Map<String, Game> gamesMap,
Map<String, Group> groupsMap,
Map<String, Player> playersMap,
) {
final matchesJson = (decodedJson['matches'] as List<dynamic>?) ?? [];
return matchesJson.map((m) {
final map = m as Map<String, dynamic>;
// Extract attributes from json
final id = map['id'] as String;
final name = map['name'] as String;
final gameId = map['gameId'] as String;
final groupId = map['groupId'] as String?;
final createdAt = DateTime.parse(map['createdAt'] as String);
final endedAt = map['endedAt'] != null
? DateTime.parse(map['endedAt'] as String)
: null;
final notes = map['notes'] as String? ?? '';
// Link attributes to objects
final game = gamesMap[gameId] ?? getFallbackGame();
final group = groupId != null ? groupsMap[groupId] : null;
final playerIds = (map['playerIds'] as List<dynamic>? ?? [])
.cast<String>();
final players = playerIds
.map((id) => playersMap[id])
.whereType<Player>()
.toList();
return Match(
id: id,
name: name,
game: game,
group: group,
players: players,
createdAt: createdAt,
endedAt: endedAt,
notes: notes,
);
}).toList();
}
/// Creates a fallback game when the referenced game is not found.
@visibleForTesting
static Game getFallbackGame() {
return Game(
name: 'Unknown',
ruleset: Ruleset.singleWinner,
description: '',
color: GameColor.blue,
icon: '',
);
}
/// Helper method to read file content from either bytes or path
static Future<String?> _readFileContent(PlatformFile file) async {
if (file.bytes != null) return utf8.decode(file.bytes!);
@@ -245,8 +328,10 @@ class DataTransferService {
return null;
}
/// Validates the given JSON string against the predefined schema.
static Future<bool> _validateJsonSchema(String jsonString) async {
/// Validates the given JSON string against the schema
/// in `assets/schema.json`.
@visibleForTesting
static Future<bool> validateJsonSchema(String jsonString) async {
final String schemaString;
schemaString = await rootBundle.loadString('assets/schema.json');

View File

@@ -1,7 +1,7 @@
name: tallee
description: "Tracking App for Card Games"
publish_to: 'none'
version: 0.0.18+252
version: 0.0.20+254
environment:
sdk: ^3.8.1
@@ -31,9 +31,9 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.5.4
build_runner: ^2.7.0
dart_pubspec_licenses: ^3.0.14
drift_dev: ^2.27.0
drift_dev: ^2.29.0
flutter_lints: ^6.0.0
flutter:

View File

@@ -3,8 +3,8 @@ import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
void main() {
late AppDatabase database;
@@ -62,7 +62,6 @@ void main() {
await database.close();
});
group('Group Tests', () {
// Verifies that a single group can be added and retrieved with all fields and members intact.
test('Adding and fetching a single group works correctly', () async {
await database.groupDao.addGroup(group: testGroup1);
@@ -277,20 +276,20 @@ void main() {
});
// Verifies that updateGroupDescription returns false for a non-existent group.
test('updateGroupDescription returns false for non-existent group',
() async {
final updated = await database.groupDao.updateGroupDescription(
groupId: 'non-existent-id',
newDescription: 'New Description',
);
expect(updated, false);
});
test(
'updateGroupDescription returns false for non-existent group',
() async {
final updated = await database.groupDao.updateGroupDescription(
groupId: 'non-existent-id',
newDescription: 'New Description',
);
expect(updated, false);
},
);
// Verifies that deleteAllGroups removes all groups from the database.
test('deleteAllGroups removes all groups', () async {
await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup2],
);
await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]);
final countBefore = await database.groupDao.getGroupCount();
expect(countBefore, 2);

View File

@@ -4,10 +4,10 @@ import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.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';
void main() {
late AppDatabase database;
@@ -42,7 +42,7 @@ void main() {
testPlayer4 = Player(name: 'Diana', description: '');
testPlayer5 = Player(name: 'Eve', description: '');
testGroup1 = Group(
name: 'Test Group 2',
name: 'Test Group 1',
description: '',
members: [testPlayer1, testPlayer2, testPlayer3],
);
@@ -296,9 +296,9 @@ void main() {
test('Setting a winner works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
await database.matchDao.setWinner(
await database.scoreEntryDao.setWinner(
matchId: testMatch1.id,
winnerId: testPlayer5.id,
playerId: testPlayer5.id,
);
final fetchedMatch = await database.matchDao.getMatchById(
@@ -307,5 +307,68 @@ void main() {
expect(fetchedMatch.winner, isNotNull);
expect(fetchedMatch.winner!.id, testPlayer5.id);
});
test(
'removeMatchGroup removes group from match with existing group',
() async {
await database.matchDao.addMatch(match: testMatch1);
final removed = await database.matchDao.removeMatchGroup(
matchId: testMatch1.id,
);
expect(removed, isTrue);
final updatedMatch = await database.matchDao.getMatchById(
matchId: testMatch1.id,
);
expect(updatedMatch.group, null);
expect(updatedMatch.game.id, testMatch1.game.id);
expect(updatedMatch.name, testMatch1.name);
expect(updatedMatch.notes, testMatch1.notes);
},
);
test(
'removeMatchGroup on match that already has no group still succeeds',
() async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final removed = await database.matchDao.removeMatchGroup(
matchId: testMatchOnlyPlayers.id,
);
expect(removed, isTrue);
final updatedMatch = await database.matchDao.getMatchById(
matchId: testMatchOnlyPlayers.id,
);
expect(updatedMatch.group, null);
},
);
test('removeMatchGroup on non-existing match returns false', () async {
final removed = await database.matchDao.removeMatchGroup(
matchId: 'non-existing-id',
);
expect(removed, isFalse);
});
test('Fetching all matches related to a group', () async {
var matches = await database.matchDao.getGroupMatches(
groupId: 'non-existing-id',
);
expect(matches, isEmpty);
await database.matchDao.addMatch(match: testMatch1);
matches = await database.matchDao.getGroupMatches(groupId: testGroup1.id);
expect(matches, isNotEmpty);
final match = matches.first;
expect(match.id, testMatch1.id);
expect(match.group, isNotNull);
expect(match.group!.id, testGroup1.id);
});
});
}

View File

@@ -2,12 +2,12 @@ import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/dto/team.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/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/team.dart';
void main() {
late AppDatabase database;
@@ -37,20 +37,23 @@ void main() {
testPlayer2 = Player(name: 'Bob', description: '');
testPlayer3 = Player(name: 'Charlie', description: '');
testPlayer4 = Player(name: 'Diana', description: '');
testTeam1 = Team(
name: 'Team Alpha',
members: [testPlayer1, testPlayer2],
testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]);
testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]);
testTeam3 = Team(name: 'Team Gamma', members: [testPlayer1, testPlayer3]);
testGame1 = Game(
name: 'Game 1',
ruleset: Ruleset.singleWinner,
description: 'Test game 1',
color: GameColor.blue,
icon: '',
);
testTeam2 = Team(
name: 'Team Beta',
members: [testPlayer3, testPlayer4],
testGame2 = Game(
name: 'Game 2',
ruleset: Ruleset.highestScore,
description: 'Test game 2',
color: GameColor.red,
icon: '',
);
testTeam3 = Team(
name: 'Team Gamma',
members: [testPlayer1, testPlayer3],
);
testGame1 = Game(name: 'Game 1', ruleset: Ruleset.singleWinner, description: 'Test game 1', color: GameColor.blue, icon: '');
testGame2 = Game(name: 'Game 2', ruleset: Ruleset.highestScore, description: 'Test game 2', color: GameColor.red, icon: '');
});
await database.playerDao.addPlayersAsList(
@@ -65,7 +68,6 @@ void main() {
});
group('Team Tests', () {
// Verifies that a single team can be added and retrieved with all fields intact.
test('Adding and fetching a single team works correctly', () async {
final added = await database.teamDao.addTeam(team: testTeam1);
@@ -285,10 +287,7 @@ void main() {
test('Updating team name to empty string works', () async {
await database.teamDao.addTeam(team: testTeam1);
await database.teamDao.updateTeamName(
teamId: testTeam1.id,
newName: '',
);
await database.teamDao.updateTeamName(teamId: testTeam1.id, newName: '');
final updatedTeam = await database.teamDao.getTeamById(
teamId: testTeam1.id,
@@ -350,9 +349,7 @@ void main() {
await database.matchDao.addMatch(match: match2);
// Add teams to database
await database.teamDao.addTeamsAsList(
teams: [testTeam1, testTeam3],
);
await database.teamDao.addTeamsAsList(teams: [testTeam1, testTeam3]);
// Associate players with teams through match1
// testTeam1: player1, player2
@@ -360,13 +357,11 @@ void main() {
playerId: testPlayer1.id,
matchId: match1.id,
teamId: testTeam1.id,
score: 0,
);
await database.playerMatchDao.addPlayerToMatch(
playerId: testPlayer2.id,
matchId: match1.id,
teamId: testTeam1.id,
score: 0,
);
// Associate players with teams through match2
@@ -375,13 +370,11 @@ void main() {
playerId: testPlayer1.id,
matchId: match2.id,
teamId: testTeam3.id,
score: 0,
);
await database.playerMatchDao.addPlayerToMatch(
playerId: testPlayer3.id,
matchId: match2.id,
teamId: testTeam3.id,
score: 0,
);
final team1 = await database.teamDao.getTeamById(teamId: testTeam1.id);
@@ -420,10 +413,11 @@ void main() {
final allTeams = await database.teamDao.getAllTeams();
expect(allTeams.length, 3);
expect(
allTeams.map((t) => t.id).toSet(),
{testTeam1.id, testTeam2.id, testTeam3.id},
);
expect(allTeams.map((t) => t.id).toSet(), {
testTeam1.id,
testTeam2.id,
testTeam3.id,
});
});
// Verifies that teamExists returns false for deleted teams.
@@ -462,9 +456,7 @@ void main() {
// Verifies that addTeam after deleteAllTeams works correctly.
test('Adding team after deleteAllTeams works correctly', () async {
await database.teamDao.addTeamsAsList(
teams: [testTeam1, testTeam2],
);
await database.teamDao.addTeamsAsList(teams: [testTeam1, testTeam2]);
expect(await database.teamDao.getTeamCount(), 2);
await database.teamDao.deleteAllTeams();

View File

@@ -4,7 +4,7 @@ import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/models/game.dart';
void main() {
late AppDatabase database;
@@ -54,7 +54,6 @@ void main() {
});
group('Game Tests', () {
// Verifies that getAllGames returns an empty list when the database has no games.
test('getAllGames returns empty list when no games exist', () async {
final allGames = await database.gameDao.getAllGames();
@@ -106,7 +105,7 @@ void main() {
// Verifies that getGameById throws a StateError when the game doesn't exist.
test('getGameById throws exception for non-existent game', () async {
expect(
() => database.gameDao.getGameById(gameId: 'non-existent-id'),
() => database.gameDao.getGameById(gameId: 'non-existent-id'),
throwsA(isA<StateError>()),
);
});
@@ -134,7 +133,13 @@ void main() {
// Verifies that a game with empty optional fields can be added and retrieved.
test('addGame handles game with null optional fields', () async {
final gameWithNulls = Game(name: 'Simple Game', ruleset: Ruleset.lowestScore, description: 'A simple game', color: GameColor.green, icon: '');
final gameWithNulls = Game(
name: 'Simple Game',
ruleset: Ruleset.lowestScore,
description: 'A simple game',
color: GameColor.green,
icon: '',
);
final result = await database.gameDao.addGame(game: gameWithNulls);
expect(result, true);
@@ -419,9 +424,7 @@ void main() {
// Verifies that getGameCount updates correctly after deleting a game.
test('getGameCount updates correctly after deletion', () async {
await database.gameDao.addGamesAsList(
games: [testGame1, testGame2],
);
await database.gameDao.addGamesAsList(games: [testGame1, testGame2]);
final countBefore = await database.gameDao.getGameCount();
expect(countBefore, 2);

View File

@@ -3,7 +3,7 @@ import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/player.dart';
void main() {
late AppDatabase database;
@@ -35,7 +35,6 @@ void main() {
});
group('Player Tests', () {
// Verifies that players can be added and retrieved with all fields intact.
test('Adding and fetching single player works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1);
@@ -264,16 +263,22 @@ void main() {
});
// Verifies that a player with special characters in name is stored correctly.
test('Player with special characters in name is stored correctly', () async {
final specialPlayer = Player(name: 'Test!@#\$%^&*()_+-=[]{}|;\':",.<>?/`~', description: '');
test(
'Player with special characters in name is stored correctly',
() async {
final specialPlayer = Player(
name: 'Test!@#\$%^&*()_+-=[]{}|;\':",.<>?/`~',
description: '',
);
await database.playerDao.addPlayer(player: specialPlayer);
await database.playerDao.addPlayer(player: specialPlayer);
final fetchedPlayer = await database.playerDao.getPlayerById(
playerId: specialPlayer.id,
);
expect(fetchedPlayer.name, specialPlayer.name);
});
final fetchedPlayer = await database.playerDao.getPlayerById(
playerId: specialPlayer.id,
);
expect(fetchedPlayer.name, specialPlayer.name);
},
);
// Verifies that a player with description is stored correctly.
test('Player with description is stored correctly', () async {
@@ -293,7 +298,10 @@ void main() {
// Verifies that a player with null description is stored correctly.
test('Player with null description is stored correctly', () async {
final playerWithoutDescription = Player(name: 'No Description Player', description: '');
final playerWithoutDescription = Player(
name: 'No Description Player',
description: '',
);
await database.playerDao.addPlayer(player: playerWithoutDescription);

View File

@@ -3,8 +3,8 @@ import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
void main() {
late AppDatabase database;
@@ -42,7 +42,6 @@ void main() {
});
group('Player-Group Tests', () {
// Verifies that a player can be added to an existing group and isPlayerInGroup returns true.
test('Adding a player to a group works correctly', () async {
await database.groupDao.addGroup(group: testGroup);
@@ -127,67 +126,83 @@ void main() {
});
// Verifies that addPlayerToGroup returns false when player already in group.
test('addPlayerToGroup returns false when player already in group', () async {
await database.groupDao.addGroup(group: testGroup);
test(
'addPlayerToGroup returns false when player already in group',
() async {
await database.groupDao.addGroup(group: testGroup);
// testPlayer1 is already in testGroup via group creation
final result = await database.playerGroupDao.addPlayerToGroup(
player: testPlayer1,
groupId: testGroup.id,
);
// testPlayer1 is already in testGroup via group creation
final result = await database.playerGroupDao.addPlayerToGroup(
player: testPlayer1,
groupId: testGroup.id,
);
expect(result, false);
});
expect(result, false);
},
);
// Verifies that addPlayerToGroup adds player to player table if not exists.
test('addPlayerToGroup adds player to player table if not exists', () async {
await database.groupDao.addGroup(group: testGroup);
test(
'addPlayerToGroup adds player to player table if not exists',
() async {
await database.groupDao.addGroup(group: testGroup);
// testPlayer4 is not in the database yet
var playerExists = await database.playerDao.playerExists(
playerId: testPlayer4.id,
);
expect(playerExists, false);
// testPlayer4 is not in the database yet
var playerExists = await database.playerDao.playerExists(
playerId: testPlayer4.id,
);
expect(playerExists, false);
await database.playerGroupDao.addPlayerToGroup(
player: testPlayer4,
groupId: testGroup.id,
);
await database.playerGroupDao.addPlayerToGroup(
player: testPlayer4,
groupId: testGroup.id,
);
// Now player should exist in player table
playerExists = await database.playerDao.playerExists(
playerId: testPlayer4.id,
);
expect(playerExists, true);
});
// Now player should exist in player table
playerExists = await database.playerDao.playerExists(
playerId: testPlayer4.id,
);
expect(playerExists, true);
},
);
// Verifies that removePlayerFromGroup returns false for non-existent player.
test('removePlayerFromGroup returns false for non-existent player', () async {
await database.groupDao.addGroup(group: testGroup);
test(
'removePlayerFromGroup returns false for non-existent player',
() async {
await database.groupDao.addGroup(group: testGroup);
final result = await database.playerGroupDao.removePlayerFromGroup(
playerId: 'non-existent-player-id',
groupId: testGroup.id,
);
final result = await database.playerGroupDao.removePlayerFromGroup(
playerId: 'non-existent-player-id',
groupId: testGroup.id,
);
expect(result, false);
});
expect(result, false);
},
);
// Verifies that removePlayerFromGroup returns false for non-existent group.
test('removePlayerFromGroup returns false for non-existent group', () async {
await database.playerDao.addPlayer(player: testPlayer1);
test(
'removePlayerFromGroup returns false for non-existent group',
() async {
await database.playerDao.addPlayer(player: testPlayer1);
final result = await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: 'non-existent-group-id',
);
final result = await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: 'non-existent-group-id',
);
expect(result, false);
});
expect(result, false);
},
);
// Verifies that getPlayersOfGroup returns empty list for group with no members.
test('getPlayersOfGroup returns empty list for empty group', () async {
final emptyGroup = Group(name: 'Empty Group', description: '', members: []);
final emptyGroup = Group(
name: 'Empty Group',
description: '',
members: [],
);
await database.groupDao.addGroup(group: emptyGroup);
final players = await database.playerGroupDao.getPlayersOfGroup(
@@ -198,13 +213,16 @@ void main() {
});
// Verifies that getPlayersOfGroup returns empty list for non-existent group.
test('getPlayersOfGroup returns empty list for non-existent group', () async {
final players = await database.playerGroupDao.getPlayersOfGroup(
groupId: 'non-existent-group-id',
);
test(
'getPlayersOfGroup returns empty list for non-existent group',
() async {
final players = await database.playerGroupDao.getPlayersOfGroup(
groupId: 'non-existent-group-id',
);
expect(players, isEmpty);
});
expect(players, isEmpty);
},
);
// Verifies that removing all players from a group leaves the group empty.
test('Removing all players from a group leaves group empty', () async {
@@ -231,7 +249,11 @@ void main() {
// Verifies that a player can be in multiple groups.
test('Player can be in multiple groups', () async {
final secondGroup = Group(name: 'Second Group', description: '', members: []);
final secondGroup = Group(
name: 'Second Group',
description: '',
members: [],
);
await database.groupDao.addGroup(group: testGroup);
await database.groupDao.addGroup(group: secondGroup);
@@ -255,29 +277,36 @@ void main() {
});
// Verifies that removing player from one group doesn't affect other groups.
test('Removing player from one group does not affect other groups', () async {
final secondGroup = Group(name: 'Second Group', description: '', members: [testPlayer1]);
await database.groupDao.addGroup(group: testGroup);
await database.groupDao.addGroup(group: secondGroup);
test(
'Removing player from one group does not affect other groups',
() async {
final secondGroup = Group(
name: 'Second Group',
description: '',
members: [testPlayer1],
);
await database.groupDao.addGroup(group: testGroup);
await database.groupDao.addGroup(group: secondGroup);
// Remove testPlayer1 from testGroup
await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
// Remove testPlayer1 from testGroup
await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
final inFirstGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
final inSecondGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id,
groupId: secondGroup.id,
);
final inFirstGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
final inSecondGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id,
groupId: secondGroup.id,
);
expect(inFirstGroup, false);
expect(inSecondGroup, true);
});
expect(inFirstGroup, false);
expect(inSecondGroup, true);
},
);
// Verifies that addPlayerToGroup returns true on successful addition.
test('addPlayerToGroup returns true on successful addition', () async {
@@ -293,21 +322,26 @@ void main() {
});
// Verifies that removing the same player twice returns false on second attempt.
test('Removing same player twice returns false on second attempt', () async {
await database.groupDao.addGroup(group: testGroup);
test(
'Removing same player twice returns false on second attempt',
() async {
await database.groupDao.addGroup(group: testGroup);
final firstRemoval = await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
expect(firstRemoval, true);
final firstRemoval = await database.playerGroupDao
.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
expect(firstRemoval, true);
final secondRemoval = await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
expect(secondRemoval, false);
});
final secondRemoval = await database.playerGroupDao
.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
expect(secondRemoval, false);
},
);
// Verifies that replaceGroupPlayers removes all existing players and replaces with new list.
test('replaceGroupPlayers replaces all group members correctly', () async {

View File

@@ -4,11 +4,11 @@ import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/dto/team.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/data/models/team.dart';
void main() {
late AppDatabase database;
@@ -48,7 +48,13 @@ void main() {
description: '',
members: [testPlayer1, testPlayer2, testPlayer3],
);
testGame = Game(name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', color: GameColor.blue, icon: '');
testGame = Game(
name: 'Test Game',
ruleset: Ruleset.singleWinner,
description: 'A test game',
color: GameColor.blue,
icon: '',
);
testMatchOnlyGroup = Match(
name: 'Test Match with Group',
game: testGame,
@@ -61,14 +67,8 @@ void main() {
players: [testPlayer4, testPlayer5, testPlayer6],
notes: '',
);
testTeam1 = Team(
name: 'Team Alpha',
members: [testPlayer1, testPlayer2],
);
testTeam2 = Team(
name: 'Team Beta',
members: [testPlayer3, testPlayer4],
);
testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]);
testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]);
});
await database.playerDao.addPlayersAsList(
players: [
@@ -88,8 +88,6 @@ void main() {
});
group('Player-Match Tests', () {
// Verifies that matchHasPlayers returns false initially and true after adding a player.
test('Match has player works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerDao.addPlayer(player: testPlayer1);
@@ -112,7 +110,6 @@ void main() {
expect(matchHasPlayers, true);
});
// Verifies that a player can be added to a match and isPlayerInMatch returns true.
test('Adding a player to a match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerDao.addPlayer(player: testPlayer5);
@@ -136,7 +133,6 @@ void main() {
expect(playerAdded, false);
});
// Verifies that a player can be removed from a match and the player count decreases.
test('Removing player from match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
@@ -153,30 +149,25 @@ void main() {
);
expect(result.players.length, testMatchOnlyPlayers.players.length - 1);
final playerExists = result.players.any(
(p) => p.id == playerToRemove.id,
);
final playerExists = result.players.any((p) => p.id == playerToRemove.id);
expect(playerExists, false);
});
// Verifies that getPlayersOfMatch returns all players of a match with correct data.
test('Retrieving players of a match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyPlayers.id,
) ?? [];
final players =
await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyPlayers.id,
) ??
[];
for (int i = 0; i < players.length; i++) {
expect(players[i].id, testMatchOnlyPlayers.players[i].id);
expect(players[i].name, testMatchOnlyPlayers.players[i].name);
expect(
players[i].createdAt,
testMatchOnlyPlayers.players[i].createdAt,
);
expect(players[i].createdAt, testMatchOnlyPlayers.players[i].createdAt);
}
});
// Verifies that updatePlayersFromMatch replaces all existing players with new ones.
test('Updating the match players works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
@@ -220,13 +211,22 @@ void main() {
}
});
// Verifies that the same player can be added to multiple different matches.
test(
'Adding the same player to separate matches works correctly',
() async {
() async {
final playersList = [testPlayer1, testPlayer2, testPlayer3];
final match1 = Match(name: 'Match 1', game: testGame, players: playersList, notes: '');
final match2 = Match(name: 'Match 2', game: testGame, players: playersList, notes: '');
final match1 = Match(
name: 'Match 1',
game: testGame,
players: playersList,
notes: '',
);
final match2 = Match(
name: 'Match 2',
game: testGame,
players: playersList,
notes: '',
);
await Future.wait([
database.matchDao.addMatch(match: match1),
@@ -267,83 +267,6 @@ void main() {
expect(players, isNull);
});
// Verifies that adding a player with initial score works correctly.
test('Adding player with initial score works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
score: 100,
);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
);
expect(score, 100);
});
// Verifies that getPlayerScore returns the correct score.
test('getPlayerScore returns correct score', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// Default score should be 0 when added through match
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 0);
});
// Verifies that getPlayerScore returns null for non-existent player-match combination.
test('getPlayerScore returns null for non-existent player in match', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: 'non-existent-player-id',
);
expect(score, isNull);
});
// Verifies that updatePlayerScore updates the score correctly.
test('updatePlayerScore updates score correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 50,
);
expect(updated, true);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 50);
});
// Verifies that updatePlayerScore returns false for non-existent player-match.
test('updatePlayerScore returns false for non-existent player-match', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: 'non-existent-player-id',
newScore: 50,
);
expect(updated, false);
});
// Verifies that adding a player with teamId works correctly.
test('Adding player with teamId works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
@@ -363,7 +286,6 @@ void main() {
expect(playersInTeam[0].id, testPlayer1.id);
});
// Verifies that updatePlayerTeam updates the team correctly.
test('updatePlayerTeam updates team correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
@@ -402,7 +324,6 @@ void main() {
expect(playersInTeam1.isEmpty, true);
});
// Verifies that updatePlayerTeam can set team to null.
test('updatePlayerTeam can remove player from team', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
@@ -430,18 +351,20 @@ void main() {
expect(playersInTeam.isEmpty, true);
});
// Verifies that updatePlayerTeam returns false for non-existent player-match.
test('updatePlayerTeam returns false for non-existent player-match', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
test(
'updatePlayerTeam returns false for non-existent player-match',
() async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
final updated = await database.playerMatchDao.updatePlayerTeam(
matchId: testMatchOnlyGroup.id,
playerId: 'non-existent-player-id',
teamId: testTeam1.id,
);
final updated = await database.playerMatchDao.updatePlayerTeam(
matchId: testMatchOnlyGroup.id,
playerId: 'non-existent-player-id',
teamId: testTeam1.id,
);
expect(updated, false);
});
expect(updated, false);
},
);
// Verifies that getPlayersInTeam returns empty list for non-existent team.
test('getPlayersInTeam returns empty list for non-existent team', () async {
@@ -455,7 +378,6 @@ void main() {
expect(players.isEmpty, true);
});
// Verifies that getPlayersInTeam returns all players of a team.
test('getPlayersInTeam returns all players of a team', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
@@ -482,43 +404,34 @@ void main() {
expect(playerIds.contains(testPlayer2.id), true);
});
// Verifies that removePlayerFromMatch returns false for non-existent player.
test('removePlayerFromMatch returns false for non-existent player', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
test(
'removePlayerFromMatch returns false for non-existent player',
() async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final removed = await database.playerMatchDao.removePlayerFromMatch(
playerId: 'non-existent-player-id',
matchId: testMatchOnlyPlayers.id,
);
final removed = await database.playerMatchDao.removePlayerFromMatch(
playerId: 'non-existent-player-id',
matchId: testMatchOnlyPlayers.id,
);
expect(removed, false);
});
expect(removed, false);
},
);
// Verifies that adding the same player twice to the same match is ignored.
test('Adding same player twice to same match is ignored', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
score: 10,
);
// Try to add the same player again with different score
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
score: 100,
);
// Score should still be 10 because insert was ignored
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
);
expect(score, 10);
// Verify player count is still 1
final players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyGroup.id,
@@ -527,30 +440,31 @@ void main() {
expect(players?.length, 1);
});
// Verifies that updatePlayersFromMatch with empty list removes all players.
test('updatePlayersFromMatch with empty list removes all players', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
test(
'updatePlayersFromMatch with empty list removes all players',
() async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// Verify players exist initially
var players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyPlayers.id,
);
expect(players?.length, 3);
// Verify players exist initially
var players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyPlayers.id,
);
expect(players?.length, 3);
// Update with empty list
await database.playerMatchDao.updatePlayersFromMatch(
matchId: testMatchOnlyPlayers.id,
newPlayer: [],
);
// Update with empty list
await database.playerMatchDao.updatePlayersFromMatch(
matchId: testMatchOnlyPlayers.id,
newPlayer: [],
);
// Verify all players are removed
players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyPlayers.id,
);
expect(players, isNull);
});
// Verify all players are removed
players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyPlayers.id,
);
expect(players, isNull);
},
);
// Verifies that updatePlayersFromMatch with same players makes no changes.
test('updatePlayersFromMatch with same players makes no changes', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
@@ -572,7 +486,6 @@ void main() {
}
});
// Verifies that matchHasPlayers returns false for non-existent match.
test('matchHasPlayers returns false for non-existent match', () async {
final hasPlayers = await database.playerMatchDao.matchHasPlayers(
matchId: 'non-existent-match-id',
@@ -581,7 +494,6 @@ void main() {
expect(hasPlayers, false);
});
// Verifies that isPlayerInMatch returns false for non-existent match.
test('isPlayerInMatch returns false for non-existent match', () async {
final isInMatch = await database.playerMatchDao.isPlayerInMatch(
matchId: 'non-existent-match-id',
@@ -591,127 +503,20 @@ void main() {
expect(isInMatch, false);
});
// Verifies that updatePlayersFromMatch preserves scores for existing players.
test('updatePlayersFromMatch only modifies player associations', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// Update score for existing player
await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 75,
);
// Update players, keeping testPlayer4 and adding testPlayer1
await database.playerMatchDao.updatePlayersFromMatch(
matchId: testMatchOnlyPlayers.id,
newPlayer: [testPlayer4, testPlayer1],
);
// Verify testPlayer4's score is preserved
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 75);
// Verify testPlayer1 was added with default score
final newPlayerScore = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer1.id,
);
expect(newPlayerScore, 0);
});
// Verifies that adding a player with both score and teamId works correctly.
test('Adding player with score and teamId works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
teamId: testTeam1.id,
score: 150,
);
// Verify score
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
);
expect(score, 150);
// Verify team assignment
final playersInTeam = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyGroup.id,
teamId: testTeam1.id,
);
expect(playersInTeam.length, 1);
expect(playersInTeam[0].id, testPlayer1.id);
});
// Verifies that updating score with negative value works.
test('updatePlayerScore with negative score works', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: -10,
);
expect(updated, true);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, -10);
});
// Verifies that updating score with zero value works.
test('updatePlayerScore with zero score works', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// First set a non-zero score
await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 100,
);
// Then update to zero
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 0,
);
expect(updated, true);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 0);
});
// Verifies that getPlayersInTeam returns empty list for non-existent match.
test('getPlayersInTeam returns empty list for non-existent match', () async {
await database.teamDao.addTeam(team: testTeam1);
test(
'getPlayersInTeam returns empty list for non-existent match',
() async {
await database.teamDao.addTeam(team: testTeam1);
final players = await database.playerMatchDao.getPlayersInTeam(
matchId: 'non-existent-match-id',
teamId: testTeam1.id,
);
final players = await database.playerMatchDao.getPlayersInTeam(
matchId: 'non-existent-match-id',
teamId: testTeam1.id,
);
expect(players.isEmpty, true);
});
expect(players.isEmpty, true);
},
);
// Verifies that players in different teams within the same match are returned correctly.
test('Players in different teams within same match are separate', () async {
@@ -759,8 +564,18 @@ void main() {
// Verifies that removePlayerFromMatch does not affect other matches.
test('removePlayerFromMatch does not affect other matches', () async {
final playersList = [testPlayer1, testPlayer2];
final match1 = Match(name: 'Match 1', game: testGame, players: playersList, notes: '');
final match2 = Match(name: 'Match 2', game: testGame, players: playersList, notes: '');
final match1 = Match(
name: 'Match 1',
game: testGame,
players: playersList,
notes: '',
);
final match2 = Match(
name: 'Match 2',
game: testGame,
players: playersList,
notes: '',
);
await Future.wait([
database.matchDao.addMatch(match: match1),
@@ -789,56 +604,20 @@ void main() {
expect(isInMatch2, true);
});
// Verifies that updating scores for players in different matches are independent.
test('Player scores are independent across matches', () async {
final playersList = [testPlayer1];
final match1 = Match(name: 'Match 1', game: testGame, players: playersList, notes: '');
final match2 = Match(name: 'Match 2', game: testGame, players: playersList, notes: '');
await Future.wait([
database.matchDao.addMatch(match: match1),
database.matchDao.addMatch(match: match2),
]);
// Update score in match1
await database.playerMatchDao.updatePlayerScore(
matchId: match1.id,
playerId: testPlayer1.id,
newScore: 100,
);
// Update score in match2
await database.playerMatchDao.updatePlayerScore(
matchId: match2.id,
playerId: testPlayer1.id,
newScore: 50,
);
// Verify scores are independent
final scoreInMatch1 = await database.playerMatchDao.getPlayerScore(
matchId: match1.id,
playerId: testPlayer1.id,
);
final scoreInMatch2 = await database.playerMatchDao.getPlayerScore(
matchId: match2.id,
playerId: testPlayer1.id,
);
expect(scoreInMatch1, 100);
expect(scoreInMatch2, 50);
});
// Verifies that updatePlayersFromMatch on non-existent match fails with constraint error.
test('updatePlayersFromMatch on non-existent match fails with foreign key constraint', () async {
// Should throw due to foreign key constraint - match doesn't exist
await expectLater(
database.playerMatchDao.updatePlayersFromMatch(
matchId: 'non-existent-match-id',
newPlayer: [testPlayer1, testPlayer2],
),
throwsA(anything),
);
});
test(
'updatePlayersFromMatch on non-existent match fails with foreign key constraint',
() async {
// Should throw due to foreign key constraint - match doesn't exist
await expectLater(
database.playerMatchDao.updatePlayersFromMatch(
matchId: 'non-existent-match-id',
newPlayer: [testPlayer1, testPlayer2],
),
throwsA(anything),
);
},
);
// Verifies that a player can be in a match without being assigned to a team.
test('Player can exist in match without team assignment', () async {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,926 @@
import 'dart:convert';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' hide isNull, isNotNull;
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.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/data/models/score_entry.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/services/data_transfer_service.dart';
void main() {
late AppDatabase database;
late Player testPlayer1;
late Player testPlayer2;
late Player testPlayer3;
late Game testGame;
late Group testGroup;
late Team testTeam;
late Match testMatch;
final fixedDate = DateTime(2025, 11, 19, 0, 11, 23);
final fakeClock = Clock(() => fixedDate);
setUp(() {
database = AppDatabase(
DatabaseConnection(
NativeDatabase.memory(),
closeStreamsSynchronously: true,
),
);
withClock(fakeClock, () {
testPlayer1 = Player(name: 'Alice', description: 'First test player');
testPlayer2 = Player(name: 'Bob', description: 'Second test player');
testPlayer3 = Player(name: 'Charlie', description: 'Third player');
testGame = Game(
name: 'Chess',
ruleset: Ruleset.singleWinner,
description: 'Strategic board game',
color: GameColor.blue,
icon: 'chess_icon',
);
testGroup = Group(
name: 'Test Group',
description: 'Group for testing',
members: [testPlayer1, testPlayer2],
);
testTeam = Team(name: 'Test Team', members: [testPlayer1, testPlayer2]);
testMatch = Match(
name: 'Test Match',
game: testGame,
group: testGroup,
players: [testPlayer1, testPlayer2],
notes: 'Test notes',
scores: {
testPlayer1.id: [
ScoreEntry(roundNumber: 1, score: 10, change: 10),
ScoreEntry(roundNumber: 2, score: 20, change: 10),
],
testPlayer2.id: [
ScoreEntry(roundNumber: 1, score: 15, change: 15),
ScoreEntry(roundNumber: 2, score: 25, change: 10),
],
},
);
});
});
tearDown(() async {
await database.close();
});
// Helper for getting BuildContext
Future<BuildContext> getContext(WidgetTester tester) async {
// Minimal widget with Provider
await tester.pumpWidget(
Provider<AppDatabase>.value(
value: database,
child: MaterialApp(
home: Builder(
builder: (context) {
return Container();
},
),
),
),
);
final BuildContext context = tester.element(find.byType(Container));
return context;
}
group('DataTransferService Tests', () {
testWidgets('deleteAllData()', (tester) async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.gameDao.addGame(game: testGame);
await database.groupDao.addGroup(group: testGroup);
await database.teamDao.addTeam(team: testTeam);
await database.matchDao.addMatch(match: testMatch);
var playerCount = await database.playerDao.getPlayerCount();
var gameCount = await database.gameDao.getGameCount();
var groupCount = await database.groupDao.getGroupCount();
var teamCount = await database.teamDao.getTeamCount();
var matchCount = await database.matchDao.getMatchCount();
expect(playerCount, greaterThan(0));
expect(gameCount, greaterThan(0));
expect(groupCount, greaterThan(0));
expect(teamCount, greaterThan(0));
expect(matchCount, greaterThan(0));
final ctx = await getContext(tester);
await DataTransferService.deleteAllData(ctx);
playerCount = await database.playerDao.getPlayerCount();
gameCount = await database.gameDao.getGameCount();
groupCount = await database.groupDao.getGroupCount();
teamCount = await database.teamDao.getTeamCount();
matchCount = await database.matchDao.getMatchCount();
expect(playerCount, 0);
expect(gameCount, 0);
expect(groupCount, 0);
expect(teamCount, 0);
expect(matchCount, 0);
});
group('getAppDataAsJson()', () {
group('Whole export', () {
testWidgets('Exporting app data works correctly', (tester) async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer2);
await database.gameDao.addGame(game: testGame);
await database.groupDao.addGroup(group: testGroup);
await database.teamDao.addTeam(team: testTeam);
await database.matchDao.addMatch(match: testMatch);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
expect(jsonString, isNotEmpty);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
expect(decoded.containsKey('players'), true);
expect(decoded.containsKey('games'), true);
expect(decoded.containsKey('groups'), true);
expect(decoded.containsKey('teams'), true);
expect(decoded.containsKey('matches'), true);
final players = decoded['players'] as List<dynamic>;
final games = decoded['games'] as List<dynamic>;
final groups = decoded['groups'] as List<dynamic>;
final teams = decoded['teams'] as List<dynamic>;
final matches = decoded['matches'] as List<dynamic>;
expect(players.length, 2);
expect(games.length, 1);
expect(groups.length, 1);
expect(teams.length, 1);
expect(matches.length, 1);
});
testWidgets('Exporting empty data works correctly', (tester) async {
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final players = decoded['players'] as List<dynamic>;
final games = decoded['games'] as List<dynamic>;
final groups = decoded['groups'] as List<dynamic>;
final teams = decoded['teams'] as List<dynamic>;
final matches = decoded['matches'] as List<dynamic>;
expect(players, isEmpty);
expect(games, isEmpty);
expect(groups, isEmpty);
expect(teams, isEmpty);
expect(matches, isEmpty);
});
});
group('Checking specific data', () {
testWidgets('Player data is correct', (tester) async {
await database.playerDao.addPlayer(player: testPlayer1);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final players = decoded['players'] as List<dynamic>;
final playerData = players[0] as Map<String, dynamic>;
expect(playerData['id'], testPlayer1.id);
expect(playerData['name'], testPlayer1.name);
expect(playerData['description'], testPlayer1.description);
expect(
playerData['createdAt'],
testPlayer1.createdAt.toIso8601String(),
);
});
testWidgets('Game data is correct', (tester) async {
await database.gameDao.addGame(game: testGame);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final games = decoded['games'] as List<dynamic>;
final gameData = games[0] as Map<String, dynamic>;
expect(gameData['id'], testGame.id);
expect(gameData['name'], testGame.name);
expect(gameData['ruleset'], testGame.ruleset.name);
expect(gameData['description'], testGame.description);
expect(gameData['color'], testGame.color.name);
expect(gameData['icon'], testGame.icon);
});
testWidgets('Group data is correct', (tester) async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer2);
await database.groupDao.addGroup(group: testGroup);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final groups = decoded['groups'] as List<dynamic>;
final groupData = groups[0] as Map<String, dynamic>;
expect(groupData['id'], testGroup.id);
expect(groupData['name'], testGroup.name);
expect(groupData['description'], testGroup.description);
expect(groupData['memberIds'], isA<List>());
final memberIds = groupData['memberIds'] as List<dynamic>;
expect(memberIds.length, 2);
expect(memberIds, containsAll([testPlayer1.id, testPlayer2.id]));
});
testWidgets('Team data is correct', (tester) async {
await database.teamDao.addTeam(team: testTeam);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final teams = decoded['teams'] as List<dynamic>;
expect(teams.length, 1);
final teamData = teams[0] as Map<String, dynamic>;
expect(teamData['id'], testTeam.id);
expect(teamData['name'], testTeam.name);
expect(teamData['memberIds'], isA<List>());
// Note: In this system, teams don't have independent members.
// Team members are only tracked through matches via PlayerMatchTable.
// Therefore, memberIds will be empty for standalone teams.
final memberIds = teamData['memberIds'] as List<dynamic>;
expect(memberIds, isEmpty);
});
testWidgets('Match data is correct', (tester) async {
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2],
);
await database.gameDao.addGame(game: testGame);
await database.groupDao.addGroup(group: testGroup);
await database.matchDao.addMatch(match: testMatch);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final matches = decoded['matches'] as List<dynamic>;
final matchData = matches[0] as Map<String, dynamic>;
expect(matchData['id'], testMatch.id);
expect(matchData['name'], testMatch.name);
expect(matchData['gameId'], testGame.id);
expect(matchData['groupId'], testGroup.id);
expect(matchData['playerIds'], isA<List>());
expect(matchData['notes'], testMatch.notes);
// Check player ids
final playerIds = matchData['playerIds'] as List<dynamic>;
expect(playerIds.length, 2);
expect(playerIds, containsAll([testPlayer1.id, testPlayer2.id]));
// Check scores structure
final scoresJson = matchData['scores'] as Map<String, dynamic>;
expect(scoresJson, isA<Map<String, dynamic>>());
final scores = scoresJson.map(
(playerId, scoreList) => MapEntry(
playerId,
(scoreList as List)
.map((s) => ScoreEntry.fromJson(s as Map<String, dynamic>))
.toList(),
),
);
expect(scores, isA<Map<String, List<ScoreEntry>>>());
/* Player 1 scores */
// General structure
expect(scores[testPlayer1.id], isNotNull);
expect(scores[testPlayer1.id]!.length, 2);
// Round 1
expect(scores[testPlayer1.id]![0].roundNumber, 1);
expect(scores[testPlayer1.id]![0].score, 10);
expect(scores[testPlayer1.id]![0].change, 10);
// Round 2
expect(scores[testPlayer1.id]![1].roundNumber, 2);
expect(scores[testPlayer1.id]![1].score, 20);
expect(scores[testPlayer1.id]![1].change, 10);
/* Player 2 scores */
// General structure
expect(scores[testPlayer2.id], isNotNull);
expect(scores[testPlayer2.id]!.length, 2);
// Round 1
expect(scores[testPlayer2.id]![0].roundNumber, 1);
expect(scores[testPlayer2.id]![0].score, 15);
expect(scores[testPlayer2.id]![0].change, 15);
// Round 2
expect(scores[testPlayer2.id]![1].roundNumber, 2);
expect(scores[testPlayer2.id]![1].score, 25);
expect(scores[testPlayer2.id]![1].change, 10);
});
testWidgets('Match without group is handled correctly', (tester) async {
final matchWithoutGroup = Match(
name: 'No Group Match',
game: testGame,
group: null,
players: [testPlayer1],
notes: 'No group',
);
await database.playerDao.addPlayer(player: testPlayer1);
await database.gameDao.addGame(game: testGame);
await database.matchDao.addMatch(match: matchWithoutGroup);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final matches = decoded['matches'] as List<dynamic>;
final matchData = matches[0] as Map<String, dynamic>;
expect(matchData['groupId'], isNull);
});
testWidgets('Match with endedAt is handled correctly', (tester) async {
final endedDate = DateTime(2025, 12, 1, 10, 0, 0);
final endedMatch = Match(
name: 'Ended Match',
game: testGame,
players: [testPlayer1],
endedAt: endedDate,
notes: 'Finished',
);
await database.playerDao.addPlayer(player: testPlayer1);
await database.gameDao.addGame(game: testGame);
await database.matchDao.addMatch(match: endedMatch);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final matches = decoded['matches'] as List<dynamic>;
final matchData = matches[0] as Map<String, dynamic>;
expect(matchData['endedAt'], endedDate.toIso8601String());
});
testWidgets('Structure is consistent', (tester) async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.gameDao.addGame(game: testGame);
final ctx = await getContext(tester);
final jsonString1 = await DataTransferService.getAppDataAsJson(ctx);
final jsonString2 = await DataTransferService.getAppDataAsJson(ctx);
expect(jsonString1, equals(jsonString2));
});
testWidgets('Empty match notes is handled correctly', (tester) async {
final matchWithEmptyNotes = Match(
name: 'Empty Notes Match',
game: testGame,
players: [testPlayer1],
notes: '',
);
await database.playerDao.addPlayer(player: testPlayer1);
await database.gameDao.addGame(game: testGame);
await database.matchDao.addMatch(match: matchWithEmptyNotes);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final matches = decoded['matches'] as List<dynamic>;
final matchData = matches[0] as Map<String, dynamic>;
expect(matchData['notes'], '');
});
testWidgets('Multiple players in match is handled correctly', (
tester,
) async {
final multiPlayerMatch = Match(
name: 'Multi Player Match',
game: testGame,
players: [testPlayer1, testPlayer2, testPlayer3],
notes: 'Three players',
);
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2, testPlayer3],
);
await database.gameDao.addGame(game: testGame);
await database.matchDao.addMatch(match: multiPlayerMatch);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final matches = decoded['matches'] as List<dynamic>;
final matchData = matches[0] as Map<String, dynamic>;
final playerIds = matchData['playerIds'] as List<dynamic>;
expect(playerIds.length, 3);
expect(
playerIds,
containsAll([testPlayer1.id, testPlayer2.id, testPlayer3.id]),
);
});
testWidgets('All game colors are handled correctly', (tester) async {
final games = [
Game(
name: 'Red Game',
ruleset: Ruleset.singleWinner,
color: GameColor.red,
icon: 'icon',
),
Game(
name: 'Blue Game',
ruleset: Ruleset.singleWinner,
color: GameColor.blue,
icon: 'icon',
),
Game(
name: 'Green Game',
ruleset: Ruleset.singleWinner,
color: GameColor.green,
icon: 'icon',
),
];
await database.gameDao.addGamesAsList(games: games);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final gamesJson = decoded['games'] as List<dynamic>;
expect(gamesJson.length, 3);
expect(
gamesJson.map((g) => g['color']),
containsAll(['red', 'blue', 'green']),
);
});
testWidgets('All rulesets are handled correctly', (tester) async {
final games = [
Game(
name: 'Highest Score Game',
ruleset: Ruleset.highestScore,
color: GameColor.blue,
icon: 'icon',
),
Game(
name: 'Lowest Score Game',
ruleset: Ruleset.lowestScore,
color: GameColor.blue,
icon: 'icon',
),
Game(
name: 'Single Winner',
ruleset: Ruleset.singleWinner,
color: GameColor.blue,
icon: 'icon',
),
];
await database.gameDao.addGamesAsList(games: games);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final gamesJson = decoded['games'] as List<dynamic>;
expect(gamesJson.length, 3);
expect(
gamesJson.map((g) => g['ruleset']),
containsAll(['highestScore', 'lowestScore', 'singleWinner']),
);
});
});
});
group('Parse Methods', () {
test('parsePlayersFromJson()', () {
final jsonMap = {
'players': [
{
'id': testPlayer1.id,
'name': testPlayer1.name,
'description': testPlayer1.description,
'createdAt': testPlayer1.createdAt.toIso8601String(),
},
{
'id': testPlayer2.id,
'name': testPlayer2.name,
'description': testPlayer2.description,
'createdAt': testPlayer2.createdAt.toIso8601String(),
},
],
};
final players = DataTransferService.parsePlayersFromJson(jsonMap);
expect(players.length, 2);
expect(players[0].id, testPlayer1.id);
expect(players[0].name, testPlayer1.name);
expect(players[1].id, testPlayer2.id);
expect(players[1].name, testPlayer2.name);
});
test('parsePlayersFromJson() empty list', () {
final jsonMap = {'players': []};
final players = DataTransferService.parsePlayersFromJson(jsonMap);
expect(players, isEmpty);
});
test('parsePlayersFromJson() missing key', () {
final jsonMap = <String, dynamic>{};
final players = DataTransferService.parsePlayersFromJson(jsonMap);
expect(players, isEmpty);
});
test('parseGamesFromJson()', () {
final jsonMap = {
'games': [
{
'id': testGame.id,
'name': testGame.name,
'ruleset': testGame.ruleset.name,
'description': testGame.description,
'color': testGame.color.name,
'icon': testGame.icon,
'createdAt': testGame.createdAt.toIso8601String(),
},
],
};
final games = DataTransferService.parseGamesFromJson(jsonMap);
expect(games.length, 1);
expect(games[0].id, testGame.id);
expect(games[0].name, testGame.name);
expect(games[0].ruleset, testGame.ruleset);
});
test('parseGamesFromJson() empty list', () {
final jsonMap = {'games': []};
final games = DataTransferService.parseGamesFromJson(jsonMap);
expect(games, isEmpty);
});
test('parseGamesFromJson() missing key', () {
final jsonMap = <String, dynamic>{};
final games = DataTransferService.parseGamesFromJson(jsonMap);
expect(games, isEmpty);
});
test('parseGroupsFromJson()', () {
final playerById = {
testPlayer1.id: testPlayer1,
testPlayer2.id: testPlayer2,
};
final jsonMap = {
'groups': [
{
'id': testGroup.id,
'name': testGroup.name,
'description': testGroup.description,
'memberIds': [testPlayer1.id, testPlayer2.id],
'createdAt': testGroup.createdAt.toIso8601String(),
},
],
};
final groups = DataTransferService.parseGroupsFromJson(
jsonMap,
playerById,
);
expect(groups.length, 1);
expect(groups[0].id, testGroup.id);
expect(groups[0].name, testGroup.name);
expect(groups[0].members.length, 2);
expect(groups[0].members[0].id, testPlayer1.id);
expect(groups[0].members[1].id, testPlayer2.id);
});
test('parseGroupsFromJson() empty list', () {
final jsonMap = {'groups': []};
final groups = DataTransferService.parseGroupsFromJson(jsonMap, {});
expect(groups, isEmpty);
});
test('parseGroupsFromJson() missing key', () {
final jsonMap = <String, dynamic>{};
final groups = DataTransferService.parseGroupsFromJson(jsonMap, {});
expect(groups, isEmpty);
});
test('parseGroupsFromJson() ignores invalid player ids', () {
final playerById = {testPlayer1.id: testPlayer1};
final jsonMap = {
'groups': [
{
'id': testGroup.id,
'name': testGroup.name,
'description': testGroup.description,
'memberIds': [testPlayer1.id, 'invalid-id'],
'createdAt': testGroup.createdAt.toIso8601String(),
},
],
};
final groups = DataTransferService.parseGroupsFromJson(
jsonMap,
playerById,
);
expect(groups.length, 1);
expect(groups[0].members.length, 1);
expect(groups[0].members[0].id, testPlayer1.id);
});
test('parseTeamsFromJson()', () {
final playerById = {testPlayer1.id: testPlayer1};
final jsonMap = {
'teams': [
{
'id': testTeam.id,
'name': testTeam.name,
'memberIds': [testPlayer1.id],
'createdAt': testTeam.createdAt.toIso8601String(),
},
],
};
final teams = DataTransferService.parseTeamsFromJson(
jsonMap,
playerById,
);
expect(teams.length, 1);
expect(teams[0].id, testTeam.id);
expect(teams[0].name, testTeam.name);
expect(teams[0].members.length, 1);
expect(teams[0].members[0].id, testPlayer1.id);
});
test('parseTeamsFromJson() empty list', () {
final jsonMap = {'teams': []};
final teams = DataTransferService.parseTeamsFromJson(jsonMap, {});
expect(teams, isEmpty);
});
test('parseTeamsFromJson() missing key', () {
final jsonMap = <String, dynamic>{};
final teams = DataTransferService.parseTeamsFromJson(jsonMap, {});
expect(teams, isEmpty);
});
test('parseMatchesFromJson()', () {
final playerById = {
testPlayer1.id: testPlayer1,
testPlayer2.id: testPlayer2,
};
final gameById = {testGame.id: testGame};
final groupById = {testGroup.id: testGroup};
final jsonMap = {
'matches': [
{
'id': testMatch.id,
'name': testMatch.name,
'gameId': testGame.id,
'groupId': testGroup.id,
'playerIds': [testPlayer1.id, testPlayer2.id],
'notes': testMatch.notes,
'createdAt': testMatch.createdAt.toIso8601String(),
},
],
};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
gameById,
groupById,
playerById,
);
expect(matches.length, 1);
expect(matches[0].id, testMatch.id);
expect(matches[0].name, testMatch.name);
expect(matches[0].game.id, testGame.id);
expect(matches[0].group?.id, testGroup.id);
expect(matches[0].players.length, 2);
});
test('parseMatchesFromJson() empty list', () {
final jsonMap = {'teams': []};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
{},
{},
{},
);
expect(matches, isEmpty);
});
test('parseMatchesFromJson() missing key', () {
final jsonMap = <String, dynamic>{};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
{},
{},
{},
);
expect(matches, isEmpty);
});
test('parseMatchesFromJson() creates unknown game for missing game', () {
final playerById = {testPlayer1.id: testPlayer1};
final gameById = <String, Game>{};
final groupById = <String, Group>{};
final jsonMap = {
'matches': [
{
'id': testMatch.id,
'name': testMatch.name,
'gameId': 'non-existent-game-id',
'playerIds': [testPlayer1.id],
'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(),
},
],
};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
gameById,
groupById,
playerById,
);
expect(matches.length, 1);
expect(matches[0].game.name, 'Unknown');
expect(matches[0].game.ruleset, Ruleset.singleWinner);
});
test('parseMatchesFromJson() handles null group', () {
final playerById = {testPlayer1.id: testPlayer1};
final gameById = {testGame.id: testGame};
final groupById = <String, Group>{};
final jsonMap = {
'matches': [
{
'id': testMatch.id,
'name': testMatch.name,
'gameId': testGame.id,
'groupId': null,
'playerIds': [testPlayer1.id],
'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(),
},
],
};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
gameById,
groupById,
playerById,
);
expect(matches.length, 1);
expect(matches[0].group, isNull);
});
test('parseMatchesFromJson() handles endedAt', () {
final playerById = {testPlayer1.id: testPlayer1};
final gameById = {testGame.id: testGame};
final groupById = <String, Group>{};
final endedDate = DateTime(2025, 12, 1, 10, 0, 0);
final jsonMap = {
'matches': [
{
'id': testMatch.id,
'name': testMatch.name,
'gameId': testGame.id,
'playerIds': [testPlayer1.id],
'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(),
'endedAt': endedDate.toIso8601String(),
},
],
};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
gameById,
groupById,
playerById,
);
expect(matches.length, 1);
expect(matches[0].endedAt, endedDate);
});
});
test('validateJsonSchema()', () async {
final validJson = json.encode({
'players': [
{
'id': testPlayer1.id,
'name': testPlayer1.name,
'description': testPlayer1.description,
'createdAt': testPlayer1.createdAt.toIso8601String(),
},
],
'games': [
{
'id': testGame.id,
'name': testGame.name,
'ruleset': testGame.ruleset.name,
'description': testGame.description,
'color': testGame.color.name,
'icon': testGame.icon,
'createdAt': testGame.createdAt.toIso8601String(),
},
],
'groups': [
{
'id': testGroup.id,
'name': testGroup.name,
'description': testGroup.description,
'memberIds': [testPlayer1.id, testPlayer2.id],
'createdAt': testGroup.createdAt.toIso8601String(),
},
],
'teams': [
{
'id': testTeam.id,
'name': testTeam.name,
'memberIds': [testPlayer1.id, testPlayer2.id],
'createdAt': testTeam.createdAt.toIso8601String(),
},
],
'matches': [
{
'id': testMatch.id,
'name': testMatch.name,
'gameId': testGame.id,
'groupId': testGroup.id,
'playerIds': [testPlayer1.id, testPlayer2.id],
'notes': testMatch.notes,
'scores': {
testPlayer1.id: [
{'roundNumber': 1, 'score': 10, 'change': 10},
{'roundNumber': 2, 'score': 20, 'change': 10},
],
testPlayer2.id: [
{'roundNumber': 1, 'score': 15, 'change': 15},
{'roundNumber': 2, 'score': 25, 'change': 10},
],
},
'createdAt': testMatch.createdAt.toIso8601String(),
'endedAt': null,
},
],
});
final isValid = await DataTransferService.validateJsonSchema(validJson);
expect(isValid, true);
});
});
}