23 Commits

Author SHA1 Message Date
67bbb15845 Merge pull request 'Datenbankstruktur für Spiele' (#16) from feature/13-datenbankstruktur-fuer-spiele into development
Reviewed-on: #16
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-11-15 15:56:40 +00:00
34bf6740ad Corrected GroupGameTable attributes 2025-11-14 22:33:22 +01:00
7be86b3d9e Changed method name 2025-11-14 22:31:30 +01:00
cbd5e1d0ba Added documentation 2025-11-14 22:30:56 +01:00
2179331455 Removed test code 2025-11-14 09:01:19 +01:00
9229f1f0a5 Added counting methods + testing those 2025-11-12 20:09:17 +01:00
39b2068121 Removed unused var 2025-11-12 20:02:18 +01:00
93a4ccaee0 Added missing methods and implemented tests 2025-11-12 20:02:01 +01:00
3f5a840675 Added toString methods 2025-11-12 20:01:44 +01:00
67d5f7e891 Updated order 2025-11-12 20:01:31 +01:00
ac99217b72 Added reference on winner 2025-11-12 13:12:26 +01:00
ca40ae668d Updated methods with named parameters 2025-11-12 13:11:48 +01:00
d07943add9 Added methods for inserting games and groups into the db 2025-11-12 12:04:33 +01:00
b6700bafd9 Added on delete cascade 2025-11-12 12:04:10 +01:00
a922f24150 Added basic structure for game implementation 2025-11-12 11:41:06 +01:00
25c7b37df3 updated userId to playerId 2025-11-12 10:57:46 +01:00
b83fe03f35 Renamed app 2025-11-12 10:54:05 +01:00
cb8ff4314b Merge branch 'development' into feature/13-datenbankstruktur-fuer-spiele 2025-11-12 10:51:58 +01:00
55e5e3cbeb .gitea/ISSUE_TEMPLATE/ENHANCEMENT.md aktualisiert 2025-11-10 13:04:10 +00:00
2c0e996795 .gitea/ISSUE_TEMPLATE/BUG_REPORT.md aktualisiert 2025-11-10 13:02:21 +00:00
e61f9e5e64 .gitea/ISSUE_TEMPLATE/FEATURE.md aktualisiert 2025-11-10 12:59:16 +00:00
4c1d91ae99 .gitea/ISSUE_TEMPLATE/FEATURE.md aktualisiert 2025-11-10 12:52:56 +00:00
e1426810f2 .gitea/PULL_REQUEST_TEMPLATE.md aktualisiert 2025-11-08 16:14:02 +00:00
27 changed files with 3114 additions and 539 deletions

View File

@@ -1,6 +1,6 @@
--- ---
name: Bug report name: Bug report
about: Create a report for something does not work as it should about: Erstelle eine Meldung für etwas, das nicht Funktioniert, wie es soll.
title: '' title: ''
labels: 'Task/Bug' labels: 'Task/Bug'
assignees: '' assignees: ''
@@ -9,28 +9,27 @@ assignees: ''
# Bug Report # Bug Report
## Description ## Beschreibung
[A clear and concise description of the bug] [Eine klare und prägnante Beschreibung des Bugs]
## Steps to Reproduce ## Schritte zur Reproduktion
1. Step 1 1. Schritt 1
2. Step 2 2. Schritt 2
3. ... 3. ...
## Expected Behavior ## Erwartetes Verhalten
[What should have happened] [Was hätte passieren sollen]
## Actual Behavior ## Tatsächliches Verhalten
[What actually happened] [Was tatsächlich passiert ist]
## Screenshots/Logs ## Screenshots/Protokolle
[If applicable, add screenshots, error logs, or stack traces] [Falls zutreffend, füge Screenshots, Error Logs oder Stack Traces hinzu]
## Environment ## Umgebung
- OS: [e.g., iOS 18.5, Android 14] - Plattform: Android, iOS, Web
- OS: [z. B. iOS 18.5, Android 14]
- Flutter Version: [z.B. 3.35.6]
## Possible Fix (Optional) ## Verwandte Issues
[Any suggestions on how to resolve the issue] [Verweisen Sie auf ähnliche Issues oder PRs]
## Related Issues (Optional)
[Reference similar issues or PRs]

View File

@@ -9,14 +9,14 @@ assignees: ''
# Enhancement # Enhancement
## Current Behavior ## Aktuelles Verhalten
[Describe the existing functionality] [Beschreibe die bestehende Funktionalität]
## Limitations/Problems ## Einschränkungen/Probleme
[What are the current shortcomings?] [Was sind die aktuellen Mängel?]
## Suggested Improvement ## Vorgeschlagene Verbesserung
[How can this be enhanced? Be specific.] [Wie kann das Problem bzw. die Einschränkung verbessert werden?]
## Benefits ## Zugehörige Issues
[How will this improve the product?] [Links zu verwandten oder blockierenden Issues]

View File

@@ -1,22 +1,19 @@
--- ---
name: Feature name: Feature
about: New feature for the app about: Neues Feature für die App
title: '' title: ''
labels: 'Task\Feature' labels: 'Task\Feature'
assignees: '' assignees: ''
--- ---
# 🚀 Feature # Feature
## Description ## Beschreibung
[Detailed explanation of the proposed feature] [Ausführliche Erläuterung der vorgeschlagenen Funktion]
## Why is this feature needed? ## Vorgeschlagene Lösung
[Explain the problem or use case this feature would solve] [Beschreibe, wie die Feature funktionieren soll]
## Proposed Solution ## Zugehörige Issues
[Describe how the feature should work] [Links zu verwandten oder blockierenden Issues]
## Related Issues (Optional)
[Links to related discussions or requests]

View File

@@ -1,16 +1,16 @@
# [PR Title] # [PR Titel]
**Related Issue(s):** **Zugehörige Issue(s):**
Closes `<issue-no>` Closes `<issue-no>`
## Description ## Beschreibung
*A clear and concise overview of the changes made. Explain the "why" behind the PR, not just the "what".* *Eine klare und prägnante Übersicht über die vorgenommenen Änderungen. Erläutere nicht nur das was gemacht wurde, sondern auch warum.*
## Changes Made ## Änderungen
- [ ] Added new feature X - [ ] Neue Funktion X hinzugefügt
- [ ] Fixed bug in component Y - [ ] Bug in Komponente Y behoben
- [ ] Refactored module Z for better performance - [ ] Modul Z für bessere Leistung refactored
- [ ] Updated dependencies - [ ] Dependencies aktualisiert
## Additional Notes ## Zusätzliche Anmerkungen
*Any extra context, limitations, or decisions that reviewers should know about?* *Gibt es zusätzlichen Kontext, Einschränkungen oder Informationen, die Reviewer wissen sollten?*

View File

@@ -2,6 +2,8 @@ import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/game_table.dart'; import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/dto/game.dart'; import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart';
part 'game_dao.g.dart'; part 'game_dao.g.dart';
@@ -16,11 +18,58 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
return result.map((row) => Game(id: row.id, name: row.name)).toList(); return result.map((row) => Game(id: row.id, name: row.name)).toList();
} }
/// Retrieves a [Game] by its [id]. /// Retrieves a [Game] by its [gameId].
Future<Game> getGameById(String id) async { Future<Game> getGameById({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(id)); final query = select(gameTable)..where((g) => g.id.equals(gameId));
final result = await query.getSingle(); final result = await query.getSingle();
return Game(id: result.id, name: result.name);
List<Player>? players;
if (await db.playerGameDao.gameHasPlayers(gameId: gameId)) {
players = await db.playerGameDao.getPlayersByGameId(gameId: gameId);
}
Group? group;
if (await db.groupGameDao.hasGameGroup(gameId: gameId)) {
group = await db.groupGameDao.getGroupByGameId(gameId: gameId);
}
return Game(
id: result.id,
name: result.name,
players: players,
group: group,
winner: result.winnerId,
);
}
/// Adds a new [Game] to the database.
/// Also adds associated players and group if they exist.
Future<void> addGame({required Game game}) async {
await db.transaction(() async {
for (final p in game.players ?? []) {
await db.playerDao.addPlayer(player: p);
await db.playerGameDao.addPlayerToGame(gameId: game.id, playerId: p.id);
}
if (game.group != null) {
await db.groupDao.addGroup(group: game.group!);
await db.groupGameDao.addGroupToGame(game.id, game.group!.id);
}
await into(gameTable).insert(
GameTableCompanion.insert(
id: game.id,
name: game.name,
winnerId: game.winner,
),
mode: InsertMode.insertOrReplace,
);
});
}
/// Deletes the game with the given [gameId] from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteGame({required String gameId}) async {
final query = delete(gameTable)..where((g) => g.id.equals(gameId));
final rowsAffected = await query.go();
return rowsAffected > 0;
} }
/// Retrieves the number of games in the database. /// Retrieves the number of games in the database.
@@ -31,4 +80,12 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
/// Checks if a game with the given [gameId] exists in the database.
/// Returns `true` if the game exists, otherwise `false`.
Future<bool> gameExists({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId));
final result = await query.getSingleOrNull();
return result != null;
}
} }

