99 Commits

Author SHA1 Message Date
cd63f5cf06 Merge pull request 'Fehlende Info in CreateGroupView' (#107) from bug/73-Fehlende-Info-in-CreateGroupView into development
Reviewed-on: #107
Reviewed-by: Mathis Kirchner <mathis.kirchner.mk@gmail.com>
2025-12-31 16:24:31 +00:00
8c3282b954 Changed color to white
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 1m57s
Pull Request Pipeline / lint (pull_request) Successful in 4m58s
2025-12-31 17:10:47 +01:00
1c0fb9f3ab Merge branch 'development' into bug/73-Fehlende-Info-in-CreateGroupView
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m19s
Pull Request Pipeline / lint (pull_request) Successful in 2m21s
2025-12-31 16:41:26 +01:00
bfca41bd36 Fixed info message placement
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m19s
Pull Request Pipeline / lint (pull_request) Successful in 2m21s
2025-12-31 16:38:09 +01:00
27916456db Merge pull request 'Fehler bei Match-Erstellung' (#104) from bug/102-fehler-bei-match-erstellung into development
Reviewed-on: #104
Reviewed-by: Mathis Kirchner <mathis.kirchner.mk@gmail.com>
2025-12-30 20:17:18 +00:00
c1789458e0 Added tests for bug causing behaviour
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2025-12-30 20:56:39 +01:00
e19f696714 Fixed tests with adding players and groups to database on setup
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
2025-12-30 18:51:57 +01:00
9cb35dc4a1 Updated comments
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m9s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-12-30 18:45:28 +01:00
b8b65c4ca1 Removed lines that added the group and players to the db again in addMatch()
Some checks failed
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
Pull Request Pipeline / test (pull_request) Failing after 2m19s
2025-12-30 18:23:15 +01:00
356cb1fb43 Updated insert mode to fix problem with replacing existing player match connections 2025-12-30 18:21:39 +01:00
4c8896d2d9 Merge pull request 'Fehlende Aktualisierung in MatchView' (#101) from bug/95-fehlende-aktualisierung-in-matchview into development
Reviewed-on: #101
Reviewed-by: Mathis Kirchner <mathis.kirchner.mk@gmail.com>
2025-12-30 16:49:02 +00:00
31c8598222 Made cleaner version
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-12-29 19:28:47 +01:00
884680d329 Merge branch 'development' into bug/95-fehlende-aktualisierung-in-matchview
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-12-29 19:23:11 +01:00
5ae569f2e7 Fixed updating error
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Successful in 2m3s
2025-12-29 19:16:25 +01:00
37d6ad81fd Merge pull request 'ChooseGroupView zeigt keine Group entspricht der Suche, obwohl nur Gruppen leer' (#98) from bug/94-choosegroupview-zeigt-keine-group-entspricht-der-suche-obwohl-nur-gruppen-leer into development
Reviewed-on: #98
Reviewed-by: Mathis Kirchner <mathis.kirchner.mk@gmail.com>
2025-12-29 17:47:11 +00:00
cea6c36407 Merge branch 'development' into bug/94-choosegroupview-zeigt-keine-group-entspricht-der-suche-obwohl-nur-gruppen-leer
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2025-12-29 17:45:00 +00:00
1b300faea9 Merge pull request '"Per Player" entfernen' (#99) from enhancement/93-per-player-entfernen into development
Reviewed-on: #99
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
Reviewed-by: Mathis Kirchner <mathis.kirchner.mk@gmail.com>
2025-12-29 17:41:50 +00:00
dac5cc8a89 Hab leider nur von Wand bis Tapete gedacht
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2025-12-29 18:37:44 +01:00
bafbde7d03 Merge remote-tracking branch 'origin/bug/94-choosegroupview-zeigt-keine-group-entspricht-der-suche-obwohl-nur-gruppen-leer' into bug/94-choosegroupview-zeigt-keine-group-entspricht-der-suche-obwohl-nur-gruppen-leer 2025-12-29 18:35:26 +01:00
fdea594a1e Merge branch 'development' into bug/94-choosegroupview-zeigt-keine-group-entspricht-der-suche-obwohl-nur-gruppen-leer
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m33s
Pull Request Pipeline / lint (pull_request) Successful in 2m36s
2025-12-29 17:28:50 +00:00
856ce43c49 Updated statistics title
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m32s
Pull Request Pipeline / lint (pull_request) Successful in 2m37s
2025-12-29 18:28:06 +01:00
5c8b93072f Merge branch 'development' into bug/94-choosegroupview-zeigt-keine-group-entspricht-der-suche-obwohl-nur-gruppen-leer 2025-12-29 18:25:30 +01:00
94fdd7026d Merge pull request 'FutureBuilder-/Skeleton-/Futurelogik verbessern' (#90) from enhancement/80-FutureBuilder-Logik-bei-Interaktion-mit-DB-verbessern into development
Reviewed-on: #90
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-12-29 17:21:49 +00:00
966f7caa54 Fixed info message
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m13s
2025-12-29 18:19:29 +01:00
88bea48956 Merge remote-tracking branch 'origin/development' into enhancement/80-FutureBuilder-Logik-bei-Interaktion-mit-DB-verbessern
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m13s
Pull Request Pipeline / lint (pull_request) Successful in 2m17s
# Conflicts:
#	lib/presentation/views/main_menu/match_view/match_view.dart
2025-12-29 18:19:03 +01:00
a2e66b9c80 Merge pull request 'Letztes Gametile nicht komplett sichtbar' (#96) from bug/79-Letztes-Gametile-in-der-GamesView-ist-nicht-lesbar into development
Reviewed-on: #96
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
2025-12-25 09:55:31 +00:00
d7a006dd87 fixed last gametile not visible
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2025-12-24 23:51:34 +01:00
12856342a8 remove dots after sentences
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
2025-12-24 22:53:17 +01:00
b0a5145490 move setState to include match loading and sorting in MatchView 2025-12-24 13:14:31 +01:00
7a80c1a792 adjust heightFactor for empty game state in home view
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2025-12-24 13:08:25 +01:00
c73f37507f put isLoading in existing setState and move mounted check up
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2025-12-24 13:05:49 +01:00
1e730cebe6 wrap isLoading into mounted and setState
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-12-24 13:01:40 +01:00
7eb25221d7 add mounted check before calling setState in match_view 2025-12-24 13:01:16 +01:00
f9722bc762 wrap isLoading = false in a mounted check and setState call 2025-12-24 13:01:05 +01:00
f2917a6813 removed empty line 2025-12-24 13:00:38 +01:00
b29cd6dff4 remove double quotes
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m15s
Pull Request Pipeline / lint (pull_request) Successful in 2m21s
2025-12-24 12:54:05 +01:00
1d92084da6 fix rangeerror when only 1 or less matches exist
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m21s
Pull Request Pipeline / lint (pull_request) Failing after 2m34s
2025-12-24 12:53:37 +01:00
7732c6ceb9 remove futurebuilder from match view and refactor
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m10s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
2025-12-23 23:17:27 +01:00
a747d91c5d refactor group loading into reusable method loadGroups and call it after adding a group 2025-12-23 23:16:57 +01:00
06a9c0cd84 remove futurebuilder from player selection and refactor 2025-12-23 22:59:01 +01:00
9ad5c4ad6f remove futurebuilder from groups view and refactor 2025-12-23 22:58:49 +01:00
9b3d61e5b0 remove futurebuilder from playerselection and refactor 2025-12-23 22:36:11 +01:00
df0a5207c2 add const
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m22s
2025-12-21 20:01:48 +01:00
4f0a1eec6d refactor
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Failing after 2m6s
2025-12-21 19:50:34 +01:00
24b60bb18b added minimumSkeletonDuration constant and changed future logic 2025-12-21 19:46:59 +01:00
c8532adfde changed skeleton duration logic in statistics view and refactored 2025-12-21 15:52:02 +01:00
76186787e7 changed futurebuilder logic in player selection 2025-12-21 15:51:24 +01:00
f05114a99e removed futurebuilder logic in groups_view 2025-12-21 15:50:39 +01:00
d96494f608 Changed futurebuilder logic in groups_view 2025-12-21 15:50:24 +01:00
0eaf3d251b added constant minimumSkeletonDuration 2025-12-21 15:49:58 +01:00
0567fce02b Merge pull request 'Game zu Match umbenennen' (#89) from refactoring/78-game-zu-match-umbenennen into development
Reviewed-on: #89
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
2025-12-12 15:34:17 +00:00
74ea540b39 Merge branch 'feature/3-creategameview-erstellen' into refactoring/78-game-zu-match-umbenennen
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
Pull Request Pipeline / test (pull_request) Successful in 2m9s
2025-12-11 20:13:55 +01:00
d578a7f9bd Missing comment 2025-12-11 20:12:56 +01:00
0e5ad1c3d9 Another missing 2025-12-11 20:12:21 +01:00
c4b249b199 Added missing game instants 2025-12-11 20:11:02 +01:00
99cea1e703 Renamed every instance of "game" to "match" 2025-12-11 20:07:32 +01:00
7b7c41b96c Merge pull request 'CreateGameView erstellen' (#67) from feature/3-creategameview-erstellen into development
Reviewed-on: #67
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-12-10 20:43:53 +00:00
d0059b44a8 Moved statement
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2025-12-10 21:41:37 +01:00
3b3d298ff5 Small refactoring
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-12-10 14:04:15 +01:00
e1626225ac Removed future builder 2025-12-10 13:56:03 +01:00
93ced81e7e Fixed filtering problem with object reference 2025-12-10 13:55:31 +01:00
23cdddfbd9 Fixed problem with player selection
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m7s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2025-12-10 12:38:39 +01:00
5d2fed74ac Fixed sorting problem
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
2025-12-09 23:59:34 +01:00
0d0806dfbb Implemented listener
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m7s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-12-09 22:25:45 +01:00
27ff599a88 Reverted method
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-12-09 20:52:38 +01:00
c4094a547e Fixed button state problem 2025-12-09 20:52:14 +01:00
701500c7e2 Fixed state problem with games list
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-12-09 18:26:05 +01:00
062c2681bf Corrected info message 2025-12-09 18:10:31 +01:00
7cff48ebc0 Added empty message
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m14s
Pull Request Pipeline / lint (pull_request) Successful in 2m14s
2025-12-07 22:23:43 +01:00
708157df54 Implemented leaving players in the player selection and filtering those who are in the group
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m11s
Pull Request Pipeline / lint (pull_request) Successful in 2m12s
2025-12-07 22:15:19 +01:00
f1f3fd7b6e Fixed deselecting game bug 2025-12-07 22:03:28 +01:00
7bc75d60a7 Fixed text overflow
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m33s
Pull Request Pipeline / lint (pull_request) Successful in 2m36s
2025-12-07 19:31:11 +01:00
a1ed17355a Minimum of 2 players required to create a game, unless group is selected 2025-12-07 19:23:38 +01:00
45abc79f95 Removed unneccesary db declaration 2025-12-07 19:17:56 +01:00
c214a26c54 Removed onChanged 2025-12-07 19:17:20 +01:00
6c0cb92e56 Removed unneccesary title in tupe 2025-12-07 19:17:13 +01:00
b4ba4f8d74 Implemented search bar in choose_group_view 2025-12-07 19:12:43 +01:00
f60c11fc08 Added searchbar for choose_group_view 2025-12-07 19:01:19 +01:00
d2d0a82c9b Implemented deselecting game 2025-12-07 18:58:49 +01:00
1044ee9bea Merge branch 'development' into feature/3-creategameview-erstellen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m10s
Pull Request Pipeline / lint (pull_request) Successful in 2m20s
2025-12-07 15:48:42 +01:00
32e812127d Merge pull request 'Group erstellen nur möglich mit min. 2 Spielern' (#82) from enhancement/75-Group-erstellen-nur-möglich-mit-min.-2-spielern into development
Reviewed-on: #82
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-12-07 14:06:37 +00:00
4500d85f78 Merge branch 'development' into enhancement/75-Group-erstellen-nur-möglich-mit-min.-2-spielern
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m13s
Pull Request Pipeline / lint (pull_request) Successful in 2m14s
2025-12-07 14:04:19 +00:00
c8bf03f462 Merge pull request 'Skeleton in GameHistoryView anpassen' (#86) from enhancement/85-skeleton-in-gamehistoryview-anpassen into development
Reviewed-on: #86
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-12-07 14:04:04 +00:00
7ecccb13c2 Updated skeleton data
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-12-07 13:04:30 +01:00
4fc1959704 Merge branch 'development' into feature/3-creategameview-erstellen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2025-12-06 23:19:13 +01:00
1e03507f75 Merge pull request 'Fix für nicht funktionierende SearchBar' (#84) from bug/83-Search-in-CreateGameView-und-CreateGroupView-funktioniert-nicht into development
Reviewed-on: #84
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-12-06 22:18:20 +00:00
e37a98fdcc hart besoffenen fehler gefixt?
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-12-06 21:53:24 +01:00
6ae0471fa2 Implemented final navigation to GameResultView
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m19s
2025-12-06 17:42:11 +01:00
a1a995777b Adjusted margin
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2025-12-06 17:23:40 +01:00
3d12f0c160 Fixed width problem in choose game view 2025-12-06 17:23:20 +01:00
10aad47124 Updated choosing mechanism
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-12-06 17:13:33 +01:00
75b62d0854 Replaced temp navigator 2025-12-06 16:58:12 +01:00
527f163346 Merge branch 'development' into feature/3-creategameview-erstellen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
2025-12-06 16:54:30 +01:00
6ae7166d34 Update state when selecting players and require at least two players to create a group
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
2025-12-06 15:18:41 +01:00
b4ccb567b5 Added conditional description
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
2025-11-28 23:15:08 +01:00
bcd7bf751b Added todo & example data 2025-11-28 23:14:29 +01:00
4dbc106e38 Added searchbar 2025-11-28 23:14:13 +01:00
236a737fd1 Implemented choose game view
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2025-11-28 14:41:50 +01:00
94b113eb95 Renamed widget
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
Pull Request Pipeline / test (pull_request) Successful in 2m8s
2025-11-28 14:07:42 +01:00
51 changed files with 2623 additions and 2314 deletions

2
lib/core/constants.dart Normal file
View File

@@ -0,0 +1,2 @@
/// Minimum duration of all app skeletons
Duration minimumSkeletonDuration = const Duration(milliseconds: 250);

View File

@@ -23,9 +23,23 @@ enum ImportResult {
/// - [ExportResult.unknownException]: An exception occurred during export. /// - [ExportResult.unknownException]: An exception occurred during export.
enum ExportResult { success, canceled, unknownException } enum ExportResult { success, canceled, unknownException }
/// Different rulesets available for games /// Different rulesets available for matches
/// - [Ruleset.singleWinner]: The game is won by a single player /// - [Ruleset.singleWinner]: The match is won by a single player
/// - [Ruleset.singleLoser]: The game is lost by a single player /// - [Ruleset.singleLoser]: The match is lost by a single player
/// - [Ruleset.mostPoints]: The player with the most points wins. /// - [Ruleset.mostPoints]: The player with the most points wins.
/// - [Ruleset.lastPoints]: The player with the fewest points wins. /// - [Ruleset.leastPoints]: The player with the fewest points wins.
enum Ruleset { singleWinner, singleLoser, mostPoints, lastPoints } enum Ruleset { singleWinner, singleLoser, mostPoints, leastPoints }
/// Translates a [Ruleset] enum value to its corresponding string representation.
String translateRulesetToString(Ruleset ruleset) {
switch (ruleset) {
case Ruleset.singleWinner:
return 'Single Winner';
case Ruleset.singleLoser:
return 'Single Loser';
case Ruleset.mostPoints:
return 'Most Points';
case Ruleset.leastPoints:
return 'Least Points';
}
}

View File

@@ -1,320 +0,0 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.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/group.dart';
import 'package:game_tracker/data/dto/player.dart';
part 'game_dao.g.dart';
@DriftAccessor(tables: [GameTable])
class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
GameDao(super.db);
/// Retrieves all games from the database.
Future<List<Game>> getAllGames() async {
final query = select(gameTable);
final result = await query.get();
return Future.wait(
result.map((row) async {
final group = await db.groupGameDao.getGroupOfGame(gameId: row.id);
final players = await db.playerGameDao.getPlayersOfGame(gameId: row.id);
final winner = row.winnerId != null
? await db.playerDao.getPlayerById(playerId: row.winnerId!)
: null;
return Game(
id: row.id,
name: row.name,
group: group,
players: players,
createdAt: row.createdAt,
winner: winner,
);
}),
);
}
/// Retrieves a [Game] by its [gameId].
Future<Game> getGameById({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId));
final result = await query.getSingle();
List<Player>? players;
if (await db.playerGameDao.gameHasPlayers(gameId: gameId)) {
players = await db.playerGameDao.getPlayersOfGame(gameId: gameId);
}
Group? group;
if (await db.groupGameDao.gameHasGroup(gameId: gameId)) {
group = await db.groupGameDao.getGroupOfGame(gameId: gameId);
}
Player? winner;
if (result.winnerId != null) {
winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
}
return Game(
id: result.id,
name: result.name,
players: players,
group: group,
winner: winner,
createdAt: result.createdAt,
);
}
/// 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 {
await into(gameTable).insert(
GameTableCompanion.insert(
id: game.id,
name: game.name,
winnerId: Value(game.winner?.id),
createdAt: game.createdAt,
),
mode: InsertMode.insertOrReplace,
);
if (game.players != null) {
await db.playerDao.addPlayersAsList(players: game.players!);
for (final p in game.players ?? []) {
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(
gameId: game.id,
groupId: game.group!.id,
);
}
});
}
/// Adds multiple [Game]s to the database in a batch operation.
/// Also adds associated players and groups if they exist.
/// If the [games] list is empty, the method returns immediately.
Future<void> addGamesAsList({required List<Game> games}) async {
if (games.isEmpty) return;
await db.transaction(() async {
// Add all games in batch
await db.batch(
(b) => b.insertAll(
gameTable,
games
.map(
(game) => GameTableCompanion.insert(
id: game.id,
name: game.name,
createdAt: game.createdAt,
winnerId: Value(game.winner?.id),
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
// Add all groups of the games in batch
await db.batch(
(b) => b.insertAll(
db.groupTable,
games
.where((game) => game.group != null)
.map(
(game) => GroupTableCompanion.insert(
id: game.group!.id,
name: game.group!.name,
createdAt: game.group!.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
// Add all players of the games in batch (unique)
final uniquePlayers = <String, Player>{};
for (final game in games) {
if (game.players != null) {
for (final p in game.players!) {
uniquePlayers[p.id] = p;
}
}
// Also include members of groups
if (game.group != null) {
for (final m in game.group!.members) {
uniquePlayers[m.id] = m;
}
}
}
if (uniquePlayers.isNotEmpty) {
await db.batch(
(b) => b.insertAll(
db.playerTable,
uniquePlayers.values
.map(
(p) => PlayerTableCompanion.insert(
id: p.id,
name: p.name,
createdAt: p.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
}
// Add all player-game associations in batch
await db.batch((b) {
for (final game in games) {
if (game.players != null) {
for (final p in game.players ?? []) {
b.insert(
db.playerGameTable,
PlayerGameTableCompanion.insert(
gameId: game.id,
playerId: p.id,
),
mode: InsertMode.insertOrReplace,
);
}
}
}
});
// Add all player-group associations in batch
await db.batch((b) {
for (final game in games) {
if (game.group != null) {
for (final m in game.group!.members) {
b.insert(
db.playerGroupTable,
PlayerGroupTableCompanion.insert(
playerId: m.id,
groupId: game.group!.id,
),
mode: InsertMode.insertOrReplace,
);
}
}
}
});
// Add all group-game associations in batch
await db.batch((b) {
for (final game in games) {
if (game.group != null) {
b.insert(
db.groupGameTable,
GroupGameTableCompanion.insert(
gameId: game.id,
groupId: game.group!.id,
),
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.
Future<int> getGameCount() async {
final count =
await (selectOnly(gameTable)..addColumns([gameTable.id.count()]))
.map((row) => row.read(gameTable.id.count()))
.getSingle();
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;
}
/// Deletes all games from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteAllGames() async {
final query = delete(gameTable);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Sets the winner of the game with the given [gameId] to the player with
/// the given [winnerId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> setWinner({
required String gameId,
required String winnerId,
}) async {
final query = update(gameTable)..where((g) => g.id.equals(gameId));
final rowsAffected = await query.write(
GameTableCompanion(winnerId: Value(winnerId)),
);
return rowsAffected > 0;
}
/// Retrieves the winner of the game with the given [gameId].
/// Returns the [Player] who won the game, or `null` if no winner is set.
Future<Player?> getWinner({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId));
final result = await query.getSingleOrNull();
if (result == null || result.winnerId == null) {
return null;
}
final winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
return winner;
}
/// Removes the winner of the game with the given [gameId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeWinner({required String gameId}) async {
final query = update(gameTable)..where((g) => g.id.equals(gameId));
final rowsAffected = await query.write(
const GameTableCompanion(winnerId: Value(null)),
);
return rowsAffected > 0;
}
/// Checks if the game with the given [gameId] has a winner set.
/// Returns `true` if a winner is set, otherwise `false`.
Future<bool> hasWinner({required String gameId}) async {
final query = select(gameTable)
..where((g) => g.id.equals(gameId) & g.winnerId.isNotNull());
final result = await query.getSingleOrNull();
return result != null;
}
/// Changes the title of the game with the given [gameId] to [newName].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGameName({
required String gameId,
required String newName,
}) async {
final query = update(gameTable)..where((g) => g.id.equals(gameId));
final rowsAffected = await query.write(
GameTableCompanion(name: Value(newName)),
);
return rowsAffected > 0;
}
}

View File

@@ -1,8 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'game_dao.dart';
// ignore_for_file: type=lint
mixin _$GameDaoMixin on DatabaseAccessor<AppDatabase> {
$GameTableTable get gameTable => attachedDatabase.gameTable;
}

View File

@@ -1,12 +1,13 @@
import 'package:drift/drift.dart'; 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/group_table.dart'; import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/player_group_table.dart';
import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
part 'group_dao.g.dart'; part 'group_dao.g.dart';
@DriftAccessor(tables: [GroupTable]) @DriftAccessor(tables: [GroupTable, PlayerGroupTable])
class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin { class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
GroupDao(super.db); GroupDao(super.db);

View File

@@ -5,4 +5,7 @@ part of 'group_dao.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$GroupDaoMixin on DatabaseAccessor<AppDatabase> { mixin _$GroupDaoMixin on DatabaseAccessor<AppDatabase> {
$GroupTableTable get groupTable => attachedDatabase.groupTable; $GroupTableTable get groupTable => attachedDatabase.groupTable;
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$PlayerGroupTableTable get playerGroupTable =>
attachedDatabase.playerGroupTable;
} }

View File

@@ -1,98 +0,0 @@
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);
/// Associates a group with a game by inserting a record into the
/// [GroupGameTable].
Future<void> addGroupToGame({
required String gameId,
required String groupId,
}) async {
if (await gameHasGroup(gameId: gameId)) {
throw Exception('Game already has a group');
}
await into(groupGameTable).insert(
GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId),
mode: InsertMode.insertOrReplace,
);
}
/// Retrieves the [Group] associated with the given [gameId].
/// Returns `null` if no group is found.
Future<Group?> getGroupOfGame({required String gameId}) async {
final result = await (select(
groupGameTable,
)..where((g) => g.gameId.equals(gameId))).getSingleOrNull();
if (result == null) {
return null;
}
final group = await db.groupDao.getGroupById(groupId: result.groupId);
return group;
}
/// Checks if there is a group associated with the given [gameId].
/// Returns `true` if there is a group, otherwise `false`.
Future<bool> gameHasGroup({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;
}
/// Checks if a specific group is associated with a specific game.
/// Returns `true` if the group is in the game, otherwise `false`.
Future<bool> isGroupInGame({
required String gameId,
required String groupId,
}) async {
final count =
await (selectOnly(groupGameTable)
..where(
groupGameTable.gameId.equals(gameId) &
groupGameTable.groupId.equals(groupId),
)
..addColumns([groupGameTable.groupId.count()]))
.map((row) => row.read(groupGameTable.groupId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Removes the association of a group from a game based on [groupId] and
/// [gameId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeGroupFromGame({
required String gameId,
required String groupId,
}) async {
final query = delete(groupGameTable)
..where((g) => g.gameId.equals(gameId) & g.groupId.equals(groupId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Updates the group associated with a game to [newGroupId] based on
/// [gameId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGroupOfGame({
required String gameId,
required String newGroupId,
}) async {
final updatedRows =
await (update(groupGameTable)..where((g) => g.gameId.equals(gameId)))
.write(GroupGameTableCompanion(groupId: Value(newGroupId)));
return updatedRows > 0;
}
}

View File

@@ -1,10 +0,0 @@
// 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;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupGameTableTable get groupGameTable => attachedDatabase.groupGameTable;
}

View File

@@ -0,0 +1,98 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/group_match_table.dart';
import 'package:game_tracker/data/dto/group.dart';
part 'group_match_dao.g.dart';
@DriftAccessor(tables: [GroupMatchTable])
class GroupMatchDao extends DatabaseAccessor<AppDatabase>
with _$GroupMatchDaoMixin {
GroupMatchDao(super.db);
/// Associates a group with a match by inserting a record into the
/// [GroupMatchTable].
Future<void> addGroupToMatch({
required String matchId,
required String groupId,
}) async {
if (await matchHasGroup(matchId: matchId)) {
throw Exception('Match already has a group');
}
await into(groupMatchTable).insert(
GroupMatchTableCompanion.insert(groupId: groupId, matchId: matchId),
mode: InsertMode.insertOrIgnore,
);
}
/// Retrieves the [Group] associated with the given [matchId].
/// Returns `null` if no group is found.
Future<Group?> getGroupOfMatch({required String matchId}) async {
final result = await (select(
groupMatchTable,
)..where((g) => g.matchId.equals(matchId))).getSingleOrNull();
if (result == null) {
return null;
}
final group = await db.groupDao.getGroupById(groupId: result.groupId);
return group;
}
/// Checks if there is a group associated with the given [matchId].
/// Returns `true` if there is a group, otherwise `false`.
Future<bool> matchHasGroup({required String matchId}) async {
final count =
await (selectOnly(groupMatchTable)
..where(groupMatchTable.matchId.equals(matchId))
..addColumns([groupMatchTable.groupId.count()]))
.map((row) => row.read(groupMatchTable.groupId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Checks if a specific group is associated with a specific match.
/// Returns `true` if the group is in the match, otherwise `false`.
Future<bool> isGroupInMatch({
required String matchId,
required String groupId,
}) async {
final count =
await (selectOnly(groupMatchTable)
..where(
groupMatchTable.matchId.equals(matchId) &
groupMatchTable.groupId.equals(groupId),
)
..addColumns([groupMatchTable.groupId.count()]))
.map((row) => row.read(groupMatchTable.groupId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Removes the association of a group from a match based on [groupId] and
/// [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeGroupFromMatch({
required String matchId,
required String groupId,
}) async {
final query = delete(groupMatchTable)
..where((g) => g.matchId.equals(matchId) & g.groupId.equals(groupId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Updates the group associated with a match to [newGroupId] based on
/// [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGroupOfMatch({
required String matchId,
required String newGroupId,
}) async {
final updatedRows =
await (update(groupMatchTable)..where((g) => g.matchId.equals(matchId)))
.write(GroupMatchTableCompanion(groupId: Value(newGroupId)));
return updatedRows > 0;
}
}

View File

@@ -0,0 +1,10 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'group_match_dao.dart';
// ignore_for_file: type=lint
mixin _$GroupMatchDaoMixin on DatabaseAccessor<AppDatabase> {
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$GroupMatchTableTable get groupMatchTable => attachedDatabase.groupMatchTable;
}

322
lib/data/dao/match_dao.dart Normal file
View File

@@ -0,0 +1,322 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
part 'match_dao.g.dart';
@DriftAccessor(tables: [MatchTable])
class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
MatchDao(super.db);
/// Retrieves all matches from the database.
Future<List<Match>> getAllMatches() async {
final query = select(matchTable);
final result = await query.get();
return Future.wait(
result.map((row) async {
final group = await db.groupMatchDao.getGroupOfMatch(matchId: row.id);
final players = await db.playerMatchDao.getPlayersOfMatch(
matchId: row.id,
);
final winner = row.winnerId != null
? await db.playerDao.getPlayerById(playerId: row.winnerId!)
: null;
return Match(
id: row.id,
name: row.name,
group: group,
players: players,
createdAt: row.createdAt,
winner: winner,
);
}),
);
}
/// Retrieves a [Match] by its [matchId].
Future<Match> getMatchById({required String matchId}) async {
final query = select(matchTable)..where((g) => g.id.equals(matchId));
final result = await query.getSingle();
List<Player>? players;
if (await db.playerMatchDao.matchHasPlayers(matchId: matchId)) {
players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
}
Group? group;
if (await db.groupMatchDao.matchHasGroup(matchId: matchId)) {
group = await db.groupMatchDao.getGroupOfMatch(matchId: matchId);
}
Player? winner;
if (result.winnerId != null) {
winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
}
return Match(
id: result.id,
name: result.name,
players: players,
group: group,
winner: winner,
createdAt: result.createdAt,
);
}
/// Adds a new [Match] to the database. Also adds players and group
/// associations. This method assumes that the players and groups added to
/// this match are already present in the database.
Future<void> addMatch({required Match match}) async {
await db.transaction(() async {
await into(matchTable).insert(
MatchTableCompanion.insert(
id: match.id,
name: match.name,
winnerId: Value(match.winner?.id),
createdAt: match.createdAt,
),
mode: InsertMode.insertOrReplace,
);
if (match.players != null) {
for (final p in match.players ?? []) {
await db.playerMatchDao.addPlayerToMatch(
matchId: match.id,
playerId: p.id,
);
}
}
if (match.group != null) {
await db.groupMatchDao.addGroupToMatch(
matchId: match.id,
groupId: match.group!.id,
);
}
});
}
/// Adds multiple [Match]s to the database in a batch operation.
/// Also adds associated players and groups if they exist.
/// If the [matches] list is empty, the method returns immediately.
/// This Method should only be used to import matches from a different device.
Future<void> addMatchAsList({required List<Match> matches}) async {
if (matches.isEmpty) return;
await db.transaction(() async {
// Add all matches in batch
await db.batch(
(b) => b.insertAll(
matchTable,
matches
.map(
(match) => MatchTableCompanion.insert(
id: match.id,
name: match.name,
createdAt: match.createdAt,
winnerId: Value(match.winner?.id),
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
// Add all groups of the matches in batch
await db.batch(
(b) => b.insertAll(
db.groupTable,
matches
.where((match) => match.group != null)
.map(
(matches) => GroupTableCompanion.insert(
id: matches.group!.id,
name: matches.group!.name,
createdAt: matches.group!.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
// Add all players of the matches in batch (unique)
final uniquePlayers = <String, Player>{};
for (final match in matches) {
if (match.players != null) {
for (final p in match.players!) {
uniquePlayers[p.id] = p;
}
}
// Also include members of groups
if (match.group != null) {
for (final m in match.group!.members) {
uniquePlayers[m.id] = m;
}
}
}
if (uniquePlayers.isNotEmpty) {
await db.batch(
(b) => b.insertAll(
db.playerTable,
uniquePlayers.values
.map(
(p) => PlayerTableCompanion.insert(
id: p.id,
name: p.name,
createdAt: p.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
}
// Add all player-match associations in batch
await db.batch((b) {
for (final match in matches) {
if (match.players != null) {
for (final p in match.players ?? []) {
b.insert(
db.playerMatchTable,
PlayerMatchTableCompanion.insert(
matchId: match.id,
playerId: p.id,
),
mode: InsertMode.insertOrIgnore,
);
}
}
}
});
// Add all player-group associations in batch
await db.batch((b) {
for (final match in matches) {
if (match.group != null) {
for (final m in match.group!.members) {
b.insert(
db.playerGroupTable,
PlayerGroupTableCompanion.insert(
playerId: m.id,
groupId: match.group!.id,
),
mode: InsertMode.insertOrIgnore,
);
}
}
}
});
// Add all group-match associations in batch
await db.batch((b) {
for (final match in matches) {
if (match.group != null) {
b.insert(
db.groupMatchTable,
GroupMatchTableCompanion.insert(
matchId: match.id,
groupId: match.group!.id,
),
mode: InsertMode.insertOrIgnore,
);
}
}
});
});
}
/// Deletes the match with the given [matchId] from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteMatch({required String matchId}) async {
final query = delete(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Retrieves the number of matches in the database.
Future<int> getMatchCount() async {
final count =
await (selectOnly(matchTable)..addColumns([matchTable.id.count()]))
.map((row) => row.read(matchTable.id.count()))
.getSingle();
return count ?? 0;
}
/// Checks if a match with the given [matchId] exists in the database.
/// Returns `true` if the match exists, otherwise `false`.
Future<bool> matchExists({required String matchId}) async {
final query = select(matchTable)..where((g) => g.id.equals(matchId));
final result = await query.getSingleOrNull();
return result != null;
}
/// Deletes all matches from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteAllMatches() async {
final query = delete(matchTable);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Sets the winner of the match with the given [matchId] to the player with
/// the given [winnerId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> setWinner({
required String matchId,
required String winnerId,
}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
MatchTableCompanion(winnerId: Value(winnerId)),
);
return rowsAffected > 0;
}
/// Retrieves the winner of the match with the given [matchId].
/// Returns the [Player] who won the match, or `null` if no winner is set.
Future<Player?> getWinner({required String matchId}) async {
final query = select(matchTable)..where((g) => g.id.equals(matchId));
final result = await query.getSingleOrNull();
if (result == null || result.winnerId == null) {
return null;
}
final winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
return winner;
}
/// Removes the winner of the match with the given [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeWinner({required String matchId}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
const MatchTableCompanion(winnerId: Value(null)),
);
return rowsAffected > 0;
}
/// Checks if the match with the given [matchId] has a winner set.
/// Returns `true` if a winner is set, otherwise `false`.
Future<bool> hasWinner({required String matchId}) async {
final query = select(matchTable)
..where((g) => g.id.equals(matchId) & g.winnerId.isNotNull());
final result = await query.getSingleOrNull();
return result != null;
}
/// Changes the title of the match with the given [matchId] to [newName].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchName({
required String matchId,
required String newName,
}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
MatchTableCompanion(name: Value(newName)),
);
return rowsAffected > 0;
}
}

View File

@@ -0,0 +1,8 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'match_dao.dart';
// ignore_for_file: type=lint
mixin _$MatchDaoMixin on DatabaseAccessor<AppDatabase> {
$MatchTableTable get matchTable => attachedDatabase.matchTable;
}

View File

@@ -1,128 +0,0 @@
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);
/// 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),
mode: InsertMode.insertOrReplace,
);
}
/// Retrieves a list of [Player]s associated with the given [gameId].
/// Returns null if no players are found.
Future<List<Player>?> getPlayersOfGame({required String gameId}) async {
final result = await (select(
playerGameTable,
)..where((p) => p.gameId.equals(gameId))).get();
if (result.isEmpty) return null;
final futures = result.map(
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
);
final players = await Future.wait(futures);
return players;
}
/// 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;
}
/// Checks if a specific player is associated with a specific game.
/// Returns `true` if the player is in the game, otherwise `false`.
Future<bool> isPlayerInGame({
required String gameId,
required String playerId,
}) async {
final count =
await (selectOnly(playerGameTable)
..where(playerGameTable.gameId.equals(gameId))
..where(playerGameTable.playerId.equals(playerId))
..addColumns([playerGameTable.playerId.count()]))
.map((row) => row.read(playerGameTable.playerId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Removes the association of a player with a game by deleting the record
/// from the [PlayerGameTable].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removePlayerFromGame({
required String gameId,
required String playerId,
}) async {
final query = delete(playerGameTable)
..where((pg) => pg.gameId.equals(gameId))
..where((pg) => pg.playerId.equals(playerId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Updates the players associated with a game based on the provided
/// [newPlayer] list. It adds new players and removes players that are no
/// longer associated with the game.
Future<void> updatePlayersFromGame({
required String gameId,
required List<Player> newPlayer,
}) async {
final currentPlayers = await getPlayersOfGame(gameId: gameId);
// Create sets of player IDs for easy comparison
final currentPlayerIds = currentPlayers?.map((p) => p.id).toSet() ?? {};
final newPlayerIdsSet = newPlayer.map((p) => p.id).toSet();
// Determine players to add and remove
final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds);
final playersToRemove = currentPlayerIds.difference(newPlayerIdsSet);
db.transaction(() async {
// Remove old players
if (playersToRemove.isNotEmpty) {
await (delete(playerGameTable)..where(
(pg) =>
pg.gameId.equals(gameId) &
pg.playerId.isIn(playersToRemove.toList()),
))
.go();
}
// Add new players
if (playersToAdd.isNotEmpty) {
final inserts = playersToAdd
.map(
(id) =>
PlayerGameTableCompanion.insert(playerId: id, gameId: gameId),
)
.toList();
await Future.wait(
inserts.map(
(c) => into(
playerGameTable,
).insert(c, mode: InsertMode.insertOrReplace),
),
);
}
});
}
}

View File

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

@@ -1,11 +1,12 @@
import 'package:drift/drift.dart'; 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/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/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
part 'player_group_dao.g.dart'; part 'player_group_dao.g.dart';
@DriftAccessor(tables: [PlayerGroupTable]) @DriftAccessor(tables: [PlayerGroupTable, PlayerTable])
class PlayerGroupDao extends DatabaseAccessor<AppDatabase> class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
with _$PlayerGroupDaoMixin { with _$PlayerGroupDaoMixin {
PlayerGroupDao(super.db); PlayerGroupDao(super.db);

View File

@@ -0,0 +1,130 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/player_match_table.dart';
import 'package:game_tracker/data/dto/player.dart';
part 'player_match_dao.g.dart';
@DriftAccessor(tables: [PlayerMatchTable])
class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
with _$PlayerMatchDaoMixin {
PlayerMatchDao(super.db);
/// Associates a player with a match by inserting a record into the
/// [PlayerMatchTable].
Future<void> addPlayerToMatch({
required String matchId,
required String playerId,
}) async {
await into(playerMatchTable).insert(
PlayerMatchTableCompanion.insert(playerId: playerId, matchId: matchId),
mode: InsertMode.insertOrIgnore,
);
}
/// Retrieves a list of [Player]s associated with the given [matchId].
/// Returns null if no players are found.
Future<List<Player>?> getPlayersOfMatch({required String matchId}) async {
final result = await (select(
playerMatchTable,
)..where((p) => p.matchId.equals(matchId))).get();
if (result.isEmpty) return null;
final futures = result.map(
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
);
final players = await Future.wait(futures);
return players;
}
/// Checks if there are any players associated with the given [matchId].
/// Returns `true` if there are players, otherwise `false`.
Future<bool> matchHasPlayers({required String matchId}) async {
final count =
await (selectOnly(playerMatchTable)
..where(playerMatchTable.matchId.equals(matchId))
..addColumns([playerMatchTable.playerId.count()]))
.map((row) => row.read(playerMatchTable.playerId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Checks if a specific player is associated with a specific match.
/// Returns `true` if the player is in the match, otherwise `false`.
Future<bool> isPlayerInMatch({
required String matchId,
required String playerId,
}) async {
final count =
await (selectOnly(playerMatchTable)
..where(playerMatchTable.matchId.equals(matchId))
..where(playerMatchTable.playerId.equals(playerId))
..addColumns([playerMatchTable.playerId.count()]))
.map((row) => row.read(playerMatchTable.playerId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Removes the association of a player with a match by deleting the record
/// from the [PlayerMatchTable].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removePlayerFromMatch({
required String matchId,
required String playerId,
}) async {
final query = delete(playerMatchTable)
..where((pg) => pg.matchId.equals(matchId))
..where((pg) => pg.playerId.equals(playerId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Updates the players associated with a match based on the provided
/// [newPlayer] list. It adds new players and removes players that are no
/// longer associated with the match.
Future<void> updatePlayersFromMatch({
required String matchId,
required List<Player> newPlayer,
}) async {
final currentPlayers = await getPlayersOfMatch(matchId: matchId);
// Create sets of player IDs for easy comparison
final currentPlayerIds = currentPlayers?.map((p) => p.id).toSet() ?? {};
final newPlayerIdsSet = newPlayer.map((p) => p.id).toSet();
// Determine players to add and remove
final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds);
final playersToRemove = currentPlayerIds.difference(newPlayerIdsSet);
db.transaction(() async {
// Remove old players
if (playersToRemove.isNotEmpty) {
await (delete(playerMatchTable)..where(
(pg) =>
pg.matchId.equals(matchId) &
pg.playerId.isIn(playersToRemove.toList()),
))
.go();
}
// Add new players
if (playersToAdd.isNotEmpty) {
final inserts = playersToAdd
.map(
(id) => PlayerMatchTableCompanion.insert(
playerId: id,
matchId: matchId,
),
)
.toList();
await Future.wait(
inserts.map(
(c) => into(
playerMatchTable,
).insert(c, mode: InsertMode.insertOrIgnore),
),
);
}
});
}
}

View File

@@ -0,0 +1,11 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'player_match_dao.dart';
// ignore_for_file: type=lint
mixin _$PlayerMatchDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$PlayerMatchTableTable get playerMatchTable =>
attachedDatabase.playerMatchTable;
}

View File

@@ -1,16 +1,16 @@
import 'package:drift/drift.dart'; 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/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/group_match_dao.dart';
import 'package:game_tracker/data/dao/match_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/dao/player_match_dao.dart';
import 'package:game_tracker/data/db/tables/group_game_table.dart'; import 'package:game_tracker/data/db/tables/group_match_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/match_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_match_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';
@@ -20,18 +20,18 @@ part 'database.g.dart';
tables: [ tables: [
PlayerTable, PlayerTable,
GroupTable, GroupTable,
GameTable, MatchTable,
PlayerGroupTable, PlayerGroupTable,
PlayerGameTable, PlayerMatchTable,
GroupGameTable, GroupMatchTable,
], ],
daos: [ daos: [
PlayerDao, PlayerDao,
GroupDao, GroupDao,
GameDao, MatchDao,
PlayerGroupDao, PlayerGroupDao,
PlayerGameDao, PlayerMatchDao,
GroupGameDao, GroupMatchDao,
], ],
) )
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
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/group_table.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
class GroupMatchTable extends Table {
TextColumn get groupId =>
text().references(GroupTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
@override
Set<Column<Object>> get primaryKey => {groupId, matchId};
}

View File

@@ -1,6 +1,6 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
class GameTable extends Table { class MatchTable extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get name => text()(); TextColumn get name => text()();
late final winnerId = text().nullable()(); late final winnerId = text().nullable()();

View File

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

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

View File

@@ -3,7 +3,7 @@ import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class Game { class Match {
final String id; final String id;
final DateTime createdAt; final DateTime createdAt;
final String name; final String name;
@@ -11,7 +11,7 @@ class Game {
final Group? group; final Group? group;
final Player? winner; final Player? winner;
Game({ Match({
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
required this.name, required this.name,
@@ -23,11 +23,11 @@ class Game {
@override @override
String toString() { String toString() {
return 'Game{\n\tid: $id,\n\tname: $name,\n\tplayers: $players,\n\tgroup: $group,\n\twinner: $winner\n}'; return 'Match{\n\tid: $id,\n\tname: $name,\n\tplayers: $players,\n\tgroup: $group,\n\twinner: $winner\n}';
} }
/// Creates a Game instance from a JSON object. /// Creates a Match instance from a JSON object.
Game.fromJson(Map<String, dynamic> json) Match.fromJson(Map<String, dynamic> json)
: id = json['id'], : id = json['id'],
name = json['name'], name = json['name'],
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
@@ -39,7 +39,7 @@ class Game {
group = json['group'] != null ? Group.fromJson(json['group']) : null, group = json['group'] != null ? Group.fromJson(json['group']) : null,
winner = json['winner'] != null ? Player.fromJson(json['winner']) : null; winner = json['winner'] != null ? Player.fromJson(json['winner']) : null;
/// Converts the Game instance to a JSON object. /// Converts the Match instance to a JSON object.
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),

View File

@@ -1,66 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
class ChooseGroupView extends StatefulWidget {
final List<Group> groups;
final int initialGroupIndex;
const ChooseGroupView({
super.key,
required this.groups,
required this.initialGroupIndex,
});
@override
State<ChooseGroupView> createState() => _ChooseGroupViewState();
}
class _ChooseGroupViewState extends State<ChooseGroupView> {
late int selectedGroupIndex;
@override
void initState() {
selectedGroupIndex = widget.initialGroupIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
title: const Text(
'Choose Group',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: widget.groups.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
setState(() {
selectedGroupIndex = index;
});
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
Navigator.of(context).pop(widget.groups[index]);
});
},
child: GroupTile(
group: widget.groups[index],
isHighlighted: selectedGroupIndex == index,
),
);
},
),
);
}
}

View File

@@ -1,120 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/presentation/widgets/tiles/ruleset_list_tile.dart';
class ChooseRulesetView extends StatefulWidget {
final List<(Ruleset, String, String)> rulesets;
final int initialRulesetIndex;
const ChooseRulesetView({
super.key,
required this.rulesets,
required this.initialRulesetIndex,
});
@override
State<ChooseRulesetView> createState() => _ChooseRulesetViewState();
}
class _ChooseRulesetViewState extends State<ChooseRulesetView> {
late int selectedRulesetIndex;
@override
void initState() {
selectedRulesetIndex = widget.initialRulesetIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
initialIndex: 0,
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
title: const Text(
'Choose Ruleset',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: Column(
children: [
Container(
color: CustomTheme.backgroundColor,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: TabBar(
padding: const EdgeInsets.symmetric(horizontal: 5),
// Label Settings
labelStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
labelColor: Colors.white,
unselectedLabelStyle: const TextStyle(fontSize: 14),
unselectedLabelColor: Colors.white70,
// Indicator Settings
indicator: CustomTheme.standardBoxDecoration,
indicatorSize: TabBarIndicatorSize.tab,
indicatorWeight: 1,
indicatorPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 0,
),
// Divider Settings
dividerHeight: 0,
tabs: const [
Tab(text: 'Rulesets'),
Tab(text: 'Gametypes'),
],
),
),
const Divider(
indent: 30,
endIndent: 30,
thickness: 3,
radius: BorderRadius.all(Radius.circular(12)),
),
Expanded(
child: TabBarView(
children: [
ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: widget.rulesets.length,
itemBuilder: (BuildContext context, int index) {
return RulesetListTile(
onPressed: () async {
setState(() {
selectedRulesetIndex = index;
});
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
Navigator.of(
context,
).pop(widget.rulesets[index].$1);
});
},
title: widget.rulesets[index].$2,
description: widget.rulesets[index].$3,
isHighlighted: selectedRulesetIndex == index,
);
},
),
const Center(
child: Text(
'No gametypes available',
style: TextStyle(color: Colors.white70),
),
),
],
),
),
],
),
),
);
}
}

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/presentation/views/main_menu/game_history_view.dart'; import 'package:game_tracker/presentation/views/main_menu/group_view/groups_view.dart';
import 'package:game_tracker/presentation/views/main_menu/groups_view.dart';
import 'package:game_tracker/presentation/views/main_menu/home_view.dart'; import 'package:game_tracker/presentation/views/main_menu/home_view.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/match_view.dart';
import 'package:game_tracker/presentation/views/main_menu/settings_view.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view.dart';
import 'package:game_tracker/presentation/views/main_menu/statistics_view.dart'; import 'package:game_tracker/presentation/views/main_menu/statistics_view.dart';
import 'package:game_tracker/presentation/widgets/navbar_item.dart'; import 'package:game_tracker/presentation/widgets/navbar_item.dart';
@@ -30,8 +30,8 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
final List<Widget> tabs = [ final List<Widget> tabs = [
KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()), KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()),
KeyedSubtree( KeyedSubtree(
key: ValueKey('games_$tabKeyCount'), key: ValueKey('matches_$tabKeyCount'),
child: const GameHistoryView(), child: const MatchView(),
), ),
KeyedSubtree( KeyedSubtree(
key: ValueKey('groups_$tabKeyCount'), key: ValueKey('groups_$tabKeyCount'),
@@ -96,7 +96,7 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
index: 1, index: 1,
isSelected: currentIndex == 1, isSelected: currentIndex == 1,
icon: Icons.gamepad_rounded, icon: Icons.gamepad_rounded,
label: 'Games', label: 'Matches',
onTabTapped: onTabTapped, onTabTapped: onTabTapped,
), ),
NavbarItem( NavbarItem(
@@ -133,7 +133,7 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
case 0: case 0:
return 'Home'; return 'Home';
case 1: case 1:
return 'Game History'; return 'Matches';
case 2: case 2:
return 'Groups'; return 'Groups';
case 3: case 3:

View File

@@ -1,150 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.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';
import 'package:game_tracker/presentation/views/main_menu/create_group_view.dart';
import 'package:game_tracker/presentation/views/main_menu/game_result_view.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/game_history_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart';
class GameHistoryView extends StatefulWidget {
const GameHistoryView({super.key});
@override
State<GameHistoryView> createState() => _GameHistoryViewState();
}
class _GameHistoryViewState extends State<GameHistoryView> {
late Future<List<Game>> _gameListFuture;
late final AppDatabase db;
late final List<Game> skeletonData = List.filled(
4,
Game(
name: 'Skeleton Game',
group: Group(
name: 'Skeleton Group',
members: [
Player(name: 'Player 1'),
Player(name: 'Player 2'),
Player(name: 'Player 3'),
Player(name: 'Long Name Player 4'),
Player(name: 'Player 5'),
],
),
winner: Player(name: 'Skeleton Player 1'),
players: [Player(name: 'Skeleton Player 6')],
),
);
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_gameListFuture = Future.delayed(
const Duration(milliseconds: 250),
() => db.gameDao.getAllGames(),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
body: Stack(
alignment: Alignment.center,
children: [
FutureBuilder<List<Game>>(
future: _gameListFuture,
builder:
(BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
if (snapshot.hasError) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'Game data could not be loaded',
),
);
}
if (snapshot.connectionState == ConnectionState.done &&
(!snapshot.hasData || snapshot.data!.isEmpty)) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Info',
message: 'No games created yet',
),
);
}
final bool isLoading =
snapshot.connectionState == ConnectionState.waiting;
final List<Game> games =
(isLoading ? skeletonData : (snapshot.data ?? [])
..sort(
(a, b) => b.createdAt.compareTo(a.createdAt),
))
.toList();
return AppSkeleton(
enabled: isLoading,
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: games.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == games.length) {
return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 80,
);
}
return GameHistoryTile(
onTap: () async {
await Navigator.push(
context,
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) =>
GameResultView(game: games[index]),
),
);
setState(() {
_gameListFuture = db.gameDao.getAllGames();
});
},
game: games[index],
);
},
),
);
},
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: CustomWidthButton(
text: 'Create Game',
sizeRelativeToWidth: 0.90,
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const CreateGroupView();
},
),
);
setState(() {
_gameListFuture = db.gameDao.getAllGames();
});
},
),
),
],
),
);
}
}

View File

@@ -66,7 +66,9 @@ class _CreateGroupViewState extends State<CreateGroupView> {
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
onChanged: (value) { onChanged: (value) {
setState(() {
selectedPlayers = [...value]; selectedPlayers = [...value];
});
}, },
), ),
), ),
@@ -75,7 +77,8 @@ class _CreateGroupViewState extends State<CreateGroupView> {
sizeRelativeToWidth: 0.95, sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary, buttonType: ButtonType.primary,
onPressed: onPressed:
(_groupNameController.text.isEmpty || selectedPlayers.isEmpty) (_groupNameController.text.isEmpty ||
(selectedPlayers.length < 2))
? null ? null
: () async { : () async {
bool success = await db.groupDao.addGroup( bool success = await db.groupDao.addGroup(

View File

@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.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';
import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart';
class GroupsView extends StatefulWidget {
const GroupsView({super.key});
@override
State<GroupsView> createState() => _GroupsViewState();
}
class _GroupsViewState extends State<GroupsView> {
late final AppDatabase db;
late List<Group> loadedGroups;
bool isLoading = true;
List<Group> groups = List.filled(
7,
Group(
name: 'Skeleton Group',
members: List.filled(6, Player(name: 'Skeleton Player')),
),
);
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
loadGroups();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
body: Stack(
alignment: Alignment.center,
children: [
AppSkeleton(
enabled: isLoading,
child: Visibility(
visible: groups.isNotEmpty,
replacement: const Center(
child: TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'No groups created yet',
),
),
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: groups.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == groups.length) {
return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 20,
);
}
return GroupTile(group: groups[index]);
},
),
),
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: CustomWidthButton(
text: 'Create Group',
sizeRelativeToWidth: 0.90,
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const CreateGroupView();
},
),
);
setState(() {
loadGroups();
});
},
),
),
],
),
);
}
void loadGroups() {
Future.wait([
db.groupDao.getAllGroups(),
Future.delayed(minimumSkeletonDuration),
]).then((results) {
loadedGroups = results[0] as List<Group>;
setState(() {
groups = loadedGroups
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
});
if (mounted) {
setState(() {
isLoading = false;
});
}
});
}
}

View File

@@ -1,119 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.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';
import 'package:game_tracker/presentation/views/main_menu/create_group_view.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart';
class GroupsView extends StatefulWidget {
const GroupsView({super.key});
@override
State<GroupsView> createState() => _GroupsViewState();
}
class _GroupsViewState extends State<GroupsView> {
late Future<List<Group>> _allGroupsFuture;
late final AppDatabase db;
final player = Player(name: 'Skeleton Player');
late final List<Group> skeletonData = List.filled(
7,
Group(
name: 'Skeleton Game',
members: [player, player, player, player, player, player],
),
);
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_allGroupsFuture = Future.delayed(
const Duration(milliseconds: 250),
() => db.groupDao.getAllGroups(),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
body: Stack(
alignment: Alignment.center,
children: [
FutureBuilder<List<Group>>(
future: _allGroupsFuture,
builder:
(BuildContext context, AsyncSnapshot<List<Group>> snapshot) {
if (snapshot.hasError) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'Group data couldn\'t\nbe loaded',
),
);
}
if (snapshot.connectionState == ConnectionState.done &&
(!snapshot.hasData || snapshot.data!.isEmpty)) {
return const Center(
child: TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'No groups created yet',
),
);
}
final bool isLoading =
snapshot.connectionState == ConnectionState.waiting;
final List<Group> groups =
isLoading ? skeletonData : (snapshot.data ?? [])
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
return AppSkeleton(
enabled: isLoading,
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: groups.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == groups.length) {
return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 20,
);
}
return GroupTile(group: groups[index]);
},
),
);
},
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: CustomWidthButton(
text: 'Create Group',
sizeRelativeToWidth: 0.90,
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const CreateGroupView();
},
),
);
setState(() {
_allGroupsFuture = db.groupDao.getAllGroups();
});
},
),
),
],
),
);
}
}

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/data/db/database.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/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart'; import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart';
@@ -18,15 +19,14 @@ class HomeView extends StatefulWidget {
} }
class _HomeViewState extends State<HomeView> { class _HomeViewState extends State<HomeView> {
late Future<int> _gameCountFuture;
late Future<int> _groupCountFuture;
late Future<List<Game>> _recentGamesFuture;
bool isLoading = true; bool isLoading = true;
int matchCount = 0;
late final List<Game> skeletonData = List.filled( int groupCount = 0;
List<Match> loadedRecentMatches = [];
List<Match> recentMatches = List.filled(
2, 2,
Game( Match(
name: 'Skeleton Game', name: 'Skeleton Match',
group: Group( group: Group(
name: 'Skeleton Group', name: 'Skeleton Group',
members: [ members: [
@@ -39,23 +39,34 @@ class _HomeViewState extends State<HomeView> {
); );
@override @override
initState() { void initState() {
super.initState(); super.initState();
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
_gameCountFuture = db.gameDao.getGameCount(); Future.wait([
_groupCountFuture = db.groupDao.getGroupCount(); db.matchDao.getMatchCount(),
_recentGamesFuture = db.gameDao.getAllGames(); db.groupDao.getGroupCount(),
db.matchDao.getAllMatches(),
Future.wait([_gameCountFuture, _groupCountFuture, _recentGamesFuture]).then( Future.delayed(minimumSkeletonDuration),
(_) async { ]).then((results) {
await Future.delayed(const Duration(milliseconds: 250)); matchCount = results[0] as int;
groupCount = results[1] as int;
loadedRecentMatches = results[2] as List<Match>;
recentMatches =
(loadedRecentMatches
..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2)
.toList();
if (loadedRecentMatches.length < 2) {
recentMatches.add(
Match(name: 'Dummy Match', winner: null, group: null, players: null),
);
}
if (mounted) { if (mounted) {
setState(() { setState(() {
isLoading = false; isLoading = false;
}); });
} }
}, });
);
} }
@override @override
@@ -63,6 +74,7 @@ class _HomeViewState extends State<HomeView> {
return LayoutBuilder( return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) { builder: (BuildContext context, BoxConstraints constraints) {
return AppSkeleton( return AppSkeleton(
fixLayoutBuilder: true,
enabled: isLoading, enabled: isLoading,
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
@@ -71,38 +83,20 @@ class _HomeViewState extends State<HomeView> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
FutureBuilder<int>( QuickInfoTile(
future: _gameCountFuture,
builder: (context, snapshot) {
final int count = (snapshot.hasData)
? snapshot.data!
: 0;
return QuickInfoTile(
width: constraints.maxWidth * 0.45, width: constraints.maxWidth * 0.45,
height: constraints.maxHeight * 0.15, height: constraints.maxHeight * 0.15,
title: 'Games', title: 'Matches',
icon: Icons.groups_rounded, icon: Icons.groups_rounded,
value: count, value: matchCount,
);
},
), ),
SizedBox(width: constraints.maxWidth * 0.05), SizedBox(width: constraints.maxWidth * 0.05),
FutureBuilder<int>( QuickInfoTile(
future: _groupCountFuture,
builder: (context, snapshot) {
final int count =
(snapshot.connectionState == ConnectionState.done &&
snapshot.hasData)
? snapshot.data!
: 0;
return QuickInfoTile(
width: constraints.maxWidth * 0.45, width: constraints.maxWidth * 0.45,
height: constraints.maxHeight * 0.15, height: constraints.maxHeight * 0.15,
title: 'Groups', title: 'Groups',
icon: Icons.groups_rounded, icon: Icons.groups_rounded,
value: count, value: groupCount,
);
},
), ),
], ],
), ),
@@ -110,84 +104,52 @@ class _HomeViewState extends State<HomeView> {
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: InfoTile( child: InfoTile(
width: constraints.maxWidth * 0.95, width: constraints.maxWidth * 0.95,
title: 'Recent Games', title: 'Recent Matches',
icon: Icons.timer, icon: Icons.timer,
content: Padding( content: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0), padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: FutureBuilder( child: Visibility(
future: _recentGamesFuture, visible: !isLoading && loadedRecentMatches.isNotEmpty,
builder: replacement: const Center(
( heightFactor: 12,
BuildContext context, child: Text('No recent games available'),
AsyncSnapshot<List<Game>> snapshot,
) {
if (snapshot.hasError) {
return const Center(
heightFactor: 4,
child: Text(
'Error while loading recent games.',
), ),
); child: Column(
}
final List<Game> games =
(isLoading
? skeletonData
: (snapshot.data ?? [])
..sort(
(a, b) => b.createdAt.compareTo(
a.createdAt,
),
))
.take(2)
.toList();
if (games.isNotEmpty) {
return Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GameTile( MatchTile(
gameTitle: games[0].name, matchTitle: recentMatches[0].name,
gameType: 'Winner', game: 'Winner',
ruleset: 'Ruleset', ruleset: 'Ruleset',
players: _getPlayerText(games[0]), players: _getPlayerText(recentMatches[0]),
winner: games[0].winner == null winner: recentMatches[0].winner == null
? 'Game in progress...' ? 'Match in progress...'
: games[0].winner!.name, : recentMatches[0].winner!.name,
), ),
const Padding( const Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(vertical: 8.0),
vertical: 8.0,
),
child: Divider(), child: Divider(),
), ),
if (games.length > 1) ...[ if (loadedRecentMatches.length > 1) ...[
GameTile( MatchTile(
gameTitle: games[1].name, matchTitle: recentMatches[1].name,
gameType: 'Winner', game: 'Winner',
ruleset: 'Ruleset', ruleset: 'Ruleset',
players: _getPlayerText(games[1]), players: _getPlayerText(recentMatches[1]),
winner: games[1].winner == null winner: recentMatches[1].winner == null
? 'Game in progress...' ? 'Game in progress...'
: games[1].winner!.name, : recentMatches[1].winner!.name,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
] else ...[ ] else ...[
const Center( const Center(
heightFactor: 4, heightFactor: 5.35,
child: Text( child: Text('No second game available'),
'No second game available.',
),
), ),
], ],
], ],
); ),
} else {
return const Center(
heightFactor: 12,
child: Text('No recent games available.'),
);
}
},
), ),
), ),
), ),
@@ -197,7 +159,6 @@ class _HomeViewState extends State<HomeView> {
title: 'Quick Create', title: 'Quick Create',
icon: Icons.add_box_rounded, icon: Icons.add_box_rounded,
content: Column( content: Column(
spacing: 8,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
@@ -249,7 +210,7 @@ class _HomeViewState extends State<HomeView> {
); );
} }
String _getPlayerText(Game game) { String _getPlayerText(Match game) {
if (game.group == null) { if (game.group == null) {
final playerCount = game.players?.length ?? 0; final playerCount = game.players?.length ?? 0;
return '$playerCount Players'; return '$playerCount Players';

View File

@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseGameView extends StatefulWidget {
final List<(String, String, Ruleset)> games;
final int initialGameIndex;
const ChooseGameView({
super.key,
required this.games,
required this.initialGameIndex,
});
@override
State<ChooseGameView> createState() => _ChooseGameViewState();
}
class _ChooseGameViewState extends State<ChooseGameView> {
late int selectedGameIndex;
final TextEditingController searchBarController = TextEditingController();
@override
void initState() {
selectedGameIndex = widget.initialGameIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(selectedGameIndex);
},
),
title: const Text(
'Choose Game',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: CustomSearchBar(
controller: searchBarController,
hintText: 'Game Name',
),
),
const SizedBox(height: 5),
Expanded(
child: ListView.builder(
itemCount: widget.games.length,
itemBuilder: (BuildContext context, int index) {
return TitleDescriptionListTile(
title: widget.games[index].$1,
description: widget.games[index].$2,
badgeText: translateRulesetToString(widget.games[index].$3),
isHighlighted: selectedGameIndex == index,
onPressed: () async {
setState(() {
if (selectedGameIndex == index) {
selectedGameIndex = -1;
} else {
selectedGameIndex = index;
}
});
},
);
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
class ChooseGroupView extends StatefulWidget {
final List<Group> groups;
final String initialGroupId;
const ChooseGroupView({
super.key,
required this.groups,
required this.initialGroupId,
});
@override
State<ChooseGroupView> createState() => _ChooseGroupViewState();
}
class _ChooseGroupViewState extends State<ChooseGroupView> {
late String selectedGroupId;
final TextEditingController controller = TextEditingController();
final String hintText = 'Group Name';
late final List<Group> filteredGroups;
@override
void initState() {
selectedGroupId = widget.initialGroupId;
filteredGroups = [...widget.groups];
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
selectedGroupId == ''
? null
: widget.groups.firstWhere(
(group) => group.id == selectedGroupId,
),
);
},
),
title: const Text(
'Choose Group',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: CustomSearchBar(
controller: controller,
hintText: hintText,
onChanged: (value) {
setState(() {
filterGroups(value);
});
},
),
),
Expanded(
child: Visibility(
visible: filteredGroups.isNotEmpty,
replacement: Visibility(
visible: widget.groups.isNotEmpty,
replacement: const TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'You have no groups created yet',
),
child: const TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'There is no group matching your search',
),
),
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: filteredGroups.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
setState(() {
if (selectedGroupId != filteredGroups[index].id) {
selectedGroupId = filteredGroups[index].id;
} else {
selectedGroupId = '';
}
});
},
child: GroupTile(
group: filteredGroups[index],
isHighlighted:
selectedGroupId == filteredGroups[index].id,
),
);
},
),
),
),
],
),
);
}
/// Filters the groups based on the search query.
/// TODO: Maybe implement also targetting player names?
void filterGroups(String query) {
setState(() {
if (query.isEmpty) {
filteredGroups.clear();
filteredGroups.addAll(widget.groups);
} else {
filteredGroups.clear();
filteredGroups.addAll(
widget.groups.where(
(group) => group.name.toLowerCase().contains(query.toLowerCase()),
),
);
}
});
}
}

View File

@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseRulesetView extends StatefulWidget {
final List<(Ruleset, String)> rulesets;
final int initialRulesetIndex;
const ChooseRulesetView({
super.key,
required this.rulesets,
required this.initialRulesetIndex,
});
@override
State<ChooseRulesetView> createState() => _ChooseRulesetViewState();
}
class _ChooseRulesetViewState extends State<ChooseRulesetView> {
late int selectedRulesetIndex;
@override
void initState() {
selectedRulesetIndex = widget.initialRulesetIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
initialIndex: 0,
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
selectedRulesetIndex == -1
? null
: widget.rulesets[selectedRulesetIndex].$1,
);
},
),
title: const Text(
'Choose Ruleset',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: widget.rulesets.length,
itemBuilder: (BuildContext context, int index) {
return TitleDescriptionListTile(
onPressed: () async {
setState(() {
if (selectedRulesetIndex == index) {
selectedRulesetIndex = -1;
} else {
selectedRulesetIndex = index;
}
});
},
title: translateRulesetToString(widget.rulesets[index].$1),
description: widget.rulesets[index].$2,
isHighlighted: selectedRulesetIndex == index,
);
},
),
),
);
}
}

