10 Commits

Author SHA1 Message Date
43e9196dca made icon optional and default to empty string & adjust all game instances
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 36s
Pull Request Pipeline / lint (pull_request) Successful in 46s
2026-03-09 16:24:04 +01:00
16dc9746bc refactor: rename callback for game creation/editing to onGameChanged
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 38s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-03-08 20:02:50 +01:00
487a921def add error message for game deletion and implement search functionality
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 39s
Pull Request Pipeline / lint (pull_request) Successful in 45s
2026-03-08 17:01:41 +01:00
69f9900f74 fix linter issues
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 38s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-03-08 15:19:20 +01:00
69d9397ab3 add functionality to create/edit/select groups
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 38s
Pull Request Pipeline / lint (pull_request) Failing after 44s
2026-03-08 14:49:48 +01:00
9c92ded4fa merge & implement choose_color_view.dart
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 39s
Pull Request Pipeline / lint (pull_request) Failing after 44s
2026-03-08 10:43:58 +01:00
d577acc185 Merge remote-tracking branch 'origin/development' into feature/119-implementierung-der-games
# Conflicts:
#	lib/data/dto/game.dart
#	lib/l10n/arb/app_de.arb
#	lib/l10n/arb/app_en.arb
#	lib/l10n/generated/app_localizations.dart
#	lib/l10n/generated/app_localizations_de.dart
#	lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart
#	lib/presentation/widgets/text_input/text_input_field.dart
#	pubspec.yaml
2026-03-08 09:53:12 +01:00
58d8d07b63 Merge remote-tracking branch 'origin/development' into feature/119-implementierung-der-games
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m41s
Pull Request Pipeline / lint (pull_request) Successful in 2m42s
# Conflicts:
#	lib/presentation/widgets/text_input/text_input_field.dart
2026-01-18 14:56:38 +01:00
c983ca22dd Merge remote-tracking branch 'origin/development' into feature/119-implementierung-der-games
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m33s
Pull Request Pipeline / lint (pull_request) Successful in 2m33s
2026-01-18 14:55:56 +01:00
7024699a61 implement create game view
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m53s
Pull Request Pipeline / lint (pull_request) Successful in 3m3s
2026-01-18 14:38:27 +01:00
67 changed files with 2913 additions and 2954 deletions

View File

@@ -1,5 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart
linter: linter:
rules: rules:
avoid_print: false avoid_print: false
@@ -11,8 +15,4 @@ linter:
prefer_const_literals_to_create_immutables: true prefer_const_literals_to_create_immutables: true
unnecessary_const: true unnecessary_const: true
lines_longer_than_80_chars: false lines_longer_than_80_chars: false
constant_identifier_names: false constant_identifier_names: false
analyzer:
exclude:
- lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart

View File

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

View File