View File

@@ -4,5 +4,6 @@ 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> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$GameTableTable get gameTable => attachedDatabase.gameTable; $GameTableTable get gameTable => attachedDatabase.gameTable;
} }

View File

@@ -19,39 +19,60 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
.toList(); .toList();
} }
/// Retrieves a [Group] by its [id], including its members. /// Retrieves a [Group] by its [groupId], including its members.
Future<Group> getGroupById(String id) async { Future<Group> getGroupById({required String groupId}) async {
final query = select(groupTable)..where((g) => g.id.equals(id)); final query = select(groupTable)..where((g) => g.id.equals(groupId));
final result = await query.getSingle(); final result = await query.getSingle();
List<Player> members = []; List<Player> members = await db.playerGroupDao.getPlayersOfGroupById(
groupId: groupId,
members = await db.playerGroupDao.getPlayersOfGroupById(id); );
return Group(id: result.id, name: result.name, members: members); return Group(id: result.id, name: result.name, members: members);
} }
/// Adds a new group with the given [id] and [name] to the database. /// Adds a new group with the given [id] and [name] to the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// This method also adds the group's members to the [PlayerGroupTable].
Future<void> addGroup(String id, String name) async { Future<void> addGroup({required Group group}) async {
await db.transaction(() async {
await into( await into(
groupTable, groupTable,
).insert(GroupTableCompanion.insert(id: id, name: name)); ).insert(GroupTableCompanion.insert(id: group.id, name: group.name));
await db.batch(
(b) => b.insertAll(
db.playerGroupTable,
group.members
.map(
(member) => PlayerGroupTableCompanion.insert(
playerId: member.id,
groupId: group.id,
),
)
.toList(),
),
);
await Future.wait(
group.members.map((player) => db.playerDao.addPlayer(player: player)),
);
});
} }
/// Deletes the group with the given [id] from the database. /// Deletes the group with the given [id] from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteGroup(String id) async { Future<bool> deleteGroup({required String groupId}) async {
final query = (delete(groupTable)..where((g) => g.id.equals(id))); final query = (delete(groupTable)..where((g) => g.id.equals(groupId)));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
/// Updates the name of the group with the given [id] to [newName]. /// Updates the name of the group with the given [id] to [newName].
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGroupname(String id, String newName) async { Future<bool> updateGroupname({
required String groupId,
required String newName,
}) async {
final rowsAffected = final rowsAffected =
await (update(groupTable)..where((g) => g.id.equals(id))).write( await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
GroupTableCompanion(name: Value(newName)), GroupTableCompanion(name: Value(newName)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -65,4 +86,12 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
/// Checks if a group with the given [groupId] exists in the database.
/// Returns `true` if the group exists, `false` otherwise.
Future<bool> groupExists({required String groupId}) async {
final query = select(groupTable)..where((g) => g.id.equals(groupId));
final result = await query.getSingleOrNull();
return result != null;
}
} }

View File

@@ -0,0 +1,42 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/group_game_table.dart';
import 'package:game_tracker/data/dto/group.dart';
part 'group_game_dao.g.dart';
@DriftAccessor(tables: [GroupGameTable])
class GroupGameDao extends DatabaseAccessor<AppDatabase>
with _$GroupGameDaoMixin {
GroupGameDao(super.db);
/// Checks if there is a group associated with the given [gameId].
/// Returns `true` if there is a group, otherwise `false`.
Future<bool> hasGameGroup({required String gameId}) async {
final count =
await (selectOnly(groupGameTable)
..where(groupGameTable.gameId.equals(gameId))
..addColumns([groupGameTable.groupId.count()]))
.map((row) => row.read(groupGameTable.groupId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Retrieves the [Group] associated with the given [gameId].
Future<Group> getGroupByGameId({required String gameId}) async {
final result = await (select(
groupGameTable,
)..where((g) => g.gameId.equals(gameId))).getSingle();
final group = await db.groupDao.getGroupById(groupId: result.groupId);
return group;
}
/// Associates a group with a game by inserting a record into the
/// [GroupGameTable].
Future<void> addGroupToGame(String gameId, String groupId) async {
await into(
groupGameTable,
).insert(GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId));
}
}

View File

@@ -0,0 +1,11 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'group_game_dao.dart';
// ignore_for_file: type=lint
mixin _$GroupGameDaoMixin on DatabaseAccessor<AppDatabase> {
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupGameTableTable get groupGameTable => attachedDatabase.groupGameTable;
}

View File

@@ -17,39 +17,57 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
} }
/// Retrieves a [Player] by their [id]. /// Retrieves a [Player] by their [id].
Future<Player> getPlayerById(String id) async { Future<Player> getPlayerById({required String playerId}) async {
final query = select(playerTable)..where((p) => p.id.equals(id)); final query = select(playerTable)..where((p) => p.id.equals(playerId));
final result = await query.getSingle(); final result = await query.getSingle();
return Player(id: result.id, name: result.name); return Player(id: result.id, name: result.name);
} }
/// Adds a new [player] to the database. /// Adds a new [player] to the database.
Future<void> addPlayer(Player player) async { /// If a player with the same ID already exists, updates their name to
/// the new one.
Future<void> addPlayer({required Player player}) async {
if (!await playerExists(playerId: player.id)) {
await into( await into(
playerTable, playerTable,
).insert(PlayerTableCompanion.insert(id: player.id, name: player.name)); ).insert(PlayerTableCompanion.insert(id: player.id, name: player.name));
} else {
await updatePlayername(playerId: player.id, newName: player.name);
}
} }
/// Deletes the player with the given [id] from the database. /// Deletes the player with the given [id] from the database.
/// Returns `true` if the player was deleted, `false` if the player did not exist. /// Returns `true` if the player was deleted, `false` if the player did not exist.
Future<bool> deletePlayer(String id) async { Future<bool> deletePlayer({required String playerId}) async {
final query = delete(playerTable)..where((p) => p.id.equals(id)); final query = delete(playerTable)..where((p) => p.id.equals(playerId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
/// Checks if a player with the given [id] exists in the database. /// Checks if a player with the given [id] exists in the database.
/// Returns `true` if the player exists, `false` otherwise. /// Returns `true` if the player exists, `false` otherwise.
Future<bool> playerExists(String id) async { Future<bool> playerExists({required String playerId}) async {
final query = select(playerTable)..where((p) => p.id.equals(id)); final query = select(playerTable)..where((p) => p.id.equals(playerId));
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return result != null; return result != null;
} }
/// Updates the name of the player with the given [id] to [newName]. /// Updates the name of the player with the given [playerId] to [newName].
Future<void> updatePlayername(String id, String newName) async { Future<void> updatePlayername({
await (update(playerTable)..where((p) => p.id.equals(id))).write( required String playerId,
required String newName,
}) async {
await (update(playerTable)..where((p) => p.id.equals(playerId))).write(
PlayerTableCompanion(name: Value(newName)), PlayerTableCompanion(name: Value(newName)),
); );
} }
/// Retrieves the total count of players in the database.
Future<int> getPlayerCount() async {
final count =
await (selectOnly(playerTable)..addColumns([playerTable.id.count()]))
.map((row) => row.read(playerTable.id.count()))
.getSingle();
return count ?? 0;
}
} }

View File

@@ -0,0 +1,51 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/player_game_table.dart';
import 'package:game_tracker/data/dto/player.dart';
part 'player_game_dao.g.dart';
@DriftAccessor(tables: [PlayerGameTable])
class PlayerGameDao extends DatabaseAccessor<AppDatabase>
with _$PlayerGameDaoMixin {
PlayerGameDao(super.db);
/// Checks if there are any players associated with the given [gameId].
/// Returns `true` if there are players, otherwise `false`.
Future<bool> gameHasPlayers({required String gameId}) async {
final count =
await (selectOnly(playerGameTable)
..where(playerGameTable.gameId.equals(gameId))
..addColumns([playerGameTable.playerId.count()]))
.map((row) => row.read(playerGameTable.playerId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Retrieves a list of [Player]s associated with the given [gameId].
/// Returns an empty list if no players are found.
Future<List<Player>> getPlayersByGameId({required String gameId}) async {
final result = await (select(
playerGameTable,
)..where((p) => p.gameId.equals(gameId))).get();
if (result.isEmpty) return <Player>[];
final futures = result.map(
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
);
final players = await Future.wait(futures);
return players.whereType<Player>().toList();
}
/// Associates a player with a game by inserting a record into the
/// [PlayerGameTable].
Future<void> addPlayerToGame({
required String gameId,
required String playerId,
}) async {
await into(playerGameTable).insert(
PlayerGameTableCompanion.insert(playerId: playerId, gameId: gameId),
);
}
}

View File

@@ -0,0 +1,10 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'player_game_dao.dart';
// ignore_for_file: type=lint
mixin _$PlayerGameDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$PlayerGameTableTable get playerGameTable => attachedDatabase.playerGameTable;
}

View File

@@ -11,34 +11,65 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
PlayerGroupDao(super.db); PlayerGroupDao(super.db);
/// Retrieves all players belonging to a specific group by [groupId]. /// Retrieves all players belonging to a specific group by [groupId].
Future<List<Player>> getPlayersOfGroupById(String groupId) async { Future<List<Player>> getPlayersOfGroupById({required String groupId}) async {
final query = select(playerGroupTable) final query = select(playerGroupTable)
..where((pG) => pG.groupId.equals(groupId)); ..where((pG) => pG.groupId.equals(groupId));
final result = await query.get(); final result = await query.get();
List<Player> groupMembers = []; List<Player> groupMembers = List.empty(growable: true);
for (var entry in result) { for (var entry in result) {
final player = await db.playerDao.getPlayerById(entry.userId); final player = await db.playerDao.getPlayerById(playerId: entry.playerId);
groupMembers.add(player); groupMembers.add(player);
} }
return groupMembers; return groupMembers;
} }
/// Removes a player from a group based on [userId] and [groupId]. /// Removes a player from a group based on [playerId] and [groupId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removePlayerFromGroup(String userId, String groupId) async { Future<bool> removePlayerFromGroup({
required String playerId,
required String groupId,
}) async {
final query = delete(playerGroupTable) final query = delete(playerGroupTable)
..where((p) => p.userId.equals(userId) & p.groupId.equals(groupId)); ..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
/// Adds a player to a group with the given [userId] and [groupId]. /// Adds a [player] to a group with the given [groupId].
Future<void> addPlayerToGroup(String userId, String groupId) async { /// If the player is already in the group, no action is taken.
/// If the player does not exist in the player table, they are added.
/// Returns `true` if the player was added, otherwise `false`.
Future<bool> addPlayerToGroup({
required Player player,
required String groupId,
}) async {
if (await isPlayerInGroup(playerId: player.id, groupId: groupId)) {
return false;
}
if (await db.playerDao.playerExists(playerId: player.id) == false) {
db.playerDao.addPlayer(player: player);
}
await into(playerGroupTable).insert( await into(playerGroupTable).insert(
PlayerGroupTableCompanion.insert(userId: userId, groupId: groupId), PlayerGroupTableCompanion.insert(playerId: player.id, groupId: groupId),
); );
return true;
}
/// Checks if a player with [playerId] is in the group with [groupId].
/// Returns `true` if the player is in the group, otherwise `false`.
Future<bool> isPlayerInGroup({
required String playerId,
required String groupId,
}) async {
final query = select(playerGroupTable)
..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId));
final result = await query.getSingleOrNull();
return result != null;
} }
} }

View File

@@ -2,10 +2,14 @@ import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'; import 'package:drift_flutter/drift_flutter.dart';
import 'package:game_tracker/data/dao/game_dao.dart'; import 'package:game_tracker/data/dao/game_dao.dart';
import 'package:game_tracker/data/dao/group_dao.dart'; import 'package:game_tracker/data/dao/group_dao.dart';
import 'package:game_tracker/data/dao/group_game_dao.dart';
import 'package:game_tracker/data/dao/player_dao.dart'; import 'package:game_tracker/data/dao/player_dao.dart';
import 'package:game_tracker/data/dao/player_game_dao.dart';
import 'package:game_tracker/data/dao/player_group_dao.dart'; import 'package:game_tracker/data/dao/player_group_dao.dart';
import 'package:game_tracker/data/db/tables/game_table.dart'; import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/group_game_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart'; import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/player_game_table.dart';
import 'package:game_tracker/data/db/tables/player_group_table.dart'; import 'package:game_tracker/data/db/tables/player_group_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart'; import 'package:game_tracker/data/db/tables/player_table.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -13,8 +17,22 @@ import 'package:path_provider/path_provider.dart';
part 'database.g.dart'; part 'database.g.dart';
@DriftDatabase( @DriftDatabase(
tables: [PlayerTable, GroupTable, PlayerGroupTable, GameTable], tables: [
daos: [GroupDao, PlayerDao, PlayerGroupDao, GameDao], PlayerTable,
GroupTable,
GameTable,
PlayerGroupTable,
PlayerGameTable,
GroupGameTable,
],
daos: [
PlayerDao,
GroupDao,
GameDao,
PlayerGroupDao,
PlayerGameDao,
GroupGameDao,
],
) )
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,11 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/player_table.dart';
class GameTable extends Table { class GameTable extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get name => text()(); TextColumn get name => text()();
TextColumn get winnerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
@override @override
Set<Column<Object>> get primaryKey => {id}; Set<Column<Object>> get primaryKey => {id};

View File

@@ -0,0 +1,13 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
class GroupGameTable extends Table {
TextColumn get groupId =>
text().references(GroupTable, #id, onDelete: KeyAction.cascade)();
TextColumn get gameId =>
text().references(GameTable, #id, onDelete: KeyAction.cascade)();
@override
Set<Column<Object>> get primaryKey => {groupId, gameId};
}

View File

@@ -0,0 +1,13 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart';
class PlayerGameTable extends Table {
TextColumn get playerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get gameId =>
text().references(GameTable, #id, onDelete: KeyAction.cascade)();
@override
Set<Column<Object>> get primaryKey => {playerId, gameId};
}

View File

@@ -3,9 +3,11 @@ import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart'; import 'package:game_tracker/data/db/tables/player_table.dart';
class PlayerGroupTable extends Table { class PlayerGroupTable extends Table {
TextColumn get userId => text().references(PlayerTable, #id)(); TextColumn get playerId =>
TextColumn get groupId => text().references(GroupTable, #id)(); text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get groupId =>
text().references(GroupTable, #id, onDelete: KeyAction.cascade)();
@override @override
Set<Column<Object>> get primaryKey => {userId, groupId}; Set<Column<Object>> get primaryKey => {playerId, groupId};
} }

View File

@@ -1,6 +1,23 @@
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart';
class Game { class Game {
final String id; final String id;
final String name; final String name;
final List<Player>? players;
final Group? group;
final String winner;
Game({required this.id, required this.name}); Game({
this.players,
this.group,
this.winner = '',
required this.id,
required this.name,
});
@override
String toString() {
return 'Game{\n\tid: $id,\n\tname: $name,\n\tplayers: $players,\n\tgroup: $group,\n\twinner: $winner\n}';
}
} }

View File

@@ -6,4 +6,9 @@ class Group {
final List<Player> members; final List<Player> members;
Group({required this.id, required this.name, required this.members}); Group({required this.id, required this.name, required this.members});
@override
String toString() {
return 'Group{id: $id, name: $name,members: $members}';
}
} }

View File

@@ -3,4 +3,9 @@ class Player {
final String name; final String name;
Player({required this.id, required this.name}); Player({required this.id, required this.name});
@override
String toString() {
return 'Player{id: $id,name: $name}';
}
} }

View File

@@ -8,14 +8,14 @@ void main() {
runApp( runApp(
Provider<AppDatabase>( Provider<AppDatabase>(
create: (context) => AppDatabase(), create: (context) => AppDatabase(),
child: const MyApp(), child: const GameTracker(),
dispose: (context, db) => db.close(), dispose: (context, db) => db.close(),
), ),
); );
} }
class MyApp extends StatelessWidget { class GameTracker extends StatelessWidget {
const MyApp({super.key}); const GameTracker({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -0,0 +1,109 @@
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart';
void main() {
late AppDatabase database;
late Player player1;
late Player player2;
late Player player3;
late Player player4;
late Group testgroup;
late Game testgame;
setUp(() {
database = AppDatabase(
DatabaseConnection(
NativeDatabase.memory(),
// Recommended for widget tests to avoid test errors.
closeStreamsSynchronously: true,
),
);
player1 = Player(id: 'p1', name: 'Alice');
player2 = Player(id: 'p2', name: 'Bob');
player3 = Player(id: 'p3', name: 'Charlie');
player4 = Player(id: 'p4', name: 'Diana');
testgroup = Group(
id: 'gr1',
name: 'Test Group',
members: [player1, player2, player3],
);
testgame = Game(
id: 'ga1',
name: 'Test Game',
group: testgroup,
players: [player4],
);
});
tearDown(() async {
await database.close();
});
group('game tests', () {
test('game is added correctly', () async {
await database.gameDao.addGame(game: testgame);
final result = await database.gameDao.getGameById(gameId: testgame.id);
expect(result.id, testgame.id);
expect(result.name, testgame.name);
expect(result.winner, testgame.winner);
if (result.group != null) {
expect(result.group!.members.length, testgroup.members.length);
for (int i = 0; i < testgroup.members.length; i++) {
expect(result.group!.members[i].id, testgroup.members[i].id);
expect(result.group!.members[i].name, testgroup.members[i].name);
}
} else {
fail('Group is null');
}
if (result.players != null) {
expect(result.players!.length, testgame.players!.length);
for (int i = 0; i < testgame.players!.length; i++) {
expect(result.players![i].id, testgame.players![i].id);
expect(result.players![i].name, testgame.players![i].name);
}
} else {
fail('Players is null');
}
});
test('game is deleted correctly', () async {
await database.gameDao.addGame(game: testgame);
final gameDeleted = await database.gameDao.deleteGame(
gameId: testgame.id,
);
expect(gameDeleted, true);
final gameExists = await database.gameDao.gameExists(gameId: testgame.id);
expect(gameExists, false);
});
test('get game count works correctly', () async {
final initialCount = await database.gameDao.getGameCount();
expect(initialCount, 0);
await database.gameDao.addGame(game: testgame);
final gameAdded = await database.gameDao.getGameCount();
expect(gameAdded, 1);
final gameRemoved = await database.gameDao.deleteGame(
gameId: testgame.id,
);
expect(gameRemoved, true);
final finalCount = await database.gameDao.getGameCount();
expect(finalCount, 0);
});
});
}

View File

@@ -0,0 +1,148 @@
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart';
void main() {
late AppDatabase database;
late Player player1;
late Player player2;
late Player player3;
late Player player4;
late Group testgroup;
setUp(() {
database = AppDatabase(
DatabaseConnection(
NativeDatabase.memory(),
// Recommended for widget tests to avoid test errors.
closeStreamsSynchronously: true,
),
);
player1 = Player(id: 'p1', name: 'Alice');
player2 = Player(id: 'p2', name: 'Bob');
player3 = Player(id: 'p3', name: 'Charlie');
player4 = Player(id: 'p4', name: 'Diana');
testgroup = Group(
id: 'gr1',
name: 'Test Group',
members: [player1, player2, player3],
);
});
tearDown(() async {
await database.close();
});
test('group and group members gets added correctly', () async {
await database.groupDao.addGroup(group: testgroup);
final result = await database.groupDao.getGroupById(groupId: testgroup.id);
expect(result.id, testgroup.id);
expect(result.name, testgroup.name);
expect(result.members.length, testgroup.members.length);
for (int i = 0; i < testgroup.members.length; i++) {
expect(result.members[i].id, testgroup.members[i].id);
expect(result.members[i].name, testgroup.members[i].name);
}
});
test('group gets deleted correctly', () async {
await database.groupDao.addGroup(group: testgroup);
final groupDeleted = await database.groupDao.deleteGroup(
groupId: testgroup.id,
);
expect(groupDeleted, true);
final groupExists = await database.groupDao.groupExists(
groupId: testgroup.id,
);
expect(groupExists, false);
});
test('group name gets updated correcly ', () async {
await database.groupDao.addGroup(group: testgroup);
const newGroupName = 'new group name';
await database.groupDao.updateGroupname(
groupId: testgroup.id,
newName: newGroupName,
);
final result = await database.groupDao.getGroupById(groupId: testgroup.id);
expect(result.name, newGroupName);
});
test('Adding player to group works correctly', () async {
await database.groupDao.addGroup(group: testgroup);
await database.playerGroupDao.addPlayerToGroup(
player: player4,
groupId: testgroup.id,
);
final playerAdded = await database.playerGroupDao.isPlayerInGroup(
playerId: player4.id,
groupId: testgroup.id,
);
expect(playerAdded, true);
final playerAdded2 = await database.playerGroupDao.isPlayerInGroup(
playerId: 'a',
groupId: testgroup.id,
);
expect(playerAdded2, false);
expect(playerAdded, true);
final result = await database.groupDao.getGroupById(groupId: testgroup.id);
expect(result.members.length, testgroup.members.length + 1);
final addedPlayer = result.members.firstWhere((p) => p.id == player4.id);
expect(addedPlayer.name, player4.name);
});
test('Removing player from group works correctly', () async {
await database.groupDao.addGroup(group: testgroup);
final playerToRemove = testgroup.members[0];
final removed = await database.playerGroupDao.removePlayerFromGroup(
playerId: playerToRemove.id,
groupId: testgroup.id,
);
expect(removed, true);
final result = await database.groupDao.getGroupById(groupId: testgroup.id);
expect(result.members.length, testgroup.members.length - 1);
final playerExists = result.members.any((p) => p.id == playerToRemove.id);
expect(playerExists, false);
});
test('get group count works correctly', () async {
final initialCount = await database.groupDao.getGroupCount();
expect(initialCount, 0);
await database.groupDao.addGroup(group: testgroup);
final groupAdded = await database.groupDao.getGroupCount();
expect(groupAdded, 1);
final groupRemoved = await database.groupDao.deleteGroup(
groupId: testgroup.id,
);
expect(groupRemoved, true);
final finalCount = await database.groupDao.getGroupCount();
expect(finalCount, 0);
});
}

View File

@@ -0,0 +1,84 @@
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/player.dart';
void main() {
late AppDatabase database;
late Player testPlayer;
setUp(() {
database = AppDatabase(
DatabaseConnection(
NativeDatabase.memory(),
// Recommended for widget tests to avoid test errors.
closeStreamsSynchronously: true,
),
);
testPlayer = Player(id: 'test_id', name: 'Test Player');
});
tearDown(() async {
await database.close();
});
group('player tests', () {
test('players get inserted correcly ', () async {
await database.playerDao.addPlayer(player: testPlayer);
final result = await database.playerDao.getPlayerById(
playerId: testPlayer.id,
);
expect(result.id, testPlayer.id);
expect(result.name, testPlayer.name);
});
test('players get deleted correcly ', () async {
await database.playerDao.addPlayer(player: testPlayer);
final playerDeleted = await database.playerDao.deletePlayer(
playerId: testPlayer.id,
);
expect(playerDeleted, true);
final playerExists = await database.playerDao.playerExists(
playerId: testPlayer.id,
);
expect(playerExists, false);
});
test('player name gets updated correcly ', () async {
await database.playerDao.addPlayer(player: testPlayer);
const newPlayerName = 'new player name';
await database.playerDao.updatePlayername(
playerId: testPlayer.id,
newName: newPlayerName,
);
final result = await database.playerDao.getPlayerById(
playerId: testPlayer.id,
);
expect(result.name, newPlayerName);
});
test('get player count works correctly', () async {
final initialCount = await database.playerDao.getPlayerCount();
expect(initialCount, 0);
await database.playerDao.addPlayer(player: testPlayer);
final playerAdded = await database.playerDao.getPlayerCount();
expect(playerAdded, 1);
final playerRemoved = await database.playerDao.deletePlayer(
playerId: testPlayer.id,
);
expect(playerRemoved, true);
final finalCount = await database.playerDao.getPlayerCount();
expect(finalCount, 0);
});
});
}

View File

@@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}