View File

@@ -1,35 +1,33 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/db/database.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/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/views/main_menu/create_game/choose_group_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_game_view.dart';
import 'package:game_tracker/presentation/views/main_menu/create_game/choose_ruleset_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_group_view.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/player_selection.dart'; import 'package:game_tracker/presentation/widgets/player_selection.dart';
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class CreateGameView extends StatefulWidget { class CreateMatchView extends StatefulWidget {
const CreateGameView({super.key}); final VoidCallback? onWinnerChanged;
const CreateMatchView({super.key, this.onWinnerChanged});
@override @override
State<CreateGameView> createState() => _CreateGameViewState(); State<CreateMatchView> createState() => _CreateMatchViewState();
} }
class _CreateGameViewState extends State<CreateGameView> { class _CreateMatchViewState extends State<CreateMatchView> {
/// Reference to the app database /// Reference to the app database
late final AppDatabase db; late final AppDatabase db;
/// Futures to load all groups and players from the database
late Future<List<Group>> _allGroupsFuture;
/// Future to load all players from the database
late Future<List<Player>> _allPlayersFuture;
/// Controller for the game name input field /// Controller for the game name input field
final TextEditingController _gameNameController = TextEditingController(); final TextEditingController _gameNameController = TextEditingController();
@@ -39,12 +37,18 @@ class _CreateGameViewState extends State<CreateGameView> {
/// List of all players from the database /// List of all players from the database
List<Player> playerList = []; List<Player> playerList = [];
/// List of players filtered based on the selected group
/// If a group is selected, this list contains all players from [playerList]
/// who are not members of the selected group. If no group is selected,
/// this list is identical to [playerList].
List<Player> filteredPlayerList = [];
/// The currently selected group /// The currently selected group
Group? selectedGroup; Group? selectedGroup;
/// The index of the currently selected group in [groupsList] to mark it in /// The index of the currently selected group in [groupsList] to mark it in
/// the [ChooseGroupView] /// the [ChooseGroupView]
int selectedGroupIndex = -1; String selectedGroupId = '';
/// The currently selected ruleset /// The currently selected ruleset
Ruleset? selectedRuleset; Ruleset? selectedRuleset;
@@ -53,46 +57,58 @@ class _CreateGameViewState extends State<CreateGameView> {
/// the [ChooseRulesetView] /// the [ChooseRulesetView]
int selectedRulesetIndex = -1; int selectedRulesetIndex = -1;
/// The index of the currently selected game in [games] to mark it in
/// the [ChooseGameView]
int selectedGameIndex = -1;
/// The currently selected players /// The currently selected players
List<Player>? selectedPlayers; List<Player>? selectedPlayers;
/// List of available rulesets with their display names and descriptions /// List of available rulesets with their descriptions
/// as tuples of (Ruleset, String, String) /// as tuples of (Ruleset, String)
List<(Ruleset, String, String)> rulesets = [ /// TODO: Replace when rulesets are implemented
List<(Ruleset, String)> rulesets = [
( (
Ruleset.singleWinner, Ruleset.singleWinner,
'Single Winner',
'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.', 'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.',
), ),
( (
Ruleset.singleLoser, Ruleset.singleLoser,
'Single Loser',
'Exactly one loser is determined; last place receives the penalty or consequence.', 'Exactly one loser is determined; last place receives the penalty or consequence.',
), ),
( (
Ruleset.mostPoints, Ruleset.mostPoints,
'Most Points',
'Traditional ruleset: the player with the most points wins.', 'Traditional ruleset: the player with the most points wins.',
), ),
( (
Ruleset.lastPoints, Ruleset.leastPoints,
'Least Points',
'Inverse scoring: the player with the fewest points wins.', 'Inverse scoring: the player with the fewest points wins.',
), ),
]; ];
// TODO: Replace when games are implemented
List<(String, String, Ruleset)> games = [
('Example Game 1', 'This is a discription', Ruleset.leastPoints),
('Example Game 2', '', Ruleset.singleWinner),
];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_gameNameController.addListener(() {
setState(() {});
});
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
_allGroupsFuture = db.groupDao.getAllGroups(); Future.wait([
_allPlayersFuture = db.playerDao.getAllPlayers(); db.groupDao.getAllGroups(),
db.playerDao.getAllPlayers(),
Future.wait([_allGroupsFuture, _allPlayersFuture]).then((result) async { ]).then((result) async {
groupsList = result[0] as List<Group>; groupsList = result[0] as List<Group>;
playerList = result[1] as List<Player>; playerList = result[1] as List<Player>;
}); });
filteredPlayerList = List.from(playerList);
} }
@override @override
@@ -113,15 +129,38 @@ class _CreateGameViewState extends State<CreateGameView> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
child: TextInputField( child: TextInputField(
controller: _gameNameController, controller: _gameNameController,
hintText: 'Game name', hintText: 'Game name',
onChanged: (value) {
setState(() {});
},
), ),
), ),
ChooseTile(
title: 'Game',
trailingText: selectedGameIndex == -1
? 'None'
: games[selectedGameIndex].$1,
onPressed: () async {
selectedGameIndex = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChooseGameView(
games: games,
initialGameIndex: selectedGameIndex,
),
),
);
setState(() {
if (selectedGameIndex != -1) {
selectedRuleset = games[selectedGameIndex].$3;
selectedRulesetIndex = rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
} else {
selectedRuleset = null;
}
});
},
),
ChooseTile( ChooseTile(
title: 'Ruleset', title: 'Ruleset',
trailingText: selectedRuleset == null trailingText: selectedRuleset == null
@@ -139,6 +178,7 @@ class _CreateGameViewState extends State<CreateGameView> {
selectedRulesetIndex = rulesets.indexWhere( selectedRulesetIndex = rulesets.indexWhere(
(r) => r.$1 == selectedRuleset, (r) => r.$1 == selectedRuleset,
); );
selectedGameIndex = -1;
setState(() {}); setState(() {});
}, },
), ),
@@ -152,28 +192,28 @@ class _CreateGameViewState extends State<CreateGameView> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ChooseGroupView( builder: (context) => ChooseGroupView(
groups: groupsList, groups: groupsList,
initialGroupIndex: selectedGroupIndex, initialGroupId: selectedGroupId,
), ),
), ),
); );
selectedGroupIndex = groupsList.indexWhere( selectedGroupId = selectedGroup?.id ?? '';
(g) => g.id == selectedGroup?.id, if (selectedGroup != null) {
); filteredPlayerList = playerList
.where(
(p) => !selectedGroup!.members.any((m) => m.id == p.id),
)
.toList();
} else {
filteredPlayerList = List.from(playerList);
}
setState(() {}); setState(() {});
}, },
), ),
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'), key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialPlayers: selectedGroup == null initialSelectedPlayers: selectedPlayers ?? [],
? playerList availablePlayers: filteredPlayerList,
: playerList
.where(
(p) => !selectedGroup!.members.any(
(m) => m.id == p.id,
),
)
.toList(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectedPlayers = value; selectedPlayers = value;
@@ -181,52 +221,46 @@ class _CreateGameViewState extends State<CreateGameView> {
}, },
), ),
), ),
CustomWidthButton( CustomWidthButton(
text: 'Create game', text: 'Create game',
sizeRelativeToWidth: 0.95, sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary, buttonType: ButtonType.primary,
onPressed: _enableCreateGameButton() onPressed: _enableCreateGameButton()
? () async { ? () async {
Game game = Game( Match match = Match(
name: _gameNameController.text.trim(), name: _gameNameController.text.trim(),
createdAt: DateTime.now(), createdAt: DateTime.now(),
group: selectedGroup!, group: selectedGroup,
players: selectedPlayers, players: selectedPlayers,
); );
// TODO: Replace with navigation to GameResultView() await db.matchDao.addMatch(match: match);
print('Created game: $game'); if (context.mounted) {
Navigator.pop(context); Navigator.pushReplacement(
context,
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) => GameResultView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
),
),
);
}
} }
: null, : null,
), ),
const SizedBox(height: 20),
], ],
), ),
), ),
); );
} }
/// Translates a [Ruleset] enum value to its corresponding string representation.
String translateRulesetToString(Ruleset ruleset) {
switch (ruleset) {
case Ruleset.singleWinner:
return 'Single Winner';
case Ruleset.singleLoser:
return 'Single Loser';
case Ruleset.mostPoints:
return 'Most Points';
case Ruleset.lastPoints:
return 'Least Points';
}
}
/// Determines whether the "Create Game" button should be enabled based on /// Determines whether the "Create Game" button should be enabled based on
/// the current state of the input fields. /// the current state of the input fields.
bool _enableCreateGameButton() { bool _enableCreateGameButton() {
return _gameNameController.text.isNotEmpty && return _gameNameController.text.isNotEmpty &&
(selectedGroup != null || (selectedGroup != null ||
(selectedPlayers != null && selectedPlayers!.isNotEmpty)) && (selectedPlayers != null && selectedPlayers!.length > 1)) &&
selectedRuleset != null; selectedRuleset != null;
} }
} }