@@ -1,6 +1,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
/// Translates a [Ruleset] enum value to its corresponding localized string. /// Translates a [Ruleset] enum value to its corresponding localized string.
@@ -20,6 +20,51 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) {
} }
} }
/// Translates a [GameColor] enum value to its corresponding localized string.
String translateGameColorToString(GameColor color, BuildContext context) {
final loc = AppLocalizations.of(context);
switch (color) {
case GameColor.red:
return loc.color_red;
case GameColor.blue:
return loc.color_blue;
case GameColor.green:
return loc.color_green;
case GameColor.yellow:
return loc.color_yellow;
case GameColor.purple:
return loc.color_purple;
case GameColor.orange:
return loc.color_orange;
case GameColor.pink:
return loc.color_pink;
case GameColor.teal:
return loc.color_teal;
}
}
/// Returns the [Color] object corresponding to a [GameColor] enum value.
Color getColorFromGameColor(GameColor color) {
switch (color) {
case GameColor.red:
return Colors.red;
case GameColor.blue:
return Colors.blue;
case GameColor.green:
return Colors.green;
case GameColor.yellow:
return Colors.yellow;
case GameColor.purple:
return Colors.purple;
case GameColor.orange:
return Colors.orange;
case GameColor.pink:
return Colors.pink;
case GameColor.teal:
return Colors.teal;
}
}
/// Counts how many players in the match are not part of the group /// Counts how many players in the match are not part of the group
/// Returns the count as a string, or an empty string if there is no group /// Returns the count as a string, or an empty string if there is no group
String getExtraPlayerCount(Match match) { String getExtraPlayerCount(Match match) {

View File

@@ -19,4 +19,7 @@ class Constants {
/// Maximum length for team names /// Maximum length for team names
static const int MAX_TEAM_NAME_LENGTH = 32; static const int MAX_TEAM_NAME_LENGTH = 32;
/// Maximum length for game descriptions
static const int MAX_GAME_DESCRIPTION_LENGTH = 256;
} }

View File

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

View File

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

View File

@@ -8,25 +8,4 @@ mixin _$GroupDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable; $PlayerTableTable get playerTable => attachedDatabase.playerTable;
$PlayerGroupTableTable get playerGroupTable => $PlayerGroupTableTable get playerGroupTable =>
attachedDatabase.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/group_table.dart';
import 'package:tallee/data/db/tables/match_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/db/tables/player_match_table.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
part 'match_dao.g.dart'; part 'match_dao.g.dart';
@@ -29,20 +29,16 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
} }
final players = final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? [];
final winner = await getWinner(matchId: row.id);
final scores = await db.scoreDao.getAllMatchScores(matchId: row.id);
final winner = await db.scoreDao.getWinner(matchId: row.id);
return Match( return Match(
id: row.id, id: row.id,
name: row.name, name: row.name ?? '',
game: game, game: game,
group: group, group: group,
players: players, players: players,
notes: row.notes ?? '', notes: row.notes ?? '',
createdAt: row.createdAt, createdAt: row.createdAt,
endedAt: row.endedAt, endedAt: row.endedAt,
scores: scores,
winner: winner, winner: winner,
); );
}), }),
@@ -64,20 +60,17 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
final players = final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
final scores = await db.scoreDao.getAllMatchScores(matchId: matchId); final winner = await getWinner(matchId: matchId);
final winner = await db.scoreDao.getWinner(matchId: matchId);
return Match( return Match(
id: result.id, id: result.id,
name: result.name, name: result.name ?? '',
game: game, game: game,
group: group, group: group,
players: players, players: players,
notes: result.notes ?? '', notes: result.notes ?? '',
createdAt: result.createdAt, createdAt: result.createdAt,
endedAt: result.endedAt, endedAt: result.endedAt,
scores: scores,
winner: winner, winner: winner,
); );
} }
@@ -92,7 +85,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
id: match.id, id: match.id,
gameId: match.game.id, gameId: match.game.id,
groupId: Value(match.group?.id), groupId: Value(match.group?.id),
name: match.name, name: Value(match.name),
notes: Value(match.notes), notes: Value(match.notes),
createdAt: match.createdAt, createdAt: match.createdAt,
endedAt: Value(match.endedAt), endedAt: Value(match.endedAt),
@@ -107,20 +100,8 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
); );
} }
for (final pid in match.scores.keys) {
final playerScores = match.scores[pid]!;
await db.scoreDao.addScoresAsList(
entrys: playerScores,
playerId: pid,
matchId: match.id,
);
}
if (match.winner != null) { if (match.winner != null) {
await db.scoreDao.setWinner( await setWinner(matchId: match.id, winnerId: match.winner!.id);
matchId: match.id,
playerId: match.winner!.id,
);
} }
}); });
} }
@@ -189,7 +170,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
id: match.id, id: match.id,
gameId: match.game.id, gameId: match.game.id,
groupId: Value(match.group?.id), groupId: Value(match.group?.id),
name: match.name, name: Value(match.name),
notes: Value(match.notes), notes: Value(match.notes),
createdAt: match.createdAt, createdAt: match.createdAt,
endedAt: Value(match.endedAt), endedAt: Value(match.endedAt),
@@ -242,6 +223,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
PlayerMatchTableCompanion.insert( PlayerMatchTableCompanion.insert(
matchId: match.id, matchId: match.id,
playerId: p.id, playerId: p.id,
score: 0,
), ),
mode: InsertMode.insertOrIgnore, mode: InsertMode.insertOrIgnore,
); );
@@ -286,34 +268,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return count ?? 0; 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.scoreDao.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. /// Checks if a match with the given [matchId] exists in the database.
/// Returns `true` if the match exists, otherwise `false`. /// Returns `true` if the match exists, otherwise `false`.
Future<bool> matchExists({required String matchId}) async { Future<bool> matchExists({required String matchId}) async {
@@ -384,17 +338,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return rowsAffected > 0; 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]. /// Updates the createdAt timestamp of the match with the given [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchCreatedAt({ Future<bool> updateMatchCreatedAt({
@@ -455,4 +398,91 @@ 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,25 +11,4 @@ mixin _$MatchDaoMixin on DatabaseAccessor<AppDatabase> {
$TeamTableTable get teamTable => attachedDatabase.teamTable; $TeamTableTable get teamTable => attachedDatabase.teamTable;
$PlayerMatchTableTable get playerMatchTable => $PlayerMatchTableTable get playerMatchTable =>
attachedDatabase.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:drift/drift.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/player_table.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
part 'player_dao.g.dart'; part 'player_dao.g.dart';

View File

@@ -5,12 +5,4 @@ part of 'player_dao.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$PlayerDaoMixin on DatabaseAccessor<AppDatabase> { mixin _$PlayerDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable; $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/database.dart';
import 'package:tallee/data/db/tables/player_group_table.dart'; import 'package:tallee/data/db/tables/player_group_table.dart';
import 'package:tallee/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/player_table.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
part 'player_group_dao.g.dart'; part 'player_group_dao.g.dart';

View File

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

View File

@@ -11,25 +11,4 @@ mixin _$PlayerMatchDaoMixin on DatabaseAccessor<AppDatabase> {
$TeamTableTable get teamTable => attachedDatabase.teamTable; $TeamTableTable get teamTable => attachedDatabase.teamTable;
$PlayerMatchTableTable get playerMatchTable => $PlayerMatchTableTable get playerMatchTable =>
attachedDatabase.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,14 +1,27 @@
import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/score_entry_table.dart'; import 'package:tallee/data/db/tables/score_table.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
part 'score_dao.g.dart'; part 'score_dao.g.dart';
@DriftAccessor(tables: [ScoreEntryTable]) /// 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 { class ScoreDao extends DatabaseAccessor<AppDatabase> with _$ScoreDaoMixin {
ScoreDao(super.db); ScoreDao(super.db);
@@ -16,134 +29,108 @@ class ScoreDao extends DatabaseAccessor<AppDatabase> with _$ScoreDaoMixin {
Future<void> addScore({ Future<void> addScore({
required String playerId, required String playerId,
required String matchId, required String matchId,
required ScoreEntry entry, required int roundNumber,
required int score,
required int change,
}) async { }) async {
await into(scoreEntryTable).insert( await into(scoreTable).insert(
ScoreEntryTableCompanion.insert( ScoreTableCompanion.insert(
playerId: playerId, playerId: playerId,
matchId: matchId, matchId: matchId,
roundNumber: entry.roundNumber, roundNumber: roundNumber,
score: entry.score, score: score,
change: entry.change, change: change,
), ),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
); );
} }
Future<void> addScoresAsList({ /// Retrieves all scores for a specific match.
required List<ScoreEntry> entrys, Future<List<ScoreEntry>> getScoresForMatch({required String matchId}) async {
required String playerId, final query = select(scoreTable)..where((s) => s.matchId.equals(matchId));
required String matchId, final result = await query.get();
}) async { return result
if (entrys.isEmpty) return;
final entries = entrys
.map( .map(
(score) => ScoreEntryTableCompanion.insert( (row) => ScoreEntry(
playerId: playerId, playerId: row.playerId,
matchId: matchId, matchId: row.matchId,
roundNumber: score.roundNumber, roundNumber: row.roundNumber,
score: score.score, score: row.score,
change: score.change, change: row.change,
), ),
) )
.toList(); .toList();
}
await batch((batch) { /// Retrieves all scores for a specific player in a match.
batch.insertAll( Future<List<ScoreEntry>> getPlayerScoresInMatch({
scoreEntryTable, required String playerId,
entries, required String matchId,
mode: InsertMode.insertOrReplace, }) 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. /// Retrieves the score for a specific round.
Future<ScoreEntry?> getScore({ Future<ScoreEntry?> getScoreForRound({
required String playerId, required String playerId,
required String matchId, required String matchId,
int roundNumber = 0, required int roundNumber,
}) async { }) async {
final query = select(scoreEntryTable) final query = select(scoreTable)
..where( ..where(
(s) => (s) =>
s.playerId.equals(playerId) & s.playerId.equals(playerId) &
s.matchId.equals(matchId) & s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber), s.roundNumber.equals(roundNumber),
); );
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
if (result == null) return null; if (result == null) return null;
return ScoreEntry( return ScoreEntry(
playerId: result.playerId,
matchId: result.matchId,
roundNumber: result.roundNumber, roundNumber: result.roundNumber,
score: result.score, score: result.score,
change: result.change, 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. /// Updates a score entry.
Future<bool> updateScore({ Future<bool> updateScore({
required String playerId, required String playerId,
required String matchId, required String matchId,
required ScoreEntry newEntry, required int roundNumber,
required int newScore,
required int newChange,
}) async { }) async {
final rowsAffected = final rowsAffected = await (update(scoreTable)
await (update(scoreEntryTable)..where( ..where(
(s) => (s) =>
s.playerId.equals(playerId) & s.playerId.equals(playerId) &
s.matchId.equals(matchId) & s.matchId.equals(matchId) &
s.roundNumber.equals(newEntry.roundNumber), s.roundNumber.equals(roundNumber),
)) ))
.write( .write(
ScoreEntryTableCompanion( ScoreTableCompanion(
score: Value(newEntry.score), score: Value(newScore),
change: Value(newEntry.change), change: Value(newChange),
), ),
); );
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -151,9 +138,9 @@ class ScoreDao extends DatabaseAccessor<AppDatabase> with _$ScoreDaoMixin {
Future<bool> deleteScore({ Future<bool> deleteScore({
required String playerId, required String playerId,
required String matchId, required String matchId,
int roundNumber = 0, required int roundNumber,
}) async { }) async {
final query = delete(scoreEntryTable) final query = delete(scoreTable)
..where( ..where(
(s) => (s) =>
s.playerId.equals(playerId) & s.playerId.equals(playerId) &
@@ -164,164 +151,41 @@ class ScoreDao extends DatabaseAccessor<AppDatabase> with _$ScoreDaoMixin {
return rowsAffected > 0; return rowsAffected > 0;
} }
Future<bool> deleteAllScoresForMatch({required String matchId}) async { /// Deletes all scores for a specific match.
final query = delete(scoreEntryTable) Future<bool> deleteScoresForMatch({required String matchId}) async {
..where((s) => s.matchId.equals(matchId)); final query = delete(scoreTable)..where((s) => s.matchId.equals(matchId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
Future<bool> deleteAllScoresForPlayerInMatch({ /// Deletes all scores for a specific player.
required String matchId, Future<bool> deleteScoresForPlayer({required String playerId}) async {
required String playerId, final query = delete(scoreTable)..where((s) => s.playerId.equals(playerId));
}) async {
final query = delete(scoreEntryTable)
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
/// Gets the highest (latest) round number for a match. /// Gets the latest round number for a match.
/// Returns `null` if there are no scores for the match. Future<int> getLatestRoundNumber({required String matchId}) async {
Future<int?> getLatestRoundNumber({required String matchId}) async { final query = selectOnly(scoreTable)
final query = selectOnly(scoreEntryTable) ..where(scoreTable.matchId.equals(matchId))
..where(scoreEntryTable.matchId.equals(matchId)) ..addColumns([scoreTable.roundNumber.max()]);
..addColumns([scoreEntryTable.roundNumber.max()]);
final result = await query.getSingle(); final result = await query.getSingle();
return result.read(scoreEntryTable.roundNumber.max()); return result.read(scoreTable.roundNumber.max()) ?? 0;
} }
/// Aggregates the total score for a player in a match by summing all their /// Gets the total score for a player in a match (sum of all changes).
/// score entry changes. Returns `0` if there are no scores for the player
/// in the match.
Future<int> getTotalScoreForPlayer({ Future<int> getTotalScoreForPlayer({
required String playerId, required String playerId,
required String matchId, required String matchId,
}) async { }) async {
final scores = await getAllPlayerScoresInMatch( final scores = await getPlayerScoresInMatch(
playerId: playerId, playerId: playerId,
matchId: matchId, matchId: matchId,
); );
if (scores.isEmpty) return 0; if (scores.isEmpty) return 0;
// Return the sum of all score changes // Return the score from the latest round
return scores.fold<int>(0, (sum, element) => sum + element.change); return scores.last.score;
}
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

@@ -8,24 +8,5 @@ mixin _$ScoreDaoMixin on DatabaseAccessor<AppDatabase> {
$GameTableTable get gameTable => attachedDatabase.gameTable; $GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupTableTable get groupTable => attachedDatabase.groupTable; $GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable; $MatchTableTable get matchTable => attachedDatabase.matchTable;
$ScoreEntryTableTable get scoreEntryTable => attachedDatabase.scoreEntryTable; $ScoreTableTable get scoreTable => attachedDatabase.scoreTable;
ScoreDaoManager get managers => ScoreDaoManager(this);
}
class ScoreDaoManager {
final _$ScoreDaoMixin _db;
ScoreDaoManager(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:drift/drift.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/team_table.dart'; import 'package:tallee/data/db/tables/team_table.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/team.dart'; import 'package:tallee/data/dto/team.dart';
part 'team_dao.g.dart'; part 'team_dao.g.dart';
@@ -144,3 +144,4 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
return rowsAffected > 0; return rowsAffected > 0;
} }
} }

View File

@@ -5,12 +5,4 @@ part of 'team_dao.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$TeamDaoMixin on DatabaseAccessor<AppDatabase> { mixin _$TeamDaoMixin on DatabaseAccessor<AppDatabase> {
$TeamTableTable get teamTable => attachedDatabase.teamTable; $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

@@ -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_group_table.dart';
import 'package:tallee/data/db/tables/player_match_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/player_table.dart';
import 'package:tallee/data/db/tables/score_entry_table.dart'; import 'package:tallee/data/db/tables/score_table.dart';
import 'package:tallee/data/db/tables/team_table.dart'; import 'package:tallee/data/db/tables/team_table.dart';
part 'database.g.dart'; part 'database.g.dart';
@@ -29,7 +29,7 @@ part 'database.g.dart';
PlayerMatchTable, PlayerMatchTable,
GameTable, GameTable,
TeamTable, TeamTable,
ScoreEntryTable, ScoreTable,
], ],
daos: [ daos: [
PlayerDao, PlayerDao,
@@ -39,7 +39,7 @@ part 'database.g.dart';
PlayerMatchDao, PlayerMatchDao,
GameDao, GameDao,
ScoreDao, ScoreDao,
TeamDao, TeamDao
], ],
) )
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {
@@ -60,9 +60,7 @@ class AppDatabase extends _$AppDatabase {
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
name: 'gametracker_db', name: 'gametracker_db',
native: const DriftNativeOptions( native: const DriftNativeOptions(databaseDirectory: getApplicationSupportDirectory),
databaseDirectory: getApplicationSupportDirectory,
),
); );
} }
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -8,7 +8,9 @@ class PlayerMatchTable extends Table {
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)(); text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId => TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)(); text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
TextColumn get teamId => text().references(TeamTable, #id).nullable()(); TextColumn get teamId =>
text().references(TeamTable, #id).nullable()();
IntColumn get score => integer()();
@override @override
Set<Column<Object>> get primaryKey => {playerId, matchId}; 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/match_table.dart';
import 'package:tallee/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/player_table.dart';
class ScoreEntryTable extends Table { class ScoreTable extends Table {
TextColumn get playerId => TextColumn get playerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)(); text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId => TextColumn get matchId =>
@@ -13,4 +13,4 @@ class ScoreEntryTable extends Table {
@override @override
Set<Column<Object>> get primaryKey => {playerId, matchId, roundNumber}; Set<Column<Object>> get primaryKey => {playerId, matchId, roundNumber};
} }

View File

@@ -1,6 +1,6 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:uuid/uuid.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:uuid/uuid.dart';
class Game { class Game {
final String id; final String id;
@@ -18,10 +18,11 @@ class Game {
required this.ruleset, required this.ruleset,
String? description, String? description,
required this.color, required this.color,
required this.icon, String? icon,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(), createdAt = createdAt ?? clock.now(),
description = description ?? ''; description = description ?? '',
icon = icon ?? '';
@override @override
String toString() { String toString() {
@@ -49,4 +50,3 @@ class Game {
'icon': icon, 'icon': icon,
}; };
} }

View File

@@ -1,5 +1,5 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class Group { class Group {

View File

@@ -1,9 +1,8 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/score_entry.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class Match { class Match {
@@ -15,7 +14,6 @@ class Match {
final Group? group; final Group? group;
final List<Player> players; final List<Player> players;
final String notes; final String notes;
Map<String, List<ScoreEntry>> scores;
Player? winner; Player? winner;
Match({ Match({
@@ -26,16 +24,15 @@ class Match {
required this.game, required this.game,
this.group, this.group,
this.players = const [], this.players = const [],
this.notes = '', String? notes,
Map<String, List<ScoreEntry>>? scores,
this.winner, this.winner,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(), createdAt = createdAt ?? clock.now(),
scores = scores ?? {for (var player in players) player.id: []}; notes = notes ?? '';
@override @override
String toString() { String toString() {
return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, winner: $winner}'; 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). /// Creates a Match instance from a JSON object (ID references format).
@@ -52,11 +49,9 @@ class Match {
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: '', description: '',
color: GameColor.blue, color: GameColor.blue,
icon: '',
), // Populated during import via DataTransferService ), // Populated during import via DataTransferService
group = null, // Populated during import via DataTransferService group = null, // Populated during import via DataTransferService
players = [], // Populated during import via DataTransferService players = [], // Populated during import via DataTransferService
scores = json['scores'],
notes = json['notes'] ?? ''; notes = json['notes'] ?? '';
/// Converts the Match instance to a JSON object using normalized format (ID references only). /// Converts the Match instance to a JSON object using normalized format (ID references only).
@@ -68,10 +63,6 @@ class Match {
'gameId': game.id, 'gameId': game.id,
'groupId': group?.id, 'groupId': group?.id,
'playerIds': players.map((player) => player.id).toList(), 'playerIds': players.map((player) => player.id).toList(),
'scores': scores.map(
(playerId, scoreList) =>
MapEntry(playerId, scoreList.map((score) => score.toJson()).toList()),
),
'notes': notes, 'notes': notes,
}; };
} }

View File

@@ -1,5 +1,5 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class Team { class Team {
@@ -37,3 +37,4 @@ class Team {
'memberIds': members.map((member) => member.id).toList(), 'memberIds': members.map((member) => member.id).toList(),
}; };
} }

View File

@@ -1,22 +0,0 @@
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

@@ -9,7 +9,9 @@
"choose_game": "Spielvorlage wählen", "choose_game": "Spielvorlage wählen",
"choose_group": "Gruppe wählen", "choose_group": "Gruppe wählen",
"choose_ruleset": "Regelwerk wählen", "choose_ruleset": "Regelwerk wählen",
"choose_color": "Farbe wählen",
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
"create_game": "Spielvorlage erstellen",
"create_group": "Gruppe erstellen", "create_group": "Gruppe erstellen",
"create_match": "Spiel erstellen", "create_match": "Spiel erstellen",
"create_new_group": "Neue Gruppe erstellen", "create_new_group": "Neue Gruppe erstellen",
@@ -24,10 +26,15 @@
"delete_all_data": "Alle Daten löschen", "delete_all_data": "Alle Daten löschen",
"delete_group": "Diese Gruppe löschen", "delete_group": "Diese Gruppe löschen",
"delete_match": "Spiel löschen", "delete_match": "Spiel löschen",
"delete_game": "Spielvorlage löschen",
"delete_group": "Gruppe löschen",
"description": "Beschreibung",
"edit_game": "Spielvorlage bearbeiten",
"edit_group": "Gruppe bearbeiten", "edit_group": "Gruppe bearbeiten",
"edit_match": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten",
"enter_results": "Ergebnisse eintragen", "enter_results": "Ergebnisse eintragen",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
"error_deleting_game": "Fehler beim Löschen der Spielvorlage, bitte erneut versuchen",
"error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen", "error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen",
"error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen", "error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen",
"error_reading_file": "Fehler beim Lesen der Datei", "error_reading_file": "Fehler beim Lesen der Datei",
@@ -106,5 +113,14 @@
"winner": "Gewinner:in", "winner": "Gewinner:in",
"winrate": "Siegquote", "winrate": "Siegquote",
"wins": "Siege", "wins": "Siege",
"yesterday_at": "Gestern um" "yesterday_at": "Gestern um",
"color_red": "Rot",
"color_blue": "Blau",
"color_green": "Grün",
"color_yellow": "Gelb",
"color_purple": "Lila",
"color_orange": "Orange",
"color_pink": "Rosa",
"color_teal": "Türkis",
"color": "Farbe"
} }

View File

@@ -27,9 +27,15 @@
"@choose_ruleset": { "@choose_ruleset": {
"description": "Label for choosing a ruleset" "description": "Label for choosing a ruleset"
}, },
"@choose_color": {
"description": "Label for choosing a color"
},
"@could_not_add_player": { "@could_not_add_player": {
"description": "Error message when adding a player fails" "description": "Error message when adding a player fails"
}, },
"@create_game": {
"description": "Button text to create a game"
},
"@create_group": { "@create_group": {
"description": "Button text to create a group" "description": "Button text to create a group"
}, },
@@ -71,12 +77,21 @@
"@delete_all_data": { "@delete_all_data": {
"description": "Confirmation dialog for deleting all data" "description": "Confirmation dialog for deleting all data"
}, },
"@delete_game": {
"description": "Button text to delete a game"
},
"@delete_group": { "@delete_group": {
"description": "Confirmation dialog for deleting a group" "description": "Confirmation dialog for deleting a group"
}, },
"@delete_match": { "@delete_match": {
"description": "Button text to delete a match" "description": "Button text to delete a match"
}, },
"description": {
"description": "Description label"
},
"edit_game": {
"description": "Button text to edit a game"
},
"@edit_group": { "@edit_group": {
"description": "Button & Appbar label for editing a group" "description": "Button & Appbar label for editing a group"
}, },
@@ -89,6 +104,9 @@
"@error_creating_group": { "@error_creating_group": {
"description": "Error message when group creation fails" "description": "Error message when group creation fails"
}, },
"@error_deleting_game": {
"description": "Error message when game deletion fails"
},
"@error_deleting_group": { "@error_deleting_group": {
"description": "Error message when group deletion fails" "description": "Error message when group deletion fails"
}, },
@@ -325,6 +343,9 @@
"@yesterday_at": { "@yesterday_at": {
"description": "Date format for yesterday" "description": "Date format for yesterday"
}, },
"@color": {
"description": "Color label"
},
"all_players": "All players", "all_players": "All players",
"all_players_selected": "All players selected", "all_players_selected": "All players selected",
"amount_of_matches": "Amount of Matches", "amount_of_matches": "Amount of Matches",
@@ -334,7 +355,9 @@
"choose_game": "Choose Game", "choose_game": "Choose Game",
"choose_group": "Choose Group", "choose_group": "Choose Group",
"choose_ruleset": "Choose Ruleset", "choose_ruleset": "Choose Ruleset",
"choose_color": "Choose Color",
"could_not_add_player": "Could not add player", "could_not_add_player": "Could not add player",
"create_game": "Create Game",
"create_group": "Create Group", "create_group": "Create Group",
"create_match": "Create match", "create_match": "Create match",
"create_new_group": "Create new group", "create_new_group": "Create new group",
@@ -347,12 +370,16 @@
"days_ago": "{count} days ago", "days_ago": "{count} days ago",
"delete": "Delete", "delete": "Delete",
"delete_all_data": "Delete all data", "delete_all_data": "Delete all data",
"delete_game": "Delete Game",
"delete_group": "Delete Group", "delete_group": "Delete Group",
"delete_match": "Delete Match", "delete_match": "Delete Match",
"description": "Description",
"edit_game": "Edit Game",
"edit_group": "Edit Group", "edit_group": "Edit Group",
"edit_match": "Edit Match", "edit_match": "Edit Match",
"enter_results": "Enter Results", "enter_results": "Enter Results",
"error_creating_group": "Error while creating group, please try again", "error_creating_group": "Error while creating group, please try again",
"error_deleting_game": "Error while deleting game, please try again",
"error_deleting_group": "Error while deleting group, please try again", "error_deleting_group": "Error while deleting group, please try again",
"error_editing_group": "Error while editing group, please try again", "error_editing_group": "Error while editing group, please try again",
"error_reading_file": "Error reading file", "error_reading_file": "Error reading file",
@@ -430,5 +457,14 @@
"winner": "Winner", "winner": "Winner",
"winrate": "Winrate", "winrate": "Winrate",
"wins": "Wins", "wins": "Wins",
"yesterday_at": "Yesterday at" "yesterday_at": "Yesterday at",
"color_red": "Red",
"color_blue": "Blue",
"color_green": "Green",
"color_yellow": "Yellow",
"color_purple": "Purple",
"color_orange": "Orange",
"color_pink": "Pink",
"color_teal": "Teal",
"color": "Color"
} }

View File

@@ -98,6 +98,18 @@ abstract class AppLocalizations {
Locale('en'), Locale('en'),
]; ];
/// No description provided for @description.
///
/// In en, this message translates to:
/// **'Description'**
String get description;
/// No description provided for @edit_game.
///
/// In en, this message translates to:
/// **'Edit Game'**
String get edit_game;
/// Label for all players list /// Label for all players list
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -152,12 +164,24 @@ abstract class AppLocalizations {
/// **'Choose Ruleset'** /// **'Choose Ruleset'**
String get choose_ruleset; String get choose_ruleset;
/// Label for choosing a color
///
/// In en, this message translates to:
/// **'Choose Color'**
String get choose_color;
/// Error message when adding a player fails /// Error message when adding a player fails
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Could not add player'** /// **'Could not add player'**
String could_not_add_player(Object playerName); String could_not_add_player(Object playerName);
/// Button text to create a game
///
/// In en, this message translates to:
/// **'Create Game'**
String get create_game;
/// Button text to create a group /// Button text to create a group
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -230,6 +254,12 @@ abstract class AppLocalizations {
/// **'Delete all data'** /// **'Delete all data'**
String get delete_all_data; String get delete_all_data;
/// Button text to delete a game
///
/// In en, this message translates to:
/// **'Delete Game'**
String get delete_game;
/// Confirmation dialog for deleting a group /// Confirmation dialog for deleting a group
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -266,6 +296,12 @@ abstract class AppLocalizations {
/// **'Error while creating group, please try again'** /// **'Error while creating group, please try again'**
String get error_creating_group; String get error_creating_group;
/// Error message when game deletion fails
///
/// In en, this message translates to:
/// **'Error while deleting game, please try again'**
String get error_deleting_game;
/// Error message when group deletion fails /// Error message when group deletion fails
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -733,6 +769,60 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Yesterday at'** /// **'Yesterday at'**
String get yesterday_at; String get yesterday_at;
/// No description provided for @color_red.
///
/// In en, this message translates to:
/// **'Red'**
String get color_red;
/// No description provided for @color_blue.
///
/// In en, this message translates to:
/// **'Blue'**
String get color_blue;
/// No description provided for @color_green.
///
/// In en, this message translates to:
/// **'Green'**
String get color_green;
/// No description provided for @color_yellow.
///
/// In en, this message translates to:
/// **'Yellow'**
String get color_yellow;
/// No description provided for @color_purple.
///
/// In en, this message translates to:
/// **'Purple'**
String get color_purple;
/// No description provided for @color_orange.
///
/// In en, this message translates to:
/// **'Orange'**
String get color_orange;
/// No description provided for @color_pink.
///
/// In en, this message translates to:
/// **'Pink'**
String get color_pink;
/// No description provided for @color_teal.
///
/// In en, this message translates to:
/// **'Teal'**
String get color_teal;
/// Color label
///
/// In en, this message translates to:
/// **'Color'**
String get color;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View File

@@ -8,6 +8,12 @@ import 'app_localizations.dart';
class AppLocalizationsDe extends AppLocalizations { class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale); AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get description => 'Beschreibung';
@override
String get edit_game => 'Spielvorlage bearbeiten';
@override @override
String get all_players => 'Alle Spieler:innen'; String get all_players => 'Alle Spieler:innen';
@@ -35,11 +41,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get choose_ruleset => 'Regelwerk wählen'; String get choose_ruleset => 'Regelwerk wählen';
@override
String get choose_color => 'Farbe wählen';
@override @override
String could_not_add_player(Object playerName) { String could_not_add_player(Object playerName) {
return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
} }
@override
String get create_game => 'Spielvorlage erstellen';
@override @override
String get create_group => 'Gruppe erstellen'; String get create_group => 'Gruppe erstellen';
@@ -79,7 +91,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get delete_all_data => 'Alle Daten löschen'; String get delete_all_data => 'Alle Daten löschen';
@override @override
String get delete_group => 'Diese Gruppe löschen'; String get delete_game => 'Spielvorlage löschen';
@override
String get delete_group => 'Gruppe löschen';
@override @override
String get delete_match => 'Spiel löschen'; String get delete_match => 'Spiel löschen';
@@ -97,6 +112,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get error_creating_group => String get error_creating_group =>
'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen';
@override
String get error_deleting_game =>
'Fehler beim Löschen der Spielvorlage, bitte erneut versuchen';
@override @override
String get error_deleting_group => String get error_deleting_group =>
'Fehler beim Löschen der Gruppe, bitte erneut versuchen'; 'Fehler beim Löschen der Gruppe, bitte erneut versuchen';
@@ -343,4 +362,31 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get yesterday_at => 'Gestern um'; String get yesterday_at => 'Gestern um';
@override
String get color_red => 'Rot';
@override
String get color_blue => 'Blau';
@override
String get color_green => 'Grün';
@override
String get color_yellow => 'Gelb';
@override
String get color_purple => 'Lila';
@override
String get color_orange => 'Orange';
@override
String get color_pink => 'Rosa';
@override
String get color_teal => 'Türkis';
@override
String get color => 'Farbe';
} }

View File

@@ -8,6 +8,12 @@ import 'app_localizations.dart';
class AppLocalizationsEn extends AppLocalizations { class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale); AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get description => 'Description';
@override
String get edit_game => 'Edit Game';
@override @override
String get all_players => 'All players'; String get all_players => 'All players';
@@ -35,11 +41,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get choose_ruleset => 'Choose Ruleset'; String get choose_ruleset => 'Choose Ruleset';
@override
String get choose_color => 'Choose Color';
@override @override
String could_not_add_player(Object playerName) { String could_not_add_player(Object playerName) {
return 'Could not add player'; return 'Could not add player';
} }
@override
String get create_game => 'Create Game';
@override @override
String get create_group => 'Create Group'; String get create_group => 'Create Group';
@@ -78,6 +90,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get delete_all_data => 'Delete all data'; String get delete_all_data => 'Delete all data';
@override
String get delete_game => 'Delete Game';
@override @override
String get delete_group => 'Delete Group'; String get delete_group => 'Delete Group';
@@ -97,6 +112,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get error_creating_group => String get error_creating_group =>
'Error while creating group, please try again'; 'Error while creating group, please try again';
@override
String get error_deleting_game =>
'Error while deleting game, please try again';
@override @override
String get error_deleting_group => String get error_deleting_group =>
'Error while deleting group, please try again'; 'Error while deleting group, please try again';
@@ -342,4 +361,31 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get yesterday_at => 'Yesterday at'; String get yesterday_at => 'Yesterday at';
@override
String get color_red => 'Red';
@override
String get color_blue => 'Blue';
@override
String get color_green => 'Green';
@override
String get color_yellow => 'Yellow';
@override
String get color_purple => 'Purple';
@override
String get color_orange => 'Orange';
@override
String get color_pink => 'Pink';
@override
String get color_teal => 'Teal';
@override
String get color => 'Color';
} }

View File

@@ -1,24 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/player_selection.dart'; import 'package:tallee/presentation/widgets/player_selection.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
class CreateGroupView extends StatefulWidget { class CreateGroupView extends StatefulWidget {
const CreateGroupView({super.key, this.groupToEdit, this.onMembersChanged}); const CreateGroupView({super.key, this.groupToEdit});
/// The group to edit, if any /// The group to edit, if any
final Group? groupToEdit; final Group? groupToEdit;
final VoidCallback? onMembersChanged;
@override @override
State<CreateGroupView> createState() => _CreateGroupViewState(); State<CreateGroupView> createState() => _CreateGroupViewState();
} }
@@ -72,6 +69,49 @@ class _CreateGroupViewState extends State<CreateGroupView> {
title: Text( title: Text(
widget.groupToEdit == null ? loc.create_new_group : loc.edit_group, 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( body: SafeArea(
child: Column( child: Column(
@@ -82,7 +122,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
child: TextInputField( child: TextInputField(
controller: _groupNameController, controller: _groupNameController,
hintText: loc.group_name, hintText: loc.group_name,
maxLength: Constants.MAX_GROUP_NAME_LENGTH,
), ),
), ),
Expanded( Expanded(
@@ -105,7 +144,42 @@ class _CreateGroupViewState extends State<CreateGroupView> {
(_groupNameController.text.isEmpty || (_groupNameController.text.isEmpty ||
(selectedPlayers.length < 2)) (selectedPlayers.length < 2))
? null ? null
: _saveGroup, : () 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,
);
}
},
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
@@ -115,104 +189,6 @@ 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. /// Displays a snackbar with the given message and optional action.
/// ///
/// [message] The message to display in the snackbar. /// [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/adaptive_page_route.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/l10n/generated/app_localizations.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/create_group_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart';
@@ -191,12 +191,7 @@ class _GroupDetailViewState extends State<GroupDetailView> {
context, context,
adaptivePageRoute( adaptivePageRoute(
builder: (context) { builder: (context) {
return CreateGroupView( return CreateGroupView(groupToEdit: _group);
groupToEdit: _group,
onMembersChanged: () {
_loadStatistics();
},
);
}, },
), ),
); );
@@ -247,8 +242,10 @@ class _GroupDetailViewState extends State<GroupDetailView> {
/// Loads statistics for this group /// Loads statistics for this group
Future<void> _loadStatistics() async { Future<void> _loadStatistics() async {
isLoading = true; final matches = await db.matchDao.getAllMatches();
final groupMatches = await db.matchDao.getGroupMatches(groupId: _group.id); final groupMatches = matches
.where((match) => match.group?.id == _group.id)
.toList();
setState(() { setState(() {
totalMatches = groupMatches.length; totalMatches = groupMatches.length;
@@ -263,9 +260,7 @@ class _GroupDetailViewState extends State<GroupDetailView> {
// Count wins for each player // Count wins for each player
for (var match in matches) { 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( bestPlayerCounts.update(
match.winner!, match.winner!,
(value) => value + 1, (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/constants.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/l10n/generated/app_localizations.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/create_group_view.dart';
import 'package:tallee/presentation/views/main_menu/group_view/group_detail_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/constants.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart';
@@ -47,7 +47,6 @@ class _HomeViewState extends State<HomeView> {
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: '', description: '',
color: GameColor.blue, color: GameColor.blue,
icon: '',
), ),
group: Group( group: Group(
name: 'Skeleton Group', name: 'Skeleton Group',
@@ -227,7 +226,7 @@ class _HomeViewState extends State<HomeView> {
/// Updates the winner information for a specific match in the recent matches list. /// Updates the winner information for a specific match in the recent matches list.
Future<void> updatedWinnerInRecentMatches(String matchId) async { Future<void> updatedWinnerInRecentMatches(String matchId) async {
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
final winner = await db.scoreDao.getWinner(matchId: matchId); final winner = await db.matchDao.getWinner(matchId: matchId);
final matchIndex = recentMatches.indexWhere((match) => match.id == matchId); final matchIndex = recentMatches.indexWhere((match) => match.id == matchId);
if (matchIndex != -1) { if (matchIndex != -1) {
setState(() { setState(() {

View File

@@ -1,26 +1,33 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/data/dto/game.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/game_view/create_game_view.dart';
import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseGameView extends StatefulWidget { class ChooseGameView extends StatefulWidget {
/// A view that allows the user to choose a game from a list of available games /// A view that allows the user to choose a game from a list of available games
/// - [games]: A list of tuples containing the game name, description and ruleset /// - [games]: The list of available games
/// - [initialGameIndex]: The index of the initially selected game /// - [initialSelectedGameId]: The id of the initially selected game
/// - [onGamesUpdated]: Optional callback invoked when the games are updated
const ChooseGameView({ const ChooseGameView({
super.key, super.key,
required this.games, required this.games,
required this.initialGameIndex, required this.initialSelectedGameId,
this.onGamesUpdated,
}); });
/// A list of tuples containing the game name, description and ruleset /// A list of tuples containing the game name, description and ruleset
final List<(String, String, Ruleset)> games; final List<Game> games;
/// The index of the initially selected game /// The index of the initially selected game
final int initialGameIndex; final String initialSelectedGameId;
/// Optional callback invoked when the games are updated
final VoidCallback? onGamesUpdated;
@override @override
State<ChooseGameView> createState() => _ChooseGameViewState(); State<ChooseGameView> createState() => _ChooseGameViewState();
@@ -30,12 +37,18 @@ class _ChooseGameViewState extends State<ChooseGameView> {
/// Controller for the search bar /// Controller for the search bar
final TextEditingController searchBarController = TextEditingController(); final TextEditingController searchBarController = TextEditingController();
/// Currently selected game index /// Currently selected game id
late int selectedGameIndex; late String selectedGameId;
/// Games filtered according to the current search query
late List<Game> filteredGames;
@override @override
void initState() { void initState() {
selectedGameIndex = widget.initialGameIndex; selectedGameId = widget.initialSelectedGameId;
// Start with all games visible
filteredGames = List<Game>.from(widget.games);
super.initState(); super.initState();
} }
@@ -49,9 +62,32 @@ class _ChooseGameViewState extends State<ChooseGameView> {
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back_ios), icon: const Icon(Icons.arrow_back_ios),
onPressed: () { onPressed: () {
Navigator.of(context).pop(selectedGameIndex); Navigator.of(context).pop(selectedGameId);
}, },
), ),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
final result = await Navigator.push(
context,
adaptivePageRoute(
builder: (context) => CreateGameView(
onGameChanged: () {
widget.onGamesUpdated?.call();
},
),
),
);
if (result != null && result.game != null) {
setState(() {
widget.games.insert(0, result.game);
});
_refreshFromSource();
}
},
),
],
title: Text(loc.choose_game), title: Text(loc.choose_game),
), ),
body: PopScope( body: PopScope(
@@ -62,7 +98,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
if (didPop) { if (didPop) {
return; return;
} }
Navigator.of(context).pop(selectedGameIndex); Navigator.of(context).pop(selectedGameId);
}, },
child: Column( child: Column(
children: [ children: [
@@ -71,30 +107,63 @@ class _ChooseGameViewState extends State<ChooseGameView> {
child: CustomSearchBar( child: CustomSearchBar(
controller: searchBarController, controller: searchBarController,
hintText: loc.game_name, hintText: loc.game_name,
onChanged: (value) {
_applySearchFilter(value);
},
), ),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: widget.games.length, itemCount: filteredGames.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final game = filteredGames[index];
return TitleDescriptionListTile( return TitleDescriptionListTile(
title: widget.games[index].$1, title: game.name,
description: widget.games[index].$2, description: game.description,
badgeText: translateRulesetToString( badgeText: translateRulesetToString(game.ruleset, context),
widget.games[index].$3, isHighlighted: selectedGameId == game.id,
context, onTap: () async {
),
isHighlighted: selectedGameIndex == index,
onPressed: () async {
setState(() { setState(() {
if (selectedGameIndex == index) { if (selectedGameId == game.id) {
selectedGameIndex = -1; selectedGameId = '';
} else { } else {
selectedGameIndex = index; selectedGameId = game.id;
} }
}); });
}, },
onLongPress: () async {
final result = await Navigator.push(
context,
adaptivePageRoute(
builder: (context) => CreateGameView(
gameToEdit: game,
onGameChanged: () {
widget.onGamesUpdated?.call();
},
),
),
);
if (result != null && result.game != null) {
// Find the index in the original list to mutate
final originalIndex = widget.games.indexWhere(
(g) => g.id == game.id,
);
if (originalIndex == -1) {
return;
}
if (result.delete) {
setState(() {
widget.games.removeAt(originalIndex);
});
} else {
setState(() {
widget.games[originalIndex] = result.game;
});
}
_refreshFromSource();
}
},
); );
}, },
), ),
@@ -104,4 +173,28 @@ class _ChooseGameViewState extends State<ChooseGameView> {
), ),
); );
} }
/// Applies the search filter to the games list based on [query].
void _applySearchFilter(String query) {
final q = query.toLowerCase().trim();
if (q.isEmpty) {
setState(() {
filteredGames = List<Game>.from(widget.games);
});
return;
}
setState(() {
filteredGames = widget.games.where((game) {
final name = game.name.toLowerCase();
final description = game.description.toLowerCase();
return name.contains(q) || description.contains(q);
}).toList();
});
}
/// Re-applies the current filter after the underlying games list changed.
void _refreshFromSource() {
_applySearchFilter(searchBarController.text);
}
} }

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/l10n/generated/app_localizations.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/text_input/custom_search_bar.dart';
import 'package:tallee/presentation/widgets/tiles/group_tile.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/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_game_view.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'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart';
@@ -18,9 +18,13 @@ import 'package:tallee/presentation/widgets/player_selection.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
import 'package:tallee/presentation/widgets/tiles/choose_tile.dart'; import 'package:tallee/presentation/widgets/tiles/choose_tile.dart';
/// A stateful widget for creating or editing a match.
class CreateMatchView extends StatefulWidget { class CreateMatchView extends StatefulWidget {
/// A view that allows creating a new match /// Constructor for `CreateMatchView`.
/// [onWinnerChanged]: Optional callback invoked when the winner is changed ///
/// [onWinnerChanged] is an optional callback invoked when the winner is changed.
/// [matchToEdit] is an optional match to prefill the fields.
/// [onMatchUpdated] is an optional callback invoked when the match is updated.
const CreateMatchView({ const CreateMatchView({
super.key, super.key,
this.onWinnerChanged, this.onWinnerChanged,
@@ -28,45 +32,49 @@ class CreateMatchView extends StatefulWidget {
this.onMatchUpdated, this.onMatchUpdated,
}); });
/// Optional callback invoked when the winner is changed /// Optional callback invoked when the winner is changed.
final VoidCallback? onWinnerChanged; final VoidCallback? onWinnerChanged;
/// Optional callback invoked when the match is updated /// Optional callback invoked when the match is updated.
final void Function(Match)? onMatchUpdated; final void Function(Match)? onMatchUpdated;
/// An optional match to prefill the fields /// An optional match to prefill the fields.
final Match? matchToEdit; final Match? matchToEdit;
@override @override
State<CreateMatchView> createState() => _CreateMatchViewState(); State<CreateMatchView> createState() => _CreateMatchViewState();
} }
/// The state class for `CreateMatchView`, managing the UI and logic for creating or editing a match.
class _CreateMatchViewState extends State<CreateMatchView> { class _CreateMatchViewState extends State<CreateMatchView> {
/// The database instance for accessing match data.
late final AppDatabase db; late final AppDatabase db;
/// Controller for the match name input field /// Controller for the match name input field.
final TextEditingController _matchNameController = TextEditingController(); final TextEditingController _matchNameController = TextEditingController();
/// Hint text for the match name input field /// Hint text for the match name input field.
String? hintText; String? hintText;
/// List of all groups from the database /// List of all games from the database.
List<Game> gamesList = [];
/// List of all groups from the database.
List<Group> groupsList = []; List<Group> groupsList = [];
/// List of all players from the database /// List of all players from the database.
List<Player> playerList = []; List<Player> playerList = [];
/// The currently selected group /// The currently selected group.
Group? selectedGroup; Group? selectedGroup;
/// The index of the currently selected game in [games] to mark it in /// The currently selected game.
/// the [ChooseGameView] Game? selectedGame;
int selectedGameIndex = -1;
/// The currently selected players /// The currently selected players.
List<Player> selectedPlayers = []; List<Player> selectedPlayers = [];
/// GlobalKey for ScaffoldMessenger to show snackbars /// GlobalKey for ScaffoldMessenger to show snackbars.
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
@override @override
@@ -78,14 +86,18 @@ class _CreateMatchViewState extends State<CreateMatchView> {
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
// Load games, groups, and players from the database.
Future.wait([ Future.wait([
db.gameDao.getAllGames(),
db.groupDao.getAllGroups(), db.groupDao.getAllGroups(),
db.playerDao.getAllPlayers(), db.playerDao.getAllPlayers(),
]).then((result) async { ]).then((result) async {
groupsList = result[0] as List<Group>; gamesList = result[0] as List<Game>;
playerList = result[1] as List<Player>; gamesList.sort((a, b) => b.createdAt.compareTo(a.createdAt));
groupsList = result[1] as List<Group>;
playerList = result[2] as List<Player>;
// If a match is provided, prefill the fields // If a match is provided, prefill the fields.
if (widget.matchToEdit != null) { if (widget.matchToEdit != null) {
prefillMatchDetails(); prefillMatchDetails();
} }
@@ -105,11 +117,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
hintText ??= loc.match_name; hintText ??= loc.match_name;
} }
List<(String, String, Ruleset)> games = [
('Example Game 1', 'This is a description', Ruleset.lowestScore),
('Example Game 2', '', Ruleset.singleWinner),
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
@@ -130,6 +137,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
// Match name input field.
Container( Container(
margin: CustomTheme.tileMargin, margin: CustomTheme.tileMargin,
child: TextInputField( child: TextInputField(
@@ -138,38 +146,48 @@ class _CreateMatchViewState extends State<CreateMatchView> {
maxLength: Constants.MAX_MATCH_NAME_LENGTH, maxLength: Constants.MAX_MATCH_NAME_LENGTH,
), ),
), ),
// Game selection tile.
ChooseTile( ChooseTile(
title: loc.game, title: loc.game,
trailingText: selectedGameIndex == -1 trailingText: selectedGame == null
? loc.none ? loc.none
: games[selectedGameIndex].$1, : selectedGame!.name,
onPressed: () async { onPressed: () async {
selectedGameIndex = await Navigator.of(context).push( final String? selectedGameId = await Navigator.of(context)
adaptivePageRoute( .push(
builder: (context) => ChooseGameView( adaptivePageRoute(
games: games, builder: (context) => ChooseGameView(
initialGameIndex: selectedGameIndex, games: gamesList,
), initialSelectedGameId: selectedGame?.id ?? '',
), onGamesUpdated: loadGames,
); ),
),
);
try {
selectedGame = gamesList.firstWhere(
(g) => g.id == selectedGameId,
);
} catch (_) {
selectedGame = null;
}
setState(() { setState(() {
if (selectedGameIndex != -1) { if (selectedGame != null) {
hintText = games[selectedGameIndex].$1; hintText = selectedGame!.name;
} else { } else {
hintText = loc.match_name; hintText = loc.match_name;
} }
}); });
}, },
), ),
// Group selection tile.
ChooseTile( ChooseTile(
title: loc.group, title: loc.group,
trailingText: selectedGroup == null trailingText: selectedGroup == null
? loc.none_group ? loc.none_group
: selectedGroup!.name, : selectedGroup!.name,
onPressed: () async { onPressed: () async {
// Remove all players from the previously selected group from
// the selected players list, in case the user deselects the
// group or selects a different group.
selectedPlayers.removeWhere( selectedPlayers.removeWhere(
(player) => (player) =>
selectedGroup?.members.any( selectedGroup?.members.any(
@@ -196,6 +214,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
}); });
}, },
), ),
// Player selection widget.
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'), key: ValueKey(selectedGroup?.id ?? 'no_group'),
@@ -208,6 +227,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
}, },
), ),
), ),
// Create or save button.
CustomWidthButton( CustomWidthButton(
text: buttonText, text: buttonText,
sizeRelativeToWidth: 0.95, sizeRelativeToWidth: 0.95,
@@ -229,16 +249,16 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// ///
/// Returns `true` if: /// Returns `true` if:
/// - A ruleset is selected AND /// - A ruleset is selected AND
/// - Either a group is selected OR at least 2 players are selected /// - Either a group is selected OR at least 2 players are selected.
bool _enableCreateGameButton() { bool _enableCreateGameButton() {
return (selectedGroup != null || return (selectedGroup != null ||
(selectedPlayers.length > 1) && selectedGameIndex != -1); (selectedPlayers.length > 1) && selectedGame != null);
} }
// If a match was provided to the view, it updates the match in the database /// Handles navigation when the create or save button is pressed.
// and navigates back to the previous screen. ///
// If no match was provided, it creates a new match in the database and /// If a match is being edited, updates the match in the database.
// navigates to the MatchResultView for the newly created match. /// Otherwise, creates a new match and navigates to the MatchResultView.
void buttonNavigation(BuildContext context) async { void buttonNavigation(BuildContext context) async {
if (widget.matchToEdit != null) { if (widget.matchToEdit != null) {
await updateMatch(); await updateMatch();
@@ -263,12 +283,8 @@ class _CreateMatchViewState extends State<CreateMatchView> {
} }
} }
/// Updates attributes of the existing match in the database based on the /// Updates the existing match in the database.
/// changes made in the edit view.
Future<void> updateMatch() async { Future<void> updateMatch() async {
//TODO: Remove when Games implemented
final tempGame = await getTemporaryGame();
final updatedMatch = Match( final updatedMatch = Match(
id: widget.matchToEdit!.id, id: widget.matchToEdit!.id,
name: _matchNameController.text.isEmpty name: _matchNameController.text.isEmpty
@@ -276,7 +292,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
: _matchNameController.text.trim(), : _matchNameController.text.trim(),
group: selectedGroup, group: selectedGroup,
players: selectedPlayers, players: selectedPlayers,
game: tempGame, game: selectedGame!,
winner: widget.matchToEdit!.winner, winner: widget.matchToEdit!.winner,
createdAt: widget.matchToEdit!.createdAt, createdAt: widget.matchToEdit!.createdAt,
endedAt: widget.matchToEdit!.endedAt, endedAt: widget.matchToEdit!.endedAt,
@@ -297,7 +313,13 @@ class _CreateMatchViewState extends State<CreateMatchView> {
); );
} }
// Add players who are in updatedMatch but not in the original match if (widget.matchToEdit!.game.id != updatedMatch.game.id) {
await db.matchDao.updateMatchGame(
matchId: widget.matchToEdit!.id,
gameId: updatedMatch.game.id,
);
}
for (var player in updatedMatch.players) { for (var player in updatedMatch.players) {
if (!widget.matchToEdit!.players.any((p) => p.id == player.id)) { if (!widget.matchToEdit!.players.any((p) => p.id == player.id)) {
await db.playerMatchDao.addPlayerToMatch( await db.playerMatchDao.addPlayerToMatch(
@@ -307,7 +329,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
} }
} }
// Remove players who are in the original match but not in updatedMatch
for (var player in widget.matchToEdit!.players) { for (var player in widget.matchToEdit!.players) {
if (!updatedMatch.players.any((p) => p.id == player.id)) { if (!updatedMatch.players.any((p) => p.id == player.id)) {
await db.playerMatchDao.removePlayerFromMatch( await db.playerMatchDao.removePlayerFromMatch(
@@ -323,11 +344,8 @@ class _CreateMatchViewState extends State<CreateMatchView> {
widget.onMatchUpdated?.call(updatedMatch); widget.onMatchUpdated?.call(updatedMatch);
} }
// Creates a new match and adds it to the database. /// Creates a new match and adds it to the database.
// Returns the created match.
Future<Match> createMatch() async { Future<Match> createMatch() async {
final tempGame = await getTemporaryGame();
Match match = Match( Match match = Match(
name: _matchNameController.text.isEmpty name: _matchNameController.text.isEmpty
? (hintText ?? '') ? (hintText ?? '')
@@ -335,43 +353,25 @@ class _CreateMatchViewState extends State<CreateMatchView> {
createdAt: DateTime.now(), createdAt: DateTime.now(),
group: selectedGroup, group: selectedGroup,
players: selectedPlayers, players: selectedPlayers,
game: tempGame, game: selectedGame!,
); );
await db.matchDao.addMatch(match: match); await db.matchDao.addMatch(match: match);
return match; return match;
} }
// TODO: Remove when games fully implemented /// Prefills the input fields if a match was provided to the view.
Future<Game> getTemporaryGame() async {
Game? game;
final selectedGame = games[selectedGameIndex];
game = Game(
name: selectedGame.$1,
description: selectedGame.$2,
ruleset: selectedGame.$3,
color: GameColor.blue,
icon: '',
);
await db.gameDao.addGame(game: game);
return game;
}
// If a match was provided to the view, this method prefills the input fields
void prefillMatchDetails() { void prefillMatchDetails() {
final match = widget.matchToEdit!; final match = widget.matchToEdit!;
_matchNameController.text = match.name; _matchNameController.text = match.name;
selectedPlayers = match.players; selectedPlayers = match.players;
selectedGameIndex = 0; selectedGame = match.game;
if (match.group != null) { if (match.group != null) {
selectedGroup = match.group; selectedGroup = match.group;
} }
} }
// If none of the selected players are from the currently selected group, /// Removes the group if none of its members are in the selected players list.
// the group is also deselected.
Future<void> removeGroupWhenNoMemberLeft() async { Future<void> removeGroupWhenNoMemberLeft() async {
if (selectedGroup == null) return; if (selectedGroup == null) return;
@@ -384,4 +384,13 @@ class _CreateMatchViewState extends State<CreateMatchView> {
}); });
} }
} }
/// Loads all games from the database and updates the state.
Future<void> loadGames() async {
final result = await db.gameDao.getAllGames();
result.sort((a, b) => b.createdAt.compareTo(a.createdAt));
setState(() {
gamesList = result;
});
}
} }

View File

@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseColorView extends StatefulWidget {
/// A view that allows the user to choose a color from a list of available game colors
/// - [initialColor]: The initially selected color
const ChooseColorView({super.key, this.initialColor});
/// The initially selected color
final GameColor? initialColor;
@override
State<ChooseColorView> createState() => _ChooseColorViewState();
}
class _ChooseColorViewState extends State<ChooseColorView> {
/// Currently selected color
GameColor? selectedColor;
@override
void initState() {
selectedColor = widget.initialColor;
super.initState();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
const colors = GameColor.values;
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(selectedColor);
},
),
title: Text(loc.choose_color),
),
body: PopScope(
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
if (didPop) return;
Navigator.of(context).pop(selectedColor);
},
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: colors.length,
itemBuilder: (BuildContext context, int index) {
final color = colors[index];
return TitleDescriptionListTile(
onTap: () {
setState(() {
if (selectedColor == color) {
selectedColor = null;
} else {
selectedColor = color;
}
});
},
title: translateGameColorToString(color, context),
description: '',
isHighlighted: selectedColor == color,
badgeText: ' ', //Breite für Color Badge
badgeColor: getColorFromGameColor(color),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseRulesetView extends StatefulWidget {
/// A view that allows the user to choose a ruleset from a list of available rulesets
/// - [rulesets]: A list of tuples containing the ruleset and its description
/// - [initialRulesetIndex]: The index of the initially selected ruleset
const ChooseRulesetView({
super.key,
required this.rulesets,
required this.initialRulesetIndex,
});
/// A list of tuples containing the ruleset and its description
final List<(Ruleset, String)> rulesets;
/// The index of the initially selected ruleset
final int initialRulesetIndex;
@override
State<ChooseRulesetView> createState() => _ChooseRulesetViewState();
}
class _ChooseRulesetViewState extends State<ChooseRulesetView> {
/// Currently selected ruleset index
late int selectedRulesetIndex;
@override
void initState() {
selectedRulesetIndex = widget.initialRulesetIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return DefaultTabController(
length: 2,
initialIndex: 0,
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
selectedRulesetIndex == -1
? null
: widget.rulesets[selectedRulesetIndex].$1,
);
},
),
title: Text(loc.choose_ruleset),
),
body: PopScope(
// This fixes that the Android Back Gesture didn't return the
// selectedRulesetIndex and therefore the selected Ruleset wasn't saved
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
if (didPop) {
return;
}
Navigator.of(context).pop(
selectedRulesetIndex == -1
? null
: widget.rulesets[selectedRulesetIndex].$1,
);
},
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: widget.rulesets.length,
itemBuilder: (BuildContext context, int index) {
return TitleDescriptionListTile(
onTap: () async {
setState(() {
if (selectedRulesetIndex == index) {
selectedRulesetIndex = -1;
} else {
selectedRulesetIndex = index;
}
});
},
title: translateRulesetToString(
widget.rulesets[index].$1,
context,
),
description: widget.rulesets[index].$2,
isHighlighted: selectedRulesetIndex == index,
);
},
),
),
),
);
}
}

View File

@@ -0,0 +1,351 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/game_view/choose_color_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/game_view/choose_ruleset_view.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
import 'package:tallee/presentation/widgets/tiles/choose_tile.dart';
/// A stateful widget for creating or editing a game.
/// - [gameToEdit] An optional game to prefill the fields
/// - [onGameChanged] Callback to invoke when the game is created or edited
class CreateGameView extends StatefulWidget {
const CreateGameView({
super.key,
this.gameToEdit,
required this.onGameChanged,
});
/// An optional game to prefill the fields
final Game? gameToEdit;
/// Callback to invoke when the game is created or edited
final VoidCallback onGameChanged;
@override
State<CreateGameView> createState() => _CreateGameViewState();
}
class _CreateGameViewState extends State<CreateGameView> {
/// GlobalKey for ScaffoldMessenger to show snackbars
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
/// The database instance for accessing game data.
late final AppDatabase db;
/// The currently selected ruleset for the game.
Ruleset? selectedRuleset;
/// The index of the currently selected ruleset.
int selectedRulesetIndex = -1;
/// A list of available rulesets and their localized names.
late List<(Ruleset, String)> _rulesets;
/// The currently selected color for the game.
GameColor? selectedColor;
/// Controller for the game name input field.
final _gameNameController = TextEditingController();
/// Controller for the game description input field.
final _descriptionController = TextEditingController();
/// The ID of the currently selected group.
late String selectedGroupId;
/// A controller for the search bar input field.
final TextEditingController controller = TextEditingController();
/// A list of groups filtered based on the search query.
late final List<Group> filteredGroups;
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_gameNameController.addListener(() => setState(() {}));
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_rulesets = [
(
Ruleset.singleWinner,
translateRulesetToString(Ruleset.singleWinner, context),
),
(
Ruleset.singleLoser,
translateRulesetToString(Ruleset.singleLoser, context),
),
(
Ruleset.highestScore,
translateRulesetToString(Ruleset.highestScore, context),
),
(
Ruleset.lowestScore,
translateRulesetToString(Ruleset.lowestScore, context),
),
(
Ruleset.multipleWinners,
translateRulesetToString(Ruleset.multipleWinners, context),
),
];
if (widget.gameToEdit != null) {
_gameNameController.text = widget.gameToEdit!.name;
_descriptionController.text = widget.gameToEdit!.description;
selectedRuleset = widget.gameToEdit!.ruleset;
selectedColor = widget.gameToEdit!.color;
selectedRulesetIndex = _rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
}
}
@override
void dispose() {
_gameNameController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var loc = AppLocalizations.of(context);
final isEditing = widget.gameToEdit != null;
return ScaffoldMessenger(
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
title: Text(isEditing ? loc.edit_game : loc.create_game),
actions: widget.gameToEdit == null
? []
: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
if (widget.gameToEdit != null) {
showDialog<bool>(
context: context,
builder: (context) => CustomAlertDialog(
title: loc.delete_game,
content: 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.gameDao.deleteGame(
gameId: widget.gameToEdit!.id,
);
if (!context.mounted) return;
if (success) {
widget.onGameChanged.call();
Navigator.of(
context,
).pop((game: widget.gameToEdit, delete: true));
} else {
if (!mounted) return;
showSnackbar(message: loc.error_deleting_game);
}
}
});
}
},
),
],
),
body: SafeArea(
child: Column(
children: [
Container(
margin: CustomTheme.tileMargin,
child: TextInputField(
controller: _gameNameController,
maxLength: Constants.MAX_MATCH_NAME_LENGTH,
hintText: loc.game_name,
),
),
ChooseTile(
title: loc.ruleset,
trailingText: selectedRuleset == null
? loc.none
: translateRulesetToString(selectedRuleset!, context),
onPressed: () async {
final result = await Navigator.of(context).push<Ruleset?>(
adaptivePageRoute(
builder: (context) => ChooseRulesetView(
rulesets: _rulesets,
initialRulesetIndex: selectedRulesetIndex,
),
),
);
if (mounted) {
setState(() {
selectedRuleset = result;
selectedRulesetIndex = result == null
? -1
: _rulesets.indexWhere((r) => r.$1 == result);
});
}
},
),
ChooseTile(
title: loc.color,
trailingText: selectedColor == null
? loc.none
: translateGameColorToString(selectedColor!, context),
onPressed: () async {
final result = await Navigator.of(context).push<GameColor?>(
adaptivePageRoute(
builder: (context) =>
ChooseColorView(initialColor: selectedColor),
),
);
if (mounted) {
setState(() {
selectedColor = result;
});
}
},
),
Container(
margin: CustomTheme.tileMargin,
child: TextInputField(
controller: _descriptionController,
hintText: loc.description,
minLines: 6,
maxLines: 6,
maxLength: Constants.MAX_GAME_DESCRIPTION_LENGTH,
showCounterText: true,
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(12.0),
child: CustomWidthButton(
text: isEditing ? loc.edit_game : loc.create_game,
sizeRelativeToWidth: 1,
buttonType: ButtonType.primary,
onPressed:
_gameNameController.text.trim().isNotEmpty &&
selectedRulesetIndex != -1 &&
selectedColor != null
? () async {
Game newGame = Game(
name: _gameNameController.text.trim(),
description: _descriptionController.text.trim(),
ruleset: selectedRuleset!,
color: selectedColor!,
);
if (isEditing) {
await handleGameUpdate(newGame);
} else {
await handleGameCreation(newGame);
}
widget.onGameChanged.call();
if (context.mounted) {
Navigator.of(
context,
).pop((game: newGame, delete: false));
}
}
: null,
),
),
],
),
),
),
);
}
/// Handles updating an existing game in the database.
///
/// [newGame] The updated game object.
Future<void> handleGameUpdate(Game newGame) async {
final oldGame = widget.gameToEdit!;
if (oldGame.name != newGame.name) {
await db.gameDao.updateGameName(
gameId: oldGame.id,
newName: newGame.name,
);
}
if (oldGame.description != newGame.description) {
await db.gameDao.updateGameDescription(
gameId: oldGame.id,
newDescription: newGame.description,
);
}
if (oldGame.ruleset != newGame.ruleset) {
await db.gameDao.updateGameRuleset(
gameId: oldGame.id,
newRuleset: newGame.ruleset,
);
}
if (oldGame.color != newGame.color) {
await db.gameDao.updateGameColor(
gameId: oldGame.id,
newColor: newGame.color,
);
}
if (oldGame.icon != newGame.icon) {
await db.gameDao.updateGameIcon(
gameId: oldGame.id,
newIcon: newGame.icon,
);
}
}
/// Handles creating a new game in the database.
///
/// [newGame] The game object to be created.
Future<void> handleGameCreation(Game newGame) async {
await db.gameDao.addGame(game: newGame);
}
/// Displays a snackbar with the given message and optional action.
///
/// [message] The message to display in the snackbar.
void showSnackbar({required String message}) {
final messenger = _scaffoldMessengerKey.currentState;
if (messenger != null) {
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
content: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: CustomTheme.boxColor,
),
);
}
}
}

View File

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

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart';
@@ -139,11 +139,11 @@ class _MatchResultViewState extends State<MatchResultView> {
/// based on the current selection. /// based on the current selection.
Future<void> _handleWinnerSaving() async { Future<void> _handleWinnerSaving() async {
if (_selectedPlayer == null) { if (_selectedPlayer == null) {
await db.scoreDao.removeWinner(matchId: widget.match.id); await db.matchDao.removeWinner(matchId: widget.match.id);
} else { } else {
await db.scoreDao.setWinner( await db.matchDao.setWinner(
matchId: widget.match.id, matchId: widget.match.id,
playerId: _selectedPlayer!.id, winnerId: _selectedPlayer!.id,
); );
} }
widget.onWinnerChanged?.call(); widget.onWinnerChanged?.call();

View File

@@ -6,10 +6,10 @@ import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.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'; import 'package:tallee/presentation/views/main_menu/match_view/match_detail_view.dart';
@@ -19,7 +19,7 @@ import 'package:tallee/presentation/widgets/tiles/match_tile.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart'; import 'package:tallee/presentation/widgets/top_centered_message.dart';
class MatchView extends StatefulWidget { class MatchView extends StatefulWidget {
/// A view that displays a list of matches /// A view that displays a list of matches.
const MatchView({super.key}); const MatchView({super.key});
@override @override
@@ -27,11 +27,14 @@ class MatchView extends StatefulWidget {
} }
class _MatchViewState extends State<MatchView> { class _MatchViewState extends State<MatchView> {
/// Database instance used to access match data.
late final AppDatabase db; late final AppDatabase db;
/// Indicates whether matches are currently being loaded.
bool isLoading = true; bool isLoading = true;
/// Loaded matches from the database, /// Loaded matches from the database,
/// initially filled with skeleton matches /// initially filled with skeleton matches.
List<Match> matches = List.filled( List<Match> matches = List.filled(
4, 4,
Match( Match(
@@ -41,7 +44,6 @@ class _MatchViewState extends State<MatchView> {
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: '', description: '',
color: GameColor.blue, color: GameColor.blue,
icon: '',
), ),
group: Group( group: Group(
name: 'Group name', name: 'Group name',

View File

@@ -36318,13 +36318,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''', SOFTWARE.''',
); );
/// sqlite3_flutter_libs 0.5.42 /// sqlite3_flutter_libs 0.5.41
const _sqlite3_flutter_libs = Package( const _sqlite3_flutter_libs = Package(
name: 'sqlite3_flutter_libs', name: 'sqlite3_flutter_libs',
description: 'Flutter plugin to include native sqlite3 libraries with your app', description: 'Flutter plugin to include native sqlite3 libraries with your app',
homepage: 'https://github.com/simolus3/sqlite3.dart/tree/v2/sqlite3_flutter_libs', homepage: 'https://github.com/simolus3/sqlite3.dart/tree/v2/sqlite3_flutter_libs',
authors: [], authors: [],
version: '0.5.42', version: '0.5.41',
spdxIdentifiers: ['MIT'], spdxIdentifiers: ['MIT'],
isMarkdown: false, isMarkdown: false,
isSdk: false, isSdk: false,
@@ -37796,12 +37796,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''', SOFTWARE.''',
); );
/// tallee 0.0.19+253 /// tallee 0.0.18+252
const _tallee = Package( const _tallee = Package(
name: 'tallee', name: 'tallee',
description: 'Tracking App for Card Games', description: 'Tracking App for Card Games',
authors: [], authors: [],
version: '0.0.19+253', version: '0.0.18+252',
spdxIdentifiers: ['LGPL-3.0'], spdxIdentifiers: ['LGPL-3.0'],
isMarkdown: false, isMarkdown: false,
isSdk: false, isSdk: false,

View File

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

View File

@@ -8,12 +8,18 @@ class TextInputField extends StatelessWidget {
/// - [onChanged]: Optional callback invoked when the text in the field changes. /// - [onChanged]: Optional callback invoked when the text in the field changes.
/// - [hintText]: The hint text displayed in the text input field when it is empty /// - [hintText]: The hint text displayed in the text input field when it is empty
/// - [maxLength]: Optional parameter for maximum length of the input text. /// - [maxLength]: Optional parameter for maximum length of the input text.
/// - [maxLines]: The maximum number of lines for the text input field. Defaults to 1.
/// - [minLines]: The minimum number of lines for the text input field. Defaults to 1.
/// - [showCounterText]: Whether to show the counter text in the text input field. Defaults to false.
const TextInputField({ const TextInputField({
super.key, super.key,
required this.controller, required this.controller,
required this.hintText, required this.hintText,
this.onChanged, this.onChanged,
this.maxLength, this.maxLength,
this.maxLines = 1,
this.minLines = 1,
this.showCounterText = false,
}); });
/// The controller for the text input field. /// The controller for the text input field.
@@ -28,6 +34,15 @@ class TextInputField extends StatelessWidget {
/// Optional parameter for maximum length of the input text. /// Optional parameter for maximum length of the input text.
final int? maxLength; final int? maxLength;
/// The maximum number of lines for the text input field.
final int? maxLines;
/// The minimum number of lines for the text input field.
final int? minLines;
/// Whether to show the counter text in the text input field.
final bool showCounterText;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextField( return TextField(
@@ -35,13 +50,14 @@ class TextInputField extends StatelessWidget {
onChanged: onChanged, onChanged: onChanged,
maxLength: maxLength, maxLength: maxLength,
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
maxLines: maxLines,
minLines: minLines,
decoration: InputDecoration( decoration: InputDecoration(
filled: true, filled: true,
fillColor: CustomTheme.boxColor, fillColor: CustomTheme.boxColor,
hintText: hintText, hintText: hintText,
hintStyle: const TextStyle(fontSize: 18), hintStyle: const TextStyle(fontSize: 18),
// Hides the character counter counterText: showCounterText ? null : '',
counterText: '',
enabledBorder: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorderColor), borderSide: BorderSide(color: CustomTheme.boxBorderColor),

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ class TitleDescriptionListTile extends StatelessWidget {
/// - [title]: The title text displayed on the tile. /// - [title]: The title text displayed on the tile.
/// - [description]: The description text displayed below the title. /// - [description]: The description text displayed below the title.
/// - [onPressed]: The callback invoked when the tile is tapped. /// - [onPressed]: The callback invoked when the tile is tapped.
/// - [onLongPress]: The callback invoked when the tile is tapped.
/// - [isHighlighted]: A boolean to determine if the tile should be highlighted. /// - [isHighlighted]: A boolean to determine if the tile should be highlighted.
/// - [badgeText]: Optional text to display in a badge on the right side of the title. /// - [badgeText]: Optional text to display in a badge on the right side of the title.
/// - [badgeColor]: Optional color for the badge background. /// - [badgeColor]: Optional color for the badge background.
@@ -13,7 +14,8 @@ class TitleDescriptionListTile extends StatelessWidget {
super.key, super.key,
required this.title, required this.title,
required this.description, required this.description,
this.onPressed, this.onTap,
this.onLongPress,
this.isHighlighted = false, this.isHighlighted = false,
this.badgeText, this.badgeText,
this.badgeColor, this.badgeColor,
@@ -26,7 +28,10 @@ class TitleDescriptionListTile extends StatelessWidget {
final String description; final String description;
/// The callback invoked when the tile is tapped. /// The callback invoked when the tile is tapped.
final VoidCallback? onPressed; final VoidCallback? onTap;
/// The callback invoked when the tile is long-pressed.
final VoidCallback? onLongPress;
/// A boolean to determine if the tile should be highlighted. /// A boolean to determine if the tile should be highlighted.
final bool isHighlighted; final bool isHighlighted;
@@ -40,7 +45,8 @@ class TitleDescriptionListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: onPressed, onTap: onTap,
onLongPress: onLongPress,
child: AnimatedContainer( child: AnimatedContainer(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),

View File

@@ -8,11 +8,11 @@ import 'package:json_schema/json_schema.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/team.dart'; import 'package:tallee/data/dto/team.dart';
class DataTransferService { class DataTransferService {
/// Deletes all data from the database. /// Deletes all data from the database.
@@ -70,20 +70,6 @@ class DataTransferService {
'gameId': m.game.id, 'gameId': m.game.id,
'groupId': m.group?.id, 'groupId': m.group?.id,
'playerIds': m.players.map((p) => p.id).toList(), '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, 'notes': m.notes,
}, },
) )
@@ -138,12 +124,128 @@ class DataTransferService {
final jsonString = await _readFileContent(path.files.single); final jsonString = await _readFileContent(path.files.single);
if (jsonString == null) return ImportResult.fileReadError; if (jsonString == null) return ImportResult.fileReadError;
final isValid = await validateJsonSchema(jsonString); final isValid = await _validateJsonSchema(jsonString);
if (!isValid) return ImportResult.invalidSchema; if (!isValid) return ImportResult.invalidSchema;
final decoded = json.decode(jsonString) as Map<String, dynamic>; final Map<String, dynamic> decoded =
json.decode(jsonString) as Map<String, dynamic>;
await importDataToDatabase(db, decoded); 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,
),
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);
return ImportResult.success; return ImportResult.success;
} on FormatException catch (e, stack) { } on FormatException catch (e, stack) {
@@ -159,160 +261,6 @@ class DataTransferService {
} }
} }
/// Imports parsed JSON data into the database.
@visibleForTesting
static Future<void> importDataToDatabase(
AppDatabase db,
Map<String, dynamic> decoded,
) async {
final importedPlayers = parsePlayersFromJson(decoded);
final playerById = {for (final p in importedPlayers) p.id: p};
final importedGames = parseGamesFromJson(decoded);
final gameById = {for (final g in importedGames) g.id: g};
final importedGroups = parseGroupsFromJson(decoded, playerById);
final groupById = {for (final g in importedGroups) g.id: g};
final importedTeams = parseTeamsFromJson(decoded, playerById);
final importedMatches = parseMatchesFromJson(
decoded,
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);
}
/// Parses players from JSON data.
@visibleForTesting
static List<Player> parsePlayersFromJson(Map<String, dynamic> decoded) {
final playersJson = (decoded['players'] as List<dynamic>?) ?? [];
return playersJson
.map((p) => Player.fromJson(p as Map<String, dynamic>))
.toList();
}
/// Parses games from JSON data.
@visibleForTesting
static List<Game> parseGamesFromJson(Map<String, dynamic> decoded) {
final gamesJson = (decoded['games'] as List<dynamic>?) ?? [];
return gamesJson
.map((g) => Game.fromJson(g as Map<String, dynamic>))
.toList();
}
/// Parses groups from JSON data.
@visibleForTesting
static List<Group> parseGroupsFromJson(
Map<String, dynamic> decoded,
Map<String, Player> playerById,
) {
final groupsJson = (decoded['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> decoded,
Map<String, Player> playerById,
) {
final teamsJson = (decoded['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> decoded,
Map<String, Game> gameById,
Map<String, Group> groupById,
Map<String, Player> playerById,
) {
final matchesJson = (decoded['matches'] as List<dynamic>?) ?? [];
return matchesJson.map((m) {
final map = m as Map<String, dynamic>;
final gameId = map['gameId'] as String;
final groupId = map['groupId'] as String?;
final playerIds = (map['playerIds'] as List<dynamic>? ?? [])
.cast<String>();
final endedAt = map['endedAt'] != null
? DateTime.parse(map['endedAt'] as String)
: null;
final game = gameById[gameId] ?? createUnknownGame();
final group = groupId != null ? groupById[groupId] : null;
final players = playerIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Match(
id: map['id'] as String,
name: map['name'] as String,
game: game,
group: group,
players: players,
createdAt: DateTime.parse(map['createdAt'] as String),
endedAt: endedAt,
notes: map['notes'] as String? ?? '',
);
}).toList();
}
/// Creates a fallback game when the referenced game is not found.
@visibleForTesting
static Game createUnknownGame() {
return Game(
name: 'Unknown',
ruleset: Ruleset.singleWinner,
description: '',
color: GameColor.blue,
icon: '',
);
}
/// Helper method to read file content from either bytes or path /// Helper method to read file content from either bytes or path
static Future<String?> _readFileContent(PlatformFile file) async { static Future<String?> _readFileContent(PlatformFile file) async {
if (file.bytes != null) return utf8.decode(file.bytes!); if (file.bytes != null) return utf8.decode(file.bytes!);
@@ -321,8 +269,7 @@ class DataTransferService {
} }
/// Validates the given JSON string against the predefined schema. /// Validates the given JSON string against the predefined schema.
@visibleForTesting static Future<bool> _validateJsonSchema(String jsonString) async {
static Future<bool> validateJsonSchema(String jsonString) async {
final String schemaString; final String schemaString;
schemaString = await rootBundle.loadString('assets/schema.json'); schemaString = await rootBundle.loadString('assets/schema.json');

View File

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

View File

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

View File

@@ -4,10 +4,10 @@ import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -42,7 +42,7 @@ void main() {
testPlayer4 = Player(name: 'Diana', description: ''); testPlayer4 = Player(name: 'Diana', description: '');
testPlayer5 = Player(name: 'Eve', description: ''); testPlayer5 = Player(name: 'Eve', description: '');
testGroup1 = Group( testGroup1 = Group(
name: 'Test Group 1', name: 'Test Group 2',
description: '', description: '',
members: [testPlayer1, testPlayer2, testPlayer3], members: [testPlayer1, testPlayer2, testPlayer3],
); );
@@ -56,7 +56,6 @@ void main() {
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A test game', description: 'A test game',
color: GameColor.blue, color: GameColor.blue,
icon: '',
); );
testMatch1 = Match( testMatch1 = Match(
name: 'First Test Match', name: 'First Test Match',
@@ -296,9 +295,9 @@ void main() {
test('Setting a winner works correctly', () async { test('Setting a winner works correctly', () async {
await database.matchDao.addMatch(match: testMatch1); await database.matchDao.addMatch(match: testMatch1);
await database.scoreDao.setWinner( await database.matchDao.setWinner(
matchId: testMatch1.id, matchId: testMatch1.id,
playerId: testPlayer5.id, winnerId: testPlayer5.id,
); );
final fetchedMatch = await database.matchDao.getMatchById( final fetchedMatch = await database.matchDao.getMatchById(
@@ -307,68 +306,5 @@ void main() {
expect(fetchedMatch.winner, isNotNull); expect(fetchedMatch.winner, isNotNull);
expect(fetchedMatch.winner!.id, testPlayer5.id); 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

@@ -4,10 +4,10 @@ import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/team.dart'; import 'package:tallee/data/dto/team.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -45,14 +45,12 @@ void main() {
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'Test game 1', description: 'Test game 1',
color: GameColor.blue, color: GameColor.blue,
icon: '',
); );
testGame2 = Game( testGame2 = Game(
name: 'Game 2', name: 'Game 2',
ruleset: Ruleset.highestScore, ruleset: Ruleset.highestScore,
description: 'Test game 2', description: 'Test game 2',
color: GameColor.red, color: GameColor.red,
icon: '',
); );
}); });
@@ -357,11 +355,13 @@ void main() {
playerId: testPlayer1.id, playerId: testPlayer1.id,
matchId: match1.id, matchId: match1.id,
teamId: testTeam1.id, teamId: testTeam1.id,
score: 0,
); );
await database.playerMatchDao.addPlayerToMatch( await database.playerMatchDao.addPlayerToMatch(
playerId: testPlayer2.id, playerId: testPlayer2.id,
matchId: match1.id, matchId: match1.id,
teamId: testTeam1.id, teamId: testTeam1.id,
score: 0,
); );
// Associate players with teams through match2 // Associate players with teams through match2
@@ -370,11 +370,13 @@ void main() {
playerId: testPlayer1.id, playerId: testPlayer1.id,
matchId: match2.id, matchId: match2.id,
teamId: testTeam3.id, teamId: testTeam3.id,
score: 0,
); );
await database.playerMatchDao.addPlayerToMatch( await database.playerMatchDao.addPlayerToMatch(
playerId: testPlayer3.id, playerId: testPlayer3.id,
matchId: match2.id, matchId: match2.id,
teamId: testTeam3.id, teamId: testTeam3.id,
score: 0,
); );
final team1 = await database.teamDao.getTeamById(teamId: testTeam1.id); final team1 = await database.teamDao.getTeamById(teamId: testTeam1.id);

View File

@@ -4,7 +4,7 @@ import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/dto/game.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -44,7 +44,6 @@ void main() {
ruleset: Ruleset.highestScore, ruleset: Ruleset.highestScore,
description: 'A board game about real estate', description: 'A board game about real estate',
color: GameColor.orange, color: GameColor.orange,
icon: '',
); );
}); });
}); });
@@ -138,7 +137,6 @@ void main() {
ruleset: Ruleset.lowestScore, ruleset: Ruleset.lowestScore,
description: 'A simple game', description: 'A simple game',
color: GameColor.green, color: GameColor.green,
icon: '',
); );
final result = await database.gameDao.addGame(game: gameWithNulls); final result = await database.gameDao.addGame(game: gameWithNulls);
expect(result, true); expect(result, true);
@@ -464,7 +462,6 @@ void main() {
ruleset: Ruleset.multipleWinners, ruleset: Ruleset.multipleWinners,
description: 'Description with émojis 🎮🎲', description: 'Description with émojis 🎮🎲',
color: GameColor.purple, color: GameColor.purple,
icon: '',
); );
await database.gameDao.addGame(game: specialGame); await database.gameDao.addGame(game: specialGame);
@@ -481,7 +478,6 @@ void main() {
name: '', name: '',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: '', description: '',
icon: '',
color: GameColor.red, color: GameColor.red,
); );
await database.gameDao.addGame(game: emptyGame); await database.gameDao.addGame(game: emptyGame);
@@ -503,7 +499,6 @@ void main() {
description: longString, description: longString,
ruleset: Ruleset.multipleWinners, ruleset: Ruleset.multipleWinners,
color: GameColor.yellow, color: GameColor.yellow,
icon: '',
); );
await database.gameDao.addGame(game: longGame); await database.gameDao.addGame(game: longGame);

View File

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

View File

@@ -3,8 +3,8 @@ import 'package:drift/drift.dart';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -42,6 +42,7 @@ void main() {
}); });
group('Player-Group Tests', () { group('Player-Group Tests', () {
// Verifies that a player can be added to an existing group and isPlayerInGroup returns true. // 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 { test('Adding a player to a group works correctly', () async {
await database.groupDao.addGroup(group: testGroup); await database.groupDao.addGroup(group: testGroup);
@@ -126,83 +127,67 @@ void main() {
}); });
// Verifies that addPlayerToGroup returns false when player already in group. // Verifies that addPlayerToGroup returns false when player already in group.
test( test('addPlayerToGroup returns false when player already in group', () async {
'addPlayerToGroup returns false when player already in group', await database.groupDao.addGroup(group: testGroup);
() async {
await database.groupDao.addGroup(group: testGroup);
// testPlayer1 is already in testGroup via group creation // testPlayer1 is already in testGroup via group creation
final result = await database.playerGroupDao.addPlayerToGroup( final result = await database.playerGroupDao.addPlayerToGroup(
player: testPlayer1, player: testPlayer1,
groupId: testGroup.id, groupId: testGroup.id,
); );
expect(result, false); expect(result, false);
}, });
);
// Verifies that addPlayerToGroup adds player to player table if not exists. // Verifies that addPlayerToGroup adds player to player table if not exists.
test( test('addPlayerToGroup adds player to player table if not exists', () async {
'addPlayerToGroup adds player to player table if not exists', await database.groupDao.addGroup(group: testGroup);
() async {
await database.groupDao.addGroup(group: testGroup);
// testPlayer4 is not in the database yet // testPlayer4 is not in the database yet
var playerExists = await database.playerDao.playerExists( var playerExists = await database.playerDao.playerExists(
playerId: testPlayer4.id, playerId: testPlayer4.id,
); );
expect(playerExists, false); expect(playerExists, false);
await database.playerGroupDao.addPlayerToGroup( await database.playerGroupDao.addPlayerToGroup(
player: testPlayer4, player: testPlayer4,
groupId: testGroup.id, groupId: testGroup.id,
); );
// Now player should exist in player table // Now player should exist in player table
playerExists = await database.playerDao.playerExists( playerExists = await database.playerDao.playerExists(
playerId: testPlayer4.id, playerId: testPlayer4.id,
); );
expect(playerExists, true); expect(playerExists, true);
}, });
);
// Verifies that removePlayerFromGroup returns false for non-existent player. // Verifies that removePlayerFromGroup returns false for non-existent player.
test( test('removePlayerFromGroup returns false for non-existent player', () async {
'removePlayerFromGroup returns false for non-existent player', await database.groupDao.addGroup(group: testGroup);
() async {
await database.groupDao.addGroup(group: testGroup);
final result = await database.playerGroupDao.removePlayerFromGroup( final result = await database.playerGroupDao.removePlayerFromGroup(
playerId: 'non-existent-player-id', playerId: 'non-existent-player-id',
groupId: testGroup.id, groupId: testGroup.id,
); );
expect(result, false); expect(result, false);
}, });
);
// Verifies that removePlayerFromGroup returns false for non-existent group. // Verifies that removePlayerFromGroup returns false for non-existent group.
test( test('removePlayerFromGroup returns false for non-existent group', () async {
'removePlayerFromGroup returns false for non-existent group', await database.playerDao.addPlayer(player: testPlayer1);
() async {
await database.playerDao.addPlayer(player: testPlayer1);
final result = await database.playerGroupDao.removePlayerFromGroup( final result = await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id, playerId: testPlayer1.id,
groupId: 'non-existent-group-id', groupId: 'non-existent-group-id',
); );
expect(result, false); expect(result, false);
}, });
);
// Verifies that getPlayersOfGroup returns empty list for group with no members. // Verifies that getPlayersOfGroup returns empty list for group with no members.
test('getPlayersOfGroup returns empty list for empty group', () async { test('getPlayersOfGroup returns empty list for empty group', () async {
final emptyGroup = Group( final emptyGroup = Group(name: 'Empty Group', description: '', members: []);
name: 'Empty Group',
description: '',
members: [],
);
await database.groupDao.addGroup(group: emptyGroup); await database.groupDao.addGroup(group: emptyGroup);
final players = await database.playerGroupDao.getPlayersOfGroup( final players = await database.playerGroupDao.getPlayersOfGroup(
@@ -213,16 +198,13 @@ void main() {
}); });
// Verifies that getPlayersOfGroup returns empty list for non-existent group. // Verifies that getPlayersOfGroup returns empty list for non-existent group.
test( test('getPlayersOfGroup returns empty list for non-existent group', () async {
'getPlayersOfGroup returns empty list for non-existent group', final players = await database.playerGroupDao.getPlayersOfGroup(
() async { groupId: 'non-existent-group-id',
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. // Verifies that removing all players from a group leaves the group empty.
test('Removing all players from a group leaves group empty', () async { test('Removing all players from a group leaves group empty', () async {
@@ -249,11 +231,7 @@ void main() {
// Verifies that a player can be in multiple groups. // Verifies that a player can be in multiple groups.
test('Player can be in multiple groups', () async { test('Player can be in multiple groups', () async {
final secondGroup = Group( final secondGroup = Group(name: 'Second Group', description: '', members: []);
name: 'Second Group',
description: '',
members: [],
);
await database.groupDao.addGroup(group: testGroup); await database.groupDao.addGroup(group: testGroup);
await database.groupDao.addGroup(group: secondGroup); await database.groupDao.addGroup(group: secondGroup);
@@ -277,36 +255,29 @@ void main() {
}); });
// Verifies that removing player from one group doesn't affect other groups. // Verifies that removing player from one group doesn't affect other groups.
test( test('Removing player from one group does not affect other groups', () async {
'Removing player from one group does not affect other groups', final secondGroup = Group(name: 'Second Group', description: '', members: [testPlayer1]);
() async { await database.groupDao.addGroup(group: testGroup);
final secondGroup = Group( await database.groupDao.addGroup(group: secondGroup);
name: 'Second Group',
description: '',
members: [testPlayer1],
);
await database.groupDao.addGroup(group: testGroup);
await database.groupDao.addGroup(group: secondGroup);
// Remove testPlayer1 from testGroup // Remove testPlayer1 from testGroup
await database.playerGroupDao.removePlayerFromGroup( await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id, playerId: testPlayer1.id,
groupId: testGroup.id, groupId: testGroup.id,
); );
final inFirstGroup = await database.playerGroupDao.isPlayerInGroup( final inFirstGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id, playerId: testPlayer1.id,
groupId: testGroup.id, groupId: testGroup.id,
); );
final inSecondGroup = await database.playerGroupDao.isPlayerInGroup( final inSecondGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id, playerId: testPlayer1.id,
groupId: secondGroup.id, groupId: secondGroup.id,
); );
expect(inFirstGroup, false); expect(inFirstGroup, false);
expect(inSecondGroup, true); expect(inSecondGroup, true);
}, });
);
// Verifies that addPlayerToGroup returns true on successful addition. // Verifies that addPlayerToGroup returns true on successful addition.
test('addPlayerToGroup returns true on successful addition', () async { test('addPlayerToGroup returns true on successful addition', () async {
@@ -322,26 +293,21 @@ void main() {
}); });
// Verifies that removing the same player twice returns false on second attempt. // Verifies that removing the same player twice returns false on second attempt.
test( test('Removing same player twice returns false on second attempt', () async {
'Removing same player twice returns false on second attempt', await database.groupDao.addGroup(group: testGroup);
() async {
await database.groupDao.addGroup(group: testGroup);
final firstRemoval = await database.playerGroupDao final firstRemoval = await database.playerGroupDao.removePlayerFromGroup(
.removePlayerFromGroup( playerId: testPlayer1.id,
playerId: testPlayer1.id, groupId: testGroup.id,
groupId: testGroup.id, );
); expect(firstRemoval, true);
expect(firstRemoval, true);
final secondRemoval = await database.playerGroupDao final secondRemoval = await database.playerGroupDao.removePlayerFromGroup(
.removePlayerFromGroup( playerId: testPlayer1.id,
playerId: testPlayer1.id, groupId: testGroup.id,
groupId: testGroup.id, );
); expect(secondRemoval, false);
expect(secondRemoval, false); });
},
);
// Verifies that replaceGroupPlayers removes all existing players and replaces with new list. // Verifies that replaceGroupPlayers removes all existing players and replaces with new list.
test('replaceGroupPlayers replaces all group members correctly', () async { 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:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/models/team.dart'; import 'package:tallee/data/dto/team.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -53,7 +53,6 @@ void main() {
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A test game', description: 'A test game',
color: GameColor.blue, color: GameColor.blue,
icon: '',
); );
testMatchOnlyGroup = Match( testMatchOnlyGroup = Match(
name: 'Test Match with Group', name: 'Test Match with Group',
@@ -88,6 +87,7 @@ void main() {
}); });
group('Player-Match Tests', () { group('Player-Match Tests', () {
// Verifies that matchHasPlayers returns false initially and true after adding a player.
test('Match has player works correctly', () async { test('Match has player works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
@@ -110,6 +110,7 @@ void main() {
expect(matchHasPlayers, true); 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 { test('Adding a player to a match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerDao.addPlayer(player: testPlayer5); await database.playerDao.addPlayer(player: testPlayer5);
@@ -133,6 +134,7 @@ void main() {
expect(playerAdded, false); 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 { test('Removing player from match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers); await database.matchDao.addMatch(match: testMatchOnlyPlayers);
@@ -153,6 +155,7 @@ void main() {
expect(playerExists, false); expect(playerExists, false);
}); });
// Verifies that getPlayersOfMatch returns all players of a match with correct data.
test('Retrieving players of a match works correctly', () async { test('Retrieving players of a match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers); await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final players = final players =
@@ -168,6 +171,7 @@ void main() {
} }
}); });
// Verifies that updatePlayersFromMatch replaces all existing players with new ones.
test('Updating the match players works correctly', () async { test('Updating the match players works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers); await database.matchDao.addMatch(match: testMatchOnlyPlayers);
@@ -211,6 +215,7 @@ void main() {
} }
}); });
// Verifies that the same player can be added to multiple different matches.
test( test(
'Adding the same player to separate matches works correctly', 'Adding the same player to separate matches works correctly',
() async { () async {
@@ -267,6 +272,89 @@ void main() {
expect(players, isNull); 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 { test('Adding player with teamId works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1); await database.teamDao.addTeam(team: testTeam1);
@@ -286,6 +374,7 @@ void main() {
expect(playersInTeam[0].id, testPlayer1.id); expect(playersInTeam[0].id, testPlayer1.id);
}); });
// Verifies that updatePlayerTeam updates the team correctly.
test('updatePlayerTeam updates team correctly', () async { test('updatePlayerTeam updates team correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1); await database.teamDao.addTeam(team: testTeam1);
@@ -324,6 +413,7 @@ void main() {
expect(playersInTeam1.isEmpty, true); expect(playersInTeam1.isEmpty, true);
}); });
// Verifies that updatePlayerTeam can set team to null.
test('updatePlayerTeam can remove player from team', () async { test('updatePlayerTeam can remove player from team', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1); await database.teamDao.addTeam(team: testTeam1);
@@ -351,6 +441,7 @@ void main() {
expect(playersInTeam.isEmpty, true); expect(playersInTeam.isEmpty, true);
}); });
// Verifies that updatePlayerTeam returns false for non-existent player-match.
test( test(
'updatePlayerTeam returns false for non-existent player-match', 'updatePlayerTeam returns false for non-existent player-match',
() async { () async {
@@ -378,6 +469,7 @@ void main() {
expect(players.isEmpty, true); expect(players.isEmpty, true);
}); });
// Verifies that getPlayersInTeam returns all players of a team.
test('getPlayersInTeam returns all players of a team', () async { test('getPlayersInTeam returns all players of a team', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1); await database.teamDao.addTeam(team: testTeam1);
@@ -404,6 +496,7 @@ void main() {
expect(playerIds.contains(testPlayer2.id), true); expect(playerIds.contains(testPlayer2.id), true);
}); });
// Verifies that removePlayerFromMatch returns false for non-existent player.
test( test(
'removePlayerFromMatch returns false for non-existent player', 'removePlayerFromMatch returns false for non-existent player',
() async { () async {
@@ -418,20 +511,31 @@ void main() {
}, },
); );
// Verifies that adding the same player twice to the same match is ignored.
test('Adding same player twice to same match is ignored', () async { test('Adding same player twice to same match is ignored', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerMatchDao.addPlayerToMatch( await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id, matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id, playerId: testPlayer1.id,
score: 10,
); );
// Try to add the same player again with different score // Try to add the same player again with different score
await database.playerMatchDao.addPlayerToMatch( await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id, matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.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 // Verify player count is still 1
final players = await database.playerMatchDao.getPlayersOfMatch( final players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyGroup.id, matchId: testMatchOnlyGroup.id,
@@ -440,6 +544,7 @@ void main() {
expect(players?.length, 1); expect(players?.length, 1);
}); });
// Verifies that updatePlayersFromMatch with empty list removes all players.
test( test(
'updatePlayersFromMatch with empty list removes all players', 'updatePlayersFromMatch with empty list removes all players',
() async { () async {
@@ -465,6 +570,7 @@ void main() {
}, },
); );
// Verifies that updatePlayersFromMatch with same players makes no changes.
test('updatePlayersFromMatch with same players makes no changes', () async { test('updatePlayersFromMatch with same players makes no changes', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers); await database.matchDao.addMatch(match: testMatchOnlyPlayers);
@@ -486,6 +592,7 @@ void main() {
} }
}); });
// Verifies that matchHasPlayers returns false for non-existent match.
test('matchHasPlayers returns false for non-existent match', () async { test('matchHasPlayers returns false for non-existent match', () async {
final hasPlayers = await database.playerMatchDao.matchHasPlayers( final hasPlayers = await database.playerMatchDao.matchHasPlayers(
matchId: 'non-existent-match-id', matchId: 'non-existent-match-id',
@@ -494,6 +601,7 @@ void main() {
expect(hasPlayers, false); expect(hasPlayers, false);
}); });
// Verifies that isPlayerInMatch returns false for non-existent match.
test('isPlayerInMatch returns false for non-existent match', () async { test('isPlayerInMatch returns false for non-existent match', () async {
final isInMatch = await database.playerMatchDao.isPlayerInMatch( final isInMatch = await database.playerMatchDao.isPlayerInMatch(
matchId: 'non-existent-match-id', matchId: 'non-existent-match-id',
@@ -503,6 +611,116 @@ void main() {
expect(isInMatch, false); 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. // Verifies that getPlayersInTeam returns empty list for non-existent match.
test( test(
'getPlayersInTeam returns empty list for non-existent match', 'getPlayersInTeam returns empty list for non-existent match',
@@ -604,6 +822,55 @@ void main() {
expect(isInMatch2, true); 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. // Verifies that updatePlayersFromMatch on non-existent match fails with constraint error.
test( test(
'updatePlayersFromMatch on non-existent match fails with foreign key constraint', 'updatePlayersFromMatch on non-existent match fails with foreign key constraint',

File diff suppressed because it is too large Load Diff

View File

@@ -1,868 +0,0 @@
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('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() 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('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() 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);
});
});
}