View File

@@ -1,16 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart'; import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class GameResultView extends StatefulWidget { class GameResultView extends StatefulWidget {
final Game game; final Match match;
const GameResultView({super.key, required this.game}); final VoidCallback? onWinnerChanged;
const GameResultView({super.key, required this.match, this.onWinnerChanged});
@override @override
State<GameResultView> createState() => _GameResultViewState(); State<GameResultView> createState() => _GameResultViewState();
} }
@@ -23,10 +24,10 @@ class _GameResultViewState extends State<GameResultView> {
@override @override
void initState() { void initState() {
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
allPlayers = getAllPlayers(widget.game); allPlayers = getAllPlayers(widget.match);
if (widget.game.winner != null) { if (widget.match.winner != null) {
_selectedPlayer = allPlayers.firstWhere( _selectedPlayer = allPlayers.firstWhere(
(p) => p.id == widget.game.winner!.id, (p) => p.id == widget.match.winner!.id,
); );
} }
super.initState(); super.initState();
@@ -37,10 +38,17 @@ class _GameResultViewState extends State<GameResultView> {
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
widget.onWinnerChanged?.call();
Navigator.of(context).pop();
},
),
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
title: Text( title: Text(
widget.game.name, widget.match.name,
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -124,16 +132,17 @@ class _GameResultViewState extends State<GameResultView> {
Future<void> _handleWinnerSaving() async { Future<void> _handleWinnerSaving() async {
if (_selectedPlayer == null) { if (_selectedPlayer == null) {
await db.gameDao.removeWinner(gameId: widget.game.id); await db.matchDao.removeWinner(matchId: widget.match.id);
} else { } else {
await db.gameDao.setWinner( await db.matchDao.setWinner(
gameId: widget.game.id, matchId: widget.match.id,
winnerId: _selectedPlayer!.id, winnerId: _selectedPlayer!.id,
); );
} }
widget.onWinnerChanged?.call();
} }
List<Player> getAllPlayers(Game game) { List<Player> getAllPlayers(Match game) {
if (game.group == null && game.players != null) { if (game.group == null && game.players != null) {
return [...game.players!]; return [...game.players!];
} else if (game.group != null && game.players != null) { } else if (game.group != null && game.players != null) {

View File

@@ -0,0 +1,132 @@
import 'dart:core' hide Match;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/create_match_view.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/game_history_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart';
class MatchView extends StatefulWidget {
const MatchView({super.key});
@override
State<MatchView> createState() => _MatchViewState();
}
class _MatchViewState extends State<MatchView> {
late final AppDatabase db;
bool isLoading = true;
List<Match> matches = List.filled(
4,
Match(
name: 'Skeleton Gamename',
group: Group(
name: 'Groupname',
members: List.filled(5, Player(name: 'Player')),
),
winner: Player(name: 'Player'),
players: [Player(name: 'Player')],
),
);
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
loadGames();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
body: Stack(
alignment: Alignment.center,
children: [
AppSkeleton(
enabled: isLoading,
child: Visibility(
visible: matches.isNotEmpty,
replacement: const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Info',
message: 'No games created yet',
),
),
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: matches.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == matches.length) {
return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 80,
);
}
return GameHistoryTile(
onTap: () async {
Navigator.push(
context,
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) => GameResultView(
match: matches[index],
onWinnerChanged: loadGames,
),
),
);
},
match: matches[index],
);
},
),
),
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: CustomWidthButton(
text: 'Create Game',
sizeRelativeToWidth: 0.90,
onPressed: () async {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
CreateMatchView(onWinnerChanged: loadGames),
),
);
},
),
),
],
),
);
}
void loadGames() {
Future.wait([
db.matchDao.getAllMatches(),
Future.delayed(minimumSkeletonDuration),
]).then((results) {
if (mounted) {
setState(() {
final loadedMatches = results[0] as List<Match>;
matches = loadedMatches
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
isLoading = false;
});
}
});
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart'; import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart';
@@ -14,10 +15,8 @@ class StatisticsView extends StatefulWidget {
} }
class _StatisticsViewState extends State<StatisticsView> { class _StatisticsViewState extends State<StatisticsView> {
late Future<List<Game>> _gamesFuture;
late Future<List<Player>> _playersFuture;
List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1)); List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1));
List<(String, int)> gameCounts = List.filled(6, ('Skeleton Player', 1)); List<(String, int)> matchCounts = List.filled(6, ('Skeleton Player', 1));
List<(String, double)> winRates = List.filled(6, ('Skeleton Player', 1)); List<(String, double)> winRates = List.filled(6, ('Skeleton Player', 1));
bool isLoading = true; bool isLoading = true;
@@ -25,16 +24,17 @@ class _StatisticsViewState extends State<StatisticsView> {
void initState() { void initState() {
super.initState(); super.initState();
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
_gamesFuture = db.gameDao.getAllGames();
_playersFuture = db.playerDao.getAllPlayers();
Future.wait([_gamesFuture, _playersFuture]).then((results) async { Future.wait([
await Future.delayed(const Duration(milliseconds: 250)); db.matchDao.getAllMatches(),
final games = results[0] as List<Game>; db.playerDao.getAllPlayers(),
Future.delayed(minimumSkeletonDuration),
]).then((results) async {
final matches = results[0] as List<Match>;
final players = results[1] as List<Player>; final players = results[1] as List<Player>;
winCounts = _calculateWinsForAllPlayers(games, players); winCounts = _calculateWinsForAllPlayers(matches, players);
gameCounts = _calculateGameAmountsForAllPlayers(games, players); matchCounts = _calculateMatchAmountsForAllPlayers(matches, players);
winRates = computeWinRatePercent(wins: winCounts, games: gameCounts); winRates = computeWinRatePercent(wins: winCounts, matches: matchCounts);
if (mounted) { if (mounted) {
setState(() { setState(() {
isLoading = false; isLoading = false;
@@ -60,7 +60,7 @@ class _StatisticsViewState extends State<StatisticsView> {
SizedBox(height: constraints.maxHeight * 0.01), SizedBox(height: constraints.maxHeight * 0.01),
StatisticsTile( StatisticsTile(
icon: Icons.sports_score, icon: Icons.sports_score,
title: 'Wins per Player', title: 'Wins',
width: constraints.maxWidth * 0.95, width: constraints.maxWidth * 0.95,
values: winCounts, values: winCounts,
itemCount: 3, itemCount: 3,
@@ -69,7 +69,7 @@ class _StatisticsViewState extends State<StatisticsView> {
SizedBox(height: constraints.maxHeight * 0.02), SizedBox(height: constraints.maxHeight * 0.02),
StatisticsTile( StatisticsTile(
icon: Icons.percent, icon: Icons.percent,
title: 'Winrate per Player', title: 'Winrate',
width: constraints.maxWidth * 0.95, width: constraints.maxWidth * 0.95,
values: winRates, values: winRates,
itemCount: 5, itemCount: 5,
@@ -78,9 +78,9 @@ class _StatisticsViewState extends State<StatisticsView> {
SizedBox(height: constraints.maxHeight * 0.02), SizedBox(height: constraints.maxHeight * 0.02),
StatisticsTile( StatisticsTile(
icon: Icons.casino, icon: Icons.casino,
title: 'Games per Player', title: 'Amount of Matches',
width: constraints.maxWidth * 0.95, width: constraints.maxWidth * 0.95,
values: gameCounts, values: matchCounts,
itemCount: 10, itemCount: 10,
barColor: Colors.green, barColor: Colors.green,
), ),
@@ -97,14 +97,14 @@ class _StatisticsViewState extends State<StatisticsView> {
/// Calculates the number of wins for each player /// Calculates the number of wins for each player
/// and returns a sorted list of tuples (playerName, winCount) /// and returns a sorted list of tuples (playerName, winCount)
List<(String, int)> _calculateWinsForAllPlayers( List<(String, int)> _calculateWinsForAllPlayers(
List<Game> games, List<Match> matches,
List<Player> players, List<Player> players,
) { ) {
List<(String, int)> winCounts = []; List<(String, int)> winCounts = [];
// Getting the winners // Getting the winners
for (var game in games) { for (var match in matches) {
final winner = game.winner; final winner = match.winner;
if (winner != null) { if (winner != null) {
final index = winCounts.indexWhere((entry) => entry.$1 == winner.id); final index = winCounts.indexWhere((entry) => entry.$1 == winner.id);
// -1 means winner not found in winCounts // -1 means winner not found in winCounts
@@ -141,83 +141,83 @@ class _StatisticsViewState extends State<StatisticsView> {
return winCounts; return winCounts;
} }
/// Calculates the number of games played for each player /// Calculates the number of matches played for each player
/// and returns a sorted list of tuples (playerName, gameCount) /// and returns a sorted list of tuples (playerName, matchCount)
List<(String, int)> _calculateGameAmountsForAllPlayers( List<(String, int)> _calculateMatchAmountsForAllPlayers(
List<Game> games, List<Match> matches,
List<Player> players, List<Player> players,
) { ) {
List<(String, int)> gameCounts = []; List<(String, int)> matchCounts = [];
// Counting games for each player // Counting matches for each player
for (var game in games) { for (var match in matches) {
if (game.group != null) { if (match.group != null) {
final members = game.group!.members.map((p) => p.id).toList(); final members = match.group!.members.map((p) => p.id).toList();
for (var playerId in members) { for (var playerId in members) {
final index = gameCounts.indexWhere((entry) => entry.$1 == playerId); final index = matchCounts.indexWhere((entry) => entry.$1 == playerId);
// -1 means player not found in gameCounts // -1 means player not found in matchCounts
if (index != -1) { if (index != -1) {
final current = gameCounts[index].$2; final current = matchCounts[index].$2;
gameCounts[index] = (playerId, current + 1); matchCounts[index] = (playerId, current + 1);
} else { } else {
gameCounts.add((playerId, 1)); matchCounts.add((playerId, 1));
} }
} }
} }
if (game.players != null) { if (match.players != null) {
final members = game.players!.map((p) => p.id).toList(); final members = match.players!.map((p) => p.id).toList();
for (var playerId in members) { for (var playerId in members) {
final index = gameCounts.indexWhere((entry) => entry.$1 == playerId); final index = matchCounts.indexWhere((entry) => entry.$1 == playerId);
// -1 means player not found in gameCounts // -1 means player not found in matchCounts
if (index != -1) { if (index != -1) {
final current = gameCounts[index].$2; final current = matchCounts[index].$2;
gameCounts[index] = (playerId, current + 1); matchCounts[index] = (playerId, current + 1);
} else { } else {
gameCounts.add((playerId, 1)); matchCounts.add((playerId, 1));
} }
} }
} }
} }
// Adding all players with zero games // Adding all players with zero matches
for (var player in players) { for (var player in players) {
final index = gameCounts.indexWhere((entry) => entry.$1 == player.id); final index = matchCounts.indexWhere((entry) => entry.$1 == player.id);
// -1 means player not found in gameCounts // -1 means player not found in matchCounts
if (index == -1) { if (index == -1) {
gameCounts.add((player.id, 0)); matchCounts.add((player.id, 0));
} }
} }
// Replace player IDs with names // Replace player IDs with names
for (int i = 0; i < gameCounts.length; i++) { for (int i = 0; i < matchCounts.length; i++) {
final playerId = gameCounts[i].$1; final playerId = matchCounts[i].$1;
final player = players.firstWhere( final player = players.firstWhere(
(p) => p.id == playerId, (p) => p.id == playerId,
orElse: () => Player(id: playerId, name: 'N.a.'), orElse: () => Player(id: playerId, name: 'N.a.'),
); );
gameCounts[i] = (player.name, gameCounts[i].$2); matchCounts[i] = (player.name, matchCounts[i].$2);
} }
gameCounts.sort((a, b) => b.$2.compareTo(a.$2)); matchCounts.sort((a, b) => b.$2.compareTo(a.$2));
return gameCounts; return matchCounts;
} }
// dart // dart
List<(String, double)> computeWinRatePercent({ List<(String, double)> computeWinRatePercent({
required List<(String, int)> wins, required List<(String, int)> wins,
required List<(String, int)> games, required List<(String, int)> matches,
}) { }) {
final Map<String, int> winsMap = {for (var e in wins) e.$1: e.$2}; final Map<String, int> winsMap = {for (var e in wins) e.$1: e.$2};
final Map<String, int> gamesMap = {for (var e in games) e.$1: e.$2}; final Map<String, int> matchesMap = {for (var e in matches) e.$1: e.$2};
// Get all unique player names // Get all unique player names
final names = {...winsMap.keys, ...gamesMap.keys}; final names = {...winsMap.keys, ...matchesMap.keys};
// Calculate win rates // Calculate win rates
final result = names.map((name) { final result = names.map((name) {
final int w = winsMap[name] ?? 0; final int w = winsMap[name] ?? 0;
final int g = gamesMap[name] ?? 0; final int g = matchesMap[name] ?? 0;
// Calculate percentage and round to 2 decimal places // Calculate percentage and round to 2 decimal places
// Avoid division by zero // Avoid division by zero
final double percent = (g > 0) final double percent = (g > 0)

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
@@ -11,12 +12,14 @@ import 'package:provider/provider.dart';
class PlayerSelection extends StatefulWidget { class PlayerSelection extends StatefulWidget {
final Function(List<Player> value) onChanged; final Function(List<Player> value) onChanged;
final List<Player> initialPlayers; final List<Player> availablePlayers;
final List<Player>? initialSelectedPlayers;
const PlayerSelection({ const PlayerSelection({
super.key, super.key,
required this.onChanged, required this.onChanged,
this.initialPlayers = const [], this.availablePlayers = const [],
this.initialSelectedPlayers,
}); });
@override @override
@@ -27,6 +30,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
List<Player> selectedPlayers = []; List<Player> selectedPlayers = [];
List<Player> suggestedPlayers = []; List<Player> suggestedPlayers = [];
List<Player> allPlayers = []; List<Player> allPlayers = [];
bool isLoading = true;
late final TextEditingController _searchBarController = late final TextEditingController _searchBarController =
TextEditingController(); TextEditingController();
late final AppDatabase db; late final AppDatabase db;
@@ -40,28 +44,45 @@ class _PlayerSelectionState extends State<PlayerSelection> {
void initState() { void initState() {
super.initState(); super.initState();
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
suggestedPlayers = skeletonData;
loadPlayerList(); loadPlayerList();
} }
void loadPlayerList() { void loadPlayerList() {
_allPlayersFuture = Future.delayed( _allPlayersFuture = Future.wait([
const Duration(milliseconds: 250), db.playerDao.getAllPlayers(),
() => db.playerDao.getAllPlayers(), Future.delayed(minimumSkeletonDuration),
); ]).then((results) => results[0] as List<Player>);
suggestedPlayers = skeletonData; if (mounted) {
_allPlayersFuture.then((loadedPlayers) { _allPlayersFuture.then((loadedPlayers) {
setState(() { setState(() {
if (widget.initialPlayers.isNotEmpty) { // If a list of available players is provided, use that list.
allPlayers = [...widget.initialPlayers]; if (widget.availablePlayers.isNotEmpty) {
suggestedPlayers = [...widget.initialPlayers]; widget.availablePlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...widget.availablePlayers];
suggestedPlayers = [...allPlayers];
if (widget.initialSelectedPlayers != null) {
// Ensures that only players available for selection are pre-selected.
selectedPlayers = widget.initialSelectedPlayers!
.where(
(p) => widget.availablePlayers.any(
(available) => available.id == p.id,
),
)
.toList();
}
} else { } else {
// Otherwise, use the loaded players from the database.
loadedPlayers.sort((a, b) => a.name.compareTo(b.name)); loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...loadedPlayers]; allPlayers = [...loadedPlayers];
suggestedPlayers = [...loadedPlayers]; suggestedPlayers = [...loadedPlayers];
} }
isLoading = false;
}); });
}); });
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -112,76 +133,67 @@ class _PlayerSelectionState extends State<PlayerSelection> {
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Wrap( SizedBox(
alignment: WrapAlignment.start, height: 50,
crossAxisAlignment: WrapCrossAlignment.start, child: selectedPlayers.isEmpty
spacing: 8.0, ? const Center(child: Text('No players selected'))
runSpacing: 8.0, : SingleChildScrollView(
children: <Widget>[ scrollDirection: Axis.horizontal,
// Generates a TextIconTile for each selected player. child: Row(
children: [
for (var player in selectedPlayers) for (var player in selectedPlayers)
TextIconTile( Padding(
padding: const EdgeInsets.only(right: 8.0),
child: TextIconTile(
text: player.name, text: player.name,
onIconTap: () { onIconTap: () {
setState(() { setState(() {
// Removes the player from the selection and notifies the parent. // Removes the player from the selection and notifies the parent.
final currentSearch = _searchBarController.text final currentSearch = _searchBarController
.text
.toLowerCase(); .toLowerCase();
selectedPlayers.remove(player); selectedPlayers.remove(player);
widget.onChanged([...selectedPlayers]); widget.onChanged([...selectedPlayers]);
// If the player matches the current search query (or search is empty), // If the player matches the current search query (or search is empty),
// they are added back to the suggestions and the list is re-sorted. // they are added back to the suggestions and the list is re-sorted.
if (currentSearch.isEmpty || if (currentSearch.isEmpty ||
player.name.toLowerCase().contains(currentSearch)) { player.name.toLowerCase().contains(
currentSearch,
)) {
suggestedPlayers.add(player); suggestedPlayers.add(player);
} }
}); });
}, },
), ),
),
], ],
), ),
),
),
const SizedBox(height: 10), const SizedBox(height: 10),
const Text( const Text(
'All players:', 'All players:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
FutureBuilder( /*
future: _allPlayersFuture,
builder: */
(BuildContext context, AsyncSnapshot<List<Player>> snapshot) { Expanded(
if (snapshot.hasError) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'Player data couldn\'t\nbe loaded.',
),
);
}
bool doneLoading =
snapshot.connectionState == ConnectionState.done;
bool snapshotDataEmpty =
!snapshot.hasData || snapshot.data!.isEmpty;
if (doneLoading &&
(snapshotDataEmpty && allPlayers.isEmpty)) {
return const Center(
child: TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'No players created yet.',
),
);
}
final bool isLoading =
snapshot.connectionState == ConnectionState.waiting;
return Expanded(
child: AppSkeleton( child: AppSkeleton(
enabled: isLoading, enabled: isLoading,
child: Visibility( child: Visibility(
visible: visible: suggestedPlayers.isNotEmpty,
(suggestedPlayers.isEmpty && allPlayers.isNotEmpty), replacement: TopCenteredMessage(
replacement: ListView.builder( icon: Icons.info,
title: 'Info',
message: allPlayers.isEmpty
? 'No players created yet'
: (selectedPlayers.length == allPlayers.length)
? 'No more players to add'
: 'No players found with that name',
),
child: ListView.builder(
itemCount: suggestedPlayers.length, itemCount: suggestedPlayers.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return TextIconListTile( return TextIconListTile(
@@ -191,31 +203,18 @@ class _PlayerSelectionState extends State<PlayerSelection> {
if (!selectedPlayers.contains( if (!selectedPlayers.contains(
suggestedPlayers[index], suggestedPlayers[index],
)) { )) {
selectedPlayers.add( selectedPlayers.add(suggestedPlayers[index]);
suggestedPlayers[index],
);
widget.onChanged([...selectedPlayers]); widget.onChanged([...selectedPlayers]);
suggestedPlayers.remove( suggestedPlayers.remove(suggestedPlayers[index]);
suggestedPlayers[index],
);
} }
}); });
}, },
); );
}, },
), ),
child: TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: (selectedPlayers.length == allPlayers.length)
? 'No more players to add.'
: 'No players found with that name.',
), ),
), ),
), ),
);
},
),
], ],
), ),
); );

View File

@@ -30,7 +30,7 @@ class CustomSearchBar extends StatelessWidget {
constraints: constraints:
constraints ?? const BoxConstraints(maxHeight: 45, minHeight: 45), constraints ?? const BoxConstraints(maxHeight: 45, minHeight: 45),
hintText: hintText, hintText: hintText,
onChanged: trailingButtonEnabled ? onChanged : null, onChanged: onChanged,
hintStyle: WidgetStateProperty.all(const TextStyle(fontSize: 16)), hintStyle: WidgetStateProperty.all(const TextStyle(fontSize: 16)),
leading: const Icon(Icons.search), leading: const Icon(Icons.search),
trailing: [ trailing: [

View File

@@ -1,14 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/game.dart'; import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class GameHistoryTile extends StatefulWidget { class GameHistoryTile extends StatefulWidget {
final Game game; final Match match;
final VoidCallback onTap; final VoidCallback onTap;
const GameHistoryTile({super.key, required this.game, required this.onTap}); const GameHistoryTile({super.key, required this.match, required this.onTap});
@override @override
State<GameHistoryTile> createState() => _GameHistoryTileState(); State<GameHistoryTile> createState() => _GameHistoryTileState();
@@ -17,8 +17,8 @@ class GameHistoryTile extends StatefulWidget {
class _GameHistoryTileState extends State<GameHistoryTile> { class _GameHistoryTileState extends State<GameHistoryTile> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final group = widget.game.group; final group = widget.match.group;
final winner = widget.game.winner; final winner = widget.match.winner;
final allPlayers = _getAllPlayers(); final allPlayers = _getAllPlayers();
return GestureDetector( return GestureDetector(
@@ -39,7 +39,7 @@ class _GameHistoryTileState extends State<GameHistoryTile> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
widget.game.name, widget.match.name,
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -48,7 +48,7 @@ class _GameHistoryTileState extends State<GameHistoryTile> {
), ),
), ),
Text( Text(
_formatDate(widget.game.createdAt), _formatDate(widget.match.createdAt),
style: const TextStyle(fontSize: 12, color: Colors.grey), style: const TextStyle(fontSize: 12, color: Colors.grey),
), ),
], ],
@@ -156,8 +156,8 @@ class _GameHistoryTileState extends State<GameHistoryTile> {
final playerIds = <String>{}; final playerIds = <String>{};
// Add players from game.players // Add players from game.players
if (widget.game.players != null) { if (widget.match.players != null) {
for (var player in widget.game.players!) { for (var player in widget.match.players!) {
if (!playerIds.contains(player.id)) { if (!playerIds.contains(player.id)) {
allPlayers.add(player); allPlayers.add(player);
playerIds.add(player.id); playerIds.add(player.id);
@@ -166,8 +166,8 @@ class _GameHistoryTileState extends State<GameHistoryTile> {
} }
// Add players from game.group.players // Add players from game.group.players
if (widget.game.group?.members != null) { if (widget.match.group?.members != null) {
for (var player in widget.game.group!.members) { for (var player in widget.match.group!.members) {
if (!playerIds.contains(player.id)) { if (!playerIds.contains(player.id)) {
allPlayers.add(player); allPlayers.add(player);
playerIds.add(player.id); playerIds.add(player.id);

View File

@@ -2,27 +2,27 @@ import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
class GameTile extends StatefulWidget { class MatchTile extends StatefulWidget {
final String gameTitle; final String matchTitle;
final String gameType; final String game;
final String ruleset; final String ruleset;
final String players; final String players;
final String winner; final String winner;
const GameTile({ const MatchTile({
super.key, super.key,
required this.gameTitle, required this.matchTitle,
required this.gameType, required this.game,
required this.ruleset, required this.ruleset,
required this.players, required this.players,
required this.winner, required this.winner,
}); });
@override @override
State<GameTile> createState() => _GameTileState(); State<MatchTile> createState() => _MatchTileState();
} }
class _GameTileState extends State<GameTile> { class _MatchTileState extends State<MatchTile> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@@ -31,12 +31,12 @@ class _GameTileState extends State<GameTile> {
Row( Row(
children: [ children: [
Text( Text(
widget.gameTitle, widget.matchTitle,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
widget.gameType, widget.game,
style: const TextStyle(fontSize: 14, color: Colors.grey), style: const TextStyle(fontSize: 14, color: Colors.grey),
), ),
], ],

View File

@@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class RulesetListTile extends StatelessWidget {
final String title;
final String description;
final VoidCallback? onPressed;
final bool isHighlighted;
const RulesetListTile({
super.key,
required this.title,
required this.description,
this.onPressed,
this.isHighlighted = false,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
child: AnimatedContainer(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
decoration: isHighlighted
? CustomTheme.highlightedBoxDecoration
: CustomTheme.standardBoxDecoration,
duration: const Duration(milliseconds: 200),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
title,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
],
),
const SizedBox(height: 5),
Text(description, style: const TextStyle(fontSize: 14)),
const SizedBox(height: 2.5),
],
),
),
);
}
}

View File

@@ -40,11 +40,11 @@ class StatisticsTile extends StatelessWidget {
child: Column( child: Column(
children: List.generate(min(values.length, itemCount), (index) { children: List.generate(min(values.length, itemCount), (index) {
/// The maximum wins among all players /// The maximum wins among all players
final maxGames = values.isNotEmpty ? values[0].$2 : 0; final maxMatches = values.isNotEmpty ? values[0].$2 : 0;
/// Fraction of wins /// Fraction of wins
final double fraction = (maxGames > 0) final double fraction = (maxMatches > 0)
? (values[index].$2 / maxGames) ? (values[index].$2 / maxMatches)
: 0.0; : 0.0;
/// Calculated width for current the bar /// Calculated width for current the bar

View File

@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class TitleDescriptionListTile extends StatelessWidget {
final String title;
final String description;
final VoidCallback? onPressed;
final bool isHighlighted;
final String? badgeText;
final Color? badgeColor;
const TitleDescriptionListTile({
super.key,
required this.title,
required this.description,
this.onPressed,
this.isHighlighted = false,
this.badgeText,
this.badgeColor,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
child: AnimatedContainer(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
decoration: isHighlighted
? CustomTheme.highlightedBoxDecoration
: CustomTheme.standardBoxDecoration,
duration: const Duration(milliseconds: 200),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 230,
child: Text(
title,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
if (badgeText != null) ...[
const Spacer(),
Container(
constraints: const BoxConstraints(maxWidth: 100),
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.symmetric(
vertical: 2,
horizontal: 6,
),
decoration: BoxDecoration(
color: badgeColor ?? CustomTheme.primaryColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
badgeText!,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
if (description.isNotEmpty) ...[
const SizedBox(height: 5),
Text(description, style: const TextStyle(fontSize: 14)),
const SizedBox(height: 2.5),
],
],
),
),
);
}
}

View File

@@ -6,8 +6,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/db/database.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/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
import 'package:json_schema/json_schema.dart'; import 'package:json_schema/json_schema.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -16,7 +16,7 @@ class DataTransferService {
/// Deletes all data from the database. /// Deletes all data from the database.
static Future<void> deleteAllData(BuildContext context) async { static Future<void> deleteAllData(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
await db.gameDao.deleteAllGames(); await db.matchDao.deleteAllMatches();
await db.groupDao.deleteAllGroups(); await db.groupDao.deleteAllGroups();
await db.playerDao.deleteAllPlayers(); await db.playerDao.deleteAllPlayers();
} }
@@ -25,13 +25,13 @@ class DataTransferService {
/// Returns the JSON string representation of the data. /// Returns the JSON string representation of the data.
static Future<String> getAppDataAsJson(BuildContext context) async { static Future<String> getAppDataAsJson(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
final games = await db.gameDao.getAllGames(); final matches = await db.matchDao.getAllMatches();
final groups = await db.groupDao.getAllGroups(); final groups = await db.groupDao.getAllGroups();
final players = await db.playerDao.getAllPlayers(); final players = await db.playerDao.getAllPlayers();
// Construct a JSON representation of the data // Construct a JSON representation of the data
final Map<String, dynamic> jsonMap = { final Map<String, dynamic> jsonMap = {
'games': games.map((game) => game.toJson()).toList(), 'matches': matches.map((match) => match.toJson()).toList(),
'groups': groups.map((group) => group.toJson()).toList(), 'groups': groups.map((group) => group.toJson()).toList(),
'players': players.map((player) => player.toJson()).toList(), 'players': players.map((player) => player.toJson()).toList(),
}; };
@@ -89,14 +89,15 @@ class DataTransferService {
final Map<String, dynamic> jsonData = final Map<String, dynamic> jsonData =
json.decode(jsonString) as Map<String, dynamic>; json.decode(jsonString) as Map<String, dynamic>;
final List<dynamic>? gamesJson = jsonData['games'] as List<dynamic>?; final List<dynamic>? matchesJson =
jsonData['matches'] as List<dynamic>?;
final List<dynamic>? groupsJson = jsonData['groups'] as List<dynamic>?; final List<dynamic>? groupsJson = jsonData['groups'] as List<dynamic>?;
final List<dynamic>? playersJson = final List<dynamic>? playersJson =
jsonData['players'] as List<dynamic>?; jsonData['players'] as List<dynamic>?;
final List<Game> importedGames = final List<Match> importedMatches =
gamesJson matchesJson
?.map((g) => Game.fromJson(g as Map<String, dynamic>)) ?.map((g) => Match.fromJson(g as Map<String, dynamic>))
.toList() ?? .toList() ??
[]; [];
final List<Group> importedGroups = final List<Group> importedGroups =
@@ -112,7 +113,7 @@ class DataTransferService {
await db.playerDao.addPlayersAsList(players: importedPlayers); await db.playerDao.addPlayersAsList(players: importedPlayers);
await db.groupDao.addGroupsAsList(groups: importedGroups); await db.groupDao.addGroupsAsList(groups: importedGroups);
await db.gameDao.addGamesAsList(games: importedGames); await db.matchDao.addMatchAsList(matches: importedMatches);
} else { } else {
return ImportResult.invalidSchema; return ImportResult.invalidSchema;
} }

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:game_tracker/data/db/database.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/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
void main() { void main() {
@@ -16,14 +16,14 @@ void main() {
late Player testPlayer5; late Player testPlayer5;
late Group testGroup1; late Group testGroup1;
late Group testGroup2; late Group testGroup2;
late Game testGame1; late Match testMatch1;
late Game testGame2; late Match testMatch2;
late Game testGameOnlyPlayers; late Match testMatchOnlyPlayers;
late Game testGameOnlyGroup; late Match testMatchOnlyGroup;
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
final fakeClock = Clock(() => fixedDate); final fakeClock = Clock(() => fixedDate);
setUp(() { setUp(() async {
database = AppDatabase( database = AppDatabase(
DatabaseConnection( DatabaseConnection(
NativeDatabase.memory(), NativeDatabase.memory(),
@@ -46,46 +46,61 @@ void main() {
name: 'Test Group 2', name: 'Test Group 2',
members: [testPlayer4, testPlayer5], members: [testPlayer4, testPlayer5],
); );
testGame1 = Game( testMatch1 = Match(
name: 'First Test Game', name: 'First Test Match',
group: testGroup1, group: testGroup1,
players: [testPlayer4, testPlayer5], players: [testPlayer4, testPlayer5],
winner: testPlayer4, winner: testPlayer4,
); );
testGame2 = Game( testMatch2 = Match(
name: 'Second Test Game', name: 'Second Test Match',
group: testGroup2, group: testGroup2,
players: [testPlayer1, testPlayer2, testPlayer3], players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer2, winner: testPlayer2,
); );
testGameOnlyPlayers = Game( testMatchOnlyPlayers = Match(
name: 'Test Game with Players', name: 'Test Match with Players',
players: [testPlayer1, testPlayer2, testPlayer3], players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer3, winner: testPlayer3,
); );
testGameOnlyGroup = Game(name: 'Test Game with Group', group: testGroup2); testMatchOnlyGroup = Match(
name: 'Test Match with Group',
group: testGroup2,
);
}); });
await database.playerDao.addPlayersAsList(
players: [
testPlayer1,
testPlayer2,
testPlayer3,
testPlayer4,
testPlayer5,
],
);
await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]);
}); });
tearDown(() async { tearDown(() async {
await database.close(); await database.close();
}); });
group('Game Tests', () { group('Match Tests', () {
test('Adding and fetching single game works correctly', () async { test('Adding and fetching single match works correctly', () async {
await database.gameDao.addGame(game: testGame1); await database.matchDao.addMatch(match: testMatch1);
final result = await database.gameDao.getGameById(gameId: testGame1.id); final result = await database.matchDao.getMatchById(
matchId: testMatch1.id,
);
expect(result.id, testGame1.id); expect(result.id, testMatch1.id);
expect(result.name, testGame1.name); expect(result.name, testMatch1.name);
expect(result.createdAt, testGame1.createdAt); expect(result.createdAt, testMatch1.createdAt);
if (result.winner != null && testGame1.winner != null) { if (result.winner != null && testMatch1.winner != null) {
expect(result.winner!.id, testGame1.winner!.id); expect(result.winner!.id, testMatch1.winner!.id);
expect(result.winner!.name, testGame1.winner!.name); expect(result.winner!.name, testMatch1.winner!.name);
expect(result.winner!.createdAt, testGame1.winner!.createdAt); expect(result.winner!.createdAt, testMatch1.winner!.createdAt);
} else { } else {
expect(result.winner, testGame1.winner); expect(result.winner, testMatch1.winner);
} }
if (result.group != null) { if (result.group != null) {
@@ -99,188 +114,203 @@ void main() {
fail('Group is null'); fail('Group is null');
} }
if (result.players != null) { if (result.players != null) {
expect(result.players!.length, testGame1.players!.length); expect(result.players!.length, testMatch1.players!.length);
for (int i = 0; i < testGame1.players!.length; i++) { for (int i = 0; i < testMatch1.players!.length; i++) {
expect(result.players![i].id, testGame1.players![i].id); expect(result.players![i].id, testMatch1.players![i].id);
expect(result.players![i].name, testGame1.players![i].name); expect(result.players![i].name, testMatch1.players![i].name);
expect(result.players![i].createdAt, testGame1.players![i].createdAt); expect(
result.players![i].createdAt,
testMatch1.players![i].createdAt,
);
} }
} else { } else {
fail('Players is null'); fail('Players is null');
} }
}); });
test('Adding and fetching multiple games works correctly', () async { test('Adding and fetching multiple matches works correctly', () async {
await database.gameDao.addGamesAsList( await database.matchDao.addMatchAsList(
games: [testGame1, testGame2, testGameOnlyGroup, testGameOnlyPlayers], matches: [
testMatch1,
testMatch2,
testMatchOnlyGroup,
testMatchOnlyPlayers,
],
); );
final allGames = await database.gameDao.getAllGames(); final allMatches = await database.matchDao.getAllMatches();
expect(allGames.length, 4); expect(allMatches.length, 4);
final testGames = { final testMatches = {
testGame1.id: testGame1, testMatch1.id: testMatch1,
testGame2.id: testGame2, testMatch2.id: testMatch2,
testGameOnlyGroup.id: testGameOnlyGroup, testMatchOnlyGroup.id: testMatchOnlyGroup,
testGameOnlyPlayers.id: testGameOnlyPlayers, testMatchOnlyPlayers.id: testMatchOnlyPlayers,
}; };
for (final game in allGames) { for (final match in allMatches) {
final testGame = testGames[game.id]!; final testMatch = testMatches[match.id]!;
// Game-Checks // Match-Checks
expect(game.id, testGame.id); expect(match.id, testMatch.id);
expect(game.name, testGame.name); expect(match.name, testMatch.name);
expect(game.createdAt, testGame.createdAt); expect(match.createdAt, testMatch.createdAt);
if (game.winner != null && testGame.winner != null) { if (match.winner != null && testMatch.winner != null) {
expect(game.winner!.id, testGame.winner!.id); expect(match.winner!.id, testMatch.winner!.id);
expect(game.winner!.name, testGame.winner!.name); expect(match.winner!.name, testMatch.winner!.name);
expect(game.winner!.createdAt, testGame.winner!.createdAt); expect(match.winner!.createdAt, testMatch.winner!.createdAt);
} else { } else {
expect(game.winner, testGame.winner); expect(match.winner, testMatch.winner);
} }
// Group-Checks // Group-Checks
if (testGame.group != null) { if (testMatch.group != null) {
expect(game.group!.id, testGame.group!.id); expect(match.group!.id, testMatch.group!.id);
expect(game.group!.name, testGame.group!.name); expect(match.group!.name, testMatch.group!.name);
expect(game.group!.createdAt, testGame.group!.createdAt); expect(match.group!.createdAt, testMatch.group!.createdAt);
// Group Members-Checks // Group Members-Checks
expect(game.group!.members.length, testGame.group!.members.length); expect(match.group!.members.length, testMatch.group!.members.length);
for (int i = 0; i < testGame.group!.members.length; i++) { for (int i = 0; i < testMatch.group!.members.length; i++) {
expect(game.group!.members[i].id, testGame.group!.members[i].id); expect(match.group!.members[i].id, testMatch.group!.members[i].id);
expect( expect(
game.group!.members[i].name, match.group!.members[i].name,
testGame.group!.members[i].name, testMatch.group!.members[i].name,
); );
expect( expect(
game.group!.members[i].createdAt, match.group!.members[i].createdAt,
testGame.group!.members[i].createdAt, testMatch.group!.members[i].createdAt,
); );
} }
} else { } else {
expect(game.group, null); expect(match.group, null);
} }
// Players-Checks // Players-Checks
if (testGame.players != null) { if (testMatch.players != null) {
expect(game.players!.length, testGame.players!.length); expect(match.players!.length, testMatch.players!.length);
for (int i = 0; i < testGame.players!.length; i++) { for (int i = 0; i < testMatch.players!.length; i++) {
expect(game.players![i].id, testGame.players![i].id); expect(match.players![i].id, testMatch.players![i].id);
expect(game.players![i].name, testGame.players![i].name); expect(match.players![i].name, testMatch.players![i].name);
expect(game.players![i].createdAt, testGame.players![i].createdAt); expect(
match.players![i].createdAt,
testMatch.players![i].createdAt,
);
} }
} else { } else {
expect(game.players, null); expect(match.players, null);
} }
} }
}); });
test('Adding the same game twice does not create duplicates', () async { test('Adding the same match twice does not create duplicates', () async {
await database.gameDao.addGame(game: testGame1); await database.matchDao.addMatch(match: testMatch1);
await database.gameDao.addGame(game: testGame1); await database.matchDao.addMatch(match: testMatch1);
final gameCount = await database.gameDao.getGameCount(); final matchCount = await database.matchDao.getMatchCount();
expect(gameCount, 1); expect(matchCount, 1);
}); });
test('Game existence check works correctly', () async { test('Match existence check works correctly', () async {
var gameExists = await database.gameDao.gameExists(gameId: testGame1.id); var matchExists = await database.matchDao.matchExists(
expect(gameExists, false); matchId: testMatch1.id,
await database.gameDao.addGame(game: testGame1);
gameExists = await database.gameDao.gameExists(gameId: testGame1.id);
expect(gameExists, true);
});
test('Deleting a game works correctly', () async {
await database.gameDao.addGame(game: testGame1);
final gameDeleted = await database.gameDao.deleteGame(
gameId: testGame1.id,
); );
expect(gameDeleted, true); expect(matchExists, false);
final gameExists = await database.gameDao.gameExists( await database.matchDao.addMatch(match: testMatch1);
gameId: testGame1.id,
matchExists = await database.matchDao.matchExists(matchId: testMatch1.id);
expect(matchExists, true);
});
test('Deleting a match works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final matchDeleted = await database.matchDao.deleteMatch(
matchId: testMatch1.id,
); );
expect(gameExists, false); expect(matchDeleted, true);
final matchExists = await database.matchDao.matchExists(
matchId: testMatch1.id,
);
expect(matchExists, false);
}); });
test('Getting the game count works correctly', () async { test('Getting the match count works correctly', () async {
var gameCount = await database.gameDao.getGameCount(); var matchCount = await database.matchDao.getMatchCount();
expect(gameCount, 0); expect(matchCount, 0);
await database.gameDao.addGame(game: testGame1); await database.matchDao.addMatch(match: testMatch1);
gameCount = await database.gameDao.getGameCount(); matchCount = await database.matchDao.getMatchCount();
expect(gameCount, 1); expect(matchCount, 1);
await database.gameDao.addGame(game: testGame2); await database.matchDao.addMatch(match: testMatch2);
gameCount = await database.gameDao.getGameCount(); matchCount = await database.matchDao.getMatchCount();
expect(gameCount, 2); expect(matchCount, 2);
await database.gameDao.deleteGame(gameId: testGame1.id); await database.matchDao.deleteMatch(matchId: testMatch1.id);
gameCount = await database.gameDao.getGameCount(); matchCount = await database.matchDao.getMatchCount();
expect(gameCount, 1); expect(matchCount, 1);
await database.gameDao.deleteGame(gameId: testGame2.id); await database.matchDao.deleteMatch(matchId: testMatch2.id);
gameCount = await database.gameDao.getGameCount(); matchCount = await database.matchDao.getMatchCount();
expect(gameCount, 0); expect(matchCount, 0);
}); });
test('Checking if game has winner works correclty', () async { test('Checking if match has winner works correctly', () async {
await database.gameDao.addGame(game: testGame1); await database.matchDao.addMatch(match: testMatch1);
await database.gameDao.addGame(game: testGameOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
var hasWinner = await database.gameDao.hasWinner(gameId: testGame1.id); var hasWinner = await database.matchDao.hasWinner(matchId: testMatch1.id);
expect(hasWinner, true); expect(hasWinner, true);
hasWinner = await database.gameDao.hasWinner( hasWinner = await database.matchDao.hasWinner(
gameId: testGameOnlyGroup.id, matchId: testMatchOnlyGroup.id,
); );
expect(hasWinner, false); expect(hasWinner, false);
}); });
test('Fetching the winner of a game works correctly', () async { test('Fetching the winner of a match works correctly', () async {
await database.gameDao.addGame(game: testGame1); await database.matchDao.addMatch(match: testMatch1);
final winner = await database.gameDao.getWinner(gameId: testGame1.id); final winner = await database.matchDao.getWinner(matchId: testMatch1.id);
if (winner == null) { if (winner == null) {
fail('Winner is null'); fail('Winner is null');
} else { } else {
expect(winner.id, testGame1.winner!.id); expect(winner.id, testMatch1.winner!.id);
expect(winner.name, testGame1.winner!.name); expect(winner.name, testMatch1.winner!.name);
expect(winner.createdAt, testGame1.winner!.createdAt); expect(winner.createdAt, testMatch1.winner!.createdAt);
} }
}); });
test('Updating the winner of a game works correctly', () async { test('Updating the winner of a match works correctly', () async {
await database.gameDao.addGame(game: testGame1); await database.matchDao.addMatch(match: testMatch1);
final winner = await database.gameDao.getWinner(gameId: testGame1.id); final winner = await database.matchDao.getWinner(matchId: testMatch1.id);
if (winner == null) { if (winner == null) {
fail('Winner is null'); fail('Winner is null');
} else { } else {
expect(winner.id, testGame1.winner!.id); expect(winner.id, testMatch1.winner!.id);
expect(winner.name, testGame1.winner!.name); expect(winner.name, testMatch1.winner!.name);
expect(winner.createdAt, testGame1.winner!.createdAt); expect(winner.createdAt, testMatch1.winner!.createdAt);
expect(winner.id, testPlayer4.id); expect(winner.id, testPlayer4.id);
expect(winner.id != testPlayer5.id, true); expect(winner.id != testPlayer5.id, true);
} }
await database.gameDao.setWinner( await database.matchDao.setWinner(
gameId: testGame1.id, matchId: testMatch1.id,
winnerId: testPlayer5.id, winnerId: testPlayer5.id,
); );
final newWinner = await database.gameDao.getWinner(gameId: testGame1.id); final newWinner = await database.matchDao.getWinner(
matchId: testMatch1.id,
);
if (newWinner == null) { if (newWinner == null) {
fail('New winner is null'); fail('New winner is null');
@@ -292,39 +322,41 @@ void main() {
}); });
test('Removing a winner works correctly', () async { test('Removing a winner works correctly', () async {
await database.gameDao.addGame(game: testGame2); await database.matchDao.addMatch(match: testMatch2);
var hasWinner = await database.gameDao.hasWinner(gameId: testGame2.id); var hasWinner = await database.matchDao.hasWinner(matchId: testMatch2.id);
expect(hasWinner, true); expect(hasWinner, true);
await database.gameDao.removeWinner(gameId: testGame2.id); await database.matchDao.removeWinner(matchId: testMatch2.id);
hasWinner = await database.gameDao.hasWinner(gameId: testGame2.id); hasWinner = await database.matchDao.hasWinner(matchId: testMatch2.id);
expect(hasWinner, false); expect(hasWinner, false);
final removedWinner = await database.gameDao.getWinner( final removedWinner = await database.matchDao.getWinner(
gameId: testGame2.id, matchId: testMatch2.id,
); );
expect(removedWinner, null); expect(removedWinner, null);
}); });
test('Renaming a game works correctly', () async { test('Renaming a match works correctly', () async {
await database.gameDao.addGame(game: testGame1); await database.matchDao.addMatch(match: testMatch1);
var fetchedGame = await database.gameDao.getGameById( var fetchedMatch = await database.matchDao.getMatchById(
gameId: testGame1.id, matchId: testMatch1.id,
); );
expect(fetchedGame.name, testGame1.name); expect(fetchedMatch.name, testMatch1.name);
const newName = 'Updated Game Name'; const newName = 'Updated Match Name';
await database.gameDao.updateGameName( await database.matchDao.updateMatchName(
gameId: testGame1.id, matchId: testMatch1.id,
newName: newName, newName: newName,
); );
fetchedGame = await database.gameDao.getGameById(gameId: testGame1.id); fetchedMatch = await database.matchDao.getMatchById(
expect(fetchedGame.name, newName); matchId: testMatch1.id,
);
expect(fetchedMatch.name, newName);
}); });
}); });
} }

View File

@@ -1,10 +1,10 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart' hide isNotNull;
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:game_tracker/data/db/database.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/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
void main() { void main() {
@@ -16,12 +16,12 @@ void main() {
late Player testPlayer5; late Player testPlayer5;
late Group testGroup1; late Group testGroup1;
late Group testGroup2; late Group testGroup2;
late Game testgameWithGroup; late Match testMatchWithGroup;
late Game testgameWithPlayers; late Match testMatchWithPlayers;
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
final fakeClock = Clock(() => fixedDate); final fakeClock = Clock(() => fixedDate);
setUp(() { setUp(() async {
database = AppDatabase( database = AppDatabase(
DatabaseConnection( DatabaseConnection(
NativeDatabase.memory(), NativeDatabase.memory(),
@@ -44,81 +44,94 @@ void main() {
name: 'Test Group', name: 'Test Group',
members: [testPlayer3, testPlayer2], members: [testPlayer3, testPlayer2],
); );
testgameWithPlayers = Game( testMatchWithPlayers = Match(
name: 'Test Game with Players', name: 'Test Match with Players',
players: [testPlayer4, testPlayer5], players: [testPlayer4, testPlayer5],
); );
testgameWithGroup = Game(name: 'Test Game with Group', group: testGroup1); testMatchWithGroup = Match(
name: 'Test Match with Group',
group: testGroup1,
);
}); });
await database.playerDao.addPlayersAsList(
players: [
testPlayer1,
testPlayer2,
testPlayer3,
testPlayer4,
testPlayer5,
],
);
await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]);
}); });
tearDown(() async { tearDown(() async {
await database.close(); await database.close();
}); });
group('Group-Game Tests', () { group('Group-Match Tests', () {
test('Game has group works correctly', () async { test('matchHasGroup() has group works correctly', () async {
await database.gameDao.addGame(game: testgameWithPlayers); await database.matchDao.addMatch(match: testMatchWithPlayers);
await database.groupDao.addGroup(group: testGroup1); await database.groupDao.addGroup(group: testGroup1);
var gameHasGroup = await database.groupGameDao.gameHasGroup( var matchHasGroup = await database.groupMatchDao.matchHasGroup(
gameId: testgameWithPlayers.id, matchId: testMatchWithPlayers.id,
); );
expect(gameHasGroup, false); expect(matchHasGroup, false);
await database.groupGameDao.addGroupToGame( await database.groupMatchDao.addGroupToMatch(
gameId: testgameWithPlayers.id, matchId: testMatchWithPlayers.id,
groupId: testGroup1.id, groupId: testGroup1.id,
); );
gameHasGroup = await database.groupGameDao.gameHasGroup( matchHasGroup = await database.groupMatchDao.matchHasGroup(
gameId: testgameWithPlayers.id, matchId: testMatchWithPlayers.id,
); );
expect(gameHasGroup, true); expect(matchHasGroup, true);
}); });
test('Adding a group to a game works correctly', () async { test('Adding a group to a match works correctly', () async {
await database.gameDao.addGame(game: testgameWithPlayers); await database.matchDao.addMatch(match: testMatchWithPlayers);
await database.groupDao.addGroup(group: testGroup1); await database.groupDao.addGroup(group: testGroup1);
await database.groupGameDao.addGroupToGame( await database.groupMatchDao.addGroupToMatch(
gameId: testgameWithPlayers.id, matchId: testMatchWithPlayers.id,
groupId: testGroup1.id, groupId: testGroup1.id,
); );
var groupAdded = await database.groupGameDao.isGroupInGame( var groupAdded = await database.groupMatchDao.isGroupInMatch(
gameId: testgameWithPlayers.id, matchId: testMatchWithPlayers.id,
groupId: testGroup1.id, groupId: testGroup1.id,
); );
expect(groupAdded, true); expect(groupAdded, true);
groupAdded = await database.groupGameDao.isGroupInGame( groupAdded = await database.groupMatchDao.isGroupInMatch(
gameId: testgameWithPlayers.id, matchId: testMatchWithPlayers.id,
groupId: '', groupId: '',
); );
expect(groupAdded, false); expect(groupAdded, false);
}); });
test('Removing group from game works correctly', () async { test('Removing group from match works correctly', () async {
await database.gameDao.addGame(game: testgameWithGroup); await database.matchDao.addMatch(match: testMatchWithGroup);
final groupToRemove = testgameWithGroup.group!; final groupToRemove = testMatchWithGroup.group!;
final removed = await database.groupGameDao.removeGroupFromGame( final removed = await database.groupMatchDao.removeGroupFromMatch(
groupId: groupToRemove.id, groupId: groupToRemove.id,
gameId: testgameWithGroup.id, matchId: testMatchWithGroup.id,
); );
expect(removed, true); expect(removed, true);
final result = await database.gameDao.getGameById( final result = await database.matchDao.getMatchById(
gameId: testgameWithGroup.id, matchId: testMatchWithGroup.id,
); );
expect(result.group, null); expect(result.group, null);
}); });
test('Retrieving group of a game works correctly', () async { test('Retrieving group of a match works correctly', () async {
await database.gameDao.addGame(game: testgameWithGroup); await database.matchDao.addMatch(match: testMatchWithGroup);
final group = await database.groupGameDao.getGroupOfGame( final group = await database.groupMatchDao.getGroupOfMatch(
gameId: testgameWithGroup.id, matchId: testMatchWithGroup.id,
); );
if (group == null) { if (group == null) {
@@ -136,11 +149,11 @@ void main() {
} }
}); });
test('Updating the group of a game works correctly', () async { test('Updating the group of a match works correctly', () async {
await database.gameDao.addGame(game: testgameWithGroup); await database.matchDao.addMatch(match: testMatchWithGroup);
var group = await database.groupGameDao.getGroupOfGame( var group = await database.groupMatchDao.getGroupOfMatch(
gameId: testgameWithGroup.id, matchId: testMatchWithGroup.id,
); );
if (group == null) { if (group == null) {
@@ -153,13 +166,13 @@ void main() {
} }
await database.groupDao.addGroup(group: testGroup2); await database.groupDao.addGroup(group: testGroup2);
await database.groupGameDao.updateGroupOfGame( await database.groupMatchDao.updateGroupOfMatch(
gameId: testgameWithGroup.id, matchId: testMatchWithGroup.id,
newGroupId: testGroup2.id, newGroupId: testGroup2.id,
); );
group = await database.groupGameDao.getGroupOfGame( group = await database.groupMatchDao.getGroupOfMatch(
gameId: testgameWithGroup.id, matchId: testMatchWithGroup.id,
); );
if (group == null) { if (group == null) {
@@ -176,5 +189,33 @@ void main() {
} }
} }
}); });
test('Adding the same group to seperate matches works correctly', () async {
final match1 = Match(name: 'Match 1', group: testGroup1);
final match2 = Match(name: 'Match 2', group: testGroup1);
await Future.wait([
database.matchDao.addMatch(match: match1),
database.matchDao.addMatch(match: match2),
]);
final group1 = await database.groupMatchDao.getGroupOfMatch(
matchId: match1.id,
);
final group2 = await database.groupMatchDao.getGroupOfMatch(
matchId: match2.id,
);
expect(group1, isNotNull);
expect(group2, isNotNull);
final groups = [group1!, group2!];
for (final group in groups) {
expect(group.members.length, testGroup1.members.length);
expect(group.id, testGroup1.id);
expect(group.name, testGroup1.name);
expect(group.createdAt, testGroup1.createdAt);
}
});
}); });
} }

View File

@@ -1,10 +1,10 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart' hide isNotNull;
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:game_tracker/data/db/database.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/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
void main() { void main() {
@@ -16,12 +16,12 @@ void main() {
late Player testPlayer5; late Player testPlayer5;
late Player testPlayer6; late Player testPlayer6;
late Group testgroup; late Group testgroup;
late Game testGameOnlyGroup; late Match testMatchOnlyGroup;
late Game testGameOnlyPlayers; late Match testMatchOnlyPlayers;
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
final fakeClock = Clock(() => fixedDate); final fakeClock = Clock(() => fixedDate);
setUp(() { setUp(() async {
database = AppDatabase( database = AppDatabase(
DatabaseConnection( DatabaseConnection(
NativeDatabase.memory(), NativeDatabase.memory(),
@@ -41,78 +41,92 @@ void main() {
name: 'Test Group', name: 'Test Group',
members: [testPlayer1, testPlayer2, testPlayer3], members: [testPlayer1, testPlayer2, testPlayer3],
); );
testGameOnlyGroup = Game(name: 'Test Game with Group', group: testgroup); testMatchOnlyGroup = Match(
testGameOnlyPlayers = Game( name: 'Test Match with Group',
name: 'Test Game with Players', group: testgroup,
);
testMatchOnlyPlayers = Match(
name: 'Test Match with Players',
players: [testPlayer4, testPlayer5, testPlayer6], players: [testPlayer4, testPlayer5, testPlayer6],
); );
}); });
await database.playerDao.addPlayersAsList(
players: [
testPlayer1,
testPlayer2,
testPlayer3,
testPlayer4,
testPlayer5,
testPlayer6,
],
);
await database.groupDao.addGroup(group: testgroup);
}); });
tearDown(() async { tearDown(() async {
await database.close(); await database.close();
}); });
group('Player-Game Tests', () { group('Player-Match Tests', () {
test('Game has player works correctly', () async { test('Match has player works correctly', () async {
await database.gameDao.addGame(game: testGameOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
var gameHasPlayers = await database.playerGameDao.gameHasPlayers( var matchHasPlayers = await database.playerMatchDao.matchHasPlayers(
gameId: testGameOnlyGroup.id, matchId: testMatchOnlyGroup.id,
); );
expect(gameHasPlayers, false); expect(matchHasPlayers, false);
await database.playerGameDao.addPlayerToGame( await database.playerMatchDao.addPlayerToMatch(
gameId: testGameOnlyGroup.id, matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id, playerId: testPlayer1.id,
); );
gameHasPlayers = await database.playerGameDao.gameHasPlayers( matchHasPlayers = await database.playerMatchDao.matchHasPlayers(
gameId: testGameOnlyGroup.id, matchId: testMatchOnlyGroup.id,
); );
expect(gameHasPlayers, true); expect(matchHasPlayers, true);
}); });
test('Adding a player to a game works correctly', () async { test('Adding a player to a match works correctly', () async {
await database.gameDao.addGame(game: testGameOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerDao.addPlayer(player: testPlayer5); await database.playerDao.addPlayer(player: testPlayer5);
await database.playerGameDao.addPlayerToGame( await database.playerMatchDao.addPlayerToMatch(
gameId: testGameOnlyGroup.id, matchId: testMatchOnlyGroup.id,
playerId: testPlayer5.id, playerId: testPlayer5.id,
); );
var playerAdded = await database.playerGameDao.isPlayerInGame( var playerAdded = await database.playerMatchDao.isPlayerInMatch(
gameId: testGameOnlyGroup.id, matchId: testMatchOnlyGroup.id,
playerId: testPlayer5.id, playerId: testPlayer5.id,
); );
expect(playerAdded, true); expect(playerAdded, true);
playerAdded = await database.playerGameDao.isPlayerInGame( playerAdded = await database.playerMatchDao.isPlayerInMatch(
gameId: testGameOnlyGroup.id, matchId: testMatchOnlyGroup.id,
playerId: '', playerId: '',
); );
expect(playerAdded, false); expect(playerAdded, false);
}); });
test('Removing player from game works correctly', () async { test('Removing player from match works correctly', () async {
await database.gameDao.addGame(game: testGameOnlyPlayers); await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final playerToRemove = testGameOnlyPlayers.players![0]; final playerToRemove = testMatchOnlyPlayers.players![0];
final removed = await database.playerGameDao.removePlayerFromGame( final removed = await database.playerMatchDao.removePlayerFromMatch(
playerId: playerToRemove.id, playerId: playerToRemove.id,
gameId: testGameOnlyPlayers.id, matchId: testMatchOnlyPlayers.id,
); );
expect(removed, true); expect(removed, true);
final result = await database.gameDao.getGameById( final result = await database.matchDao.getMatchById(
gameId: testGameOnlyPlayers.id, matchId: testMatchOnlyPlayers.id,
); );
expect(result.players!.length, testGameOnlyPlayers.players!.length - 1); expect(result.players!.length, testMatchOnlyPlayers.players!.length - 1);
final playerExists = result.players!.any( final playerExists = result.players!.any(
(p) => p.id == playerToRemove.id, (p) => p.id == playerToRemove.id,
@@ -120,10 +134,10 @@ void main() {
expect(playerExists, false); expect(playerExists, false);
}); });
test('Retrieving players of a game works correctly', () async { test('Retrieving players of a match works correctly', () async {
await database.gameDao.addGame(game: testGameOnlyPlayers); await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final players = await database.playerGameDao.getPlayersOfGame( final players = await database.playerMatchDao.getPlayersOfMatch(
gameId: testGameOnlyPlayers.id, matchId: testMatchOnlyPlayers.id,
); );
if (players == null) { if (players == null) {
@@ -131,34 +145,37 @@ void main() {
} }
for (int i = 0; i < players.length; i++) { for (int i = 0; i < players.length; i++) {
expect(players[i].id, testGameOnlyPlayers.players![i].id); expect(players[i].id, testMatchOnlyPlayers.players![i].id);
expect(players[i].name, testGameOnlyPlayers.players![i].name); expect(players[i].name, testMatchOnlyPlayers.players![i].name);
expect(players[i].createdAt, testGameOnlyPlayers.players![i].createdAt); expect(
players[i].createdAt,
testMatchOnlyPlayers.players![i].createdAt,
);
} }
}); });
test('Updating the games players works coreclty', () async { test('Updating the match players works coreclty', () async {
await database.gameDao.addGame(game: testGameOnlyPlayers); await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final newPlayers = [testPlayer1, testPlayer2, testPlayer4]; final newPlayers = [testPlayer1, testPlayer2, testPlayer4];
await database.playerDao.addPlayersAsList(players: newPlayers); await database.playerDao.addPlayersAsList(players: newPlayers);
// First, remove all existing players // First, remove all existing players
final existingPlayers = await database.playerGameDao.getPlayersOfGame( final existingPlayers = await database.playerMatchDao.getPlayersOfMatch(
gameId: testGameOnlyPlayers.id, matchId: testMatchOnlyPlayers.id,
); );
if (existingPlayers == null || existingPlayers.isEmpty) { if (existingPlayers == null || existingPlayers.isEmpty) {
fail('Existing players should not be null or empty'); fail('Existing players should not be null or empty');
} }
await database.playerGameDao.updatePlayersFromGame( await database.playerMatchDao.updatePlayersFromMatch(
gameId: testGameOnlyPlayers.id, matchId: testMatchOnlyPlayers.id,
newPlayer: newPlayers, newPlayer: newPlayers,
); );
final updatedPlayers = await database.playerGameDao.getPlayersOfGame( final updatedPlayers = await database.playerMatchDao.getPlayersOfMatch(
gameId: testGameOnlyPlayers.id, matchId: testMatchOnlyPlayers.id,
); );
if (updatedPlayers == null) { if (updatedPlayers == null) {
@@ -179,5 +196,42 @@ void main() {
expect(player.createdAt, testPlayer.createdAt); expect(player.createdAt, testPlayer.createdAt);
} }
}); });
test(
'Adding the same player to seperate matches works correctly',
() async {
final playersList = [testPlayer1, testPlayer2, testPlayer3];
final match1 = Match(name: 'Match 1', players: playersList);
final match2 = Match(name: 'Match 2', players: playersList);
await Future.wait([
database.matchDao.addMatch(match: match1),
database.matchDao.addMatch(match: match2),
]);
final players1 = await database.playerMatchDao.getPlayersOfMatch(
matchId: match1.id,
);
final players2 = await database.playerMatchDao.getPlayersOfMatch(
matchId: match2.id,
);
expect(players1, isNotNull);
expect(players2, isNotNull);
expect(
players1!.map((p) => p.id).toList(),
equals(players2!.map((p) => p.id).toList()),
);
expect(
players1.map((p) => p.name).toList(),
equals(players2.map((p) => p.name).toList()),
);
expect(
players1.map((p) => p.createdAt).toList(),
equals(players2.map((p) => p.createdAt).toList()),
);
},
);
}); });
} }