diff --git a/.gitea/workflows/pull_request.yaml b/.gitea/workflows/pull_request.yaml index 26f4404..825305b 100644 --- a/.gitea/workflows/pull_request.yaml +++ b/.gitea/workflows/pull_request.yaml @@ -20,11 +20,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Analyze Formatting @@ -46,11 +46,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Run tests diff --git a/.gitea/workflows/push.yaml b/.gitea/workflows/push.yaml index e24f7ad..cfe987a 100644 --- a/.gitea/workflows/push.yaml +++ b/.gitea/workflows/push.yaml @@ -32,11 +32,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Build APK @@ -58,11 +58,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Run tests @@ -118,11 +118,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Generate oss_licenses.dart @@ -161,11 +161,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Check code format diff --git a/assets/schema.json b/assets/schema.json index a3899dc..7f6aebd 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -18,9 +18,6 @@ }, "description": { "type": "string" - }, - "deleted": { - "type": "boolean" } }, "additionalProperties": false, @@ -57,9 +54,6 @@ }, "icon": { "type": "string" - }, - "deleted": { - "type": "boolean" } }, "additionalProperties": false, @@ -96,9 +90,6 @@ "items": { "type": "string" } - }, - "deleted": { - "type": "boolean" } }, "additionalProperties": false, @@ -111,39 +102,6 @@ ] } }, - "teams": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "memberIds": { - "type": "array", - "items": { - "type": "string" - } - }, - "deleted": { - "type": "boolean" - } - }, - "additionalProperties": false, - "required": [ - "id", - "name", - "createdAt", - "memberIds" - ] - } - }, "matches": { "type": "array", "items": { @@ -208,8 +166,8 @@ "notes": { "type": "string" }, - "deleted": { - "type": "boolean" + "teams": { + "type": ["array", "null"] } }, "additionalProperties": false, @@ -229,7 +187,6 @@ "players", "games", "groups", - "teams", "matches" ] } \ No newline at end of file diff --git a/lib/core/common.dart b/lib/core/common.dart index 8027180..312e3fa 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -1,4 +1,5 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; @@ -18,11 +19,78 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) { return loc.single_loser; case Ruleset.multipleWinners: return loc.multiple_winners; + case Ruleset.placement: + return loc.placement; } } -/// Counts how many players in the match are not part of the group -/// Returns the count as a string, or an empty string if there is no group +/// Translates a [GameColor] enum value to its corresponding localized string. +String translateGameColorToString(GameColor color, BuildContext context) { + final loc = AppLocalizations.of(context); + switch (color) { + case GameColor.red: + return loc.color_red; + case GameColor.blue: + return loc.color_blue; + case GameColor.green: + return loc.color_green; + case GameColor.yellow: + return loc.color_yellow; + case GameColor.purple: + return loc.color_purple; + case GameColor.orange: + return loc.color_orange; + case GameColor.pink: + return loc.color_pink; + case GameColor.teal: + return loc.color_teal; + } +} + +/// Returns the [Color] object corresponding to a [GameColor] enum value. +Color getColorFromGameColor(GameColor color) { + switch (color) { + case GameColor.red: + return Colors.red; + case GameColor.blue: + return Colors.blue; + case GameColor.green: + return Colors.green; + case GameColor.yellow: + return const Color(0xFFF7CA28); + case GameColor.purple: + return Colors.purple; + case GameColor.orange: + return const Color(0xFFef681f); + case GameColor.pink: + return Colors.pink; + case GameColor.teal: + return Colors.teal; + } +} + +/// Returns [IconData] corresponding to a [Ruleset] enum value. +IconData getRulesetIcon(Ruleset ruleset) { + switch (ruleset) { + case Ruleset.highestScore: + return Icons.arrow_upward; + case Ruleset.lowestScore: + return Icons.arrow_downward; + case Ruleset.singleWinner: + return Icons.emoji_events; + case Ruleset.singleLoser: + return Icons.sentiment_dissatisfied; + case Ruleset.multipleWinners: + return Icons.group; + case Ruleset.placement: + return RpgAwesome.podium; + } +} + +/// Counts how many players in the [match] are not part of the group +/// +/// Returns the text you append after the group name, e.g. " + 5" or an empty +/// string if there are no extra players String getExtraPlayerCount(Match match) { int count = 0; diff --git a/lib/core/constants.dart b/lib/core/constants.dart index c1bc0fe..86e3ad7 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -19,4 +19,7 @@ class Constants { /// Maximum length for team names static const int MAX_TEAM_NAME_LENGTH = 32; + + /// Maximum length for game descriptions + static const int MAX_GAME_DESCRIPTION_LENGTH = 256; } diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index 3274db9..b32ce63 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -63,9 +63,8 @@ class CustomTheme { static BoxDecoration highlightedBoxDecoration = BoxDecoration( color: boxColor, - border: Border.all(color: primaryColor), + border: Border.all(color: textColor, width: 2), borderRadius: standardBorderRadiusAll, - boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)], ); // ==================== Component Themes ==================== diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 6b33124..99141e4 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -32,21 +32,15 @@ enum ExportResult { success, canceled, unknownException } /// - [Ruleset.singleWinner]: The match is won by a single player. /// - [Ruleset.singleLoser]: The match has a single loser. /// - [Ruleset.multipleWinners]: Multiple players can be winners. +/// - [Ruleset.placement]: The player with the highest placement wins. enum Ruleset { + singleWinner, + multipleWinners, highestScore, lowestScore, - singleWinner, + placement, singleLoser, - multipleWinners, } -/// Different colors available for games -/// - [GameColor.red]: Red color -/// - [GameColor.blue]: Blue color -/// - [GameColor.green]: Green color -/// - [GameColor.yellow]: Yellow color -/// - [GameColor.purple]: Purple color -/// - [GameColor.orange]: Orange color -/// - [GameColor.pink]: Pink color -/// - [GameColor.teal]: Teal color -enum GameColor { red, blue, green, yellow, purple, orange, pink, teal } +/// Different colors for highlighting games +enum GameColor { red, orange, yellow, green, teal, blue, purple, pink } diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index f07e2c7..a4c2300 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -10,39 +10,7 @@ part 'game_dao.g.dart'; class GameDao extends DatabaseAccessor with _$GameDaoMixin { GameDao(super.db); - /// Retrieves all games from the database. - Future> getAllGames() async { - final query = select(gameTable); - final result = await query.get(); - return result - .map( - (row) => Game( - id: row.id, - name: row.name, - ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), - description: row.description, - color: GameColor.values.firstWhere((e) => e.name == row.color), - icon: row.icon, - createdAt: row.createdAt, - ), - ) - .toList(); - } - - /// Retrieves a [Game] by its [gameId]. - Future getGameById({required String gameId}) async { - final query = select(gameTable)..where((g) => g.id.equals(gameId)); - final result = await query.getSingle(); - return Game( - id: result.id, - name: result.name, - ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset), - description: result.description, - color: GameColor.values.firstWhere((e) => e.name == result.color), - icon: result.icon, - createdAt: result.createdAt, - ); - } + /* Create */ /// Adds a new [game] to the database. /// If a game with the same ID already exists, no action is taken. @@ -94,71 +62,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { return true; } - /// Deletes the game with the given [gameId] from the database. - /// Returns `true` if the game was deleted, `false` if the game did not exist. - Future deleteGame({required String gameId}) async { - final query = delete(gameTable)..where((g) => g.id.equals(gameId)); - final rowsAffected = await query.go(); - return rowsAffected > 0; - } - - /// Checks if a game with the given [gameId] exists in the database. - /// Returns `true` if the game exists, `false` otherwise. - Future gameExists({required String gameId}) async { - final query = select(gameTable)..where((g) => g.id.equals(gameId)); - final result = await query.getSingleOrNull(); - return result != null; - } - - /// Updates the name of the game with the given [gameId] to [newName]. - Future updateGameName({ - required String gameId, - required String newName, - }) async { - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(name: Value(newName)), - ); - } - - /// Updates the ruleset of the game with the given [gameId]. - Future updateGameRuleset({ - required String gameId, - required Ruleset newRuleset, - }) async { - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(ruleset: Value(newRuleset.name)), - ); - } - - /// Updates the description of the game with the given [gameId]. - Future updateGameDescription({ - required String gameId, - required String newDescription, - }) async { - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(description: Value(newDescription)), - ); - } - - /// Updates the color of the game with the given [gameId]. - Future updateGameColor({ - required String gameId, - required GameColor newColor, - }) async { - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(color: Value(newColor.name)), - ); - } - - /// Updates the icon of the game with the given [gameId]. - Future updateGameIcon({ - required String gameId, - required String newIcon, - }) async { - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(icon: Value(newIcon)), - ); - } + /* Read */ /// Retrieves the total count of games in the database. Future getGameCount() async { @@ -169,6 +73,120 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { return count ?? 0; } + /// Checks if a game with the given [gameId] exists in the database. + /// Returns `true` if the game exists, `false` otherwise. + Future gameExists({required String gameId}) async { + final query = select(gameTable)..where((g) => g.id.equals(gameId)); + final result = await query.getSingleOrNull(); + return result != null; + } + + /// Retrieves all games from the database. + Future> getAllGames() async { + final query = select(gameTable); + final result = await query.get(); + return result + .map( + (row) => Game( + id: row.id, + name: row.name, + ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), + description: row.description, + color: GameColor.values.firstWhere((e) => e.name == row.color), + icon: row.icon, + createdAt: row.createdAt, + ), + ) + .toList(); + } + + /// Retrieves a [Game] by its [gameId]. + Future getGameById({required String gameId}) async { + final query = select(gameTable)..where((g) => g.id.equals(gameId)); + final result = await query.getSingle(); + return Game( + id: result.id, + name: result.name, + ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset), + description: result.description, + color: GameColor.values.firstWhere((e) => e.name == result.color), + icon: result.icon, + createdAt: result.createdAt, + ); + } + + /* Update */ + + /// Updates the name of the game with the given [gameId] to [name]. + Future updateGameName({ + required String gameId, + required String name, + }) async { + final rowsAffected = + await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + GameTableCompanion(name: Value(name)), + ); + return rowsAffected > 0; + } + + /// Updates the ruleset of the game with the given [gameId]. + Future updateGameRuleset({ + required String gameId, + required Ruleset ruleset, + }) async { + final rowsAffected = + await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + GameTableCompanion(ruleset: Value(ruleset.name)), + ); + return rowsAffected > 0; + } + + /// Updates the description of the game with the given [gameId]. + Future updateGameDescription({ + required String gameId, + required String description, + }) async { + final rowsAffected = + await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + GameTableCompanion(description: Value(description)), + ); + return rowsAffected > 0; + } + + /// Updates the color of the game with the given [gameId]. + Future updateGameColor({ + required String gameId, + required GameColor color, + }) async { + final rowsAffected = + await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + GameTableCompanion(color: Value(color.name)), + ); + return rowsAffected > 0; + } + + /// Updates the icon of the game with the given [gameId]. + Future updateGameIcon({ + required String gameId, + required String icon, + }) async { + final rowsAffected = + await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + GameTableCompanion(icon: Value(icon)), + ); + return rowsAffected > 0; + } + + /* Delete */ + + /// Deletes the game with the given [gameId] from the database. + /// Returns `true` if the game was deleted, `false` if the game did not exist. + Future deleteGame({required String gameId}) async { + final query = delete(gameTable)..where((g) => g.id.equals(gameId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + /// Deletes all games from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteAllGames() async { @@ -176,4 +194,25 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { final rowsAffected = await query.go(); return rowsAffected > 0; } + + /// Retrieves all games with their respective match counts. + /// Returns a list of tuples (Game, matchCount). + Future> getGameUsage() async { + final games = await getAllGames(); + + final results = <(Game, int)>[]; + + for (final game in games) { + final matchCount = + await (selectOnly(db.matchTable) + ..where(db.matchTable.gameId.equals(game.id)) + ..addColumns([db.matchTable.id.count()])) + .map((row) => row.read(db.matchTable.id.count())) + .getSingle(); + + results.add((game, matchCount ?? 0)); + } + + return results; + } } diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 0d66ef6..bffe5a4 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -12,43 +12,7 @@ part 'group_dao.g.dart'; class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { GroupDao(super.db); - /// Retrieves all groups from the database. - Future> getAllGroups() async { - final query = select(groupTable); - final result = await query.get(); - return Future.wait( - result.map((groupData) async { - final members = await db.playerGroupDao.getPlayersOfGroup( - groupId: groupData.id, - ); - return Group( - id: groupData.id, - name: groupData.name, - description: groupData.description, - members: members, - createdAt: groupData.createdAt, - ); - }), - ); - } - - /// Retrieves a [Group] by its [groupId], including its members. - Future getGroupById({required String groupId}) async { - final query = select(groupTable)..where((g) => g.id.equals(groupId)); - final result = await query.getSingle(); - - List members = await db.playerGroupDao.getPlayersOfGroup( - groupId: groupId, - ); - - return Group( - id: result.id, - name: result.name, - description: result.description, - members: members, - createdAt: result.createdAt, - ); - } + /* Create */ /// Adds a new group with the given [id] and [name] to the database. /// This method also adds the group's members to the [PlayerGroupTable]. @@ -172,38 +136,44 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { }); } - /// Deletes the group with the given [id] from the database. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future deleteGroup({required String groupId}) async { - final query = (delete(groupTable)..where((g) => g.id.equals(groupId))); - final rowsAffected = await query.go(); - return rowsAffected > 0; + /* Read */ + + /// Retrieves all groups from the database. + Future> getAllGroups() async { + final query = select(groupTable); + final result = await query.get(); + return Future.wait( + result.map((groupData) async { + final members = await db.playerGroupDao.getPlayersOfGroup( + groupId: groupData.id, + ); + return Group( + id: groupData.id, + name: groupData.name, + description: groupData.description, + members: members, + createdAt: groupData.createdAt, + ); + }), + ); } - /// Updates the name of the group with the given [id] to [newName]. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future updateGroupName({ - required String groupId, - required String newName, - }) async { - final rowsAffected = - await (update(groupTable)..where((g) => g.id.equals(groupId))).write( - GroupTableCompanion(name: Value(newName)), - ); - return rowsAffected > 0; - } + /// Retrieves a [Group] by its [groupId], including its members. + Future getGroupById({required String groupId}) async { + final query = select(groupTable)..where((g) => g.id.equals(groupId)); + final result = await query.getSingle(); - /// Updates the description of the group with the given [groupId] to [newDescription]. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future updateGroupDescription({ - required String groupId, - required String newDescription, - }) async { - final rowsAffected = - await (update(groupTable)..where((g) => g.id.equals(groupId))).write( - GroupTableCompanion(description: Value(newDescription)), - ); - return rowsAffected > 0; + List members = await db.playerGroupDao.getPlayersOfGroup( + groupId: groupId, + ); + + return Group( + id: result.id, + name: result.name, + description: result.description, + members: members, + createdAt: result.createdAt, + ); } /// Retrieves the number of groups in the database. @@ -223,6 +193,16 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { return result != null; } + /* Delete */ + + /// Deletes the group with the given [id] from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteGroup({required String groupId}) async { + final query = (delete(groupTable)..where((g) => g.id.equals(groupId))); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + /// Deletes all groups from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteAllGroups() async { @@ -231,47 +211,31 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { return rowsAffected > 0; } - /// Replaces all players in a group with the provided list of players. - /// Removes all existing players from the group and adds the new players. - /// Also adds any new players to the player table if they don't exist. - /// Returns `true` if the group exists and players were replaced, `false` otherwise. - Future replaceGroupPlayers({ + /* Update */ + + /// Updates the name of the group with the given [id] to [name]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future updateGroupName({ required String groupId, - required List newPlayers, + required String name, }) async { - if (!await groupExists(groupId: groupId)) return false; + final rowsAffected = + await (update(groupTable)..where((g) => g.id.equals(groupId))).write( + GroupTableCompanion(name: Value(name)), + ); + return rowsAffected > 0; + } - await db.transaction(() async { - // Remove all existing players from the group - final deleteQuery = delete(db.playerGroupTable) - ..where((p) => p.groupId.equals(groupId)); - await deleteQuery.go(); - - // Add new players to the player table if they don't exist - await Future.wait( - newPlayers.map((player) async { - if (!await db.playerDao.playerExists(playerId: player.id)) { - await db.playerDao.addPlayer(player: player); - } - }), - ); - - // Add the new players to the group - await db.batch( - (b) => b.insertAll( - db.playerGroupTable, - newPlayers - .map( - (player) => PlayerGroupTableCompanion.insert( - playerId: player.id, - groupId: groupId, - ), - ) - .toList(), - mode: InsertMode.insertOrReplace, - ), - ); - }); - return true; + /// Updates the description of the group with the given [groupId] to [description]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future updateGroupDescription({ + required String groupId, + required String description, + }) async { + final rowsAffected = + await (update(groupTable)..where((g) => g.id.equals(groupId))).write( + GroupTableCompanion(description: Value(description)), + ); + return rowsAffected > 0; } } diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 93df7d7..88cca35 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -8,6 +8,7 @@ import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/team.dart'; part 'match_dao.g.dart'; @@ -15,74 +16,13 @@ part 'match_dao.g.dart'; class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { MatchDao(super.db); - /// Retrieves all matches from the database. - Future> getAllMatches() async { - final query = select(matchTable); - final result = await query.get(); + /* Create */ - return Future.wait( - result.map((row) async { - final game = await db.gameDao.getGameById(gameId: row.gameId); - Group? group; - if (row.groupId != null) { - group = await db.groupDao.getGroupById(groupId: row.groupId!); - } - final players = - await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; - - final scores = await db.scoreEntryDao.getAllMatchScores( - matchId: row.id, - ); - - return Match( - id: row.id, - name: row.name, - game: game, - group: group, - players: players, - notes: row.notes ?? '', - createdAt: row.createdAt, - endedAt: row.endedAt, - scores: scores, - ); - }), - ); - } - - /// Retrieves a [Match] by its [matchId]. - Future getMatchById({required String matchId}) async { - final query = select(matchTable)..where((g) => g.id.equals(matchId)); - final result = await query.getSingle(); - - final game = await db.gameDao.getGameById(gameId: result.gameId); - - Group? group; - if (result.groupId != null) { - group = await db.groupDao.getGroupById(groupId: result.groupId!); - } - - final players = - await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; - - final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId); - - return Match( - id: result.id, - name: result.name, - game: game, - group: group, - players: players, - notes: result.notes ?? '', - createdAt: result.createdAt, - endedAt: result.endedAt, - scores: scores, - ); - } - - /// Adds a new [Match] to the database. Also adds players associations. + /// Adds a new [Match] to the database. Also adds players associations and teams. /// This method assumes that the game and group (if any) are already present /// in the database. - Future addMatch({required Match match}) async { + Future addMatch({required Match match}) async { + if (await matchExists(matchId: match.id)) return false; await db.transaction(() async { await into(matchTable).insert( MatchTableCompanion.insert( @@ -90,18 +30,36 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { gameId: match.game.id, groupId: Value(match.group?.id), name: match.name, - notes: Value(match.notes), + notes: match.notes, createdAt: match.createdAt, endedAt: Value(match.endedAt), ), mode: InsertMode.insertOrReplace, ); + // Add teams + if (match.teams != null && match.teams!.isNotEmpty) { + await db.teamDao.addTeamsAsList(teams: match.teams!, matchId: match.id); + } + + // Collect all player IDs that are already in teams + final playersInTeams = {}; + if (match.teams != null) { + for (final team in match.teams!) { + for (final member in team.members) { + playersInTeams.add(member.id); + } + } + } + + // Add players that are not in teams for (final p in match.players) { - await db.playerMatchDao.addPlayerToMatch( - matchId: match.id, - playerId: p.id, - ); + if (!playersInTeams.contains(p.id)) { + await db.playerMatchDao.addPlayerToMatch( + matchId: match.id, + playerId: p.id, + ); + } } for (final pid in match.scores.keys) { @@ -115,14 +73,15 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { } } }); + return true; } /// Adds multiple [Match]es 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 addMatchAsList({required List matches}) async { - if (matches.isEmpty) return; + Future addMatchesAsList({required List matches}) async { + if (matches.isEmpty) return false; await db.transaction(() async { // Add all games first (deduplicated) final uniqueGames = {}; @@ -183,7 +142,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { gameId: match.game.id, groupId: Value(match.group?.id), name: match.name, - notes: Value(match.notes), + notes: match.notes, createdAt: match.createdAt, endedAt: Value(match.endedAt), ), @@ -279,15 +238,28 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { } } }); + + // Add teams for matches + for (final match in matches) { + if (match.teams != null && match.teams!.isNotEmpty) { + await db.teamDao.addTeamsAsList( + teams: match.teams!, + matchId: match.id, + ); + } + } }); + return true; } - /// Deletes the match with the given [matchId] from the database. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future deleteMatch({required String matchId}) async { - final query = delete(matchTable)..where((g) => g.id.equals(matchId)); - final rowsAffected = await query.go(); - return rowsAffected > 0; + /* Read */ + + /// Checks if a match with the given [matchId] exists in the database. + /// Returns `true` if the match exists, otherwise `false`. + Future matchExists({required String matchId}) async { + final query = select(matchTable)..where((g) => g.id.equals(matchId)); + final result = await query.getSingleOrNull(); + return result != null; } /// Retrieves the number of matches in the database. @@ -299,9 +271,90 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return count ?? 0; } + /// Retrieves all matches from the database. + Future> getAllMatches() async { + final query = select(matchTable); + final result = await query.get(); + + return Future.wait( + result.map((row) async { + final game = await db.gameDao.getGameById(gameId: row.gameId); + Group? group; + if (row.groupId != null) { + group = await db.groupDao.getGroupById(groupId: row.groupId!); + } + final players = await db.playerMatchDao.getPlayersOfMatch( + matchId: row.id, + ); + + final scores = await db.scoreEntryDao.getAllMatchScores( + matchId: row.id, + ); + + final teams = await _getMatchTeams(matchId: row.id); + + return Match( + id: row.id, + name: row.name, + game: game, + group: group, + players: players, + teams: teams.isEmpty ? null : teams, + notes: row.notes, + createdAt: row.createdAt, + endedAt: row.endedAt, + scores: scores, + ); + }), + ); + } + + /// Retrieves a [Match] by its [matchId]. + Future getMatchById({required String matchId}) async { + final query = select(matchTable)..where((g) => g.id.equals(matchId)); + final result = await query.getSingle(); + + final game = await db.gameDao.getGameById(gameId: result.gameId); + + Group? group; + if (result.groupId != null) { + group = await db.groupDao.getGroupById(groupId: result.groupId!); + } + + final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId); + + final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId); + + final teams = await _getMatchTeams(matchId: matchId); + + return Match( + id: result.id, + name: result.name, + game: game, + group: group, + players: players, + teams: teams.isEmpty ? null : teams, + notes: result.notes, + createdAt: result.createdAt, + endedAt: result.endedAt, + scores: scores, + ); + } + + /// Retrieves the number of matches associated with a specific game. + Future getMatchCountByGame({required String gameId}) async { + final count = + await (selectOnly(matchTable) + ..where(matchTable.gameId.equals(gameId)) + ..addColumns([matchTable.id.count()])) + .map((row) => row.read(matchTable.id.count())) + .getSingle(); + return count ?? 0; + } + /// Retrieves all matches associated with the given [groupId]. /// Queries the database directly, filtering by [groupId]. - Future> getGroupMatches({required String groupId}) async { + Future> getMatchesByGroup({required String groupId}) async { final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); final rows = await query.get(); @@ -309,15 +362,18 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { rows.map((row) async { final game = await db.gameDao.getGameById(gameId: row.gameId); final group = await db.groupDao.getGroupById(groupId: groupId); - final players = - await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; + final players = await db.playerMatchDao.getPlayersOfMatch( + matchId: row.id, + ); + final teams = await _getMatchTeams(matchId: row.id); return Match( id: row.id, name: row.name, game: game, group: group, players: players, - notes: row.notes ?? '', + teams: teams.isEmpty ? null : teams, + notes: row.notes, createdAt: row.createdAt, endedAt: row.endedAt, ); @@ -325,19 +381,56 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); } - /// Checks if a match with the given [matchId] exists in the database. - /// Returns `true` if the match exists, otherwise `false`. - Future matchExists({required String matchId}) async { - final query = select(matchTable)..where((g) => g.id.equals(matchId)); - final result = await query.getSingleOrNull(); - return result != null; + /// Helper method to retrieve teams for a specific match + Future> _getMatchTeams({required String matchId}) async { + // Get all unique team IDs from PlayerMatchTable for this match + final playerMatchQuery = select(db.playerMatchTable) + ..where((pm) => pm.matchId.equals(matchId) & pm.teamId.isNotNull()); + final playerMatches = await playerMatchQuery.get(); + + if (playerMatches.isEmpty) return []; + + final teamIds = playerMatches + .map((pm) => pm.teamId) + .whereType() + .toSet() + .toList(); + + // Fetch all teams + final teams = await Future.wait( + teamIds.map((teamId) => db.teamDao.getTeamById(teamId: teamId)), + ); + + return teams; } - /// Deletes all matches from the database. + /* Update */ + + /// Changes the name of the match with the given [matchId] to [name]. /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future deleteAllMatches() async { - final query = delete(matchTable); - final rowsAffected = await query.go(); + Future updateMatchName({ + required String matchId, + required String name, + }) async { + final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final rowsAffected = await query.write( + MatchTableCompanion(name: Value(name)), + ); + return rowsAffected > 0; + } + + /// Updates the group of the match with the given [matchId]. + /// Replaces the existing group association with the new group specified by [groupId]. + /// Pass null to remove the group association. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future updateMatchGroup({ + required String matchId, + required String? groupId, + }) async { + final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final rowsAffected = await query.write( + MatchTableCompanion(groupId: Value(groupId)), + ); return rowsAffected > 0; } @@ -345,7 +438,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateMatchNotes({ required String matchId, - required String? notes, + required String notes, }) async { final query = update(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.write( @@ -354,47 +447,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return rowsAffected > 0; } - /// Changes the name of the match with the given [matchId] to [newName]. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future 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; - } - - /// Updates the game of the match with the given [matchId]. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future updateMatchGame({ - required String matchId, - required String gameId, - }) async { - final query = update(matchTable)..where((g) => g.id.equals(matchId)); - final rowsAffected = await query.write( - MatchTableCompanion(gameId: Value(gameId)), - ); - return rowsAffected > 0; - } - - /// Updates the group of the match with the given [matchId]. - /// Replaces the existing group association with the new group specified by [newGroupId]. - /// Pass null to remove the group association. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future updateMatchGroup({ - required String matchId, - required String? newGroupId, - }) async { - final query = update(matchTable)..where((g) => g.id.equals(matchId)); - final rowsAffected = await query.write( - MatchTableCompanion(groupId: Value(newGroupId)), - ); - return rowsAffected > 0; - } - /// Removes the group association of the match with the given [matchId]. /// Sets the groupId to null. /// Returns `true` if more than 0 rows were affected, otherwise `false`. @@ -406,25 +458,12 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return rowsAffected > 0; } - /// Updates the createdAt timestamp of the match with the given [matchId]. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future updateMatchCreatedAt({ - required String matchId, - required DateTime createdAt, - }) async { - final query = update(matchTable)..where((g) => g.id.equals(matchId)); - final rowsAffected = await query.write( - MatchTableCompanion(createdAt: Value(createdAt)), - ); - return rowsAffected > 0; - } - /// Updates the endedAt timestamp of the match with the given [matchId]. /// Pass null to remove the ended time (mark match as ongoing). /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateMatchEndedAt({ required String matchId, - required DateTime? endedAt, + required DateTime endedAt, }) async { final query = update(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.write( @@ -433,37 +472,29 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return rowsAffected > 0; } - /// Replaces all players in a match with the provided list of players. - /// Removes all existing players from the match and adds the new players. - /// Also adds any new players to the player table if they don't exist. - Future replaceMatchPlayers({ - required String matchId, - required List newPlayers, - }) async { - await db.transaction(() async { - // Remove all existing players from the match - final deleteQuery = delete(db.playerMatchTable) - ..where((p) => p.matchId.equals(matchId)); - await deleteQuery.go(); + /* Delete */ - // Add new players to the player table if they don't exist - await Future.wait( - newPlayers.map((player) async { - if (!await db.playerDao.playerExists(playerId: player.id)) { - await db.playerDao.addPlayer(player: player); - } - }), - ); + /// Deletes the match with the given [matchId] from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteMatch({required String matchId}) async { + final query = delete(matchTable)..where((g) => g.id.equals(matchId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } - // Add the new players to the match - await Future.wait( - newPlayers.map( - (player) => db.playerMatchDao.addPlayerToMatch( - matchId: matchId, - playerId: player.id, - ), - ), - ); - }); + /// Deletes all matches from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllMatches() async { + final query = delete(matchTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + + /// Deletes all matches associated with a specific game. + /// Returns the number of matches deleted. + Future deleteMatchesByGame({required String gameId}) async { + final query = delete(matchTable)..where((m) => m.gameId.equals(gameId)); + final rowsAffected = await query.go(); + return rowsAffected; } } diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 5d46343..51e5845 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -10,35 +10,7 @@ part 'player_dao.g.dart'; class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { PlayerDao(super.db); - /// Retrieves all players from the database. - Future> getAllPlayers() async { - final query = select(playerTable); - final result = await query.get(); - return result - .map( - (row) => Player( - id: row.id, - name: row.name, - description: row.description, - createdAt: row.createdAt, - nameCount: row.nameCount, - ), - ) - .toList(); - } - - /// Retrieves a [Player] by their [id]. - Future getPlayerById({required String playerId}) async { - final query = select(playerTable)..where((p) => p.id.equals(playerId)); - final result = await query.getSingle(); - return Player( - id: result.id, - name: result.name, - description: result.description, - createdAt: result.createdAt, - nameCount: result.nameCount, - ); - } + /* Create */ /// Adds a new [player] to the database. /// If a player with the same ID already exists, updates their name to @@ -135,12 +107,15 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return true; } - /// Deletes the player with the given [id] from the database. - /// Returns `true` if the player was deleted, `false` if the player did not exist. - Future deletePlayer({required String playerId}) async { - final query = delete(playerTable)..where((p) => p.id.equals(playerId)); - final rowsAffected = await query.go(); - return rowsAffected > 0; + /* Read */ + + /// Retrieves the total count of players in the database. + Future getPlayerCount() async { + final count = + await (selectOnly(playerTable)..addColumns([playerTable.id.count()])) + .map((row) => row.read(playerTable.id.count())) + .getSingle(); + return count ?? 0; } /// Checks if a player with the given [playerId] exists in the database. @@ -151,10 +126,42 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return result != null; } - /// Updates the name of the player with the given [playerId] to [newName]. - Future updatePlayerName({ + /// Retrieves all players from the database. + Future> getAllPlayers() async { + final query = select(playerTable); + final result = await query.get(); + return result + .map( + (row) => Player( + id: row.id, + name: row.name, + description: row.description, + createdAt: row.createdAt, + nameCount: row.nameCount, + ), + ) + .toList(); + } + + /// Retrieves a [Player] by their [id]. + Future getPlayerById({required String playerId}) async { + final query = select(playerTable)..where((p) => p.id.equals(playerId)); + final result = await query.getSingle(); + return Player( + id: result.id, + name: result.name, + description: result.description, + createdAt: result.createdAt, + nameCount: result.nameCount, + ); + } + + /* Update */ + + /// Updates the name of the player with the given [playerId] to [name]. + Future updatePlayerName({ required String playerId, - required String newName, + required String name, }) async { // Get previous name and name count for the player before updating final previousPlayerName = @@ -164,14 +171,15 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { ''; final previousNameCount = await getNameCount(name: previousPlayerName); - await (update(playerTable)..where((p) => p.id.equals(playerId))).write( - PlayerTableCompanion(name: Value(newName)), - ); + final rowsAffected = + await (update(playerTable)..where((p) => p.id.equals(playerId))).write( + PlayerTableCompanion(name: Value(name)), + ); // Update name count for the new name - final count = await calculateNameCount(name: newName); + final count = await calculateNameCount(name: name); if (count > 0) { - await (update(playerTable)..where((p) => p.name.equals(newName))).write( + await (update(playerTable)..where((p) => p.name.equals(name))).write( PlayerTableCompanion(nameCount: Value(count)), ); } @@ -188,17 +196,35 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { ); } } + return rowsAffected > 0; } - /// Retrieves the total count of players in the database. - Future getPlayerCount() async { - final count = - await (selectOnly(playerTable)..addColumns([playerTable.id.count()])) - .map((row) => row.read(playerTable.id.count())) - .getSingle(); - return count ?? 0; + /// Updates the description of the player with the given [playerId] to + /// [description]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future updatePlayerDescription({ + required String playerId, + required String description, + }) async { + final rowsAffected = + await (update(playerTable)..where((g) => g.id.equals(playerId))).write( + PlayerTableCompanion(description: Value(description)), + ); + return rowsAffected > 0; } + /* Delete */ + + /// Deletes the player with the given [id] from the database. + /// Returns `true` if the player was deleted, `false` if the player did not exist. + Future deletePlayer({required String playerId}) async { + final query = delete(playerTable)..where((p) => p.id.equals(playerId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + + /* Name count management */ + /// Retrieves the count of players with the given [name]. Future getNameCount({required String name}) async { final query = select(playerTable)..where((p) => p.name.equals(name)); diff --git a/lib/data/dao/player_group_dao.dart b/lib/data/dao/player_group_dao.dart index 9411486..48c5653 100644 --- a/lib/data/dao/player_group_dao.dart +++ b/lib/data/dao/player_group_dao.dart @@ -11,8 +11,7 @@ class PlayerGroupDao extends DatabaseAccessor with _$PlayerGroupDaoMixin { PlayerGroupDao(super.db); - /// No need for a groupHasPlayers method since the members attribute is - /// not nullable + /* Create */ /// Adds a [player] to a group with the given [groupId]. /// If the player is already in the group, no action is taken. @@ -33,10 +32,11 @@ class PlayerGroupDao extends DatabaseAccessor await into(playerGroupTable).insert( PlayerGroupTableCompanion.insert(playerId: player.id, groupId: groupId), ); - return true; } + /* Read */ + /// Retrieves all players belonging to a specific group by [groupId]. Future> getPlayersOfGroup({required String groupId}) async { final query = select(playerGroupTable) @@ -53,18 +53,6 @@ class PlayerGroupDao extends DatabaseAccessor return groupMembers; } - /// Removes a player from a group based on [playerId] and [groupId]. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future removePlayerFromGroup({ - required String playerId, - required String groupId, - }) async { - final query = delete(playerGroupTable) - ..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId)); - final rowsAffected = await query.go(); - return rowsAffected > 0; - } - /// Checks if a player with [playerId] is in the group with [groupId]. /// Returns `true` if the player is in the group, otherwise `false`. Future isPlayerInGroup({ @@ -76,4 +64,65 @@ class PlayerGroupDao extends DatabaseAccessor final result = await query.getSingleOrNull(); return result != null; } + + /* Update */ + + /// Replaces all players in a group with the provided list of players. + /// Removes all existing players from the group and adds the new players. + /// Also adds any new players to the player table if they don't exist. + /// Returns `true` if the group exists and players were replaced, `false` otherwise. + Future replaceGroupPlayers({ + required String groupId, + required List newPlayers, + }) async { + if (!await db.groupDao.groupExists(groupId: groupId)) return false; + if (newPlayers.isEmpty) return false; + + await db.transaction(() async { + // Remove all existing players from the group + final deleteQuery = delete(db.playerGroupTable) + ..where((p) => p.groupId.equals(groupId)); + await deleteQuery.go(); + + // Add new players to the player table if they don't exist + await Future.wait( + newPlayers.map((player) async { + if (!await db.playerDao.playerExists(playerId: player.id)) { + await db.playerDao.addPlayer(player: player); + } + }), + ); + + // Add the new players to the group + await db.batch( + (b) => b.insertAll( + db.playerGroupTable, + newPlayers + .map( + (player) => PlayerGroupTableCompanion.insert( + playerId: player.id, + groupId: groupId, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + }); + return true; + } + + /* Delete */ + + /// Removes a player from a group based on [playerId] and [groupId]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future removePlayerFromGroup({ + required String playerId, + required String groupId, + }) async { + final query = delete(playerGroupTable) + ..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } } diff --git a/lib/data/dao/player_match_dao.dart b/lib/data/dao/player_match_dao.dart index b467a1b..d119468 100644 --- a/lib/data/dao/player_match_dao.dart +++ b/lib/data/dao/player_match_dao.dart @@ -11,14 +11,16 @@ class PlayerMatchDao extends DatabaseAccessor with _$PlayerMatchDaoMixin { PlayerMatchDao(super.db); + /* Create */ + /// Associates a player with a match by inserting a record into the /// [PlayerMatchTable]. Optionally associates with a team and sets initial score. - Future addPlayerToMatch({ + Future addPlayerToMatch({ required String matchId, required String playerId, String? teamId, }) async { - await into(playerMatchTable).insert( + final rowsAffected = await into(playerMatchTable).insert( PlayerMatchTableCompanion.insert( playerId: playerId, matchId: matchId, @@ -26,42 +28,14 @@ class PlayerMatchDao extends DatabaseAccessor ), mode: InsertMode.insertOrReplace, ); - } - - /// Retrieves a list of [Player]s associated with the given [matchId]. - /// Returns null if no players are found. - Future?> 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; - } - - /// Updates the team for a player in a match. - /// Returns `true` if the update was successful, otherwise `false`. - Future updatePlayerTeam({ - required String matchId, - required String playerId, - required String? teamId, - }) async { - final rowsAffected = - await (update(playerMatchTable)..where( - (p) => p.matchId.equals(matchId) & p.playerId.equals(playerId), - )) - .write(PlayerMatchTableCompanion(teamId: Value(teamId))); return rowsAffected > 0; } + /* Read */ + /// Checks if there are any players associated with the given [matchId]. /// Returns `true` if there are players, otherwise `false`. - Future matchHasPlayers({required String matchId}) async { + Future hasMatchPlayers({required String matchId}) async { final count = await (selectOnly(playerMatchTable) ..where(playerMatchTable.matchId.equals(matchId)) @@ -87,31 +61,79 @@ class PlayerMatchDao extends DatabaseAccessor 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 removePlayerFromMatch({ + /// Retrieves a list of [Player]s associated with the given [matchId]. + /// Returns empty list if no players are found. + Future> getPlayersOfMatch({required String matchId}) async { + final result = await (select( + playerMatchTable, + )..where((p) => p.matchId.equals(matchId))).get(); + + if (result.isEmpty) return []; + + final futures = result.map( + (row) => db.playerDao.getPlayerById(playerId: row.playerId), + ); + final players = await Future.wait(futures); + return players; + } + + /// Retrieves a list of [Player]s associated with a specific team in a match. + /// Returns empty list if no players are found for the team in the match. + Future> getPlayersOfTeamInMatch({ + required String matchId, + required String teamId, + }) async { + final result = + await (select(playerMatchTable) + ..where((p) => p.matchId.equals(matchId)) + ..where((p) => p.teamId.equals(teamId))) + .get(); + + if (result.isEmpty) return []; + + final futures = result.map( + (row) => db.playerDao.getPlayerById(playerId: row.playerId), + ); + final players = await Future.wait(futures); + return players; + } + + /* Updated */ + + /// Updates the team for a player in a match. + /// Returns `true` if the update was successful, otherwise `false`. + Future updatePlayersTeam({ required String matchId, required String playerId, + required String? teamId, }) async { - final query = delete(playerMatchTable) - ..where((pg) => pg.matchId.equals(matchId)) - ..where((pg) => pg.playerId.equals(playerId)); - final rowsAffected = await query.go(); + final rowsAffected = + await (update(playerMatchTable)..where( + (p) => p.matchId.equals(matchId) & p.playerId.equals(playerId), + )) + .write(PlayerMatchTableCompanion(teamId: Value(teamId))); 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 + /// [player] list. It adds new players and removes players that are no /// longer associated with the match. - Future updatePlayersFromMatch({ + Future updateMatchPlayers({ required String matchId, - required List newPlayer, + required List player, }) async { + if (player.isEmpty) return false; + 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(); + final currentPlayerIds = currentPlayers.map((p) => p.id).toSet(); + final newPlayerIdsSet = player.map((p) => p.id).toSet(); + + // Are the current and new player identical? + if (currentPlayerIds.containsAll(newPlayerIdsSet) && + newPlayerIdsSet.containsAll(currentPlayerIds)) { + return false; + } // Determine players to add and remove final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds); @@ -147,22 +169,22 @@ class PlayerMatchDao extends DatabaseAccessor ); } }); + return true; } - /// Retrieves all players in a specific team for a match. - Future> getPlayersInTeam({ + /* Delete */ + + /// 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 removePlayerFromMatch({ required String matchId, - required String teamId, + required String playerId, }) async { - final result = await (select( - playerMatchTable, - )..where((p) => p.matchId.equals(matchId) & p.teamId.equals(teamId))).get(); - - if (result.isEmpty) return []; - - final futures = result.map( - (row) => db.playerDao.getPlayerById(playerId: row.playerId), - ); - return Future.wait(futures); + final query = delete(playerMatchTable) + ..where((pg) => pg.matchId.equals(matchId)) + ..where((pg) => pg.playerId.equals(playerId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; } } diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index 566b9d1..830135d 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -13,6 +13,8 @@ class ScoreEntryDao extends DatabaseAccessor with _$ScoreEntryDaoMixin { ScoreEntryDao(super.db); + /* Create */ + /// Adds a score entry to the database. Future addScore({ required String playerId, @@ -58,6 +60,8 @@ class ScoreEntryDao extends DatabaseAccessor }); } + /* Read */ + /// Retrieves the score for a specific round. Future getScore({ required String playerId, @@ -126,28 +130,58 @@ class ScoreEntryDao extends DatabaseAccessor ); } + /// Gets the highest (latest) round number for a match. + /// Returns `null` if there are no scores for the match. + Future getLatestRoundNumber({required String matchId}) async { + final query = selectOnly(scoreEntryTable) + ..where(scoreEntryTable.matchId.equals(matchId)) + ..addColumns([scoreEntryTable.roundNumber.max()]); + final result = await query.getSingle(); + return result.read(scoreEntryTable.roundNumber.max()); + } + + /// Aggregates the total score for a player in a match by summing all their + /// score entry changes. Returns `0` if there are no scores for the player + /// in the match. + Future getTotalScoreForPlayer({ + required String playerId, + required String matchId, + }) async { + final scores = await getAllPlayerScoresInMatch( + playerId: playerId, + matchId: matchId, + ); + if (scores.isEmpty) return 0; + // Return the sum of all score changes + return scores.fold(0, (sum, element) => sum + element.change); + } + + /* Update */ + /// Updates a score entry. Future updateScore({ required String playerId, required String matchId, - required ScoreEntry newEntry, + required ScoreEntry entry, }) async { final rowsAffected = await (update(scoreEntryTable)..where( (s) => s.playerId.equals(playerId) & s.matchId.equals(matchId) & - s.roundNumber.equals(newEntry.roundNumber), + s.roundNumber.equals(entry.roundNumber), )) .write( ScoreEntryTableCompanion( - score: Value(newEntry.score), - change: Value(newEntry.change), + score: Value(entry.score), + change: Value(entry.change), ), ); return rowsAffected > 0; } + /* Delete */ + /// Deletes a score entry. Future deleteScore({ required String playerId, @@ -182,31 +216,7 @@ class ScoreEntryDao extends DatabaseAccessor return rowsAffected > 0; } - /// Gets the highest (latest) round number for a match. - /// Returns `null` if there are no scores for the match. - Future getLatestRoundNumber({required String matchId}) async { - final query = selectOnly(scoreEntryTable) - ..where(scoreEntryTable.matchId.equals(matchId)) - ..addColumns([scoreEntryTable.roundNumber.max()]); - final result = await query.getSingle(); - return result.read(scoreEntryTable.roundNumber.max()); - } - - /// Aggregates the total score for a player in a match by summing all their - /// score entry changes. Returns `0` if there are no scores for the player - /// in the match. - Future getTotalScoreForPlayer({ - required String playerId, - required String matchId, - }) async { - final scores = await getAllPlayerScoresInMatch( - playerId: playerId, - matchId: matchId, - ); - if (scores.isEmpty) return 0; - // Return the sum of all score changes - return scores.fold(0, (sum, element) => sum + element.change); - } + /* Winner handling */ Future hasWinner({required String matchId}) async { return await getWinner(matchId: matchId) != null; @@ -218,7 +228,7 @@ class ScoreEntryDao extends DatabaseAccessor required String playerId, }) async { // Clear previous winner if exists - deleteAllScoresForMatch(matchId: matchId); + await deleteAllScoresForMatch(matchId: matchId); // Set the winner's score to 1 final rowsAffected = await into(scoreEntryTable).insert( @@ -235,7 +245,7 @@ class ScoreEntryDao extends DatabaseAccessor return rowsAffected > 0; } - // Retrieves the winner of a match by looking for a score entry where score + /// Retrieves the winner of a match by looking for a score entry where score /// is 1. Returns `null` if no player found, else the first with the score. Future getWinner({required String matchId}) async { final query = @@ -266,21 +276,52 @@ class ScoreEntryDao extends DatabaseAccessor /// Returns `true` if the winner was removed, `false` if there are multiple /// scores or if the winner cannot be removed. Future removeWinner({required String matchId}) async { - final scores = await getAllMatchScores(matchId: matchId); - - if (scores.length > 1) { - return false; - } else { - return await deleteAllScoresForMatch(matchId: matchId); - } + return await deleteAllScoresForMatch(matchId: matchId); } - Future hasLooser({required String matchId}) async { - return await getLooser(matchId: matchId) != null; + /* multiple winners handling */ + + /// Sets the winners for a match. + /// + /// Returns `true` if more than 0 rows were affected + Future setWinners({ + required List winners, + required String matchId, + }) async { + // Clear previous winners if exists + await deleteAllScoresForMatch(matchId: matchId); + + if (winners.isEmpty) return false; + + await batch((batch) { + batch.insertAll( + scoreEntryTable, + winners + .map( + (player) => ScoreEntryTableCompanion.insert( + playerId: player.id, + matchId: matchId, + roundNumber: 0, + score: 1, + change: 0, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ); + }); + + return true; + } + + /* Loser handling */ + + Future hasLoser({required String matchId}) async { + return await getLoser(matchId: matchId) != null; } // Setting the looser for a game and clearing previous looser if exists. - Future setLooser({ + Future setLoser({ required String matchId, required String playerId, }) async { @@ -304,7 +345,7 @@ class ScoreEntryDao extends DatabaseAccessor /// Retrieves the looser of a match by looking for a score entry where score /// is 0. Returns `null` if no player found, else the first with the score. - Future getLooser({required String matchId}) async { + Future getLoser({required String matchId}) async { final query = select(scoreEntryTable).join([ innerJoin( @@ -332,7 +373,7 @@ class ScoreEntryDao extends DatabaseAccessor /// /// Returns `true` if the looser was removed, `false` if there are multiple /// scores or if the looser cannot be removed. - Future removeLooser({required String matchId}) async { + Future removeLoser({required String matchId}) async { final scores = await getAllMatchScores(matchId: matchId); if (scores.length > 1) { @@ -341,4 +382,21 @@ class ScoreEntryDao extends DatabaseAccessor return await deleteAllScoresForMatch(matchId: matchId); } } + + /* placement handling */ + + /// Sets the placement for each player in a match. + /// The highest score is assigned to the first player, the second highest to the second player, and so on. + Future setPlacements({ + required String matchId, + required List players, + }) async { + for (int i = 0; i < players.length; i++) { + await db.scoreEntryDao.addScore( + matchId: matchId, + playerId: players[i].id, + entry: ScoreEntry(roundNumber: 0, score: players.length - i, change: 0), + ); + } + } } diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index 01dc724..cba68fb 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -1,17 +1,105 @@ import 'package:drift/drift.dart'; import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/team_table.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/team.dart'; part 'team_dao.g.dart'; -@DriftAccessor(tables: [TeamTable]) +@DriftAccessor(tables: [TeamTable, PlayerMatchTable]) class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { TeamDao(super.db); + /* Create */ + + /// Adds a new [team] to the database. + /// Returns `true` if the team was added, `false` otherwise. + Future addTeam({required Team team, required String matchId}) async { + if (await teamExists(teamId: team.id)) return false; + await into(teamTable).insert( + TeamTableCompanion.insert( + id: team.id, + name: team.name, + createdAt: team.createdAt, + ), + mode: InsertMode.insertOrReplace, + ); + await db.batch((batch) async { + for (final player in team.members) { + await into(playerMatchTable).insert( + PlayerMatchTableCompanion.insert( + playerId: player.id, + matchId: matchId, + teamId: Value(team.id), + ), + mode: InsertMode.insertOrReplace, + ); + } + }); + return true; + } + + /// Adds multiple [teams] to the database in a batch operation. + Future addTeamsAsList({ + required List teams, + required String matchId, + }) async { + if (teams.isEmpty) return false; + + await db.batch( + (b) => b.insertAll( + teamTable, + teams + .map( + (team) => TeamTableCompanion.insert( + id: team.id, + name: team.name, + createdAt: team.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrIgnore, + ), + ); + + for (final team in teams) { + await db.batch((batch) async { + for (final player in team.members) { + await into(db.playerMatchTable).insert( + PlayerMatchTableCompanion.insert( + playerId: player.id, + matchId: matchId, + teamId: Value(team.id), + ), + mode: InsertMode.insertOrReplace, + ); + } + }); + } + return true; + } + + /* Read */ + + /// Retrieves the total count of teams in the database. + Future getTeamCount() async { + final count = + await (selectOnly(teamTable)..addColumns([teamTable.id.count()])) + .map((row) => row.read(teamTable.id.count())) + .getSingle(); + return count ?? 0; + } + + /// Checks if a team with the given [teamId] exists in the database. + /// Returns `true` if the team exists, `false` otherwise. + Future teamExists({required String teamId}) async { + final query = select(teamTable)..where((t) => t.id.equals(teamId)); + final result = await query.getSingleOrNull(); + return result != null; + } + /// Retrieves all teams from the database. - /// Note: This returns teams without their members. Use getTeamById for full team data. Future> getAllTeams() async { final query = select(teamTable); final result = await query.get(); @@ -41,8 +129,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { ); } - /// Helper method to get team members from player_match_table. - /// This assumes team members are tracked via the player_match_table. + /// Helper method to get team members from PlayerMatchTable. Future> _getTeamMembers({required String teamId}) async { // Get all player_match entries with this teamId final playerMatchQuery = select(db.playerMatchTable) @@ -61,44 +148,28 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { return players; } - /// Adds a new [team] to the database. - /// Returns `true` if the team was added, `false` otherwise. - Future addTeam({required Team team}) async { - if (!await teamExists(teamId: team.id)) { - await into(teamTable).insert( - TeamTableCompanion.insert( - id: team.id, - name: team.name, - createdAt: team.createdAt, - ), - mode: InsertMode.insertOrReplace, - ); - return true; - } - return false; + /* Update */ + + /// Updates the name of the team with the given [teamId]. + Future updateTeamName({ + required String teamId, + required String name, + }) async { + final rowsAffected = + await (update(teamTable)..where((t) => t.id.equals(teamId))).write( + TeamTableCompanion(name: Value(name)), + ); + return rowsAffected > 0; } - /// Adds multiple [teams] to the database in a batch operation. - Future addTeamsAsList({required List teams}) async { - if (teams.isEmpty) return false; + /* Delete */ - await db.batch( - (b) => b.insertAll( - teamTable, - teams - .map( - (team) => TeamTableCompanion.insert( - id: team.id, - name: team.name, - createdAt: team.createdAt, - ), - ) - .toList(), - mode: InsertMode.insertOrIgnore, - ), - ); - - return true; + /// Deletes all teams from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllTeams() async { + final query = delete(teamTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; } /// Deletes the team with the given [teamId] from the database. @@ -108,39 +179,4 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { final rowsAffected = await query.go(); return rowsAffected > 0; } - - /// Checks if a team with the given [teamId] exists in the database. - /// Returns `true` if the team exists, `false` otherwise. - Future teamExists({required String teamId}) async { - final query = select(teamTable)..where((t) => t.id.equals(teamId)); - final result = await query.getSingleOrNull(); - return result != null; - } - - /// Updates the name of the team with the given [teamId]. - Future updateTeamName({ - required String teamId, - required String newName, - }) async { - await (update(teamTable)..where((t) => t.id.equals(teamId))).write( - TeamTableCompanion(name: Value(newName)), - ); - } - - /// Retrieves the total count of teams in the database. - Future getTeamCount() async { - final count = - await (selectOnly(teamTable)..addColumns([teamTable.id.count()])) - .map((row) => row.read(teamTable.id.count())) - .getSingle(); - return count ?? 0; - } - - /// Deletes all teams from the database. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future deleteAllTeams() async { - final query = delete(teamTable); - final rowsAffected = await query.go(); - return rowsAffected > 0; - } } diff --git a/lib/data/dao/team_dao.g.dart b/lib/data/dao/team_dao.g.dart index 3b78c03..7b468dd 100644 --- a/lib/data/dao/team_dao.g.dart +++ b/lib/data/dao/team_dao.g.dart @@ -5,6 +5,12 @@ part of 'team_dao.dart'; // ignore_for_file: type=lint mixin _$TeamDaoMixin on DatabaseAccessor { $TeamTableTable get teamTable => attachedDatabase.teamTable; + $PlayerTableTable get playerTable => attachedDatabase.playerTable; + $GameTableTable get gameTable => attachedDatabase.gameTable; + $GroupTableTable get groupTable => attachedDatabase.groupTable; + $MatchTableTable get matchTable => attachedDatabase.matchTable; + $PlayerMatchTableTable get playerMatchTable => + attachedDatabase.playerMatchTable; TeamDaoManager get managers => TeamDaoManager(this); } @@ -13,4 +19,17 @@ class TeamDaoManager { TeamDaoManager(this._db); $$TeamTableTableTableManager get teamTable => $$TeamTableTableTableManager(_db.attachedDatabase, _db.teamTable); + $$PlayerTableTableTableManager get playerTable => + $$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable); + $$GameTableTableTableManager get gameTable => + $$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable); + $$GroupTableTableTableManager get groupTable => + $$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable); + $$MatchTableTableTableManager get matchTable => + $$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable); + $$PlayerMatchTableTableTableManager get playerMatchTable => + $$PlayerMatchTableTableTableManager( + _db.attachedDatabase, + _db.playerMatchTable, + ); } diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index 2190c3d..c8d0faa 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -1190,9 +1190,9 @@ class $MatchTableTable extends MatchTable late final GeneratedColumn notes = GeneratedColumn( 'notes', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, ); static const VerificationMeta _createdAtMeta = const VerificationMeta( 'createdAt', @@ -1270,6 +1270,8 @@ class $MatchTableTable extends MatchTable _notesMeta, notes.isAcceptableOrUnknown(data['notes']!, _notesMeta), ); + } else if (isInserting) { + context.missing(_notesMeta); } if (data.containsKey('created_at')) { context.handle( @@ -1313,7 +1315,7 @@ class $MatchTableTable extends MatchTable notes: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}notes'], - ), + )!, createdAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}created_at'], @@ -1336,7 +1338,7 @@ class MatchTableData extends DataClass implements Insertable { final String gameId; final String? groupId; final String name; - final String? notes; + final String notes; final DateTime createdAt; final DateTime? endedAt; const MatchTableData({ @@ -1344,7 +1346,7 @@ class MatchTableData extends DataClass implements Insertable { required this.gameId, this.groupId, required this.name, - this.notes, + required this.notes, required this.createdAt, this.endedAt, }); @@ -1357,9 +1359,7 @@ class MatchTableData extends DataClass implements Insertable { map['group_id'] = Variable(groupId); } map['name'] = Variable(name); - if (!nullToAbsent || notes != null) { - map['notes'] = Variable(notes); - } + map['notes'] = Variable(notes); map['created_at'] = Variable(createdAt); if (!nullToAbsent || endedAt != null) { map['ended_at'] = Variable(endedAt); @@ -1375,9 +1375,7 @@ class MatchTableData extends DataClass implements Insertable { ? const Value.absent() : Value(groupId), name: Value(name), - notes: notes == null && nullToAbsent - ? const Value.absent() - : Value(notes), + notes: Value(notes), createdAt: Value(createdAt), endedAt: endedAt == null && nullToAbsent ? const Value.absent() @@ -1395,7 +1393,7 @@ class MatchTableData extends DataClass implements Insertable { gameId: serializer.fromJson(json['gameId']), groupId: serializer.fromJson(json['groupId']), name: serializer.fromJson(json['name']), - notes: serializer.fromJson(json['notes']), + notes: serializer.fromJson(json['notes']), createdAt: serializer.fromJson(json['createdAt']), endedAt: serializer.fromJson(json['endedAt']), ); @@ -1408,7 +1406,7 @@ class MatchTableData extends DataClass implements Insertable { 'gameId': serializer.toJson(gameId), 'groupId': serializer.toJson(groupId), 'name': serializer.toJson(name), - 'notes': serializer.toJson(notes), + 'notes': serializer.toJson(notes), 'createdAt': serializer.toJson(createdAt), 'endedAt': serializer.toJson(endedAt), }; @@ -1419,7 +1417,7 @@ class MatchTableData extends DataClass implements Insertable { String? gameId, Value groupId = const Value.absent(), String? name, - Value notes = const Value.absent(), + String? notes, DateTime? createdAt, Value endedAt = const Value.absent(), }) => MatchTableData( @@ -1427,7 +1425,7 @@ class MatchTableData extends DataClass implements Insertable { gameId: gameId ?? this.gameId, groupId: groupId.present ? groupId.value : this.groupId, name: name ?? this.name, - notes: notes.present ? notes.value : this.notes, + notes: notes ?? this.notes, createdAt: createdAt ?? this.createdAt, endedAt: endedAt.present ? endedAt.value : this.endedAt, ); @@ -1478,7 +1476,7 @@ class MatchTableCompanion extends UpdateCompanion { final Value gameId; final Value groupId; final Value name; - final Value notes; + final Value notes; final Value createdAt; final Value endedAt; final Value rowid; @@ -1497,13 +1495,14 @@ class MatchTableCompanion extends UpdateCompanion { required String gameId, this.groupId = const Value.absent(), required String name, - this.notes = const Value.absent(), + required String notes, required DateTime createdAt, this.endedAt = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), gameId = Value(gameId), name = Value(name), + notes = Value(notes), createdAt = Value(createdAt); static Insertable custom({ Expression? id, @@ -1532,7 +1531,7 @@ class MatchTableCompanion extends UpdateCompanion { Value? gameId, Value? groupId, Value? name, - Value? notes, + Value? notes, Value? createdAt, Value? endedAt, Value? rowid, @@ -2122,7 +2121,7 @@ class $PlayerMatchTableTable extends PlayerMatchTable type: DriftSqlType.string, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES team_table (id)', + 'REFERENCES team_table (id) ON DELETE SET NULL', ), ); @override @@ -2820,6 +2819,13 @@ abstract class _$AppDatabase extends GeneratedDatabase { ), result: [TableUpdate('player_match_table', kind: UpdateKind.delete)], ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'team_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('player_match_table', kind: UpdateKind.update)], + ), WritePropagation( on: TableUpdateQuery.onTableName( 'player_table', @@ -4086,7 +4092,7 @@ typedef $$MatchTableTableCreateCompanionBuilder = required String gameId, Value groupId, required String name, - Value notes, + required String notes, required DateTime createdAt, Value endedAt, Value rowid, @@ -4097,7 +4103,7 @@ typedef $$MatchTableTableUpdateCompanionBuilder = Value gameId, Value groupId, Value name, - Value notes, + Value notes, Value createdAt, Value endedAt, Value rowid, @@ -4560,7 +4566,7 @@ class $$MatchTableTableTableManager Value gameId = const Value.absent(), Value groupId = const Value.absent(), Value name = const Value.absent(), - Value notes = const Value.absent(), + Value notes = const Value.absent(), Value createdAt = const Value.absent(), Value endedAt = const Value.absent(), Value rowid = const Value.absent(), @@ -4580,7 +4586,7 @@ class $$MatchTableTableTableManager required String gameId, Value groupId = const Value.absent(), required String name, - Value notes = const Value.absent(), + required String notes, required DateTime createdAt, Value endedAt = const Value.absent(), Value rowid = const Value.absent(), diff --git a/lib/data/db/tables/match_table.dart b/lib/data/db/tables/match_table.dart index dec86bc..eaf7ff7 100644 --- a/lib/data/db/tables/match_table.dart +++ b/lib/data/db/tables/match_table.dart @@ -12,7 +12,7 @@ class MatchTable extends Table { .references(GroupTable, #id, onDelete: KeyAction.setNull) .nullable()(); TextColumn get name => text()(); - TextColumn get notes => text().nullable()(); + TextColumn get notes => text()(); DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get endedAt => dateTime().nullable()(); BoolColumn get deleted => boolean().withDefault(const Constant(false))(); diff --git a/lib/data/db/tables/player_match_table.dart b/lib/data/db/tables/player_match_table.dart index d71f4c5..50dda0f 100644 --- a/lib/data/db/tables/player_match_table.dart +++ b/lib/data/db/tables/player_match_table.dart @@ -8,8 +8,9 @@ class PlayerMatchTable extends Table { text().references(PlayerTable, #id, onDelete: KeyAction.cascade)(); TextColumn get matchId => text().references(MatchTable, #id, onDelete: KeyAction.cascade)(); - TextColumn get teamId => text().references(TeamTable, #id).nullable()(); - BoolColumn get deleted => boolean().withDefault(const Constant(false))(); + TextColumn get teamId => text() + .references(TeamTable, #id, onDelete: KeyAction.setNull) + .nullable()(); @override Set> get primaryKey => {playerId, matchId}; diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index 42c3b33..89bbd30 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -10,27 +10,60 @@ class Game { final String description; final GameColor color; final String icon; - final bool deleted; Game({ - String? id, - DateTime? createdAt, required this.name, required this.ruleset, - String? description, - required this.color, - required this.icon, - this.deleted = false, + this.color = GameColor.orange, + this.description = '', + this.icon = '', + String? id, + DateTime? createdAt, }) : id = id ?? const Uuid().v4(), - createdAt = createdAt ?? clock.now(), - description = description ?? ''; + createdAt = createdAt ?? clock.now(); @override String toString() { return 'Game{id: $id, name: $name, ruleset: $ruleset, description: $description, color: $color, icon: $icon}'; } - /// Creates a Game instance from a JSON object. + Game copyWith({ + String? id, + DateTime? createdAt, + String? name, + Ruleset? ruleset, + String? description, + GameColor? color, + String? icon, + }) { + return Game( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + name: name ?? this.name, + ruleset: ruleset ?? this.ruleset, + description: description ?? this.description, + color: color ?? this.color, + icon: icon ?? this.icon, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Game && + runtimeType == other.runtimeType && + id == other.id && + createdAt == other.createdAt && + name == other.name && + ruleset == other.ruleset && + description == other.description && + color == other.color && + icon == other.icon; + + @override + int get hashCode => + Object.hash(id, createdAt, name, ruleset, description, color, icon); + Game.fromJson(Map json) : id = json['id'], createdAt = DateTime.parse(json['createdAt']), @@ -41,10 +74,8 @@ class Game { ), description = json['description'], color = GameColor.values.firstWhere((e) => e.name == json['color']), - icon = json['icon'], - deleted = json['deleted'] ?? false; + icon = json['icon']; - /// Converts the Game instance to a JSON object. Map toJson() => { 'id': id, 'createdAt': createdAt.toIso8601String(), @@ -53,6 +84,5 @@ class Game { 'description': description, 'color': color.name, 'icon': icon, - 'deleted': deleted, }; } diff --git a/lib/data/models/group.dart b/lib/data/models/group.dart index 9433087..c1d2a98 100644 --- a/lib/data/models/group.dart +++ b/lib/data/models/group.dart @@ -1,4 +1,5 @@ import 'package:clock/clock.dart'; +import 'package:collection/collection.dart'; import 'package:tallee/data/models/player.dart'; import 'package:uuid/uuid.dart'; @@ -26,6 +27,42 @@ class Group { return 'Group{id: $id, name: $name, description: $description, members: $members}'; } + Group copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + List? members, + }) { + return Group( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + members: members ?? this.members, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Group && + runtimeType == other.runtimeType && + id == other.id && + name == other.name && + description == other.description && + createdAt == other.createdAt && + const DeepCollectionEquality().equals(members, other.members); + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + const DeepCollectionEquality().hash(members), + ); + /// Creates a Group instance from a JSON object where the related [Player] /// objects are represented by their IDs. Group.fromJson(Map json) diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 1cb8c60..9918b2f 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -1,9 +1,11 @@ import 'package:clock/clock.dart'; +import 'package:collection/collection.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/score_entry.dart'; +import 'package:tallee/data/models/team.dart'; import 'package:uuid/uuid.dart'; class Match { @@ -14,6 +16,7 @@ class Match { final Game game; final Group? group; final List players; + final List? teams; final String notes; Map scores; final bool deleted; @@ -24,6 +27,7 @@ class Match { required this.players, this.endedAt, this.group, + this.teams, this.notes = '', String? id, DateTime? createdAt, @@ -38,9 +42,62 @@ class Match { return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, mvp: $mvp}'; } - /// Creates a Match instance from a JSON object where related objects are - /// represented by their IDs. Therefore, the game, group, and players are not - /// fully constructed here. + Match copyWith({ + String? id, + DateTime? createdAt, + DateTime? endedAt, + String? name, + Game? game, + Group? group, + List? players, + List? teams, + String? notes, + Map? scores, + }) { + return Match( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + endedAt: endedAt ?? this.endedAt, + name: name ?? this.name, + game: game ?? this.game, + group: group ?? this.group, + players: players ?? this.players, + teams: teams ?? this.teams, + notes: notes ?? this.notes, + scores: scores ?? this.scores, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Match && + runtimeType == other.runtimeType && + id == other.id && + createdAt == other.createdAt && + endedAt == other.endedAt && + name == other.name && + game == other.game && + group == other.group && + const DeepCollectionEquality().equals(players, other.players) && + const DeepCollectionEquality().equals(teams, other.teams) && + notes == other.notes && + const DeepCollectionEquality().equals(scores, other.scores); + + @override + int get hashCode => Object.hash( + id, + createdAt, + endedAt, + name, + game, + group, + const DeepCollectionEquality().hash(players), + const DeepCollectionEquality().hash(teams), + notes, + const DeepCollectionEquality().hash(scores), + ); + Match.fromJson(Map json) : id = json['id'], createdAt = DateTime.parse(json['createdAt']), @@ -57,6 +114,7 @@ class Match { ), group = null, players = [], + teams = [], scores = json['scores'] != null ? (json['scores'] as Map).map( (key, value) => MapEntry( @@ -70,9 +128,6 @@ class Match { notes = json['notes'] ?? '', deleted = json['deleted'] ?? false; - /// Converts the Match instance to a JSON object. Related objects are - /// represented by their IDs, so the game, group, and players are not fully - /// serialized here. Map toJson() => { 'id': id, 'createdAt': createdAt.toIso8601String(), @@ -81,6 +136,7 @@ class Match { 'gameId': game.id, 'groupId': group?.id, 'playerIds': players.map((player) => player.id).toList(), + 'teams': teams?.map((team) => team.toJson()).toList(), 'scores': scores.map((key, value) => MapEntry(key, value?.toJson())), 'notes': notes, 'deleted': deleted, @@ -103,7 +159,10 @@ class Match { return _getPlayersWithLowestScore().take(1).toList(); case Ruleset.multipleWinners: - return []; + return _getPlayersWithHighestScore().toList(); + + case Ruleset.placement: + return _getPlayersWithHighestScore().take(1).toList(); } } diff --git a/lib/data/models/player.dart b/lib/data/models/player.dart index d7e5e33..19fd79a 100644 --- a/lib/data/models/player.dart +++ b/lib/data/models/player.dart @@ -25,7 +25,36 @@ class Player { return 'Player{id: $id, createdAt: $createdAt, name: $name, nameCount: $nameCount, description: $description}'; } - /// Creates a Player instance from a JSON object. + Player copyWith({ + String? id, + DateTime? createdAt, + String? name, + int? nameCount, + String? description, + }) { + return Player( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + name: name ?? this.name, + nameCount: nameCount ?? this.nameCount, + description: description ?? this.description, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Player && + runtimeType == other.runtimeType && + id == other.id && + createdAt == other.createdAt && + name == other.name && + nameCount == other.nameCount && + description == other.description; + + @override + int get hashCode => Object.hash(id, createdAt, name, nameCount, description); + Player.fromJson(Map json) : id = json['id'], createdAt = DateTime.parse(json['createdAt']), @@ -34,7 +63,6 @@ class Player { description = json['description'], deleted = json['deleted'] ?? false; - /// Converts the Player instance to a JSON object. Map toJson() => { 'id': id, 'createdAt': createdAt.toIso8601String(), diff --git a/lib/data/models/score_entry.dart b/lib/data/models/score_entry.dart index 42924d3..844cfe9 100644 --- a/lib/data/models/score_entry.dart +++ b/lib/data/models/score_entry.dart @@ -11,6 +11,26 @@ class ScoreEntry { return 'ScoreEntry{roundNumber: $roundNumber, score: $score, change: $change}'; } + ScoreEntry copyWith({int? roundNumber, int? score, int? change}) { + return ScoreEntry( + roundNumber: roundNumber ?? this.roundNumber, + score: score ?? this.score, + change: change ?? this.change, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ScoreEntry && + runtimeType == other.runtimeType && + roundNumber == other.roundNumber && + score == other.score && + change == other.change; + + @override + int get hashCode => Object.hash(roundNumber, score, change); + ScoreEntry.fromJson(Map json) : roundNumber = json['roundNumber'], score = json['score'], diff --git a/lib/data/models/team.dart b/lib/data/models/team.dart index 2dbe8dd..c4d9356 100644 --- a/lib/data/models/team.dart +++ b/lib/data/models/team.dart @@ -1,4 +1,5 @@ import 'package:clock/clock.dart'; +import 'package:collection/collection.dart'; import 'package:tallee/data/models/player.dart'; import 'package:uuid/uuid.dart'; @@ -23,8 +24,38 @@ class Team { return 'Team{id: $id, name: $name, members: $members}'; } - /// Creates a Team instance from a JSON object (memberIds format). - /// Player objects are reconstructed from memberIds by the DataTransferService. + Team copyWith({ + String? id, + String? name, + DateTime? createdAt, + List? members, + }) { + return Team( + id: id ?? this.id, + name: name ?? this.name, + createdAt: createdAt ?? this.createdAt, + members: members ?? this.members, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Team && + runtimeType == other.runtimeType && + id == other.id && + name == other.name && + createdAt == other.createdAt && + const DeepCollectionEquality().equals(members, other.members); + + @override + int get hashCode => Object.hash( + id, + name, + createdAt, + const DeepCollectionEquality().hash(members), + ); + Team.fromJson(Map json) : id = json['id'], name = json['name'], @@ -32,8 +63,6 @@ class Team { members = [], deleted = json['deleted'] ?? false; - /// Converts the Team instance to a JSON object. Related objects are - /// represented by their IDs. Map toJson() => { 'id': id, 'name': name, diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 46c780a..f9093a2 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -6,10 +6,21 @@ "app_name": "Tallee", "best_player": "Beste:r Spieler:in", "cancel": "Abbrechen", + "choose_color": "Farbe wählen", "choose_game": "Spielvorlage wählen", "choose_group": "Gruppe wählen", "choose_ruleset": "Regelwerk wählen", + "color": "Farbe", + "color_blue": "Blau", + "color_green": "Grün", + "color_orange": "Orange", + "color_pink": "Rosa", + "color_purple": "Lila", + "color_red": "Rot", + "color_teal": "Türkis", + "color_yellow": "Gelb", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", + "create_game": "Spielvorlage erstellen", "create_group": "Gruppe erstellen", "create_match": "Spiel erstellen", "create_new_group": "Neue Gruppe erstellen", @@ -22,16 +33,30 @@ "days_ago": "vor {count} Tagen", "delete": "Löschen", "delete_all_data": "Alle Daten löschen", + "delete_game": "Spielvorlage löschen", + "delete_game_with_matches_warning": "Wenn du diese Spielvorlage löschst, {count, plural, =1{wird 1 Spiel} other{werden {count} Spiele}} mit dieser Spielvorlage ebenfalls gelöscht.", + "@delete_game_with_matches_warning": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "delete_group": "Gruppe löschen", "delete_match": "Spiel löschen", + "drag_to_set_placement": "Ziehen um Platzierung zu setzen", + "description": "Beschreibung", + "edit_game": "Spielvorlage bearbeiten", "edit_group": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten", "enter_points": "Punkte eingeben", "enter_results": "Ergebnisse eintragen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", + "error_deleting_game": "Fehler beim Löschen der Spielvorlage, bitte erneut versuchen", "error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen", "error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen", "error_reading_file": "Fehler beim Lesen der Datei", + "exit_view": "Ansicht verlassen", "export_canceled": "Export abgebrochen", "export_data": "Daten exportieren", "format_exception": "Formatfehler (siehe Konsole)", @@ -50,6 +75,7 @@ "legal": "Rechtliches", "legal_notice": "Impressum", "licenses": "Lizenzen", + "live_edit_mode": "Live-Bearbeitungsmodus", "match_in_progress": "Spiel läuft...", "match_name": "Spieltitel", "match_profile": "Spielprofil", @@ -57,6 +83,7 @@ "members": "Mitglieder", "most_points": "Höchste Punkte", "no_data_available": "Keine Daten verfügbar", + "no_games_created_yet": "Noch keine Spielvorlagen erstellt", "no_groups_created_yet": "Noch keine Gruppen erstellt", "no_licenses_found": "Keine Lizenzen gefunden", "no_license_text_available": "Kein Lizenztext verfügbar", @@ -71,10 +98,11 @@ "none": "Kein", "none_group": "Keine", "not_available": "Nicht verfügbar", + "placement": "Platzierung", + "place": "Platz", "played_matches": "Gespielte Spiele", "player_name": "Spieler:innenname", "players": "Spieler:innen", - "players_count": "{count} Spieler", "point": "Punkt", "points": "Punkte", "privacy_policy": "Datenschutzerklärung", @@ -85,12 +113,14 @@ "ruleset": "Regelwerk", "ruleset_least_points": "Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.", "ruleset_most_points": "Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.", + "ruleset_placement": "Spieler:innen können in einer Reihenfolge angeordnet werden, die ihre Platzierung reflektiert.", "ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.", "ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.", "save_changes": "Änderungen speichern", "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", "select_winner": "Gewinner:in wählen", + "select_winners": "Gewinner:innen wählen", "select_loser": "Verlierer:in wählen", "selected_players": "Ausgewählte Spieler:innen", "settings": "Einstellungen", @@ -103,6 +133,7 @@ "statistics": "Statistiken", "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", + "there_are_no_games_matching_your_search": "Es gibt keine Spielvorlagen, die deiner Suche entspricht", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "tie": "Unentschieden", @@ -110,6 +141,7 @@ "undo": "Rückgängig", "unknown_exception": "Unbekannter Fehler (siehe Konsole)", "winner": "Gewinner:in", + "winners": "Gewinner:innen", "winrate": "Siegquote", "wins": "Siege", "yesterday_at": "Gestern um" diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a85e1b0..b7da7f2 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,349 +1,27 @@ { "@@locale": "en", - "@all_players": { - "description": "Label for all players list" - }, - "@all_players_selected": { - "description": "Message when all players are added to selection" - }, - "@amount_of_matches": { - "description": "Label for amount of matches statistic" - }, - "@app_name": { - "description": "The name of the App" - }, - "@best_player": { - "description": "Label for best player statistic" - }, - "@cancel": { - "description": "Cancel button text" - }, - "@choose_game": { - "description": "Label for choosing a game" - }, - "@choose_group": { - "description": "Label for choosing a group" - }, - "@choose_ruleset": { - "description": "Label for choosing a ruleset" - }, - "@could_not_add_player": { - "description": "Error message when adding a player fails" - }, - "@create_group": { - "description": "Button text to create a group" - }, - "@create_match": { - "description": "Button text to create a match" - }, - "@create_new_group": { - "description": "Appbar text to create a new group" - }, - "@create_new_match": { - "description": "Appbar text to create a new match" - }, - "@created_on": { - "description": "Label for creation date" - }, - "@data": { - "description": "Data label" - }, - "@data_successfully_deleted": { - "description": "Success message after deleting data" - }, - "@data_successfully_exported": { - "description": "Success message after exporting data" - }, - "@data_successfully_imported": { - "description": "Success message after importing data" - }, - "@days_ago": { - "description": "Date format for days ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@delete": { - "description": "Delete button text" - }, - "@delete_all_data": { - "description": "Confirmation dialog for deleting all data" - }, - "@delete_group": { - "description": "Confirmation dialog for deleting a group" - }, - "@delete_match": { - "description": "Button text to delete a match" - }, - "@edit_group": { - "description": "Button & Appbar label for editing a group" - }, - "@edit_match": { - "description": "Button & Appbar label for editing a match" - }, - "@enter_points": { - "description": "Label to enter players points" - }, - "@enter_results": { - "description": "Button text to enter match results" - }, - "@error_creating_group": { - "description": "Error message when group creation fails" - }, - "@error_deleting_group": { - "description": "Error message when group deletion fails" - }, - "@error_editing_group": { - "description": "Error message when group editing fails" - }, - "@error_reading_file": { - "description": "Error message when file cannot be read" - }, - "@export_canceled": { - "description": "Message when export is canceled" - }, - "@export_data": { - "description": "Export data menu item" - }, - "@format_exception": { - "description": "Error message for format exceptions" - }, - "@game": { - "description": "Game label" - }, - "@game_name": { - "description": "Placeholder for game name search" - }, - "@group": { - "description": "Group label" - }, - "@group_name": { - "description": "Placeholder for group name input" - }, - "@group_profile": { - "description": "Title for group profile view" - }, - "@groups": { - "description": "Label for groups" - }, - "@home": { - "description": "Home tab label" - }, - "@import_canceled": { - "description": "Message when import is canceled" - }, - "@import_data": { - "description": "Import data menu item" - }, - "@info": { - "description": "Info label" - }, - "@invalid_schema": { - "description": "Error message for invalid schema" - }, - "@least_points": { - "description": "Title for least points ruleset" - }, - "@legal": { - "description": "Legal section header" - }, - "@legal_notice": { - "description": "Legal notice menu item" - }, - "@licenses": { - "description": "Licenses menu item" - }, - "@match_in_progress": { - "description": "Message when match is in progress" - }, - "@match_name": { - "description": "Placeholder for match name input" - }, - "@match_profile": { - "description": "Title for match profile view" - }, - "@matches": { - "description": "Label for matches" - }, - "@members": { - "description": "Label for group members" - }, - "@most_points": { - "description": "Title for most points ruleset" - }, - "@no_data_available": { - "description": "Message when no data in the statistic tiles is given" - }, - "@no_groups_created_yet": { - "description": "Message when no groups exist" - }, - "@no_licenses_found": { - "description": "Message when no licenses are found" - }, - "@no_license_text_available": { - "description": "Message when no license text is available" - }, - "@no_matches_created_yet": { - "description": "Message when no matches exist" - }, - "@no_players_created_yet": { - "description": "Message when no players exist" - }, - "@no_players_found_with_that_name": { - "description": "Message when search returns no results" - }, - "@no_players_selected": { - "description": "Message when no players are selected" - }, - "@no_recent_matches_available": { - "description": "Message when no recent matches exist" - }, - "@no_results_entered_yet": { - "description": "Message when no results have been entered yet" - }, - "@no_second_match_available": { - "description": "Message when no second match exists" - }, - "@no_statistics_available": { - "description": "Message when no statistics are available, because no matches were played yet" - }, - "@none": { - "description": "None option label" - }, - "@none_group": { - "description": "None group option label" - }, - "@not_available": { - "description": "Abbreviation for not available" - }, - "@played_matches": { - "description": "Label for played matches statistic" - }, - "@player_name": { - "description": "Placeholder for player name input" - }, - "@players": { - "description": "Players label" - }, - "@players_count": { - "description": "Shows the number of players", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@points": { - "description": "Points label" - }, - "@privacy_policy": { - "description": "Privacy policy menu item" - }, - "@quick_create": { - "description": "Title for quick create section" - }, - "@recent_matches": { - "description": "Title for recent matches section" - }, - "@results": { - "description": "Label for match results" - }, - "@ruleset": { - "description": "Ruleset label" - }, - "@ruleset_least_points": { - "description": "Description for least points ruleset" - }, - "@ruleset_most_points": { - "description": "Description for most points ruleset" - }, - "@ruleset_single_loser": { - "description": "Description for single loser ruleset" - }, - "@ruleset_single_winner": { - "description": "Description for single winner ruleset" - }, - "@save_changes": { - "description": "Save changes button text" - }, - "@search_for_groups": { - "description": "Hint text for group search input field" - }, - "@search_for_players": { - "description": "Hint text for player search input field" - }, - "@select_winner": { - "description": "Label to select the winner" - }, - "@select_loser": { - "description": "Label to select the loser" - }, - "@selected_players": { - "description": "Shows the number of selected players" - }, - "@settings": { - "description": "Label for the App Settings" - }, - "@single_loser": { - "description": "Title for single loser ruleset" - }, - "@single_winner": { - "description": "Title for single winner ruleset" - }, - "@statistics": { - "description": "Statistics tab label" - }, - "@stats": { - "description": "Stats tab label (short)" - }, - "@successfully_added_player": { - "description": "Success message when adding a player", - "placeholders": { - "playerName": { - "type": "String", - "example": "John" - } - } - }, - "@there_is_no_group_matching_your_search": { - "description": "Message when search returns no groups" - }, - "@this_cannot_be_undone": { - "description": "Warning message for irreversible actions" - }, - "@today_at": { - "description": "Date format for today" - }, - "@undo": { - "description": "Undo button text" - }, - "@unknown_exception": { - "description": "Error message for unknown exceptions" - }, - "@winner": { - "description": "Winner label" - }, - "@winrate": { - "description": "Label for winrate statistic" - }, - "@wins": { - "description": "Label for wins statistic" - }, - "@yesterday_at": { - "description": "Date format for yesterday" - }, + "all_players": "All players", "all_players_selected": "All players selected", "amount_of_matches": "Amount of Matches", "app_name": "Tallee", "best_player": "Best Player", "cancel": "Cancel", + "choose_color": "Choose Color", "choose_game": "Choose Game", "choose_group": "Choose Group", "choose_ruleset": "Choose Ruleset", + "color": "Color", + "color_blue": "Blue", + "color_green": "Green", + "color_orange": "Orange", + "color_pink": "Pink", + "color_purple": "Purple", + "color_red": "Red", + "color_teal": "Teal", + "color_yellow": "Yellow", "could_not_add_player": "Could not add player", + "create_game": "Create Game", "create_group": "Create Group", "create_match": "Create match", "create_new_group": "Create new group", @@ -356,16 +34,30 @@ "days_ago": "{count} days ago", "delete": "Delete", "delete_all_data": "Delete all data", + "delete_game": "Delete Game", + "delete_game_with_matches_warning": "If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.", + "@delete_game_with_matches_warning": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "delete_group": "Delete Group", "delete_match": "Delete Match", + "drag_to_set_placement": "Drag to set placement", + "description": "Description", + "edit_game": "Edit Game", "edit_group": "Edit Group", "edit_match": "Edit Match", "enter_points": "Enter points", "enter_results": "Enter Results", "error_creating_group": "Error while creating group, please try again", + "error_deleting_game": "Error while deleting game, please try again", "error_deleting_group": "Error while deleting group, please try again", "error_editing_group": "Error while editing group, please try again", "error_reading_file": "Error reading file", + "exit_view": "Exit View", "export_canceled": "Export canceled", "export_data": "Export data", "format_exception": "Format Exception (see console)", @@ -384,6 +76,7 @@ "legal": "Legal", "legal_notice": "Legal Notice", "licenses": "Licenses", + "live_edit_mode": "Live Edit Mode", "match_in_progress": "Match in progress...", "match_name": "Match name", "match_profile": "Match Profile", @@ -391,6 +84,7 @@ "members": "Members", "most_points": "Most Points", "no_data_available": "No data available", + "no_games_created_yet": "No games created yet", "no_groups_created_yet": "No groups created yet", "no_licenses_found": "No licenses found", "no_license_text_available": "No license text available", @@ -405,10 +99,11 @@ "none": "None", "none_group": "None", "not_available": "Not available", + "placement": "Placement", + "place": "place", "played_matches": "Played Matches", "player_name": "Player name", "players": "Players", - "players_count": "{count} Players", "point": "Point", "points": "Points", "privacy_policy": "Privacy Policy", @@ -418,12 +113,14 @@ "ruleset": "Ruleset", "ruleset_least_points": "Inverse scoring: the player with the fewest points wins.", "ruleset_most_points": "Traditional ruleset: the player with the most points wins.", + "ruleset_placement": "Players can be arranged in an order, which reflects their placement.", "ruleset_single_loser": "Exactly one loser is determined; last place receives the penalty or consequence.", "ruleset_single_winner": "Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.", "save_changes": "Save Changes", "search_for_groups": "Search for groups", "search_for_players": "Search for players", "select_winner": "Select Winner", + "select_winners": "Select Winners", "select_loser": "Select Loser", "selected_players": "Selected players", "settings": "Settings", @@ -436,6 +133,16 @@ "statistics": "Statistics", "stats": "Stats", "successfully_added_player": "Successfully added player {playerName}", + "@successfully_added_player": { + "description": "Success message when adding a player", + "placeholders": { + "playerName": { + "type": "String", + "example": "John" + } + } + }, + "there_are_no_games_matching_your_search": "There are no games matching your search", "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", "tie": "Tie", @@ -443,6 +150,7 @@ "undo": "Undo", "unknown_exception": "Unknown Exception (see console)", "winner": "Winner", + "winners": "Winners", "winrate": "Winrate", "wins": "Wins", "yesterday_at": "Yesterday at" diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 99c9317..1bff731 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -98,571 +98,709 @@ abstract class AppLocalizations { Locale('en'), ]; - /// Label for all players list + /// No description provided for @all_players. /// /// In en, this message translates to: /// **'All players'** String get all_players; - /// Message when all players are added to selection + /// No description provided for @all_players_selected. /// /// In en, this message translates to: /// **'All players selected'** String get all_players_selected; - /// Label for amount of matches statistic + /// No description provided for @amount_of_matches. /// /// In en, this message translates to: /// **'Amount of Matches'** String get amount_of_matches; - /// The name of the App + /// No description provided for @app_name. /// /// In en, this message translates to: /// **'Tallee'** String get app_name; - /// Label for best player statistic + /// No description provided for @best_player. /// /// In en, this message translates to: /// **'Best Player'** String get best_player; - /// Cancel button text + /// No description provided for @cancel. /// /// In en, this message translates to: /// **'Cancel'** String get cancel; - /// Label for choosing a game + /// No description provided for @choose_color. + /// + /// In en, this message translates to: + /// **'Choose Color'** + String get choose_color; + + /// No description provided for @choose_game. /// /// In en, this message translates to: /// **'Choose Game'** String get choose_game; - /// Label for choosing a group + /// No description provided for @choose_group. /// /// In en, this message translates to: /// **'Choose Group'** String get choose_group; - /// Label for choosing a ruleset + /// No description provided for @choose_ruleset. /// /// In en, this message translates to: /// **'Choose Ruleset'** String get choose_ruleset; - /// Error message when adding a player fails + /// No description provided for @color. + /// + /// In en, this message translates to: + /// **'Color'** + String get color; + + /// No description provided for @color_blue. + /// + /// In en, this message translates to: + /// **'Blue'** + String get color_blue; + + /// No description provided for @color_green. + /// + /// In en, this message translates to: + /// **'Green'** + String get color_green; + + /// No description provided for @color_orange. + /// + /// In en, this message translates to: + /// **'Orange'** + String get color_orange; + + /// No description provided for @color_pink. + /// + /// In en, this message translates to: + /// **'Pink'** + String get color_pink; + + /// No description provided for @color_purple. + /// + /// In en, this message translates to: + /// **'Purple'** + String get color_purple; + + /// No description provided for @color_red. + /// + /// In en, this message translates to: + /// **'Red'** + String get color_red; + + /// No description provided for @color_teal. + /// + /// In en, this message translates to: + /// **'Teal'** + String get color_teal; + + /// No description provided for @color_yellow. + /// + /// In en, this message translates to: + /// **'Yellow'** + String get color_yellow; + + /// No description provided for @could_not_add_player. /// /// In en, this message translates to: /// **'Could not add player'** String could_not_add_player(Object playerName); - /// Button text to create a group + /// No description provided for @create_game. + /// + /// In en, this message translates to: + /// **'Create Game'** + String get create_game; + + /// No description provided for @create_group. /// /// In en, this message translates to: /// **'Create Group'** String get create_group; - /// Button text to create a match + /// No description provided for @create_match. /// /// In en, this message translates to: /// **'Create match'** String get create_match; - /// Appbar text to create a new group + /// No description provided for @create_new_group. /// /// In en, this message translates to: /// **'Create new group'** String get create_new_group; - /// Label for creation date + /// No description provided for @created_on. /// /// In en, this message translates to: /// **'Created on'** String get created_on; - /// Appbar text to create a new match + /// No description provided for @create_new_match. /// /// In en, this message translates to: /// **'Create new match'** String get create_new_match; - /// Data label + /// No description provided for @data. /// /// In en, this message translates to: /// **'Data'** String get data; - /// Success message after deleting data + /// No description provided for @data_successfully_deleted. /// /// In en, this message translates to: /// **'Data successfully deleted'** String get data_successfully_deleted; - /// Success message after exporting data + /// No description provided for @data_successfully_exported. /// /// In en, this message translates to: /// **'Data successfully exported'** String get data_successfully_exported; - /// Success message after importing data + /// No description provided for @data_successfully_imported. /// /// In en, this message translates to: /// **'Data successfully imported'** String get data_successfully_imported; - /// Date format for days ago + /// No description provided for @days_ago. /// /// In en, this message translates to: /// **'{count} days ago'** - String days_ago(int count); + String days_ago(Object count); - /// Delete button text + /// No description provided for @delete. /// /// In en, this message translates to: /// **'Delete'** String get delete; - /// Confirmation dialog for deleting all data + /// No description provided for @delete_all_data. /// /// In en, this message translates to: /// **'Delete all data'** String get delete_all_data; - /// Confirmation dialog for deleting a group + /// No description provided for @delete_game. + /// + /// In en, this message translates to: + /// **'Delete Game'** + String get delete_game; + + /// No description provided for @delete_game_with_matches_warning. + /// + /// In en, this message translates to: + /// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'** + String delete_game_with_matches_warning(int count); + + /// No description provided for @delete_group. /// /// In en, this message translates to: /// **'Delete Group'** String get delete_group; - /// Button text to delete a match + /// No description provided for @delete_match. /// /// In en, this message translates to: /// **'Delete Match'** String get delete_match; - /// Button & Appbar label for editing a group + /// No description provided for @drag_to_set_placement. + /// + /// In en, this message translates to: + /// **'Drag to set placement'** + String get drag_to_set_placement; + + /// No description provided for @description. + /// + /// In en, this message translates to: + /// **'Description'** + String get description; + + /// No description provided for @edit_game. + /// + /// In en, this message translates to: + /// **'Edit Game'** + String get edit_game; + + /// No description provided for @edit_group. /// /// In en, this message translates to: /// **'Edit Group'** String get edit_group; - /// Button & Appbar label for editing a match + /// No description provided for @edit_match. /// /// In en, this message translates to: /// **'Edit Match'** String get edit_match; - /// Label to enter players points + /// No description provided for @enter_points. /// /// In en, this message translates to: /// **'Enter points'** String get enter_points; - /// Button text to enter match results + /// No description provided for @enter_results. /// /// In en, this message translates to: /// **'Enter Results'** String get enter_results; - /// Error message when group creation fails + /// No description provided for @error_creating_group. /// /// In en, this message translates to: /// **'Error while creating group, please try again'** String get error_creating_group; - /// Error message when group deletion fails + /// No description provided for @error_deleting_game. + /// + /// In en, this message translates to: + /// **'Error while deleting game, please try again'** + String get error_deleting_game; + + /// No description provided for @error_deleting_group. /// /// In en, this message translates to: /// **'Error while deleting group, please try again'** String get error_deleting_group; - /// Error message when group editing fails + /// No description provided for @error_editing_group. /// /// In en, this message translates to: /// **'Error while editing group, please try again'** String get error_editing_group; - /// Error message when file cannot be read + /// No description provided for @error_reading_file. /// /// In en, this message translates to: /// **'Error reading file'** String get error_reading_file; - /// Message when export is canceled + /// No description provided for @exit_view. + /// + /// In en, this message translates to: + /// **'Exit View'** + String get exit_view; + + /// No description provided for @export_canceled. /// /// In en, this message translates to: /// **'Export canceled'** String get export_canceled; - /// Export data menu item + /// No description provided for @export_data. /// /// In en, this message translates to: /// **'Export data'** String get export_data; - /// Error message for format exceptions + /// No description provided for @format_exception. /// /// In en, this message translates to: /// **'Format Exception (see console)'** String get format_exception; - /// Game label + /// No description provided for @game. /// /// In en, this message translates to: /// **'Game'** String get game; - /// Placeholder for game name search + /// No description provided for @game_name. /// /// In en, this message translates to: /// **'Game Name'** String get game_name; - /// Group label + /// No description provided for @group. /// /// In en, this message translates to: /// **'Group'** String get group; - /// Placeholder for group name input + /// No description provided for @group_name. /// /// In en, this message translates to: /// **'Group name'** String get group_name; - /// Title for group profile view + /// No description provided for @group_profile. /// /// In en, this message translates to: /// **'Group Profile'** String get group_profile; - /// Label for groups + /// No description provided for @groups. /// /// In en, this message translates to: /// **'Groups'** String get groups; - /// Home tab label + /// No description provided for @home. /// /// In en, this message translates to: /// **'Home'** String get home; - /// Message when import is canceled + /// No description provided for @import_canceled. /// /// In en, this message translates to: /// **'Import canceled'** String get import_canceled; - /// Import data menu item + /// No description provided for @import_data. /// /// In en, this message translates to: /// **'Import data'** String get import_data; - /// Info label + /// No description provided for @info. /// /// In en, this message translates to: /// **'Info'** String get info; - /// Error message for invalid schema + /// No description provided for @invalid_schema. /// /// In en, this message translates to: /// **'Invalid Schema'** String get invalid_schema; - /// Title for least points ruleset + /// No description provided for @least_points. /// /// In en, this message translates to: /// **'Least Points'** String get least_points; - /// Legal section header + /// No description provided for @legal. /// /// In en, this message translates to: /// **'Legal'** String get legal; - /// Legal notice menu item + /// No description provided for @legal_notice. /// /// In en, this message translates to: /// **'Legal Notice'** String get legal_notice; - /// Licenses menu item + /// No description provided for @licenses. /// /// In en, this message translates to: /// **'Licenses'** String get licenses; - /// Message when match is in progress + /// No description provided for @live_edit_mode. + /// + /// In en, this message translates to: + /// **'Live Edit Mode'** + String get live_edit_mode; + + /// No description provided for @match_in_progress. /// /// In en, this message translates to: /// **'Match in progress...'** String get match_in_progress; - /// Placeholder for match name input + /// No description provided for @match_name. /// /// In en, this message translates to: /// **'Match name'** String get match_name; - /// Title for match profile view + /// No description provided for @match_profile. /// /// In en, this message translates to: /// **'Match Profile'** String get match_profile; - /// Label for matches + /// No description provided for @matches. /// /// In en, this message translates to: /// **'Matches'** String get matches; - /// Label for group members + /// No description provided for @members. /// /// In en, this message translates to: /// **'Members'** String get members; - /// Title for most points ruleset + /// No description provided for @most_points. /// /// In en, this message translates to: /// **'Most Points'** String get most_points; - /// Message when no data in the statistic tiles is given + /// No description provided for @no_data_available. /// /// In en, this message translates to: /// **'No data available'** String get no_data_available; - /// Message when no groups exist + /// No description provided for @no_games_created_yet. + /// + /// In en, this message translates to: + /// **'No games created yet'** + String get no_games_created_yet; + + /// No description provided for @no_groups_created_yet. /// /// In en, this message translates to: /// **'No groups created yet'** String get no_groups_created_yet; - /// Message when no licenses are found + /// No description provided for @no_licenses_found. /// /// In en, this message translates to: /// **'No licenses found'** String get no_licenses_found; - /// Message when no license text is available + /// No description provided for @no_license_text_available. /// /// In en, this message translates to: /// **'No license text available'** String get no_license_text_available; - /// Message when no matches exist + /// No description provided for @no_matches_created_yet. /// /// In en, this message translates to: /// **'No matches created yet'** String get no_matches_created_yet; - /// Message when no players exist + /// No description provided for @no_players_created_yet. /// /// In en, this message translates to: /// **'No players created yet'** String get no_players_created_yet; - /// Message when search returns no results + /// No description provided for @no_players_found_with_that_name. /// /// In en, this message translates to: /// **'No players found with that name'** String get no_players_found_with_that_name; - /// Message when no players are selected + /// No description provided for @no_players_selected. /// /// In en, this message translates to: /// **'No players selected'** String get no_players_selected; - /// Message when no recent matches exist + /// No description provided for @no_recent_matches_available. /// /// In en, this message translates to: /// **'No recent matches available'** String get no_recent_matches_available; - /// Message when no results have been entered yet + /// No description provided for @no_results_entered_yet. /// /// In en, this message translates to: /// **'No results entered yet'** String get no_results_entered_yet; - /// Message when no second match exists + /// No description provided for @no_second_match_available. /// /// In en, this message translates to: /// **'No second match available'** String get no_second_match_available; - /// Message when no statistics are available, because no matches were played yet + /// No description provided for @no_statistics_available. /// /// In en, this message translates to: /// **'No statistics available'** String get no_statistics_available; - /// None option label + /// No description provided for @none. /// /// In en, this message translates to: /// **'None'** String get none; - /// None group option label + /// No description provided for @none_group. /// /// In en, this message translates to: /// **'None'** String get none_group; - /// Abbreviation for not available + /// No description provided for @not_available. /// /// In en, this message translates to: /// **'Not available'** String get not_available; - /// Label for played matches statistic + /// No description provided for @placement. + /// + /// In en, this message translates to: + /// **'Placement'** + String get placement; + + /// No description provided for @place. + /// + /// In en, this message translates to: + /// **'place'** + String get place; + + /// No description provided for @played_matches. /// /// In en, this message translates to: /// **'Played Matches'** String get played_matches; - /// Placeholder for player name input + /// No description provided for @player_name. /// /// In en, this message translates to: /// **'Player name'** String get player_name; - /// Players label + /// No description provided for @players. /// /// In en, this message translates to: /// **'Players'** String get players; - /// Shows the number of players - /// - /// In en, this message translates to: - /// **'{count} Players'** - String players_count(int count); - /// No description provided for @point. /// /// In en, this message translates to: /// **'Point'** String get point; - /// Points label + /// No description provided for @points. /// /// In en, this message translates to: /// **'Points'** String get points; - /// Privacy policy menu item + /// No description provided for @privacy_policy. /// /// In en, this message translates to: /// **'Privacy Policy'** String get privacy_policy; - /// Title for quick create section + /// No description provided for @quick_create. /// /// In en, this message translates to: /// **'Quick Create'** String get quick_create; - /// Title for recent matches section + /// No description provided for @recent_matches. /// /// In en, this message translates to: /// **'Recent Matches'** String get recent_matches; - /// Label for match results + /// No description provided for @results. /// /// In en, this message translates to: /// **'Results'** String get results; - /// Ruleset label + /// No description provided for @ruleset. /// /// In en, this message translates to: /// **'Ruleset'** String get ruleset; - /// Description for least points ruleset + /// No description provided for @ruleset_least_points. /// /// In en, this message translates to: /// **'Inverse scoring: the player with the fewest points wins.'** String get ruleset_least_points; - /// Description for most points ruleset + /// No description provided for @ruleset_most_points. /// /// In en, this message translates to: /// **'Traditional ruleset: the player with the most points wins.'** String get ruleset_most_points; - /// Description for single loser ruleset + /// No description provided for @ruleset_placement. + /// + /// In en, this message translates to: + /// **'Players can be arranged in an order, which reflects their placement.'** + String get ruleset_placement; + + /// No description provided for @ruleset_single_loser. /// /// In en, this message translates to: /// **'Exactly one loser is determined; last place receives the penalty or consequence.'** String get ruleset_single_loser; - /// Description for single winner ruleset + /// No description provided for @ruleset_single_winner. /// /// In en, this message translates to: /// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'** String get ruleset_single_winner; - /// Save changes button text + /// No description provided for @save_changes. /// /// In en, this message translates to: /// **'Save Changes'** String get save_changes; - /// Hint text for group search input field + /// No description provided for @search_for_groups. /// /// In en, this message translates to: /// **'Search for groups'** String get search_for_groups; - /// Hint text for player search input field + /// No description provided for @search_for_players. /// /// In en, this message translates to: /// **'Search for players'** String get search_for_players; - /// Label to select the winner + /// No description provided for @select_winner. /// /// In en, this message translates to: /// **'Select Winner'** String get select_winner; - /// Label to select the loser + /// No description provided for @select_winners. + /// + /// In en, this message translates to: + /// **'Select Winners'** + String get select_winners; + + /// No description provided for @select_loser. /// /// In en, this message translates to: /// **'Select Loser'** String get select_loser; - /// Shows the number of selected players + /// No description provided for @selected_players. /// /// In en, this message translates to: /// **'Selected players'** String get selected_players; - /// Label for the App Settings + /// No description provided for @settings. /// /// In en, this message translates to: /// **'Settings'** String get settings; - /// Title for single loser ruleset + /// No description provided for @single_loser. /// /// In en, this message translates to: /// **'Single Loser'** String get single_loser; - /// Title for single winner ruleset + /// No description provided for @single_winner. /// /// In en, this message translates to: /// **'Single Winner'** @@ -692,13 +830,13 @@ abstract class AppLocalizations { /// **'Multiple Winners'** String get multiple_winners; - /// Statistics tab label + /// No description provided for @statistics. /// /// In en, this message translates to: /// **'Statistics'** String get statistics; - /// Stats tab label (short) + /// No description provided for @stats. /// /// In en, this message translates to: /// **'Stats'** @@ -710,13 +848,19 @@ abstract class AppLocalizations { /// **'Successfully added player {playerName}'** String successfully_added_player(String playerName); - /// Message when search returns no groups + /// No description provided for @there_are_no_games_matching_your_search. + /// + /// In en, this message translates to: + /// **'There are no games matching your search'** + String get there_are_no_games_matching_your_search; + + /// No description provided for @there_is_no_group_matching_your_search. /// /// In en, this message translates to: /// **'There is no group matching your search'** String get there_is_no_group_matching_your_search; - /// Warning message for irreversible actions + /// No description provided for @this_cannot_be_undone. /// /// In en, this message translates to: /// **'This can\'t be undone.'** @@ -728,43 +872,49 @@ abstract class AppLocalizations { /// **'Tie'** String get tie; - /// Date format for today + /// No description provided for @today_at. /// /// In en, this message translates to: /// **'Today at'** String get today_at; - /// Undo button text + /// No description provided for @undo. /// /// In en, this message translates to: /// **'Undo'** String get undo; - /// Error message for unknown exceptions + /// No description provided for @unknown_exception. /// /// In en, this message translates to: /// **'Unknown Exception (see console)'** String get unknown_exception; - /// Winner label + /// No description provided for @winner. /// /// In en, this message translates to: /// **'Winner'** String get winner; - /// Label for winrate statistic + /// No description provided for @winners. + /// + /// In en, this message translates to: + /// **'Winners'** + String get winners; + + /// No description provided for @winrate. /// /// In en, this message translates to: /// **'Winrate'** String get winrate; - /// Label for wins statistic + /// No description provided for @wins. /// /// In en, this message translates to: /// **'Wins'** String get wins; - /// Date format for yesterday + /// No description provided for @yesterday_at. /// /// In en, this message translates to: /// **'Yesterday at'** diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 51b4c62..ea8e1f2 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -26,6 +26,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get cancel => 'Abbrechen'; + @override + String get choose_color => 'Farbe wählen'; + @override String get choose_game => 'Spielvorlage wählen'; @@ -35,11 +38,41 @@ class AppLocalizationsDe extends AppLocalizations { @override String get choose_ruleset => 'Regelwerk wählen'; + @override + String get color => 'Farbe'; + + @override + String get color_blue => 'Blau'; + + @override + String get color_green => 'Grün'; + + @override + String get color_orange => 'Orange'; + + @override + String get color_pink => 'Rosa'; + + @override + String get color_purple => 'Lila'; + + @override + String get color_red => 'Rot'; + + @override + String get color_teal => 'Türkis'; + + @override + String get color_yellow => 'Gelb'; + @override String could_not_add_player(Object playerName) { return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; } + @override + String get create_game => 'Spielvorlage erstellen'; + @override String get create_group => 'Gruppe erstellen'; @@ -68,7 +101,7 @@ class AppLocalizationsDe extends AppLocalizations { String get data_successfully_imported => 'Daten erfolgreich importiert'; @override - String days_ago(int count) { + String days_ago(Object count) { return 'vor $count Tagen'; } @@ -78,12 +111,35 @@ class AppLocalizationsDe extends AppLocalizations { @override String get delete_all_data => 'Alle Daten löschen'; + @override + String get delete_game => 'Spielvorlage löschen'; + + @override + String delete_game_with_matches_warning(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'werden $count Spiele', + one: 'wird 1 Spiel', + ); + return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; + } + @override String get delete_group => 'Gruppe löschen'; @override String get delete_match => 'Spiel löschen'; + @override + String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen'; + + @override + String get description => 'Beschreibung'; + + @override + String get edit_game => 'Spielvorlage bearbeiten'; + @override String get edit_group => 'Gruppe bearbeiten'; @@ -100,6 +156,10 @@ class AppLocalizationsDe extends AppLocalizations { String get error_creating_group => 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; + @override + String get error_deleting_game => + 'Fehler beim Löschen der Spielvorlage, bitte erneut versuchen'; + @override String get error_deleting_group => 'Fehler beim Löschen der Gruppe, bitte erneut versuchen'; @@ -111,6 +171,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get error_reading_file => 'Fehler beim Lesen der Datei'; + @override + String get exit_view => 'Ansicht verlassen'; + @override String get export_canceled => 'Export abgebrochen'; @@ -165,6 +228,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get licenses => 'Lizenzen'; + @override + String get live_edit_mode => 'Live-Bearbeitungsmodus'; + @override String get match_in_progress => 'Spiel läuft...'; @@ -186,6 +252,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get no_data_available => 'Keine Daten verfügbar'; + @override + String get no_games_created_yet => 'Noch keine Spielvorlagen erstellt'; + @override String get no_groups_created_yet => 'Noch keine Gruppen erstellt'; @@ -229,6 +298,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get not_available => 'Nicht verfügbar'; + @override + String get placement => 'Platzierung'; + + @override + String get place => 'Platz'; + @override String get played_matches => 'Gespielte Spiele'; @@ -238,11 +313,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get players => 'Spieler:innen'; - @override - String players_count(int count) { - return '$count Spieler'; - } - @override String get point => 'Punkt'; @@ -272,6 +342,10 @@ class AppLocalizationsDe extends AppLocalizations { String get ruleset_most_points => 'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.'; + @override + String get ruleset_placement => + 'Spieler:innen können in einer Reihenfolge angeordnet werden, die ihre Platzierung reflektiert.'; + @override String get ruleset_single_loser => 'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.'; @@ -292,6 +366,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get select_winner => 'Gewinner:in wählen'; + @override + String get select_winners => 'Gewinner:innen wählen'; + @override String get select_loser => 'Verlierer:in wählen'; @@ -330,6 +407,10 @@ class AppLocalizationsDe extends AppLocalizations { return 'Spieler:in $playerName erfolgreich hinzugefügt'; } + @override + String get there_are_no_games_matching_your_search => + 'Es gibt keine Spielvorlagen, die deiner Suche entspricht'; + @override String get there_is_no_group_matching_your_search => 'Es gibt keine Gruppe, die deiner Suche entspricht'; @@ -353,6 +434,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get winner => 'Gewinner:in'; + @override + String get winners => 'Gewinner:innen'; + @override String get winrate => 'Siegquote'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 2b42e47..48f054b 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -26,6 +26,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cancel => 'Cancel'; + @override + String get choose_color => 'Choose Color'; + @override String get choose_game => 'Choose Game'; @@ -35,11 +38,41 @@ class AppLocalizationsEn extends AppLocalizations { @override String get choose_ruleset => 'Choose Ruleset'; + @override + String get color => 'Color'; + + @override + String get color_blue => 'Blue'; + + @override + String get color_green => 'Green'; + + @override + String get color_orange => 'Orange'; + + @override + String get color_pink => 'Pink'; + + @override + String get color_purple => 'Purple'; + + @override + String get color_red => 'Red'; + + @override + String get color_teal => 'Teal'; + + @override + String get color_yellow => 'Yellow'; + @override String could_not_add_player(Object playerName) { return 'Could not add player'; } + @override + String get create_game => 'Create Game'; + @override String get create_group => 'Create Group'; @@ -68,7 +101,7 @@ class AppLocalizationsEn extends AppLocalizations { String get data_successfully_imported => 'Data successfully imported'; @override - String days_ago(int count) { + String days_ago(Object count) { return '$count days ago'; } @@ -78,12 +111,35 @@ class AppLocalizationsEn extends AppLocalizations { @override String get delete_all_data => 'Delete all data'; + @override + String get delete_game => 'Delete Game'; + + @override + String delete_game_with_matches_warning(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count matches', + one: '1 match', + ); + return 'If you delete this game template, $_temp0 using this game template will also be deleted.'; + } + @override String get delete_group => 'Delete Group'; @override String get delete_match => 'Delete Match'; + @override + String get drag_to_set_placement => 'Drag to set placement'; + + @override + String get description => 'Description'; + + @override + String get edit_game => 'Edit Game'; + @override String get edit_group => 'Edit Group'; @@ -100,6 +156,10 @@ class AppLocalizationsEn extends AppLocalizations { String get error_creating_group => 'Error while creating group, please try again'; + @override + String get error_deleting_game => + 'Error while deleting game, please try again'; + @override String get error_deleting_group => 'Error while deleting group, please try again'; @@ -111,6 +171,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get error_reading_file => 'Error reading file'; + @override + String get exit_view => 'Exit View'; + @override String get export_canceled => 'Export canceled'; @@ -165,6 +228,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get licenses => 'Licenses'; + @override + String get live_edit_mode => 'Live Edit Mode'; + @override String get match_in_progress => 'Match in progress...'; @@ -186,6 +252,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no_data_available => 'No data available'; + @override + String get no_games_created_yet => 'No games created yet'; + @override String get no_groups_created_yet => 'No groups created yet'; @@ -229,6 +298,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get not_available => 'Not available'; + @override + String get placement => 'Placement'; + + @override + String get place => 'place'; + @override String get played_matches => 'Played Matches'; @@ -238,11 +313,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get players => 'Players'; - @override - String players_count(int count) { - return '$count Players'; - } - @override String get point => 'Point'; @@ -272,6 +342,10 @@ class AppLocalizationsEn extends AppLocalizations { String get ruleset_most_points => 'Traditional ruleset: the player with the most points wins.'; + @override + String get ruleset_placement => + 'Players can be arranged in an order, which reflects their placement.'; + @override String get ruleset_single_loser => 'Exactly one loser is determined; last place receives the penalty or consequence.'; @@ -292,6 +366,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get select_winner => 'Select Winner'; + @override + String get select_winners => 'Select Winners'; + @override String get select_loser => 'Select Loser'; @@ -330,6 +407,10 @@ class AppLocalizationsEn extends AppLocalizations { return 'Successfully added player $playerName'; } + @override + String get there_are_no_games_matching_your_search => + 'There are no games matching your search'; + @override String get there_is_no_group_matching_your_search => 'There is no group matching your search'; @@ -352,6 +433,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get winner => 'Winner'; + @override + String get winners => 'Winners'; + @override String get winrate => 'Winrate'; diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 16316ad..bf6ded3 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -3,7 +3,6 @@ import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart'; -import 'package:tallee/presentation/views/main_menu/home_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view.dart'; @@ -31,7 +30,6 @@ class _CustomNavigationBarState extends State final loc = AppLocalizations.of(context); // Pretty ugly but works final List tabs = [ - KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()), KeyedSubtree( key: ValueKey('matches_$tabKeyCount'), child: const MatchView(), @@ -101,27 +99,20 @@ class _CustomNavigationBarState extends State NavbarItem( index: 0, isSelected: currentIndex == 0, - icon: Icons.home_rounded, - label: loc.home, - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 1, - isSelected: currentIndex == 1, icon: Icons.gamepad_rounded, label: loc.matches, onTabTapped: onTabTapped, ), NavbarItem( - index: 2, - isSelected: currentIndex == 2, + index: 1, + isSelected: currentIndex == 1, icon: Icons.group_rounded, label: loc.groups, onTabTapped: onTabTapped, ), NavbarItem( - index: 3, - isSelected: currentIndex == 3, + index: 2, + isSelected: currentIndex == 2, icon: Icons.bar_chart_rounded, label: loc.statistics, onTabTapped: onTabTapped, @@ -145,12 +136,10 @@ class _CustomNavigationBarState extends State final loc = AppLocalizations.of(context); switch (currentIndex) { case 0: - return loc.home; - case 1: return loc.matches; - case 2: + case 1: return loc.groups; - case 3: + case 2: return loc.statistics; default: return ''; diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index f88e2db..b4a5b97 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -172,12 +172,12 @@ class _CreateGroupViewState extends State { if (widget.groupToEdit!.name != groupName) { successfullNameChange = await db.groupDao.updateGroupName( groupId: widget.groupToEdit!.id, - newName: groupName, + name: groupName, ); } if (widget.groupToEdit!.members != selectedPlayers) { - successfullMemberChange = await db.groupDao.replaceGroupPlayers( + successfullMemberChange = await db.playerGroupDao.replaceGroupPlayers( groupId: widget.groupToEdit!.id, newPlayers: selectedPlayers, ); @@ -197,7 +197,7 @@ class _CreateGroupViewState extends State { /// obsolete. For each such match, the group association is removed by setting /// its [groupId] to null. Future deleteObsoleteMatchGroupRelations() async { - final groupMatches = await db.matchDao.getGroupMatches( + final groupMatches = await db.matchDao.getMatchesByGroup( groupId: widget.groupToEdit!.id, ); diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 92c3bba..3d5e805 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -244,7 +244,9 @@ class _GroupDetailViewState extends State { /// Loads statistics for this group Future _loadStatistics() async { isLoading = true; - final groupMatches = await db.matchDao.getGroupMatches(groupId: _group.id); + final groupMatches = await db.matchDao.getMatchesByGroup( + groupId: _group.id, + ); setState(() { totalMatches = groupMatches.length; diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart deleted file mode 100644 index 321f12b..0000000 --- a/lib/presentation/views/main_menu/home_view.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:tallee/core/adaptive_page_route.dart'; -import 'package:tallee/core/constants.dart'; -import 'package:tallee/core/enums.dart'; -import 'package:tallee/data/db/database.dart'; -import 'package:tallee/data/models/game.dart'; -import 'package:tallee/data/models/group.dart'; -import 'package:tallee/data/models/match.dart'; -import 'package:tallee/data/models/player.dart'; -import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; -import 'package:tallee/presentation/widgets/app_skeleton.dart'; -import 'package:tallee/presentation/widgets/buttons/quick_create_button.dart'; -import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; -import 'package:tallee/presentation/widgets/tiles/match_tile.dart'; -import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart'; - -class HomeView extends StatefulWidget { - /// The main home view of the application, displaying quick info, - /// recent matches, and quick create options. - const HomeView({super.key}); - - @override - State createState() => _HomeViewState(); -} - -class _HomeViewState extends State { - bool isLoading = true; - - /// Amount of matches in the database - int matchCount = 0; - - /// Amount of groups in the database - int groupCount = 0; - - /// Loaded recent matches from the database - List loadedRecentMatches = []; - - /// Recent matches to display, initially filled with skeleton matches - List recentMatches = List.filled( - 2, - Match( - name: 'Skeleton Match', - game: Game( - name: 'Skeleton Game', - ruleset: Ruleset.singleWinner, - description: 'This is a skeleton game description.', - color: GameColor.blue, - icon: '', - ), - group: Group( - name: 'Skeleton Group', - description: 'This is a skeleton group description.', - members: [ - Player( - name: - 'Skeleton Player 1' - '', - ), - Player( - name: - 'Skeleton Player 2' - '', - ), - ], - ), - notes: 'These are skeleton notes.', - players: [ - Player( - name: - 'Skeleton Player 1' - '', - ), - Player( - name: - 'Skeleton Player 2' - '', - ), - ], - ), - ); - - @override - void initState() { - super.initState(); - loadHomeViewData(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return AppSkeleton( - fixLayoutBuilder: true, - enabled: isLoading, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, - title: loc.matches, - icon: Icons.groups_rounded, - value: matchCount, - ), - SizedBox(width: constraints.maxWidth * 0.05), - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, - title: loc.groups, - icon: Icons.groups_rounded, - value: groupCount, - ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: InfoTile( - width: constraints.maxWidth * 0.95, - title: loc.recent_matches, - icon: Icons.history_rounded, - content: Column( - children: [ - if (recentMatches.isNotEmpty) - for (Match match in recentMatches) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 6.0, - ), - child: MatchTile( - compact: true, - width: constraints.maxWidth * 0.9, - match: match, - onTap: () async { - await Navigator.of(context).push( - adaptivePageRoute( - fullscreenDialog: true, - builder: (context) => - MatchResultView(match: match), - ), - ); - await loadRecentMatches(); - - setState(() { - print('loaded'); - }); - }, - ), - ) - else - Center( - heightFactor: 5, - child: Text(loc.no_recent_matches_available), - ), - ], - ), - ), - ), - Padding( - padding: EdgeInsets.zero, - child: InfoTile( - width: constraints.maxWidth * 0.95, - title: loc.quick_create, - icon: Icons.add_box_rounded, - content: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - QuickCreateButton( - text: 'Category 1', - onPressed: () {}, - ), - QuickCreateButton( - text: 'Category 2', - onPressed: () {}, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - QuickCreateButton( - text: 'Category 3', - onPressed: () {}, - ), - QuickCreateButton( - text: 'Category 4', - onPressed: () {}, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - QuickCreateButton( - text: 'Category 5', - onPressed: () {}, - ), - QuickCreateButton( - text: 'Category 6', - onPressed: () {}, - ), - ], - ), - ], - ), - ), - ), - SizedBox(height: MediaQuery.paddingOf(context).bottom), - ], - ), - ), - ); - }, - ); - } - - /// Loads the data for the HomeView from the database. - /// This includes the match count, group count, and recent matches. - Future loadHomeViewData() async { - final db = Provider.of(context, listen: false); - Future.wait([ - db.matchDao.getMatchCount(), - db.groupDao.getGroupCount(), - db.matchDao.getAllMatches(), - Future.delayed(Constants.MINIMUM_SKELETON_DURATION), - ]).then((results) { - matchCount = results[0] as int; - groupCount = results[1] as int; - loadedRecentMatches = results[2] as List; - recentMatches = - (loadedRecentMatches - ..sort((a, b) => b.createdAt.compareTo(a.createdAt))) - .take(2) - .toList(); - if (mounted) { - setState(() { - isLoading = false; - }); - } - }); - } - - Future loadRecentMatches() async { - final db = Provider.of(context, listen: false); - final matches = await db.matchDao.getAllMatches(); - recentMatches = - (matches..sort((a, b) => b.createdAt.compareTo(a.createdAt))) - .take(2) - .toList(); - } -} diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 51512f9..c019213 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -1,19 +1,26 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game_view.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; -import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; +import 'package:tallee/presentation/widgets/top_centered_message.dart'; class ChooseGameView extends StatefulWidget { /// A view that allows the user to choose a game from a list of available games - /// - [games]: A list of tuples containing the game name, description and ruleset - /// - [initialGameIndex]: The index of the initially selected game + /// - [games]: The list of available games + /// - [initialGameId]: The id of the initially selected game + /// - [onGamesUpdated]: Optional callback invoked when the games are updated const ChooseGameView({ super.key, required this.games, required this.initialGameId, + this.onGamesUpdated, }); /// A list of tuples containing the game name, description and ruleset @@ -22,20 +29,37 @@ class ChooseGameView extends StatefulWidget { /// The id of the initially selected game final String initialGameId; + /// Optional callback invoked when the games are updated + final VoidCallback? onGamesUpdated; + @override State createState() => _ChooseGameViewState(); } class _ChooseGameViewState extends State { + late final AppDatabase db; + + late List<(Game, int)> gameCounts = []; + /// Controller for the search bar final TextEditingController searchBarController = TextEditingController(); /// Currently selected game index late String selectedGameId; + /// Games filtered according to the current search query + late List filteredGames; + @override void initState() { + db = Provider.of(context, listen: false); + fetchGameCounts(); + selectedGameId = widget.initialGameId; + + // Start with all games visible + filteredGames = List.from(widget.games); + super.initState(); } @@ -58,6 +82,30 @@ class _ChooseGameViewState extends State { ); }, ), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () async { + final result = await Navigator.push( + context, + adaptivePageRoute( + builder: (context) => CreateGameView( + onGameChanged: () { + widget.onGamesUpdated?.call(); + }, + ), + ), + ); + if (result != null && result.game != null) { + setState(() { + widget.games.insert(0, result.game); + }); + _refreshFromSource(); + } + }, + ), + ], + title: Text(loc.choose_game), ), body: PopScope( @@ -72,37 +120,101 @@ class _ChooseGameViewState extends State { }, child: Column( children: [ + // Search Bar Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: CustomSearchBar( controller: searchBarController, hintText: loc.game_name, + onChanged: (value) { + _applySearchFilter(value); + }, ), ), const SizedBox(height: 5), + + // Game list Expanded( - child: ListView.builder( - itemCount: widget.games.length, - itemBuilder: (BuildContext context, int index) { - return TitleDescriptionListTile( - title: widget.games[index].name, - description: widget.games[index].description, - badgeText: translateRulesetToString( - widget.games[index].ruleset, + child: Visibility( + visible: filteredGames.isNotEmpty, + replacement: Visibility( + visible: widget.games.isNotEmpty, + replacement: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: loc.no_games_created_yet, + ), + child: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: AppLocalizations.of( context, - ), - isHighlighted: selectedGameId == widget.games[index].id, - onPressed: () async { - setState(() { - if (selectedGameId != widget.games[index].id) { - selectedGameId = widget.games[index].id; - } else { - selectedGameId = ''; + ).there_are_no_games_matching_your_search, + ), + ), + child: ListView.builder( + itemCount: filteredGames.length, + itemBuilder: (BuildContext context, int index) { + final game = filteredGames[index]; + return GameTile( + title: game.name, + description: game.description, + badgeText: translateRulesetToString( + game.ruleset, + context, + ), + badgeColor: getColorFromGameColor(game.color), + isHighlighted: selectedGameId == game.id, + onTap: () async { + setState(() { + if (selectedGameId == game.id) { + selectedGameId = ''; + } else { + selectedGameId = game.id; + } + }); + }, + onLongPress: () async { + final result = await Navigator.push( + context, + adaptivePageRoute( + builder: (context) => CreateGameView( + gameToEdit: game, + matchCount: getMatchCount(game), + onGameChanged: () { + widget.onGamesUpdated?.call(); + }, + ), + ), + ); + if (result != null && result.game != null) { + // Find the index in the original list to mutate + final originalIndex = widget.games.indexWhere( + (g) => g.id == game.id, + ); + if (originalIndex == -1) { + return; + } + if (result.delete) { + setState(() { + // deselect the game + if (selectedGameId == game.id) { + selectedGameId = ''; + } + widget.games.removeAt(originalIndex); + widget.onGamesUpdated?.call(); + }); + } else { + setState(() { + widget.games[originalIndex] = result.game; + }); + } + _refreshFromSource(); } - }); - }, - ); - }, + }, + ); + }, + ), ), ), ], @@ -110,4 +222,39 @@ class _ChooseGameViewState extends State { ), ); } + + /// Applies the search filter to the games list based on [query]. + void _applySearchFilter(String query) { + final q = query.toLowerCase().trim(); + if (q.isEmpty) { + setState(() { + filteredGames = List.from(widget.games); + }); + return; + } + + setState(() { + filteredGames = widget.games.where((game) { + final name = game.name.toLowerCase(); + final description = game.description.toLowerCase(); + return name.contains(q) || description.contains(q); + }).toList(); + }); + } + + /// Re-applies the current filter after the underlying games list changed. + void _refreshFromSource() { + _applySearchFilter(searchBarController.text); + } + + Future fetchGameCounts() async { + gameCounts = await db.gameDao.getGameUsage(); + } + + // Returns the number of matches that use the given [game]. + int getMatchCount(Game game) { + return gameCounts + .firstWhere((gc) => gc.$1.id == game.id, orElse: () => (game, 0)) + .$2; + } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart new file mode 100644 index 0000000..3156476 --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -0,0 +1,505 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_popup/flutter_popup.dart'; +import 'package:provider/provider.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; +import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; +import 'package:tallee/presentation/widgets/tiles/choose_tile.dart'; + +/// A stateful widget for creating or editing a game. +/// - [gameToEdit] An optional game to prefill the fields +/// - [onGameChanged] Callback to invoke when the game is created or edited +class CreateGameView extends StatefulWidget { + const CreateGameView({ + super.key, + required this.onGameChanged, + this.gameToEdit, + this.matchCount = 0, + }); + + /// Callback to invoke when the game is created or edited + final VoidCallback onGameChanged; + + /// An optional game to prefill the fields + final Game? gameToEdit; + + final int matchCount; + + @override + State createState() => _CreateGameViewState(); +} + +class _CreateGameViewState extends State { + /// GlobalKey for ScaffoldMessenger to show snackbars + final _scaffoldMessengerKey = GlobalKey(); + + late final AppDatabase db; + + late List<(Ruleset, String)> _rulesets; + late List<(GameColor, String)> _colors; + + Ruleset? selectedRuleset = Ruleset.singleWinner; + GameColor? selectedColor = GameColor.orange; + + /// Controller for the game name input field. + final _gameNameController = TextEditingController(); + + /// Controller for the game description input field. + final _descriptionController = TextEditingController(); + + /// The ID of the currently selected group. + late String selectedGroupId; + + /// A controller for the search bar input field. + final TextEditingController controller = TextEditingController(); + + /// A list of groups filtered based on the search query. + late final List filteredGroups; + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + _gameNameController.addListener(() => setState(() {})); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _rulesets = List.generate( + Ruleset.values.length, + (index) => ( + Ruleset.values[index], + translateRulesetToString(Ruleset.values[index], context), + ), + ); + _colors = List.generate( + GameColor.values.length, + (index) => ( + GameColor.values[index], + translateGameColorToString(GameColor.values[index], context), + ), + ); + + if (widget.gameToEdit != null) { + _gameNameController.text = widget.gameToEdit!.name; + _descriptionController.text = widget.gameToEdit!.description; + selectedRuleset = widget.gameToEdit!.ruleset; + selectedColor = widget.gameToEdit!.color; + selectedRuleset = widget.gameToEdit!.ruleset; + } + } + + @override + void dispose() { + _gameNameController.dispose(); + _descriptionController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var loc = AppLocalizations.of(context); + final isEditing = widget.gameToEdit != null; + + return ScaffoldMessenger( + child: Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + title: Text(isEditing ? loc.edit_game : loc.create_game), + actions: [ + if (isEditMode()) + IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + if (!context.mounted) return; + + // Build the dialog content based on match count + final String dialogContent = widget.matchCount > 0 + ? loc.delete_game_with_matches_warning(widget.matchCount) + : loc.this_cannot_be_undone; + + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: loc.delete_game, + content: Text( + dialogContent, + style: const TextStyle(fontSize: 15), + ), + actions: [ + CustomDialogAction( + isDestructive: true, + onPressed: () => Navigator.of(context).pop(true), + text: loc.delete, + ), + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(false), + buttonType: ButtonType.secondary, + text: loc.cancel, + ), + ], + ), + ).then((confirmed) async { + if (confirmed == true && context.mounted) { + // Delete assocaited matches + if (widget.matchCount > 0) { + await db.matchDao.deleteMatchesByGame( + gameId: widget.gameToEdit!.id, + ); + } + + // Delete the targetted game + bool success = await db.gameDao.deleteGame( + gameId: widget.gameToEdit!.id, + ); + + if (!context.mounted) return; + if (success) { + widget.onGameChanged.call(); + Navigator.of( + context, + ).pop((game: widget.gameToEdit, delete: true)); + } else { + if (!mounted) return; + showSnackbar(message: loc.error_deleting_game); + } + } + }); + }, + ), + ], + ), + body: SafeArea( + child: Column( + children: [ + // Game name input field + Container( + margin: CustomTheme.tileMargin, + child: TextInputField( + controller: _gameNameController, + maxLength: Constants.MAX_MATCH_NAME_LENGTH, + hintText: loc.game_name, + ), + ), + + // Choose ruleset tile + if (!isEditMode()) + ChooseTile( + title: loc.ruleset, + trailing: getRulesetDropdown(loc), + ), + + // Choose color tile + ChooseTile(title: loc.color, trailing: getColorDropdown(loc)), + + // Description input field + Container( + margin: CustomTheme.tileMargin, + child: TextInputField( + controller: _descriptionController, + hintText: loc.description, + minLines: 6, + maxLines: 6, + maxLength: Constants.MAX_GAME_DESCRIPTION_LENGTH, + showCounterText: true, + ), + ), + + const Spacer(), + + // Create/Edit game button + Padding( + padding: const EdgeInsets.all(12.0), + child: CustomWidthButton( + text: isEditing ? loc.edit_game : loc.create_game, + sizeRelativeToWidth: 1, + buttonType: ButtonType.primary, + onPressed: + _gameNameController.text.trim().isNotEmpty && + selectedRuleset != null && + selectedColor != null + ? () async { + Game newGame = Game( + name: _gameNameController.text.trim(), + description: _descriptionController.text.trim(), + ruleset: selectedRuleset!, + color: selectedColor!, + ); + if (isEditing) { + await handleGameUpdate(newGame); + } else { + await handleGameCreation(newGame); + } + widget.onGameChanged.call(); + if (context.mounted) { + Navigator.of( + context, + ).pop((game: newGame, delete: false)); + } + } + : null, + ), + ), + ], + ), + ), + ), + ); + } + + /// Handles updating an existing game in the database. + /// + /// [newGame] The updated game object. + Future handleGameUpdate(Game newGame) async { + final oldGame = widget.gameToEdit!; + + if (oldGame.name != newGame.name) { + await db.gameDao.updateGameName(gameId: oldGame.id, name: newGame.name); + } + + if (oldGame.description != newGame.description) { + await db.gameDao.updateGameDescription( + gameId: oldGame.id, + description: newGame.description, + ); + } + + if (oldGame.ruleset != newGame.ruleset) { + await db.gameDao.updateGameRuleset( + gameId: oldGame.id, + ruleset: newGame.ruleset, + ); + } + + if (oldGame.color != newGame.color) { + await db.gameDao.updateGameColor( + gameId: oldGame.id, + color: newGame.color, + ); + } + + if (oldGame.icon != newGame.icon) { + await db.gameDao.updateGameIcon(gameId: oldGame.id, icon: newGame.icon); + } + } + + /// Handles creating a new game in the database. + /// + /// [newGame] The game object to be created. + Future handleGameCreation(Game newGame) async { + await db.gameDao.addGame(game: newGame); + } + + /// Displays a snackbar with the given message and optional action. + /// + /// [message] The message to display in the snackbar. + void showSnackbar({required String message}) { + final messenger = _scaffoldMessengerKey.currentState; + if (messenger != null) { + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message, style: const TextStyle(color: Colors.white)), + backgroundColor: CustomTheme.boxColor, + ), + ); + } + } + + bool isEditMode() { + return widget.gameToEdit != null; + } + + Widget getRulesetDropdown(AppLocalizations loc) { + return CustomPopup( + showArrow: true, + arrowColor: CustomTheme.boxBorderColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10), + barrierColor: Colors.transparent, + contentDecoration: CustomTheme.standardBoxDecoration, + content: StatefulBuilder( + builder: (context, setPopupState) => SizedBox( + width: 280, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + _rulesets.length, + (index) => GestureDetector( + onTap: () { + setState(() { + selectedRuleset = _rulesets[index].$1; + }); + setPopupState(() {}); + }, + child: Column( + children: [ + Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + color: selectedRuleset == _rulesets[index].$1 + ? CustomTheme.textColor.withAlpha(20) + : Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: Row( + spacing: 8, + children: [ + Icon(getRulesetIcon(_rulesets[index].$1), size: 16), + Text( + _rulesets[index].$2, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 15, + ), + ), + ], + ), + ), + ), + if (index < _rulesets.length - 1) + const Divider(indent: 15, endIndent: 15), + ], + ), + ), + ), + ), + ), + ), + child: Row( + spacing: 8, + children: [ + Icon(getRulesetIcon(selectedRuleset!), size: 16), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Text( + translateRulesetToString(selectedRuleset!, context), + textAlign: TextAlign.right, + ), + ), + Transform.rotate( + angle: pi / 2, + child: const Icon(Icons.arrow_forward_ios, size: 16), + ), + ], + ), + ); + } + + Widget getColorDropdown(AppLocalizations loc) { + return CustomPopup( + showArrow: true, + arrowColor: CustomTheme.boxBorderColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10), + barrierColor: Colors.transparent, + contentDecoration: CustomTheme.standardBoxDecoration, + content: StatefulBuilder( + builder: (context, setPopupState) => SizedBox( + width: 150, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + _colors.length, + (index) => GestureDetector( + onTap: () { + setState(() { + selectedColor = _colors[index].$1; + }); + setPopupState(() {}); + }, + child: Column( + children: [ + // Selected Highlighting + Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + color: selectedColor == _colors[index].$1 + ? CustomTheme.textColor.withAlpha(20) + : Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + spacing: 8, + children: selectedColor == null + ? [Text(loc.none)] + : [ + Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(left: 12), + decoration: BoxDecoration( + color: getColorFromGameColor( + _colors[index].$1, + ), + shape: BoxShape.circle, + ), + ), + Text( + _colors[index].$2, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 15, + ), + ), + ], + ), + ), + ), + if (index < _colors.length - 1) + const Divider(indent: 15, endIndent: 15), + ], + ), + ), + ), + ), + ), + ), + child: Row( + spacing: 8, + children: [ + // Selected Color + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: getColorFromGameColor(selectedColor!), + shape: BoxShape.circle, + ), + ), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Text(translateGameColorToString(selectedColor!, context)), + ), + Transform.rotate( + angle: pi / 2, + child: const Icon(Icons.arrow_forward_ios, size: 16), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 1a04c78..fd98691 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -28,10 +28,13 @@ class CreateMatchView extends StatefulWidget { this.onWinnerChanged, this.matchToEdit, this.onMatchUpdated, + this.onMatchesUpdated, }); final VoidCallback? onWinnerChanged; + final VoidCallback? onMatchesUpdated; + final void Function(Match)? onMatchUpdated; /// An optional match to prefill the fields for editing. @@ -115,6 +118,7 @@ class _CreateMatchViewState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ + // Match name input field. Container( margin: CustomTheme.tileMargin, child: TextInputField( @@ -123,34 +127,40 @@ class _CreateMatchViewState extends State { maxLength: Constants.MAX_MATCH_NAME_LENGTH, ), ), - ChooseTile( - title: loc.game, - trailingText: selectedGame == null - ? loc.none_group - : selectedGame!.name, - onPressed: () async { - selectedGame = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseGameView( - games: gamesList, - initialGameId: selectedGame?.id ?? '', + + // Game selection tile. + if (!isEditMode()) + ChooseTile( + title: loc.game, + trailing: selectedGame == null + ? Text(loc.none_group) + : Text(selectedGame!.name), + onPressed: () async { + selectedGame = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => ChooseGameView( + games: gamesList, + initialGameId: selectedGame?.id ?? '', + onGamesUpdated: widget.onMatchesUpdated, + ), ), - ), - ); - setState(() { - if (selectedGame != null) { - hintText = selectedGame!.name; - } else { - hintText = loc.match_name; - } - }); - }, - ), + ); + setState(() { + if (selectedGame != null) { + hintText = selectedGame!.name; + } else { + hintText = loc.match_name; + } + }); + }, + ), + + // Group selection tile. ChooseTile( title: loc.group, - trailingText: selectedGroup == null - ? loc.none_group - : selectedGroup!.name, + trailing: selectedGroup == null + ? Text(loc.none_group) + : Text(selectedGroup!.name), onPressed: () async { // Remove all players from the previously selected group from // the selected players list, in case the user deselects the @@ -181,6 +191,8 @@ class _CreateMatchViewState extends State { }); }, ), + + // Player selection widget. Expanded( child: PlayerSelection( key: ValueKey(selectedGroup?.id ?? 'no_group'), @@ -193,6 +205,8 @@ class _CreateMatchViewState extends State { }, ), ), + + // Create or save button. CustomWidthButton( text: buttonText, sizeRelativeToWidth: 0.95, @@ -217,17 +231,17 @@ class _CreateMatchViewState extends State { /// Determines whether the "Create Match" button should be enabled. /// /// Returns `true` if: - /// - A ruleset is selected AND - /// - Either a group is selected OR at least 2 players are selected + /// - A game is selected AND + /// - Either a group is selected OR at least 2 players are selected. bool _enableCreateGameButton() { - return (selectedGroup != null || - (selectedPlayers.length > 1) && selectedGame != null); + return ((selectedGroup != null || selectedPlayers.length > 1) && + selectedGame != null); } - // If a match was provided to the view, it updates the match in the database - // and navigates back to the previous screen. - // If no match was provided, it creates a new match in the database and - // navigates to the MatchResultView for the newly created match. + /// Handles navigation when the create or save button is pressed. + /// + /// If a match is being edited, updates the match in the database. + /// Otherwise, creates a new match and navigates to the MatchResultView. void buttonNavigation(BuildContext context) async { if (isEditMode()) { await updateMatch(); @@ -252,8 +266,7 @@ class _CreateMatchViewState extends State { } } - /// Updates attributes of the existing match in the database based on the - /// changes made in the edit view. + /// Updates the existing match in the database. Future updateMatch() async { final updatedMatch = Match( id: widget.matchToEdit!.id, @@ -262,7 +275,7 @@ class _CreateMatchViewState extends State { : _matchNameController.text.trim(), group: selectedGroup, players: selectedPlayers, - game: widget.matchToEdit!.game, + game: selectedGame!, createdAt: widget.matchToEdit!.createdAt, endedAt: widget.matchToEdit!.endedAt, notes: widget.matchToEdit!.notes, @@ -271,14 +284,14 @@ class _CreateMatchViewState extends State { if (widget.matchToEdit!.name != updatedMatch.name) { await db.matchDao.updateMatchName( matchId: widget.matchToEdit!.id, - newName: updatedMatch.name, + name: updatedMatch.name, ); } if (widget.matchToEdit!.group?.id != updatedMatch.group?.id) { await db.matchDao.updateMatchGroup( matchId: widget.matchToEdit!.id, - newGroupId: updatedMatch.group?.id, + groupId: updatedMatch.group?.id, ); } diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 2117b77..73d534d 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.dart'; @@ -14,6 +15,7 @@ import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; +import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -102,6 +104,7 @@ class _MatchDetailViewState extends State { bottom: 100, ), children: [ + // Controller Icon const Center( child: ColoredIconContainer( icon: Icons.sports_esports, @@ -110,6 +113,8 @@ class _MatchDetailViewState extends State { ), ), const SizedBox(height: 10), + + // Match Name Text( match.name, style: const TextStyle( @@ -120,6 +125,8 @@ class _MatchDetailViewState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 5), + + // Creation Date Text( '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}', style: const TextStyle( @@ -129,6 +136,8 @@ class _MatchDetailViewState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 10), + + // Group Name if (match.group != null) ...[ Row( mainAxisAlignment: MainAxisAlignment.center, @@ -143,6 +152,8 @@ class _MatchDetailViewState extends State { ), const SizedBox(height: 20), ], + + // Players InfoTile( title: loc.players, icon: Icons.people, @@ -162,6 +173,30 @@ class _MatchDetailViewState extends State { ), ), const SizedBox(height: 15), + + // Game + InfoTile( + title: loc.game, + icon: RpgAwesome.clovers_card, + horizontalAlignment: CrossAxisAlignment.start, + content: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ), + child: GameLabel( + title: match.game.name, + description: translateRulesetToString( + match.game.ruleset, + context, + ), + color: match.game.color, + ), + ), + ), + const SizedBox(height: 15), + + // Results InfoTile( title: loc.results, icon: Icons.emoji_events, @@ -205,7 +240,9 @@ class _MatchDetailViewState extends State { match: match, onWinnerChanged: () { widget.onMatchUpdate.call(); - setState(() {}); + setState(() { + updateScoresForCurrentMatch(); + }); }, ), ), @@ -234,103 +271,180 @@ class _MatchDetailViewState extends State { Widget getResultWidget(AppLocalizations loc) { if (isSingleRowResult()) { return Row( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: getSingleResultRow(loc), ); } else { - return getScoreResultWidget(loc); + return getMultiResultRows(loc); } } /// Returns the result row for single winner/loser rulesets or a placeholder /// if no result is entered yet List getSingleResultRow(AppLocalizations loc) { - // Single Winner - if (match.mvp.isNotEmpty && match.game.ruleset == Ruleset.singleWinner) { - return [ - Text( - loc.winner, - style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), - ), - Text( - match.mvp.first.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, - ), - ), - ]; - // Single Loser - } else if (match.game.ruleset == Ruleset.singleLoser) { - return [ - Text( - loc.loser, - style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), - ), - Text( - match.mvp.first.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, - ), - ), - ]; - // No result entered yet - } else { - return [ - Text( - loc.no_results_entered_yet, - style: const TextStyle(fontSize: 14, color: CustomTheme.textColor), - ), - ]; - } - } + if (match.mvp.isNotEmpty) { + final ruleset = match.game.ruleset; - /// Returns the result widget for scores - Widget getScoreResultWidget(AppLocalizations loc) { - List<(String, int)> playerScores = []; - for (var player in match.players) { - int score = match.scores[player.id]?.score ?? 0; - playerScores.add((player.name, score)); - } - if (widget.match.game.ruleset == Ruleset.highestScore) { - playerScores.sort((a, b) => b.$2.compareTo(a.$2)); - } else if (widget.match.game.ruleset == Ruleset.lowestScore) { - playerScores.sort((a, b) => a.$2.compareTo(b.$2)); - } - - return Column( - children: [ - for (var score in playerScores) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - score.$1, - style: const TextStyle( - fontSize: 16, - color: CustomTheme.textColor, - ), - ), - Text( - getPointLabel(loc, score.$2), + if (ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser) { + return [ + Text( + ruleset == Ruleset.singleWinner ? loc.winner : loc.loser, + style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), + ), + Text( + match.mvp.first.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), + ), + ]; + } else if (match.game.ruleset == Ruleset.multipleWinners) { + return [ + Text( + loc.winners, + style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), + ), + Flexible( + child: Container( + padding: const EdgeInsets.only(left: 10), + child: Text( + match.mvp.map((player) => player.name).join(', '), + textAlign: TextAlign.end, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: CustomTheme.primaryColor, ), ), + ), + ), + ]; + } + } + + // No results yet + return [ + Text( + loc.no_results_entered_yet, + style: const TextStyle(fontSize: 14, color: CustomTheme.textColor), + ), + ]; + } + + /// Returns the result widget for scores or placement + Widget getMultiResultRows(AppLocalizations loc) { + List<(String, int)> playerScores = []; + for (var player in match.players) { + int score = match.scores[player.id]?.score ?? 0; + playerScores.add((player.name, score)); + } + + final ruleset = match.game.ruleset; + + if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) { + playerScores.sort((a, b) => b.$2.compareTo(a.$2)); + } else if (ruleset == Ruleset.lowestScore) { + playerScores.sort((a, b) => a.$2.compareTo(b.$2)); + } + + return Column( + children: [ + for (var i = 0; i < playerScores.length; i++) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + playerScores[i].$1, + style: const TextStyle( + fontSize: 16, + color: CustomTheme.textColor, + ), + ), + getResultValueText(loc, i, playerScores[i].$2), ], ), ], ); } + Widget getResultValueText(AppLocalizations loc, int index, int score) { + final ruleset = match.game.ruleset; + + if (ruleset == Ruleset.placement) { + return Text( + getPlacementText(context, index + 1), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getPlacementTextcolor(index), + ), + ); + } else { + return Text( + getPointLabel(loc, score), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), + ); + } + } + + Color getPlacementTextcolor(int placement) { + switch (placement) { + case 0: + return const Color(0xFFFFBF00); + case 1: + return const Color(0xBBFFFFFF); + case 2: + return const Color(0xFFCD7F32); + default: + return CustomTheme.textColor; + } + } + // Returns if the result can be displayed in a single row bool isSingleRowResult() { return match.game.ruleset == Ruleset.singleWinner || - match.game.ruleset == Ruleset.singleLoser; + match.game.ruleset == Ruleset.singleLoser || + match.game.ruleset == Ruleset.multipleWinners; + } + + String getPlacementText(BuildContext context, int rank) { + final loc = AppLocalizations.of(context); + final locale = Localizations.localeOf(context).languageCode; + + if (locale == 'de') { + return '$rank. ${loc.place}'; + } + + return '${_ordinalEn(rank)} ${loc.place}'; + } + + String _ordinalEn(int number) { + if (number % 100 >= 11 && number % 100 <= 13) { + return '${number}th'; + } + + switch (number % 10) { + case 1: + return '${number}st'; + case 2: + return '${number}nd'; + case 3: + return '${number}rd'; + default: + return '${number}th'; + } + } + + void updateScoresForCurrentMatch() { + db.scoreEntryDao + .getAllMatchScores(matchId: match.id) + .then((scores) => match.scores = scores); } } diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 8b41920..ba138d6 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -8,8 +8,11 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart'; -import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_list_tile.dart'; class MatchResultView extends StatefulWidget { /// A view that allows selecting and saving the winner of a match @@ -30,6 +33,8 @@ class MatchResultView extends StatefulWidget { class _MatchResultViewState extends State { late final AppDatabase db; + bool isLiveEditMode = false; + late final Ruleset ruleset; /// List of all players who participated in the match @@ -38,11 +43,15 @@ class _MatchResultViewState extends State { /// List of text controllers for score entry, one for each player late final List controller; + /// Flag to indicate if the save button should be enabled late bool canSave; - /// Currently selected winner player + /// Currently selected player (single winner / looser) Player? _selectedPlayer; + /// Currently selected players (multiple winners) + final Set _selectedPlayers = {}; + @override void initState() { db = Provider.of(context, listen: false); @@ -57,17 +66,32 @@ class _MatchResultViewState extends State { (index) => TextEditingController()..addListener(() => onTextEnter()), ); + // Prefill fields if (widget.match.mvp.isNotEmpty) { - if (rulesetSupportsWinnerSelection()) { - _selectedPlayer = allPlayers.firstWhere( - (p) => p.id == widget.match.mvp.first.id, - ); + if (rulesetSupportsPlayerSelection()) { + if (ruleset == Ruleset.multipleWinners) { + for (int i = 0; i < allPlayers.length; i++) { + if (widget.match.scores[allPlayers[i].id]?.score == 1) { + _selectedPlayers.add(allPlayers[i]); + } + } + } else { + _selectedPlayer = allPlayers.firstWhere( + (p) => p.id == widget.match.mvp.first.id, + ); + } } else if (rulesetSupportsScoreEntry()) { for (int i = 0; i < allPlayers.length; i++) { final scoreList = widget.match.scores[allPlayers[i].id]; final score = scoreList?.score ?? 0; controller[i].text = score.toString(); } + } else if (rulesetSupportsDragBehaviour()) { + allPlayers.sort((a, b) { + final scoreA = widget.match.scores[a.id]?.score ?? 0; + final scoreB = widget.match.scores[b.id]?.score ?? 0; + return scoreB.compareTo(scoreA); + }); } super.initState(); } @@ -101,86 +125,262 @@ class _MatchResultViewState extends State { child: Column( children: [ Expanded( - child: Container( - margin: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 10, - ), - decoration: BoxDecoration( - color: CustomTheme.boxColor, - border: Border.all(color: CustomTheme.boxBorderColor), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${getTitleForRuleset(loc)}:', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - if (rulesetSupportsWinnerSelection()) - Expanded( - child: RadioGroup( - groupValue: _selectedPlayer, - onChanged: (Player? value) async { + child: isLiveEditMode + // Live Edit Mode + ? ListView.builder( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return LiveEditListTile( + title: allPlayers[index].name, + onChanged: (value) { setState(() { - _selectedPlayer = value; + controller[index].text = value.toString(); }); }, - child: ListView.builder( - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return CustomRadioListTile( - text: allPlayers[index].name, - value: allPlayers[index], - onContainerTap: (value) async { - setState(() { - // Check if the already selected player is the same as the newly tapped player. - if (_selectedPlayer == value) { - // If yes deselected the player by setting it to null. - _selectedPlayer = null; - } else { - // If no assign the newly tapped player to the selected player. - (_selectedPlayer = value); - } - }); - }, - ); - }, + value: int.tryParse(controller[index].text) ?? 0, + ); + }, + ) + // Normal Container + : Container( + margin: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 10, + ), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorderColor), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + getTitleForRuleset(loc), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), - ), + const SizedBox(height: 10), + + // Show player selection + if (rulesetSupportsPlayerSelection()) + Expanded( + child: ruleset == Ruleset.multipleWinners + // Multiple winners + ? ListView.builder( + physics: + const NeverScrollableScrollPhysics(), + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomCheckboxListTile( + text: allPlayers[index].name, + value: _selectedPlayers.contains( + allPlayers[index], + ), + onChanged: (bool value) { + setState(() { + if (value) { + _selectedPlayers.add( + allPlayers[index], + ); + } else { + _selectedPlayers.remove( + allPlayers[index], + ); + } + }); + }, + ); + }, + ) + // Single winner / looser + : RadioGroup( + groupValue: _selectedPlayer, + onChanged: (Player? value) async { + setState(() { + _selectedPlayer = value; + }); + }, + child: ListView.builder( + physics: + const NeverScrollableScrollPhysics(), + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomRadioListTile( + text: allPlayers[index].name, + value: allPlayers[index], + onContainerTap: (value) async { + setState(() { + // Check if the already selected player is the same as the newly tapped player. + if (_selectedPlayer == value) { + // If yes deselected the player by setting it to null. + _selectedPlayer = null; + } else { + // If no assign the newly tapped player to the selected player. + (_selectedPlayer = value); + } + }); + }, + ); + }, + ), + ), + ), + + // Show score entry + if (rulesetSupportsScoreEntry()) + Expanded( + child: ListView.separated( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return ScoreListTile( + text: allPlayers[index].name, + controller: controller[index], + ); + }, + separatorBuilder: + (BuildContext context, int index) { + return const Padding( + padding: EdgeInsets.symmetric( + vertical: 8.0, + ), + child: Divider(indent: 20), + ); + }, + ), + ), + + // Show draggable placement list + if (rulesetSupportsDragBehaviour()) + Expanded( + child: Row( + children: [ + // Placement indicators + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Column( + children: [ + for ( + int i = 0; + i < allPlayers.length; + i++ + ) + Container( + alignment: Alignment.center, + height: 60, + child: Container( + decoration: BoxDecoration( + color: + CustomTheme.boxBorderColor, + borderRadius: CustomTheme + .standardBorderRadiusAll, + ), + alignment: Alignment.center, + height: 50, + width: 50, + child: Text( + ' #${i + 1} ', + style: const TextStyle( + color: CustomTheme.textColor, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ), + ], + ), + ), + + // Drag list + Expanded( + child: ReorderableListView.builder( + physics: + const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + proxyDecorator: (child, index, animation) { + return AnimatedBuilder( + animation: animation, + child: child, + builder: (context, child) { + final alpha = + (Curves.easeInOut.transform( + animation.value, + ) * + 40) + .toInt(); + return Stack( + children: [ + child!, + Positioned.fill( + left: 4, + top: 4, + right: 4, + bottom: 4, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white + .withAlpha(alpha), + borderRadius: CustomTheme + .standardBorderRadiusAll, + ), + ), + ), + ], + ); + }, + ); + }, + onReorder: (int oldIndex, int newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final Player item = allPlayers + .removeAt(oldIndex); + allPlayers.insert(newIndex, item); + }); + }, + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return TextIconListTile( + key: ValueKey(allPlayers[index].id), + text: allPlayers[index].name, + icon: Icons.drag_handle, + ); + }, + ), + ), + ], + ), + ), + ], ), - if (rulesetSupportsScoreEntry()) - Expanded( - child: ListView.separated( - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return ScoreListTile( - text: allPlayers[index].name, - controller: controller[index], - ); - }, - separatorBuilder: (BuildContext context, int index) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Divider(indent: 20), - ); - }, - ), - ), - ], - ), - ), + ), ), + + if (rulesetSupportsScoreEntry()) + // Button to switch to live edit mode + ...[ + CustomWidthButton( + text: isLiveEditMode ? loc.exit_view : loc.live_edit_mode, + sizeRelativeToWidth: 0.95, + buttonType: ButtonType.secondary, + onPressed: () => setState(() { + isLiveEditMode = !isLiveEditMode; + }), + ), + const SizedBox(height: 10), + ], + + // Save Changes Button CustomWidthButton( text: loc.save_changes, sizeRelativeToWidth: 0.95, @@ -222,12 +422,16 @@ class _MatchResultViewState extends State { } else if (ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore) { await _handleScores(); + } else if (ruleset == Ruleset.placement) { + await _handlePlacement(); + } else if (ruleset == Ruleset.multipleWinners) { + await _handleWinners(); } widget.onWinnerChanged?.call(); } - /// Handles saving or removing the winner in the database. + /// Handles saving or removing the (single) winner in the database. Future _handleWinner() async { if (_selectedPlayer == null) { return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); @@ -239,12 +443,24 @@ class _MatchResultViewState extends State { } } + /// Handles saving the (multiple) winners to the database. + Future _handleWinners() async { + if (_selectedPlayers.isEmpty) { + return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); + } else { + return await db.scoreEntryDao.setWinners( + matchId: widget.match.id, + winners: allPlayers.where((p) => _selectedPlayers.contains(p)).toList(), + ); + } + } + /// Handles saving or removing the loser in the database. Future _handleLoser() async { if (_selectedPlayer == null) { - return await db.scoreEntryDao.removeLooser(matchId: widget.match.id); + return await db.scoreEntryDao.removeLoser(matchId: widget.match.id); } else { - return await db.scoreEntryDao.setLooser( + return await db.scoreEntryDao.setLoser( matchId: widget.match.id, playerId: _selectedPlayer!.id, ); @@ -267,22 +483,40 @@ class _MatchResultViewState extends State { } } + /// Handles saving the placement for each player in the database. + Future _handlePlacement() async { + await db.scoreEntryDao.setPlacements( + matchId: widget.match.id, + players: allPlayers, + ); + } + String getTitleForRuleset(AppLocalizations loc) { switch (ruleset) { case Ruleset.singleWinner: return loc.select_winner; case Ruleset.singleLoser: return loc.select_loser; + case Ruleset.placement: + return loc.drag_to_set_placement; + case Ruleset.multipleWinners: + return loc.select_winners; default: return loc.enter_points; } } - bool rulesetSupportsWinnerSelection() { - return ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser; + bool rulesetSupportsPlayerSelection() { + return ruleset == Ruleset.singleWinner || + ruleset == Ruleset.singleLoser || + ruleset == Ruleset.multipleWinners; } bool rulesetSupportsScoreEntry() { return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore; } + + bool rulesetSupportsDragBehaviour() { + return ruleset == Ruleset.placement; + } } diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 2fb36e7..a7f60c6 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -10,6 +10,7 @@ import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_detail_view.dart'; @@ -30,8 +31,7 @@ class _MatchViewState extends State { late final AppDatabase db; bool isLoading = true; - /// Loaded matches from the database, - /// initially filled with skeleton matches + /// Loaded matches from the database, initially filled with skeleton matches List matches = List.filled( 4, Match( @@ -46,7 +46,15 @@ class _MatchViewState extends State { name: 'Group name', members: List.filled(5, Player(name: 'Player')), ), - players: [Player(name: 'Player')], + players: [ + Player(name: 'Player'), + Player(name: 'Player'), + Player(name: 'Player'), + Player(name: 'Player'), + Player(id: 'mvp_id', name: 'Player'), + ], + scores: {'mvp_id': ScoreEntry(score: 1)}, + endedAt: DateTime.now(), ), ); @@ -118,8 +126,10 @@ class _MatchViewState extends State { Navigator.push( context, adaptivePageRoute( - builder: (context) => - CreateMatchView(onWinnerChanged: loadMatches), + builder: (context) => CreateMatchView( + onWinnerChanged: loadMatches, + onMatchesUpdated: loadMatches, + ), ), ); }, diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 8811411..1cab79e 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -54,7 +54,9 @@ const allDependencies = [ _flutter, _flutter_lints, _flutter_localizations, + _flutter_numeric_text, _flutter_plugin_android_lifecycle, + _flutter_popup, _flutter_test, _flutter_web_plugins, _fluttericon, @@ -71,7 +73,6 @@ const allDependencies = [ _io, _jni, _jni_flutter, - _js, _json_annotation, _json_schema, _leak_tracker, @@ -160,6 +161,7 @@ const allDependencies = [ /// Direct `dependencies`. const dependencies = [ _clock, + _collection, _cupertino_icons, _drift, _drift_flutter, @@ -167,6 +169,8 @@ const dependencies = [ _file_saver, _flutter, _flutter_localizations, + _flutter_numeric_text, + _flutter_popup, _fluttericon, _font_awesome_flutter, _intl, @@ -239,13 +243,13 @@ class PackageRef { Package resolve() => allDependencies.firstWhere((d) => d.name == name); } -/// _fe_analyzer_shared 91.0.0 +/// _fe_analyzer_shared 92.0.0 const __fe_analyzer_shared = Package( name: '_fe_analyzer_shared', description: 'Logic that is shared between the front_end and analyzer packages.', repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/_fe_analyzer_shared', authors: [], - version: '91.0.0', + version: '92.0.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -280,13 +284,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// analyzer 8.4.1 +/// analyzer 9.0.0 const _analyzer = Package( name: 'analyzer', description: 'This package provides a library that performs static analysis of Dart code.', repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/analyzer', authors: [], - version: '8.4.1', + version: '9.0.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -444,13 +448,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// build 4.0.5 +/// build 4.0.6 const _build = Package( name: 'build', description: 'A package for authoring build_runner compatible code generators.', repository: 'https://github.com/dart-lang/build/tree/master/build', authors: [], - version: '4.0.5', + version: '4.0.6', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -567,13 +571,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// build_runner 2.14.0 +/// build_runner 2.15.0 const _build_runner = Package( name: 'build_runner', description: 'A build system for Dart code generation and modular compilation.', repository: 'https://github.com/dart-lang/build/tree/master/build_runner', authors: [], - version: '2.14.0', + version: '2.15.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -651,14 +655,14 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// built_value 8.12.5 +/// built_value 8.12.6 const _built_value = Package( name: 'built_value', description: '''Value types with builders, Dart classes as enums, and serialization. This library is the runtime dependency. ''', repository: 'https://github.com/google/built_value.dart/tree/master/built_value', authors: [], - version: '8.12.5', + version: '8.12.6', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -694,13 +698,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// characters 1.4.0 +/// characters 1.4.1 const _characters = Package( name: 'characters', description: 'String replacement with operations that are Unicode/grapheme cluster aware.', repository: 'https://github.com/dart-lang/core/tree/main/pkgs/characters', authors: [], - version: '1.4.0', + version: '1.4.1', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -2498,13 +2502,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// flutter 3.38.6 +/// flutter 3.41.0 const _flutter = Package( name: 'flutter', description: 'A framework for writing Flutter applications', homepage: 'https://flutter.dev', authors: [], - version: '3.38.6', + version: '3.41.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: true, @@ -2588,6 +2592,42 @@ const _flutter_localizations = Package( devDependencies: [PackageRef('flutter_test')], ); +/// flutter_numeric_text 1.3.3 +const _flutter_numeric_text = Package( + name: 'flutter_numeric_text', + description: 'This widget allows you to animate any text. The widget is easy to use and allows you to seamlessly replace Text(data) with NumericText(data).', + homepage: 'https://github.com/strash/flutter_numeric_text', + repository: 'https://github.com/strash/flutter_numeric_text', + authors: [], + version: '1.3.3', + spdxIdentifiers: ['MIT'], + isMarkdown: false, + isSdk: false, + dependencies: [PackageRef('flutter')], + devDependencies: [PackageRef('flutter_test'), PackageRef('flutter_lints')], + license: '''MIT License + +Copyright (c) 2025 Strash One + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''', + ); + /// flutter_plugin_android_lifecycle 2.0.34 const _flutter_plugin_android_lifecycle = Package( name: 'flutter_plugin_android_lifecycle', @@ -2627,6 +2667,41 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); +/// flutter_popup 3.3.9 +const _flutter_popup = Package( + name: 'flutter_popup', + description: 'The flutter_popup package is a versatile tool for creating customizable popups in Flutter apps. Its highlight feature effectively guides user attention to specific areas', + homepage: 'https://github.com/herowws/flutter_popup', + authors: [], + version: '3.3.9', + spdxIdentifiers: ['MIT'], + isMarkdown: false, + isSdk: false, + dependencies: [PackageRef('flutter')], + devDependencies: [PackageRef('flutter_lints'), PackageRef('flutter_test')], + license: '''MIT License + +Copyright (c) 2023 mopriestt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''', + ); + /// flutter_test null const _flutter_test = Package( name: 'flutter_test', @@ -3216,47 +3291,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// js 0.7.2 -const _js = Package( - name: 'js', - description: 'Annotations to create static Dart interfaces for JavaScript APIs.', - repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/js', - authors: [], - version: '0.7.2', - spdxIdentifiers: ['BSD-3-Clause'], - isMarkdown: false, - isSdk: false, - dependencies: [], - devDependencies: [PackageRef('lints')], - license: '''Copyright 2012, the Dart project authors. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google LLC nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', - ); - /// json_annotation 4.11.0 const _json_annotation = Package( name: 'json_annotation', @@ -3595,18 +3629,18 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// matcher 0.12.17 +/// matcher 0.12.18 const _matcher = Package( name: 'matcher', description: 'Support for specifying test expectations via an extensible Matcher class. Also includes a number of built-in Matcher implementations for common cases.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/matcher', authors: [], - version: '0.12.17', + version: '0.12.18', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, dependencies: [PackageRef('async'), PackageRef('meta'), PackageRef('stack_trace'), PackageRef('term_glyph'), PackageRef('test_api')], - devDependencies: [PackageRef('fake_async'), PackageRef('lints'), PackageRef('test')], + devDependencies: [PackageRef('fake_async'), PackageRef('test')], license: '''Copyright 2014, the Dart project authors. Redistribution and use in source and binary forms, with or without @@ -3636,18 +3670,18 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// material_color_utilities 0.11.1 +/// material_color_utilities 0.13.0 const _material_color_utilities = Package( name: 'material_color_utilities', description: 'Algorithms and utilities that power the Material Design 3 color system, including choosing theme colors from images and creating tones of colors; all in a new color space.', repository: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart', authors: [], - version: '0.11.1', + version: '0.13.0', spdxIdentifiers: ['Apache-2.0'], isMarkdown: false, isSdk: false, dependencies: [PackageRef('collection')], - devDependencies: [PackageRef('matcher'), PackageRef('lints'), PackageRef('test')], + devDependencies: [PackageRef('matcher'), PackageRef('test')], license: '''Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -6212,6 +6246,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- +icu json @@ -6837,6 +6872,7 @@ skia limitations under the License. -------------------------------------------------------------------------------- angle +benchmark boringssl cpu_features flatbuffers @@ -10266,6 +10302,7 @@ prospectively choose to deem waived or otherwise exclude such Section(s) of the License, but only in their entirety and only with respect to the Combined Software. -------------------------------------------------------------------------------- +icu include json @@ -13836,34 +13873,6 @@ License & terms of use: http://www.unicode.org/copyright.html All Rights Reserved. --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (C) 2002-2010, International Business Machines -Corporation and others. All Rights Reserved. - - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2001-2003 International Business Machines -Corporation and others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2001-2010 International Business Machines -Corporation and others. All Rights Reserved. - -------------------------------------------------------------------------------- icu @@ -13878,14 +13887,6 @@ icu Copyright (C) 2016 and later: Unicode, Inc. and others. License & terms of use: http://www.unicode.org/copyright.html -Copyright (c) 2002-2010, International Business Machines Corporation and others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - Copyright (c) 2002-2015, International Business Machines Corporation and others. All Rights Reserved. @@ -13900,22 +13901,6 @@ Copyright (c) 2002-2016 International Business Machines Corporation and others. All Rights Reserved. --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2003-2005, International Business Machines Corporation and others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2003-2010, International Business Machines Corporation and others. All Rights Reserved. - -------------------------------------------------------------------------------- icu @@ -14090,17 +14075,6 @@ License & terms of use: http://www.unicode.org/copyright.html -------------------------------------------------------------------------------- icu -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -*************************************************************************** -* -* Copyright (C) 2009 International Business Machines -* Corporation and others. All Rights Reserved. -* -*************************************************************************** --------------------------------------------------------------------------------- -icu - Copyright (C) 2016 and later: Unicode, Inc. and others. License & terms of use: http://www.unicode.org/copyright.html ***************************************************************************** @@ -14111,19 +14085,6 @@ License & terms of use: http://www.unicode.org/copyright.html ***************************************************************************** --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -******************************************************************************* -* -* Copyright (C) 1995-2001, International Business Machines -* Corporation and others. All Rights Reserved. -* -******************************************************************************* - - -------------------------------------------------------------------------------- icu @@ -14202,6 +14163,15 @@ License & terms of use: http://www.unicode.org/copyright.html ******************************************************************************* +-------------------------------------------------------------------------------- +icu + +Copyright (C) 2016 and later: Unicode, Inc. and others. +License & terms of use: http://www.unicode.org/copyright.html +--------------------------------------------------------- +Copyright (C) 2013, International Business Machines +Corporation and others. All Rights Reserved. + -------------------------------------------------------------------------------- icu @@ -15485,6 +15455,22 @@ angle Copyright (c) 2008-2021 The Khronos Group Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +angle + +Copyright (c) 2008-2023 The Khronos Group Inc. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -17969,6 +17955,28 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- +volk + +Copyright (c) 2018-2024 Arseny Kapoulkine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +-------------------------------------------------------------------------------- vulkan-validation-layers Copyright (c) 2018-2024 The Khronos Group Inc. @@ -18493,7 +18501,6 @@ Copyright (c) 2020 The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- -angle spirv-tools Copyright (c) 2020 The Khronos Group Inc. @@ -19304,6 +19311,22 @@ spirv-tools Copyright (c) 2023 LunarG Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +angle + +Copyright (c) 2023 The Khronos Group Inc. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -19341,6 +19364,7 @@ for details. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- dart +perfetto Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file for details. All rights reserved. Use of this source code is governed by a @@ -19874,6 +19898,12 @@ Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file for details. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +perfetto + +Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +for details. All rights reserved. Use of this source code is governed by a +BSD-style license that can be found in the LICENSE file. +-------------------------------------------------------------------------------- glfw Copyright (c) Camilla Löwy @@ -20733,11 +20763,6 @@ Copyright 2014-2022 The Khronos Group Inc. SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- swiftshader - -Copyright 2014-2023 The Khronos Group Inc. - -SPDX-License-Identifier: Apache-2.0 --------------------------------------------------------------------------------- vulkan vulkan-headers @@ -20775,6 +20800,7 @@ tree. An additional intellectual property rights grant can be found in the file PATENTS. All contributing project authors may be found in the AUTHORS file in the root of the source tree. -------------------------------------------------------------------------------- +benchmark flatbuffers Copyright 2015 Google Inc. All rights reserved. @@ -20860,12 +20886,6 @@ See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- swiftshader - -Copyright 2015-2023 The Khronos Group Inc. - -SPDX-License-Identifier: Apache-2.0 --------------------------------------------------------------------------------- -swiftshader vulkan vulkan-headers @@ -20875,6 +20895,7 @@ Copyright 2015-2023 LunarG, Inc. SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- +swiftshader vulkan vulkan-headers @@ -20902,18 +20923,6 @@ skia Copyright 2016 Google Inc. -Use of this source code is governed by a BSD-style license that can be -found in the LICENSE file. --------------------------------------------------------------------------------- -skia - -Copyright 2016 Google Inc. - -Use of this source code is governed by a BSD-style license that can be -found in the LICENSE file. - -Copyright 2014 Google Inc. - Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -20931,6 +20940,39 @@ flatbuffers Copyright 2016 Google Inc. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +benchmark + +Copyright 2016 Ismael Jimenez Martinez. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +benchmark + +Copyright 2016 Ismael Jimenez Martinez. All rights reserved. +Copyright 2017 Roman Lebedev. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -21361,6 +21403,7 @@ tree. An additional intellectual property rights grant can be found in the file PATENTS. All contributing project authors may be found in the AUTHORS file in the root of the source tree. -------------------------------------------------------------------------------- +benchmark flatbuffers Copyright 2018 Google Inc. All rights reserved. @@ -22137,7 +22180,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- -angle swiftshader Copyright 2020 The SwiftShader Authors. All Rights Reserved. @@ -22187,6 +22229,7 @@ tree. An additional intellectual property rights grant can be found in the file PATENTS. All contributing project authors may be found in the AUTHORS file in the root of the source tree. -------------------------------------------------------------------------------- +benchmark flatbuffers Copyright 2021 Google Inc. All rights reserved. @@ -22619,6 +22662,22 @@ Copyright 2023 The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +angle + +Copyright 2023 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- skia Copyright 2023 The Android Open Source Project @@ -22696,6 +22755,7 @@ Copyright 2023-2025 The Khronos Group Inc. SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- +swiftshader vulkan-utility-libraries Copyright 2023-2025 The Khronos Group Inc. @@ -22713,6 +22773,12 @@ skia Copyright 2024 Google Inc. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +angle + +Copyright 2024 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -22748,6 +22814,12 @@ found in the LICENSE file. -------------------------------------------------------------------------------- skia +Copyright 2024 Google LLC. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +skia + Copyright 2024 Google LLC. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -22758,6 +22830,17 @@ Copyright 2024 Google, LLC Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +angle + +Copyright 2024 The ANGLE Project Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +angle + +Copyright 2024 The ANGLE Project Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. +-------------------------------------------------------------------------------- skia Copyright 2024 The Android Open Source Project @@ -22839,6 +22922,19 @@ skia Copyright 2025 Google LLC. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +skia + +Copyright 2025 Google, LLC + +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +angle + +Copyright 2025 The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -22903,6 +22999,12 @@ Copyright The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +angle + +Copyright {copyright_year} The ANGLE Project Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- harfbuzz Copyright © 1998-2004 David Turner and Werner Lemberg @@ -26647,6 +26749,48 @@ FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -------------------------------------------------------------------------------- +flutter + +License for the Ahem font embedded below is from: +https://www.w3.org/Style/CSS/Test/Fonts/Ahem/COPYING + +The Ahem font in this directory belongs to the public domain. In +jurisdictions that do not recognize public domain ownership of these +files, the following Creative Commons Zero declaration applies: + + + +which is quoted below: + + The person who has associated a work with this document (the "Work") + affirms that he or she (the "Affirmer") is the/an author or owner of + the Work. The Work may be any work of authorship, including a + database. + + The Affirmer hereby fully, permanently and irrevocably waives and + relinquishes all of her or his copyright and related or neighboring + legal rights in the Work available under any federal or state law, + treaty or contract, including but not limited to moral rights, + publicity and privacy rights, rights protecting against unfair + competition and any rights protecting the extraction, dissemination + and reuse of data, whether such rights are present or future, vested + or contingent (the "Waiver"). The Affirmer makes the Waiver for the + benefit of the public at large and to the detriment of the Affirmer's + heirs or successors. + + The Affirmer understands and intends that the Waiver has the effect + of eliminating and entirely removing from the Affirmer's control all + the copyright and related or neighboring legal rights previously held + by the Affirmer in the Work, to that extent making the Work freely + available to the public for any and all uses and purposes without + restriction of any kind, including commercial use and uses in media + and formats or by methods that have not yet been invented or + conceived. Should the Waiver for any reason be judged legally + ineffective in any jurisdiction, the Affirmer hereby grants a free, + full, permanent, irrevocable, nonexclusive and worldwide license for + all her or his copyright and related or neighboring legal rights in + the Work. +-------------------------------------------------------------------------------- fallback_root_certificates Mozilla Public License Version 2.0 @@ -27029,8 +27173,8 @@ libpng PNG Reference Library License version 2 --------------------------------------- - * Copyright (c) 1995-2024 The PNG Reference Library Authors. - * Copyright (c) 2018-2024 Cosmin Truta. + * Copyright (c) 1995-2025 The PNG Reference Library Authors. + * Copyright (c) 2018-2025 Cosmin Truta. * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. * Copyright (c) 1996-1997 Andreas Dilger. * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. @@ -27164,8 +27308,8 @@ libpng PNG Reference Library License version 2 --------------------------------------- - * Copyright (c) 1995-2024 The PNG Reference Library Authors. - * Copyright (c) 2018-2024 Cosmin Truta. + * Copyright (c) 1995-2025 The PNG Reference Library Authors. + * Copyright (c) 2018-2025 Cosmin Truta. * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. * Copyright (c) 1996-1997 Andreas Dilger. * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. @@ -28632,7 +28776,7 @@ UNICODE LICENSE V3 COPYRIGHT AND PERMISSION NOTICE -Copyright © 2016-2023 Unicode, Inc. +Copyright © 2016-2025 Unicode, Inc. NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR @@ -28668,6 +28812,8 @@ not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder. +SPDX-License-Identifier: Unicode-3.0 + ---------------------------------------------------------------------- Third-Party Software Licenses @@ -29061,6 +29207,34 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------- +JSON parsing library (nlohmann/json) + +File: vendor/json/upstream/single_include/nlohmann/json.hpp (only for ICU4C) + +MIT License + +Copyright (c) 2013-2022 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---------------------------------------------------------------------- + File: install-sh (only for ICU4C) @@ -32346,17 +32520,6 @@ Copyright (C) 2003-2016, International Business Machines --------------------------------------------------------------------------------- -icu - -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - - -Copyright (C) 2008-2013, International Business Machines Corporation and -others. All Rights Reserved. - - -------------------------------------------------------------------------------- icu @@ -32396,16 +32559,6 @@ icu License & terms of use: http://www.unicode.org/copyright.html -Copyright (c) 1999-2007, International Business Machines Corporation and -others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - - Copyright (c) 1999-2010, International Business Machines Corporation and others. All Rights Reserved. @@ -35896,15 +36049,6 @@ others. All Rights Reserved. --------------------------------------------------------------------------------- -icu - -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -Copyright (C) 2008-2014, International Business Machines Corporation and -others. All Rights Reserved. - - -------------------------------------------------------------------------------- icu @@ -36144,14 +36288,6 @@ others. All Rights Reserved. -------------------------------------------------------------------------------- icu -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -Copyright (c) 2008-2014, International Business Machines Corporation and -others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - © 2016 and later: Unicode, Inc. and others. License & terms of use: http://www.unicode.org/copyright.html Copyright (c) IBM Corporation, 2000-2010. All rights reserved. @@ -36204,13 +36340,13 @@ Copyright (C) 2009-2017, International Business Machines Corporation, Google, and others. All Rights Reserved.''', ); -/// source_gen 4.2.2 +/// source_gen 4.2.3 const _source_gen = Package( name: 'source_gen', description: 'Source code generation builders and utilities for the Dart build system', repository: 'https://github.com/dart-lang/source_gen/tree/master/source_gen', authors: [], - version: '4.2.2', + version: '4.2.3', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -36679,17 +36815,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// test 1.26.3 +/// test 1.28.0 const _test = Package( name: 'test', description: 'A full featured library for writing and running Dart tests across platforms.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/test', authors: [], - version: '1.26.3', + version: '1.28.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('analyzer'), PackageRef('async'), PackageRef('boolean_selector'), PackageRef('collection'), PackageRef('coverage'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('js'), PackageRef('matcher'), PackageRef('node_preamble'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('shelf'), PackageRef('shelf_packages_handler'), PackageRef('shelf_static'), PackageRef('shelf_web_socket'), PackageRef('source_span'), PackageRef('stack_trace'), PackageRef('stream_channel'), PackageRef('test_api'), PackageRef('test_core'), PackageRef('typed_data'), PackageRef('web_socket_channel'), PackageRef('webkit_inspection_protocol'), PackageRef('yaml')], + dependencies: [PackageRef('analyzer'), PackageRef('async'), PackageRef('boolean_selector'), PackageRef('collection'), PackageRef('coverage'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('matcher'), PackageRef('node_preamble'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('shelf'), PackageRef('shelf_packages_handler'), PackageRef('shelf_static'), PackageRef('shelf_web_socket'), PackageRef('source_span'), PackageRef('stack_trace'), PackageRef('stream_channel'), PackageRef('test_api'), PackageRef('test_core'), PackageRef('typed_data'), PackageRef('web_socket_channel'), PackageRef('webkit_inspection_protocol'), PackageRef('yaml')], devDependencies: [PackageRef('fake_async'), PackageRef('glob')], license: '''Copyright 2014, the Dart project authors. @@ -36720,13 +36856,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// test_api 0.7.7 +/// test_api 0.7.8 const _test_api = Package( name: 'test_api', description: 'The user facing API for structuring Dart tests and checking expectations.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/test_api', authors: [], - version: '0.7.7', + version: '0.7.8', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -36761,13 +36897,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// test_core 0.6.12 +/// test_core 0.6.14 const _test_core = Package( name: 'test_core', description: 'A basic library for writing tests and running them on the VM.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/test_core', authors: [], - version: '0.6.12', + version: '0.6.14', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -37115,21 +37251,19 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// url_launcher_web 2.4.2 +/// url_launcher_web 2.4.3 const _url_launcher_web = Package( name: 'url_launcher_web', description: 'Web platform implementation of url_launcher', repository: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_web', authors: [], - version: '2.4.2', - spdxIdentifiers: ['Apache-2.0', 'BSD-3-Clause'], + version: '2.4.3', + spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, dependencies: [PackageRef('flutter'), PackageRef('flutter_web_plugins'), PackageRef('url_launcher_platform_interface'), PackageRef('web')], devDependencies: [PackageRef('flutter_test')], - license: '''url_launcher_web - -Copyright 2013 The Flutter Authors + license: '''Copyright 2013 The Flutter Authors Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -37153,211 +37287,7 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --------------------------------------------------------------------------------- -platform_detect - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2017 Workiva Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License.''', +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); /// url_launcher_windows 3.1.5 @@ -37481,13 +37411,13 @@ freely, subject to the following restrictions: 3. This notice may not be removed or altered from any source distribution.''', ); -/// vm_service 15.1.0 +/// vm_service 15.2.0 const _vm_service = Package( name: 'vm_service', description: 'A library to communicate with a service implementing the Dart VM service protocol.', repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/vm_service', authors: [], - version: '15.1.0', + version: '15.2.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -37881,16 +37811,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.23+257 +/// tallee 0.0.32+266 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.23+257', + version: '0.0.32+266', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('clock'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], + dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('flutter_numeric_text'), PackageRef('flutter_popup'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], devDependencies: [PackageRef('flutter_test'), PackageRef('build_runner'), PackageRef('dart_pubspec_licenses'), PackageRef('drift_dev'), PackageRef('flutter_lints')], license: '''GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 98a8e1d..8659a2e 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -6,6 +6,7 @@ import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; import 'package:tallee/presentation/widgets/top_centered_message.dart'; @@ -18,6 +19,9 @@ class StatisticsView extends StatefulWidget { } class _StatisticsViewState extends State { + int matchCount = 0; + int groupCount = 0; + List<(Player, int)> winCounts = List.filled(6, ( Player(name: 'Skeleton Player'), 1, @@ -53,7 +57,27 @@ class _StatisticsViewState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox(height: constraints.maxHeight * 0.01), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.13, + title: loc.matches, + icon: Icons.groups_rounded, + value: matchCount, + ), + SizedBox(width: constraints.maxWidth * 0.05), + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.13, + title: loc.groups, + icon: Icons.groups_rounded, + value: groupCount, + ), + ], + ), + SizedBox(height: constraints.maxHeight * 0.02), Visibility( visible: winCounts.isEmpty && @@ -115,11 +139,17 @@ class _StatisticsViewState extends State { Future.wait([ db.matchDao.getAllMatches(), db.playerDao.getAllPlayers(), + db.matchDao.getMatchCount(), + db.groupDao.getGroupCount(), Future.delayed(Constants.MINIMUM_SKELETON_DURATION), ]).then((results) async { if (!mounted) return; + final matches = results[0] as List; final players = results[1] as List; + matchCount = results[2] as int; + groupCount = results[3] as int; + winCounts = _calculateWinsForAllPlayers( matches: matches, players: players, @@ -134,6 +164,7 @@ class _StatisticsViewState extends State { winCounts: winCounts, matchCounts: matchCounts, ); + setState(() { isLoading = false; }); diff --git a/lib/presentation/widgets/buttons/animated_dialog_button.dart b/lib/presentation/widgets/buttons/animated_dialog_button.dart index 70deea6..8c8765e 100644 --- a/lib/presentation/widgets/buttons/animated_dialog_button.dart +++ b/lib/presentation/widgets/buttons/animated_dialog_button.dart @@ -14,6 +14,7 @@ class AnimatedDialogButton extends StatefulWidget { required this.onPressed, this.buttonConstraints, this.buttonType = ButtonType.primary, + this.isDescructive = false, }); final String buttonText; @@ -24,6 +25,8 @@ class AnimatedDialogButton extends StatefulWidget { final ButtonType buttonType; + final bool isDescructive; + @override State createState() => _AnimatedDialogButtonState(); } @@ -33,28 +36,8 @@ class _AnimatedDialogButtonState extends State { @override Widget build(BuildContext context) { - final textStyling = TextStyle( - color: widget.buttonType == ButtonType.primary - ? Colors.black - : Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ); - - final buttonDecoration = widget.buttonType == ButtonType.primary - // Primary - ? BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ) - : widget.buttonType == ButtonType.secondary - // Secondary - ? BoxDecoration( - border: BoxBorder.all(color: Colors.white, width: 2), - borderRadius: BorderRadius.circular(12), - ) - // Tertiary - : const BoxDecoration(); + final textStyling = _getTextStyling(); + final buttonDecoration = _getButtonDecoration(); return GestureDetector( onTapDown: (_) => setState(() => _isPressed = true), @@ -84,4 +67,42 @@ class _AnimatedDialogButtonState extends State { ), ); } + + TextStyle _getTextStyling() { + late Color textColor; + if (widget.buttonType == ButtonType.primary) { + textColor = widget.isDescructive ? Colors.white : Colors.black; + } else if (widget.buttonType == ButtonType.secondary) { + textColor = widget.isDescructive ? Colors.red : Colors.white; + } else { + textColor = widget.isDescructive ? Colors.red : Colors.white; + } + + return TextStyle( + color: textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ); + } + + BoxDecoration _getButtonDecoration() { + if (widget.buttonType == ButtonType.primary) { + // Primary + return BoxDecoration( + color: widget.isDescructive ? Colors.red : Colors.white, + borderRadius: BorderRadius.circular(12), + ); + } else if (widget.buttonType == ButtonType.secondary) { + // Secondary + return BoxDecoration( + border: BoxBorder.all( + color: widget.isDescructive ? Colors.red : Colors.white, + width: 2, + ), + borderRadius: BorderRadius.circular(12), + ); + } + // Tertiary + return const BoxDecoration(); + } } diff --git a/lib/presentation/widgets/buttons/custom_width_button.dart b/lib/presentation/widgets/buttons/custom_width_button.dart index 489ceae..4fde6f8 100644 --- a/lib/presentation/widgets/buttons/custom_width_button.dart +++ b/lib/presentation/widgets/buttons/custom_width_button.dart @@ -89,7 +89,7 @@ class CustomWidthButton extends StatelessWidget { MediaQuery.sizeOf(context).width * sizeRelativeToWidth, 60, ), - side: BorderSide(color: borderSideColor, width: 2), + side: BorderSide(color: borderSideColor, width: 3), shape: RoundedRectangleBorder( borderRadius: CustomTheme.standardBorderRadiusAll, ), diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index c583456..c5c7a34 100644 --- a/lib/presentation/widgets/buttons/main_menu_button.dart +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; class MainMenuButton extends StatefulWidget { @@ -10,6 +12,7 @@ class MainMenuButton extends StatefulWidget { required this.onPressed, required this.icon, this.text, + this.onLongPressed, }); /// The callback to be invoked when the button is pressed. @@ -21,6 +24,8 @@ class MainMenuButton extends StatefulWidget { /// The text of the button. final String? text; + final void Function()? onLongPressed; + @override State createState() => _MainMenuButtonState(); } @@ -30,6 +35,14 @@ class _MainMenuButtonState extends State late AnimationController _animationController; late Animation _scaleAnimation; + /// How long the button needs to be pressed to register it as long press + Timer? _longPressTimer; + + /// How much time between two onLongPressed calls + Timer? _repeatTimer; + + bool _isLongPressing = false; + @override void initState() { super.initState(); @@ -51,14 +64,29 @@ class _MainMenuButtonState extends State child: GestureDetector( onTapDown: (_) { _animationController.forward(); - }, - onTapUp: (_) async { - await _animationController.reverse(); - if (mounted) { - widget.onPressed(); + if (widget.onLongPressed != null) { + _longPressTimer = Timer(const Duration(milliseconds: 400), () { + _isLongPressing = true; + widget.onLongPressed?.call(); + _repeatTimer = Timer.periodic( + const Duration(milliseconds: 250), + (_) => widget.onLongPressed?.call(), + ); + }); } }, + onTapUp: (_) async { + _cancelTimers(); + if (mounted && !_isLongPressing) { + widget.onPressed(); + } + _isLongPressing = false; + await Future.delayed(const Duration(milliseconds: 100)); + await _animationController.reverse(); + }, onTapCancel: () { + _isLongPressing = false; + _cancelTimers(); _animationController.reverse(); }, child: Container( @@ -92,7 +120,15 @@ class _MainMenuButtonState extends State @override void dispose() { + _cancelTimers(); _animationController.dispose(); super.dispose(); } + + void _cancelTimers() { + _longPressTimer?.cancel(); + _longPressTimer = null; + _repeatTimer?.cancel(); + _repeatTimer = null; + } } diff --git a/lib/presentation/widgets/dialog/custom_dialog_action.dart b/lib/presentation/widgets/dialog/custom_dialog_action.dart index aec0dfa..47024dc 100644 --- a/lib/presentation/widgets/dialog/custom_dialog_action.dart +++ b/lib/presentation/widgets/dialog/custom_dialog_action.dart @@ -12,6 +12,7 @@ class CustomDialogAction extends StatelessWidget { required this.onPressed, required this.text, this.buttonType = ButtonType.primary, + this.isDestructive = false, }); final String text; @@ -20,12 +21,15 @@ class CustomDialogAction extends StatelessWidget { final VoidCallback onPressed; + final bool isDestructive; + @override Widget build(BuildContext context) { return AnimatedDialogButton( onPressed: onPressed, buttonText: text, buttonType: buttonType, + isDescructive: isDestructive, buttonConstraints: const BoxConstraints(minWidth: 300), ); } diff --git a/lib/presentation/widgets/game_label.dart b/lib/presentation/widgets/game_label.dart new file mode 100644 index 0000000..553e637 --- /dev/null +++ b/lib/presentation/widgets/game_label.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/enums.dart'; + +class GameLabel extends StatelessWidget { + const GameLabel({ + super.key, + required this.title, + required this.description, + required this.color, + }); + + final String title; + final String description; + final GameColor color; + + @override + Widget build(BuildContext context) { + final backgroundColor = getColorFromGameColor(color); + final fontColor = backgroundColor.computeLuminance() > 0.5 + ? Colors.black + : Colors.white; + + return IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Title + Container( + decoration: BoxDecoration( + color: backgroundColor.withAlpha(230), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Text( + title, + style: TextStyle( + fontSize: 12, + color: fontColor, + fontWeight: FontWeight.bold, + ), + ), + ), + + // Description + Container( + decoration: BoxDecoration( + color: backgroundColor.withAlpha(140), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Text( + description, + style: TextStyle( + fontSize: 12, + color: fontColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/navbar_item.dart b/lib/presentation/widgets/navbar_item.dart index 17c055c..160c000 100644 --- a/lib/presentation/widgets/navbar_item.dart +++ b/lib/presentation/widgets/navbar_item.dart @@ -44,24 +44,49 @@ class _NavbarItemState extends State /// Scale animation for the icon when selected late Animation _scaleAnimation; + /// Color animation for the icon + late Animation _iconColorAnimation; + + /// Background color animation for the icon container + late Animation _bgColorAnimation; + + /// Font size animation for the label + late Animation _fontSizeAnimation; + + /// A simple double tween used to lerp between two font weights + late Animation _fontWeightT; + @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, + // Set initial value directly so the visual state matches widget.isSelected + value: widget.isSelected ? 1.0 : 0.0, ); - _scaleAnimation = Tween(begin: 1.0, end: 1.2).animate( - CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOutBack, - ), + final curved = CurvedAnimation( + parent: _animationController, + curve: Curves.easeOut, ); - if (widget.isSelected) { - _animationController.forward(); - } + _scaleAnimation = Tween(begin: 1.0, end: 1.2).animate(curved); + + _iconColorAnimation = ColorTween( + begin: CustomTheme.navBarItemUnselectedColor, + end: CustomTheme.navBarItemSelectedColor, + ).animate(curved); + + _bgColorAnimation = ColorTween( + begin: Colors.transparent, + end: CustomTheme.primaryColor.withAlpha(50), + ).animate(curved); + + _fontSizeAnimation = Tween(begin: 11.0, end: 12.0).animate(curved); + + // drives font weight interpolation + _fontWeightT = Tween(begin: 0.0, end: 1.0).animate(curved); } // Retrigger animation on selection change @@ -83,46 +108,44 @@ class _NavbarItemState extends State behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.symmetric(vertical: 5.0), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedContainer( - width: 50, - height: 50, - decoration: BoxDecoration( - color: widget.isSelected - ? CustomTheme.primaryColor.withAlpha(50) - : Colors.transparent, - borderRadius: const BorderRadius.all(Radius.circular(15)), - ), - duration: const Duration(milliseconds: 200), - child: ScaleTransition( - scale: widget.isSelected - ? _scaleAnimation - : const AlwaysStoppedAnimation(1.0), - child: Icon( - widget.icon, - color: widget.isSelected - ? CustomTheme.navBarItemSelectedColor - : CustomTheme.navBarItemUnselectedColor, - size: 32, + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final iconColor = _iconColorAnimation.value!; + final bgColor = _bgColorAnimation.value!; + final fontSize = _fontSizeAnimation.value; + final fontWeight = FontWeight.lerp( + FontWeight.w500, + FontWeight.bold, + _fontWeightT.value, + ); + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: bgColor, + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + child: ScaleTransition( + scale: _scaleAnimation, + child: Icon(widget.icon, color: iconColor, size: 32), + ), ), - ), - ), - Text( - widget.label, - style: TextStyle( - color: widget.isSelected - ? CustomTheme.navBarItemSelectedColor - : CustomTheme.navBarItemUnselectedColor, - fontSize: widget.isSelected ? 12 : 11, - fontWeight: widget.isSelected - ? FontWeight.bold - : FontWeight.w500, - ), - ), - ], + Text( + widget.label, + style: TextStyle( + color: iconColor, + fontSize: fontSize, + fontWeight: fontWeight, + ), + ), + ], + ); + }, ), ), ), diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 0fc8ea0..cdcc2ed 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -196,6 +196,7 @@ class _PlayerSelectionState extends State { return TextIconListTile( text: suggestedPlayers[index].name, suffixText: getNameCountText(suggestedPlayers[index]), + icon: Icons.add, onPressed: () { setState(() { // If the player is not already selected diff --git a/lib/presentation/widgets/text_input/text_input_field.dart b/lib/presentation/widgets/text_input/text_input_field.dart index 541ae6f..b074638 100644 --- a/lib/presentation/widgets/text_input/text_input_field.dart +++ b/lib/presentation/widgets/text_input/text_input_field.dart @@ -8,12 +8,18 @@ class TextInputField extends StatelessWidget { /// - [onChanged]: Optional callback invoked when the text in the field changes. /// - [hintText]: The hint text displayed in the text input field when it is empty /// - [maxLength]: Optional parameter for maximum length of the input text. + /// - [maxLines]: The maximum number of lines for the text input field. Defaults to 1. + /// - [minLines]: The minimum number of lines for the text input field. Defaults to 1. + /// - [showCounterText]: Whether to show the counter text in the text input field. Defaults to false. const TextInputField({ super.key, required this.controller, required this.hintText, this.onChanged, this.maxLength, + this.maxLines = 1, + this.minLines = 1, + this.showCounterText = false, }); /// The controller for the text input field. @@ -28,6 +34,15 @@ class TextInputField extends StatelessWidget { /// Optional parameter for maximum length of the input text. final int? maxLength; + /// The maximum number of lines for the text input field. + final int? maxLines; + + /// The minimum number of lines for the text input field. + final int? minLines; + + /// Whether to show the counter text in the text input field. + final bool showCounterText; + @override Widget build(BuildContext context) { return TextField( @@ -35,13 +50,15 @@ class TextInputField extends StatelessWidget { onChanged: onChanged, maxLength: maxLength, maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, + maxLines: maxLines, + minLines: minLines, + decoration: InputDecoration( filled: true, fillColor: CustomTheme.boxColor, hintText: hintText, hintStyle: const TextStyle(fontSize: 18), - // Hides the character counter - counterText: '', + counterText: showCounterText ? null : '', enabledBorder: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), borderSide: BorderSide(color: CustomTheme.boxBorderColor), diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index 10ded6b..41cc7f0 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -4,12 +4,12 @@ import 'package:tallee/core/custom_theme.dart'; class ChooseTile extends StatefulWidget { /// A tile widget that allows users to choose an option by tapping on it. /// - [title]: The title text displayed on the tile. - /// - [trailingText]: Optional trailing text displayed on the tile. + /// - [trailing]: Optional trailing text displayed on the tile. /// - [onPressed]: The callback invoked when the tile is tapped. const ChooseTile({ super.key, required this.title, - this.trailingText, + this.trailing, this.onPressed, }); @@ -20,7 +20,7 @@ class ChooseTile extends StatefulWidget { final VoidCallback? onPressed; /// Optional trailing text displayed on the tile. - final String? trailingText; + final Widget? trailing; @override State createState() => _ChooseTileState(); @@ -42,9 +42,11 @@ class _ChooseTileState extends State { style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const Spacer(), - if (widget.trailingText != null) Text(widget.trailingText!), - const SizedBox(width: 10), - const Icon(Icons.arrow_forward_ios, size: 16), + if (widget.trailing != null) widget.trailing!, + if (widget.onPressed != null) ...[ + const SizedBox(width: 10), + const Icon(Icons.arrow_forward_ios, size: 16), + ], ], ), ), diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart new file mode 100644 index 0000000..1d494b9 --- /dev/null +++ b/lib/presentation/widgets/tiles/game_tile.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; + +class GameTile extends StatelessWidget { + /// A list tile widget that displays a title and description, with optional highlighting and badge. + /// - [title]: The title text displayed on the tile. + /// - [description]: The description text displayed below the title. + /// - [onTap]: The callback invoked when the tile is tapped. + /// - [onLongPress]: The callback invoked when the tile is tapped. + /// - [isHighlighted]: A boolean to determine if the tile should be highlighted. + /// - [badgeText]: Optional text to display in a badge on the right side of the title. + /// - [badgeColor]: Optional color for the badge background. + const GameTile({ + super.key, + required this.title, + required this.description, + this.onTap, + this.onLongPress, + this.isHighlighted = false, + this.badgeText, + this.badgeColor, + }); + + /// The title text displayed on the tile. + final String title; + + /// The description text displayed below the title. + final String description; + + /// The callback invoked when the tile is tapped. + final VoidCallback? onTap; + + /// The callback invoked when the tile is long-pressed. + final VoidCallback? onLongPress; + + /// A boolean to determine if the tile should be highlighted. + final bool isHighlighted; + + /// Optional text to display in a badge on the right side of the title. + final String? badgeText; + + /// Optional color for the badge background. + final Color? badgeColor; + + @override + Widget build(BuildContext context) { + final badgeTextColor = badgeColor != null + ? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white) + : Colors.white; + + final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange); + + return GestureDetector( + onTap: onTap, + onLongPress: onLongPress, + child: AnimatedContainer( + margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + decoration: !isHighlighted + ? CustomTheme.standardBoxDecoration + : CustomTheme.highlightedBoxDecoration.copyWith( + border: Border.all( + color: gameColor.withValues(alpha: 0.9), + width: 2, + ), + ), + duration: const Duration(milliseconds: 200), + child: Stack( + children: [ + // Gradient overlay + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + gameColor.withValues(alpha: 0.08), + gameColor.withValues(alpha: 0.02), + Colors.transparent, + ], + stops: const [0.0, 0.5, 1.0], + ), + ), + ), + ), + + // Content + Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // Title + Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + + // Badge + if (badgeText != null) ...[ + const SizedBox(height: 5), + Container( + constraints: const BoxConstraints(maxWidth: 250), + padding: const EdgeInsets.symmetric( + vertical: 2, + horizontal: 6, + ), + decoration: BoxDecoration( + color: gameColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + badgeText!, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: TextStyle( + color: badgeTextColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + + // Description + if (description.isNotEmpty) ...[ + const SizedBox(height: 10), + Text(description, style: const TextStyle(fontSize: 14)), + const SizedBox(height: 2.5), + ], + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart new file mode 100644 index 0000000..77c9242 --- /dev/null +++ b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/custom_theme.dart'; + +class CustomCheckboxListTile extends StatelessWidget { + const CustomCheckboxListTile({ + super.key, + required this.text, + required this.value, + required this.onChanged, + }); + + final String text; + final bool value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onChanged(!value), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorderColor), + borderRadius: CustomTheme.standardBorderRadiusAll, + ), + child: Row( + children: [ + Checkbox( + value: value, + onChanged: (bool? v) { + if (v == null) return; + onChanged(v); + }, + ), + Expanded( + child: Text( + text, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/custom_radio_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart similarity index 100% rename from lib/presentation/widgets/tiles/custom_radio_list_tile.dart rename to lib/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart diff --git a/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart new file mode 100644 index 0000000..d663efc --- /dev/null +++ b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_numeric_text/flutter_numeric_text.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; + +class LiveEditListTile extends StatefulWidget { + const LiveEditListTile({ + super.key, + required this.title, + required this.value, + this.onChanged, + }); + + final String title; + + final int value; + + final void Function(int newValue)? onChanged; + + @override + State createState() => _LiveEditListTileState(); +} + +class _LiveEditListTileState extends State { + int _score = 0; + final int maxScore = 9999; + final int minScore = -9999; + + @override + void initState() { + _score = widget.value; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), + margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: CustomTheme.standardBoxDecoration, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + MainMenuButton( + onPressed: () => _score > minScore + ? { + setState(() { + _score--; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + onLongPressed: () => _score > minScore + ? { + setState(() { + _score -= 10; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.remove_rounded, + ), + Expanded( + child: Column( + children: [ + Text( + widget.title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + width: 150, + child: NumericText( + _score.toString(), + maxLines: 1, + textAlign: TextAlign.center, + textWidthBasis: TextWidthBasis.longestLine, + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), + style: const TextStyle( + fontSize: 48, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + MainMenuButton( + onPressed: () => _score < maxScore + ? { + setState(() { + _score++; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + onLongPressed: () => _score > minScore + ? { + setState(() { + _score += 10; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.add_rounded, + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/score_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart similarity index 75% rename from lib/presentation/widgets/tiles/score_list_tile.dart rename to lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart index 52103fa..e4cfff9 100644 --- a/lib/presentation/widgets/tiles/score_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart @@ -40,9 +40,13 @@ class ScoreListTile extends StatelessWidget { height: 40, child: TextField( controller: controller, - keyboardType: TextInputType.number, - maxLength: 4, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], + keyboardType: const TextInputType.numberWithOptions(signed: true), + maxLength: 5, + inputFormatters: [ + TextInputFormatter.withFunction((oldValue, newValue) { + return isValidScoreInput(newValue.text) ? newValue : oldValue; + }), + ], textAlign: TextAlign.center, style: const TextStyle( fontSize: 16, @@ -62,7 +66,7 @@ class ScoreListTile extends StatelessWidget { enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( - color: CustomTheme.textColor.withAlpha(100), + color: CustomTheme.textColor.withAlpha(250), width: 2, ), ), @@ -80,4 +84,21 @@ class ScoreListTile extends StatelessWidget { ), ); } + + /// Validates the input for the score text field. + bool isValidScoreInput(String text) { + if (text.isEmpty || text == '-') { + return true; + } + + final isNegative = text.startsWith('-'); + final digits = isNegative ? text.substring(1) : text; + + if (digits.isEmpty || digits.length > 4) { + return false; + } + + // CHeck if all characters are digits 0 <= x <= 9 + return digits.codeUnits.every((unit) => unit >= 48 && unit <= 57); + } } diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index f7585d6..018c896 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -7,6 +7,7 @@ import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; class MatchTile extends StatefulWidget { @@ -116,56 +117,13 @@ class _MatchTileState extends State { // Game + Ruleset Badge if (!widget.compact) - IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // Game - Container( - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(230), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - bottomLeft: Radius.circular(8), - ), - ), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ), - child: Text( - match.game.name, - style: const TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - // Ruleset - Container( - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(140), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(8), - bottomRight: Radius.circular(8), - ), - ), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ), - child: Text( - translateRulesetToString(match.game.ruleset, context), - style: const TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ], + GameLabel( + title: match.game.name, + description: translateRulesetToString( + match.game.ruleset, + context, ), + color: match.game.color, ), const SizedBox(height: 12), @@ -303,30 +261,32 @@ class _MatchTileState extends State { final mvp = widget.match.mvp; final mvpScore = widget.match.scores[mvp.first.id]?.score ?? 0; final mvpNames = mvp.map((player) => player.name).join(', '); - return '${loc.winner}: $mvpNames (${getPointLabel(loc, mvpScore)})'; + } else if (ruleset == Ruleset.placement) { + return '${loc.winner}: ${widget.match.mvp.first.name}'; + } else if (ruleset == Ruleset.multipleWinners) { + final mvpNames = widget.match.mvp.map((player) => player.name).join(', '); + return '${loc.winners}: $mvpNames'; } return '${loc.winner}: n.A.'; } Icon getMvpIcon() { - const Icon(Icons.emoji_events, size: 20, color: Colors.amber); + final icon = getRulesetIcon(widget.match.game.ruleset); switch (widget.match.game.ruleset) { case Ruleset.singleWinner: - return const Icon(Icons.emoji_events, size: 20, color: Colors.amber); + return Icon(icon, size: 20, color: Colors.amber); case Ruleset.singleLoser: - return const Icon( - Icons.sentiment_dissatisfied_outlined, - size: 20, - color: Colors.blue, - ); + return Icon(icon, size: 20, color: Colors.blue); case Ruleset.lowestScore: - return const Icon(Icons.arrow_downward, size: 20, color: Colors.orange); + return Icon(icon, size: 20, color: Colors.orange); case Ruleset.highestScore: - return const Icon(Icons.arrow_upward, size: 20, color: Colors.green); - default: - return const Icon(Icons.emoji_events, size: 20, color: Colors.amber); + return Icon(icon, size: 20, color: Colors.green); + case Ruleset.multipleWinners: + return Icon(icon, size: 20, color: Colors.amber); + case Ruleset.placement: + return Icon(icon, size: 20, color: Colors.deepOrangeAccent); } } } diff --git a/lib/presentation/widgets/tiles/quick_info_tile.dart b/lib/presentation/widgets/tiles/quick_info_tile.dart index 5646fa5..c36aa92 100644 --- a/lib/presentation/widgets/tiles/quick_info_tile.dart +++ b/lib/presentation/widgets/tiles/quick_info_tile.dart @@ -50,7 +50,7 @@ class _QuickInfoTileState extends State { width: widget.width ?? 180, decoration: CustomTheme.standardBoxDecoration, child: Column( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ @@ -65,7 +65,6 @@ class _QuickInfoTileState extends State { ), ], ), - const Spacer(), Text( widget.value.toString(), style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold), diff --git a/lib/presentation/widgets/tiles/text_icon_list_tile.dart b/lib/presentation/widgets/tiles/text_icon_list_tile.dart index a31f2ae..04a0803 100644 --- a/lib/presentation/widgets/tiles/text_icon_list_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_list_tile.dart @@ -10,7 +10,7 @@ class TextIconListTile extends StatelessWidget { super.key, required this.text, this.suffixText = '', - this.iconEnabled = true, + this.icon, this.onPressed, }); @@ -20,8 +20,8 @@ class TextIconListTile extends StatelessWidget { /// An optional suffix text to display after the main text. final String suffixText; - /// A boolean to determine if the icon should be displayed. - final bool iconEnabled; + /// The icon to display in the tile. + final IconData? icon; /// The callback to be invoked when the icon is pressed. final VoidCallback? onPressed; @@ -64,11 +64,8 @@ class TextIconListTile extends StatelessWidget { ), ), ), - if (iconEnabled) - GestureDetector( - onTap: onPressed, - child: const Icon(Icons.add, size: 20), - ), + if (icon != null) + GestureDetector(onTap: onPressed, child: Icon(icon, size: 20)), ], ), ); diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart index 9dc8f33..bf45c1e 100644 --- a/lib/presentation/widgets/tiles/title_description_list_tile.dart +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -2,21 +2,17 @@ import 'package:flutter/material.dart'; import 'package:tallee/core/custom_theme.dart'; class TitleDescriptionListTile extends StatelessWidget { - /// A list tile widget that displays a title and description, with optional highlighting and badge. + /// A list tile widget that displays a title and description /// - [title]: The title text displayed on the tile. /// - [description]: The description text displayed below the title. - /// - [onPressed]: The callback invoked when the tile is tapped. + /// - [onTap]: The callback invoked when the tile is tapped. /// - [isHighlighted]: A boolean to determine if the tile should be highlighted. - /// - [badgeText]: Optional text to display in a badge on the right side of the title. - /// - [badgeColor]: Optional color for the badge background. const TitleDescriptionListTile({ super.key, required this.title, required this.description, - this.onPressed, + this.onTap, this.isHighlighted = false, - this.badgeText, - this.badgeColor, }); /// The title text displayed on the tile. @@ -26,21 +22,15 @@ class TitleDescriptionListTile extends StatelessWidget { final String description; /// The callback invoked when the tile is tapped. - final VoidCallback? onPressed; + final VoidCallback? onTap; /// A boolean to determine if the tile should be highlighted. final bool isHighlighted; - /// Optional text to display in a badge on the right side of the title. - final String? badgeText; - - /// Optional color for the badge background. - final Color? badgeColor; - @override Widget build(BuildContext context) { return GestureDetector( - onTap: onPressed, + onTap: onTap, child: AnimatedContainer( margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), @@ -51,53 +41,26 @@ class TitleDescriptionListTile extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, 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, - ), - ), + // Title + 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: 115), - 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, - ), - ), - ), - ], - ], + ), ), + + // Description if (description.isNotEmpty) ...[ - const SizedBox(height: 5), + const SizedBox(height: 10), Text(description, style: const TextStyle(fontSize: 14)), const SizedBox(height: 2.5), ], diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index daf4768..29199f8 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -35,13 +35,11 @@ class DataTransferService { final groups = await db.groupDao.getAllGroups(); final players = await db.playerDao.getAllPlayers(); final games = await db.gameDao.getAllGames(); - final teams = await db.teamDao.getAllTeams(); final Map jsonMap = { 'players': players.map((player) => player.toJson()).toList(), 'games': games.map((game) => game.toJson()).toList(), 'groups': groups.map((group) => group.toJson()).toList(), - 'teams': teams.map((team) => team.toJson()).toList(), 'matches': matches.map((match) => match.toJson()).toList(), }; @@ -130,8 +128,6 @@ class DataTransferService { final importedGroups = parseGroupsFromJson(decodedJson, playerById); final groupById = {for (final g in importedGroups) g.id: g}; - final importedTeams = parseTeamsFromJson(decodedJson, playerById); - final importedMatches = parseMatchesFromJson( decodedJson, gameById, @@ -142,8 +138,7 @@ class DataTransferService { await db.playerDao.addPlayersAsList(players: importedPlayers); await db.gameDao.addGamesAsList(games: importedGames); await db.groupDao.addGroupsAsList(groups: importedGroups); - await db.teamDao.addTeamsAsList(teams: importedTeams); - await db.matchDao.addMatchAsList(matches: importedMatches); + await db.matchDao.addMatchesAsList(matches: importedMatches); } /* Parsing Methods */ @@ -190,13 +185,12 @@ class DataTransferService { }).toList(); } - /// Parses teams from JSON data. + /// Parses teams from a list of JSON objects. @visibleForTesting static List parseTeamsFromJson( - Map decodedJson, + List teamsJson, Map playerById, ) { - final teamsJson = (decodedJson['teams'] as List?) ?? []; return teamsJson.map((t) { final map = t as Map; final memberIds = (map['memberIds'] as List? ?? []) @@ -259,12 +253,16 @@ class DataTransferService { .whereType() .toList(); + final teamsJson = (map['teams'] as List?) ?? []; + final teams = parseTeamsFromJson(teamsJson, playersMap); + return Match( id: id, name: name, game: game, group: group, players: players, + teams: teams.isEmpty ? null : teams, createdAt: createdAt, endedAt: endedAt, notes: notes, diff --git a/pubspec.yaml b/pubspec.yaml index 363ea7f..8857c57 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,14 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.23+257 +version: 0.0.32+266 environment: sdk: ^3.8.1 dependencies: clock: ^1.1.2 + collection: ^1.19.1 cupertino_icons: ^1.0.6 drift: ^2.27.0 drift_flutter: ^0.2.4 @@ -17,6 +18,8 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + flutter_numeric_text: ^1.3.3 + flutter_popup: ^3.3.9 fluttericon: ^2.0.0 font_awesome_flutter: ^11.0.0 intl: any diff --git a/test/db_tests/aggregates/group_test.dart b/test/db_tests/aggregates/group_test.dart index 3d51a06..1498523 100644 --- a/test/db_tests/aggregates/group_test.dart +++ b/test/db_tests/aggregates/group_test.dart @@ -35,25 +35,23 @@ void main() { testPlayer4 = Player(name: 'Diana'); testGroup1 = Group( name: 'Test Group', - description: '', members: [testPlayer1, testPlayer2, testPlayer3], + description: 'description of the test group 1', ); testGroup2 = Group( id: 'gr2', name: 'Second Group', - description: '', members: [testPlayer2, testPlayer3, testPlayer4], + description: 'description of the test group 2', ); testGroup3 = Group( id: 'gr2', name: 'Second Group', - description: '', members: [testPlayer2, testPlayer4], ); testGroup4 = Group( id: 'gr2', name: 'Second Group', - description: '', members: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); }); @@ -62,312 +60,355 @@ void main() { await database.close(); }); group('Group Tests', () { - // Verifies that a single group can be added and retrieved with all fields and members intact. - test('Adding and fetching a single group works correctly', () async { - await database.groupDao.addGroup(group: testGroup1); + group('CREATE', () { + test('Adding and fetching a single group works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); - final fetchedGroup = await database.groupDao.getGroupById( - groupId: testGroup1.id, - ); - - expect(fetchedGroup.id, testGroup1.id); - expect(fetchedGroup.name, testGroup1.name); - expect(fetchedGroup.createdAt, testGroup1.createdAt); - - expect(fetchedGroup.members.length, testGroup1.members.length); - for (int i = 0; i < testGroup1.members.length; i++) { - expect(fetchedGroup.members[i].id, testGroup1.members[i].id); - expect(fetchedGroup.members[i].name, testGroup1.members[i].name); - expect( - fetchedGroup.members[i].createdAt, - testGroup1.members[i].createdAt, + final fetchedGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, ); - } - }); - // Verifies that multiple groups can be added and retrieved with correct members. - test('Adding and fetching multiple groups works correctly', () async { - await database.groupDao.addGroupsAsList( - groups: [testGroup1, testGroup2, testGroup3, testGroup4], - ); + expect(fetchedGroup.id, testGroup1.id); + expect(fetchedGroup.name, testGroup1.name); + expect(fetchedGroup.createdAt, testGroup1.createdAt); - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups.length, 2); - - final testGroups = {testGroup1.id: testGroup1, testGroup2.id: testGroup2}; - - for (final group in allGroups) { - final testGroup = testGroups[group.id]!; - - expect(group.id, testGroup.id); - expect(group.name, testGroup.name); - expect(group.createdAt, testGroup.createdAt); - - expect(group.members.length, testGroup.members.length); - for (int i = 0; i < testGroup.members.length; i++) { - expect(group.members[i].id, testGroup.members[i].id); - expect(group.members[i].name, testGroup.members[i].name); - expect(group.members[i].createdAt, testGroup.members[i].createdAt); + expect(fetchedGroup.members.length, testGroup1.members.length); + for (int i = 0; i < testGroup1.members.length; i++) { + expect(fetchedGroup.members[i].id, testGroup1.members[i].id); + expect(fetchedGroup.members[i].name, testGroup1.members[i].name); + expect( + fetchedGroup.members[i].createdAt, + testGroup1.members[i].createdAt, + ); } - } - }); + }); - // Verifies that adding the same group twice does not create duplicates. - test('Adding the same group twice does not create duplicates', () async { - await database.groupDao.addGroup(group: testGroup1); - await database.groupDao.addGroup(group: testGroup1); + test('Adding the same group twice does not create duplicates', () async { + await database.groupDao.addGroup(group: testGroup1); + await database.groupDao.addGroup(group: testGroup1); - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups.length, 1); - }); + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 1); - // Verifies that groupExists returns correct boolean based on group presence. - test('Group existence check works correctly', () async { - var groupExists = await database.groupDao.groupExists( - groupId: testGroup1.id, - ); - expect(groupExists, false); - - await database.groupDao.addGroup(group: testGroup1); - - groupExists = await database.groupDao.groupExists(groupId: testGroup1.id); - expect(groupExists, true); - }); - - // Verifies that deleteGroup removes the group and returns true. - test('Deleting a group works correctly', () async { - await database.groupDao.addGroup(group: testGroup1); - - final groupDeleted = await database.groupDao.deleteGroup( - groupId: testGroup1.id, - ); - expect(groupDeleted, true); - - final groupExists = await database.groupDao.groupExists( - groupId: testGroup1.id, - ); - expect(groupExists, false); - }); - - // Verifies that updateGroupName correctly updates only the name field. - test('Updating a group name works correctly', () async { - await database.groupDao.addGroup(group: testGroup1); - - const newGroupName = 'new group name'; - - await database.groupDao.updateGroupName( - groupId: testGroup1.id, - newName: newGroupName, - ); - - final result = await database.groupDao.getGroupById( - groupId: testGroup1.id, - ); - expect(result.name, newGroupName); - }); - - // Verifies that getGroupCount returns correct count through add/delete operations. - test('Getting the group count works correctly', () async { - final initialCount = await database.groupDao.getGroupCount(); - expect(initialCount, 0); - - await database.groupDao.addGroup(group: testGroup1); - - final groupAdded = await database.groupDao.getGroupCount(); - expect(groupAdded, 1); - - final groupRemoved = await database.groupDao.deleteGroup( - groupId: testGroup1.id, - ); - expect(groupRemoved, true); - - final finalCount = await database.groupDao.getGroupCount(); - expect(finalCount, 0); - }); - - // Verifies that getAllGroups returns an empty list when no groups exist. - test('getAllGroups returns empty list when no groups exist', () async { - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups, isEmpty); - }); - - // Verifies that getGroupById throws StateError for non-existent group ID. - test('getGroupById throws exception for non-existent group', () async { - expect( - () => database.groupDao.getGroupById(groupId: 'non-existent-id'), - throwsA(isA()), - ); - }); - - // Verifies that addGroup returns false when trying to add a duplicate group. - test('addGroup returns false when group already exists', () async { - final firstAdd = await database.groupDao.addGroup(group: testGroup1); - expect(firstAdd, true); - - final secondAdd = await database.groupDao.addGroup(group: testGroup1); - expect(secondAdd, false); - - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups.length, 1); - }); - - // Verifies that addGroupsAsList handles an empty list without errors. - test('addGroupsAsList handles empty list correctly', () async { - await database.groupDao.addGroupsAsList(groups: []); - - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups.length, 0); - }); - - // Verifies that deleteGroup returns false for a non-existent group ID. - test('deleteGroup returns false for non-existent group', () async { - final deleted = await database.groupDao.deleteGroup( - groupId: 'non-existent-id', - ); - expect(deleted, false); - }); - - // Verifies that updateGroupName returns false for a non-existent group ID. - test('updateGroupName returns false for non-existent group', () async { - final updated = await database.groupDao.updateGroupName( - groupId: 'non-existent-id', - newName: 'New Name', - ); - expect(updated, false); - }); - - // Verifies that updateGroupDescription correctly updates the description field. - test('Updating a group description works correctly', () async { - await database.groupDao.addGroup(group: testGroup1); - - const newDescription = 'This is a new description'; - - final updated = await database.groupDao.updateGroupDescription( - groupId: testGroup1.id, - newDescription: newDescription, - ); - expect(updated, true); - - final result = await database.groupDao.getGroupById( - groupId: testGroup1.id, - ); - expect(result.description, newDescription); - }); - - // Verifies that updateGroupDescription can set the description to null. - test('updateGroupDescription can set description to null', () async { - final groupWithDescription = Group( - name: 'Group with description', - description: 'Initial description', - members: [testPlayer1], - ); - await database.groupDao.addGroup(group: groupWithDescription); - - final updated = await database.groupDao.updateGroupDescription( - groupId: groupWithDescription.id, - newDescription: 'Updated description', - ); - expect(updated, true); - - final result = await database.groupDao.getGroupById( - groupId: groupWithDescription.id, - ); - expect(result.description, 'Updated description'); - }); - - // Verifies that updateGroupDescription returns false for a non-existent group. - test( - 'updateGroupDescription returns false for non-existent group', - () async { - final updated = await database.groupDao.updateGroupDescription( - groupId: 'non-existent-id', - newDescription: 'New Description', + final fetchedGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, ); - expect(updated, false); - }, - ); + expect(fetchedGroup.id, testGroup1.id); + expect(fetchedGroup.members.length, testGroup1.members.length); + }); - // Verifies that deleteAllGroups removes all groups from the database. - test('deleteAllGroups removes all groups', () async { - await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]); + test('addGroup() returns false when group already exists', () async { + final firstAdd = await database.groupDao.addGroup(group: testGroup1); + expect(firstAdd, isTrue); - final countBefore = await database.groupDao.getGroupCount(); - expect(countBefore, 2); + final secondAdd = await database.groupDao.addGroup(group: testGroup1); + expect(secondAdd, isFalse); - final deleted = await database.groupDao.deleteAllGroups(); - expect(deleted, true); + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 1); + }); - final countAfter = await database.groupDao.getGroupCount(); - expect(countAfter, 0); + test('Adding and fetching multiple groups works correctly', () async { + await database.groupDao.addGroupsAsList( + groups: [testGroup1, testGroup2, testGroup3, testGroup4], + ); + + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 2); + + final testGroups = { + testGroup1.id: testGroup1, + testGroup2.id: testGroup2, + }; + + for (final group in allGroups) { + final testGroup = testGroups[group.id]!; + + expect(group.id, testGroup.id); + expect(group.name, testGroup.name); + expect(group.createdAt, testGroup.createdAt); + + expect(group.members.length, testGroup.members.length); + for (int i = 0; i < testGroup.members.length; i++) { + expect(group.members[i].id, testGroup.members[i].id); + expect(group.members[i].name, testGroup.members[i].name); + expect(group.members[i].createdAt, testGroup.members[i].createdAt); + } + } + }); + + test('addGroupsAsList() handles empty list correctly', () async { + await database.groupDao.addGroupsAsList(groups: []); + + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 0); + }); }); - // Verifies that deleteAllGroups returns false when no groups exist. - test('deleteAllGroups returns false when no groups exist', () async { - final deleted = await database.groupDao.deleteAllGroups(); - expect(deleted, false); + group('READ', () { + test('groupExists() works correctly', () async { + var groupExists = await database.groupDao.groupExists( + groupId: testGroup1.id, + ); + expect(groupExists, isFalse); + + await database.groupDao.addGroup(group: testGroup1); + + groupExists = await database.groupDao.groupExists( + groupId: testGroup1.id, + ); + expect(groupExists, isTrue); + }); + + test('getGroupCount() works correctly', () async { + var count = await database.groupDao.getGroupCount(); + expect(count, 0); + + var added = await database.groupDao.addGroup(group: testGroup1); + expect(added, isTrue); + count = await database.groupDao.getGroupCount(); + expect(count, 1); + + added = await database.groupDao.addGroup(group: testGroup2); + expect(added, isTrue); + count = await database.groupDao.getGroupCount(); + expect(count, 2); + + final removed = await database.groupDao.deleteGroup( + groupId: testGroup1.id, + ); + expect(removed, isTrue); + count = await database.groupDao.getGroupCount(); + expect(count, 1); + }); + + test('getGroupById() throws exception for non-existent group', () async { + expect( + () => database.groupDao.getGroupById(groupId: 'non-existent-id'), + throwsA(isA()), + ); + }); + + test('getAllGroups() returns empty list when no groups exist', () async { + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups, isEmpty); + }); + + test('addGroupsAsList() with duplicate groups only adds once', () async { + await database.groupDao.addGroupsAsList( + groups: [testGroup1, testGroup1, testGroup1], + ); + + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 1); + }); }); - // Verifies that groups with special characters (quotes, emojis) are stored correctly. - test('Group with special characters in name is stored correctly', () async { - final specialGroup = Group( - name: 'Group\'s & "Special" ', - description: 'Description with émojis 🎮🎲', - members: [testPlayer1], - ); - await database.groupDao.addGroup(group: specialGroup); + group('UPDATE', () { + test('updateGroupName() works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); - final fetchedGroup = await database.groupDao.getGroupById( - groupId: specialGroup.id, + const newName = 'New name'; + await database.groupDao.updateGroupName( + groupId: testGroup1.id, + name: newName, + ); + + final result = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(result.name, newName); + }); + + test('updateGroupName() returns false for non-existent group', () async { + final updated = await database.groupDao.updateGroupName( + groupId: 'non-existent-id', + name: 'New name', + ); + expect(updated, isFalse); + }); + + test('updateGroupDescription() works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); + + const newDescription = 'New description'; + final updated = await database.groupDao.updateGroupDescription( + groupId: testGroup1.id, + description: newDescription, + ); + expect(updated, isTrue); + + final group = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(group.description, newDescription); + }); + + test( + 'updateGroupDescription() returns false for non-existent group', + () async { + final updated = await database.groupDao.updateGroupDescription( + groupId: 'non-existent-id', + description: 'New description', + ); + expect(updated, isFalse); + }, + ); + + test('Multiple updates to the same group work correctly', () async { + await database.groupDao.addGroup(group: testGroup1); + const newName = 'New name'; + const newDescription = 'New description'; + + await database.groupDao.updateGroupName( + groupId: testGroup1.id, + name: newName, + ); + await database.groupDao.updateGroupDescription( + groupId: testGroup1.id, + description: newDescription, + ); + + final updatedGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(updatedGroup.name, newName); + expect(updatedGroup.description, newDescription); + }); + + test('replaceGroupPlayers() works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); + + final initialGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(initialGroup.members.length, 3); + expect( + initialGroup.members + .map((p) => p.id) + .toList() + .contains(testPlayer1.id), + isTrue, + ); + expect( + initialGroup.members + .map((p) => p.id) + .toList() + .contains(testPlayer2.id), + isTrue, + ); + expect( + initialGroup.members + .map((p) => p.id) + .toList() + .contains(testPlayer3.id), + isTrue, + ); + + final newPlayers = [testPlayer2, testPlayer4]; + final replaced = await database.playerGroupDao.replaceGroupPlayers( + groupId: testGroup1.id, + newPlayers: newPlayers, + ); + expect(replaced, isTrue); + + final updatedGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(updatedGroup.members.length, 2); + + final memberIds = updatedGroup.members.map((p) => p.id).toList(); + expect(memberIds.contains(testPlayer2.id), isTrue); + expect(memberIds.contains(testPlayer4.id), isTrue); + expect(memberIds.contains(testPlayer1.id), isFalse); + expect(memberIds.contains(testPlayer3.id), isFalse); + }); + + test('replaceGroupPlayers() ignores empty list ', () async { + await database.groupDao.addGroup(group: testGroup1); + + final replaced = await database.playerGroupDao.replaceGroupPlayers( + groupId: testGroup1.id, + newPlayers: [], + ); + expect(replaced, isFalse); + + final updatedGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(updatedGroup.members.length, testGroup1.members.length); + }); + + test( + 'replaceGroupPlayers() returns false for non-existent group', + () async { + final replaced = await database.playerGroupDao.replaceGroupPlayers( + groupId: 'non-existent-id', + newPlayers: [testPlayer1], + ); + expect(replaced, isFalse); + }, ); - expect(fetchedGroup.name, 'Group\'s & "Special" '); - expect(fetchedGroup.description, 'Description with émojis 🎮🎲'); }); - // Verifies that a group with an empty members list can be stored and retrieved. - test('Group with empty members list is stored correctly', () async { - final emptyGroup = Group( - name: 'Empty Group', - description: '', - members: [], - ); - await database.groupDao.addGroup(group: emptyGroup); + group('DELETE', () { + test('deleteGroup() works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); - final fetchedGroup = await database.groupDao.getGroupById( - groupId: emptyGroup.id, - ); - expect(fetchedGroup.name, 'Empty Group'); - expect(fetchedGroup.members, isEmpty); + final groupDeleted = await database.groupDao.deleteGroup( + groupId: testGroup1.id, + ); + expect(groupDeleted, isTrue); + + final groupExists = await database.groupDao.groupExists( + groupId: testGroup1.id, + ); + expect(groupExists, isFalse); + }); + + test('deleteGroup() returns false for non-existent group', () async { + final deleted = await database.groupDao.deleteGroup( + groupId: 'non-existent-id', + ); + expect(deleted, isFalse); + }); + + test('deleteAllGroups() works correctly', () async { + await database.groupDao.addGroupsAsList( + groups: [testGroup1, testGroup2], + ); + + var count = await database.groupDao.getGroupCount(); + expect(count, 2); + + final deleted = await database.groupDao.deleteAllGroups(); + expect(deleted, isTrue); + + count = await database.groupDao.getGroupCount(); + expect(count, 0); + }); + + test('deleteAllGroups() returns false when no groups exist', () async { + final deleted = await database.groupDao.deleteAllGroups(); + expect(deleted, isFalse); + }); }); - // Verifies that multiple sequential updates to the same group work correctly. - test('Multiple updates to the same group work correctly', () async { - await database.groupDao.addGroup(group: testGroup1); + group('Edge Cases', () { + test('Group with special characters is stored correctly', () async { + final specialGroup = Group( + name: 'Group\'s & "Special" ', + description: 'Description with émojis 🎮🎲', + members: [testPlayer1], + ); + await database.groupDao.addGroup(group: specialGroup); - await database.groupDao.updateGroupName( - groupId: testGroup1.id, - newName: 'Updated Name', - ); - await database.groupDao.updateGroupDescription( - groupId: testGroup1.id, - newDescription: 'Updated Description', - ); - - final updatedGroup = await database.groupDao.getGroupById( - groupId: testGroup1.id, - ); - expect(updatedGroup.name, 'Updated Name'); - expect(updatedGroup.description, 'Updated Description'); - expect(updatedGroup.members.length, testGroup1.members.length); - }); - - // Verifies that addGroupsAsList with duplicate groups only adds unique ones. - test('addGroupsAsList with duplicate groups only adds once', () async { - await database.groupDao.addGroupsAsList( - groups: [testGroup1, testGroup1, testGroup1], - ); - - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups.length, 1); + final fetchedGroup = await database.groupDao.getGroupById( + groupId: specialGroup.id, + ); + expect(fetchedGroup.name, 'Group\'s & "Special" '); + expect(fetchedGroup.description, 'Description with émojis 🎮🎲'); + }); }); }); } diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 3305b9a..37c1cd0 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -101,212 +101,284 @@ void main() { }); group('Match Tests', () { - // Verifies that a single match can be added and retrieved with all fields, group, and players intact. - test('Adding and fetching single match works correctly', () async { - await database.matchDao.addMatch(match: testMatch1); + group('CREATE', () { + test('Adding and fetching single match works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); - final result = await database.matchDao.getMatchById( - matchId: testMatch1.id, - ); + final result = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); - expect(result.id, testMatch1.id); - expect(result.name, testMatch1.name); - expect(result.createdAt, testMatch1.createdAt); + expect(result.id, testMatch1.id); + expect(result.name, testMatch1.name); + expect(result.createdAt, testMatch1.createdAt); - if (result.group != null) { - expect(result.group!.members.length, testGroup1.members.length); + if (result.group != null) { + expect(result.group!.members.length, testGroup1.members.length); - for (int i = 0; i < testGroup1.members.length; i++) { - expect(result.group!.members[i].id, testGroup1.members[i].id); - expect(result.group!.members[i].name, testGroup1.members[i].name); - } - } else { - fail('Group is null'); - } - expect(result.players.length, testMatch1.players.length); - - for (int i = 0; i < testMatch1.players.length; i++) { - expect(result.players[i].id, testMatch1.players[i].id); - expect(result.players[i].name, testMatch1.players[i].name); - expect(result.players[i].createdAt, testMatch1.players[i].createdAt); - } - }); - - // Verifies that multiple matches can be added and retrieved with correct groups and players. - test('Adding and fetching multiple matches works correctly', () async { - await database.matchDao.addMatchAsList( - matches: [ - testMatch1, - testMatch2, - testMatchOnlyGroup, - testMatchOnlyPlayers, - ], - ); - - final allMatches = await database.matchDao.getAllMatches(); - expect(allMatches.length, 4); - - final testMatches = { - testMatch1.id: testMatch1, - testMatch2.id: testMatch2, - testMatchOnlyGroup.id: testMatchOnlyGroup, - testMatchOnlyPlayers.id: testMatchOnlyPlayers, - }; - - for (final match in allMatches) { - final testMatch = testMatches[match.id]!; - - // Match-Checks - expect(match.id, testMatch.id); - expect(match.name, testMatch.name); - expect(match.createdAt, testMatch.createdAt); - - // Group-Checks - if (testMatch.group != null) { - expect(match.group!.id, testMatch.group!.id); - expect(match.group!.name, testMatch.group!.name); - expect(match.group!.createdAt, testMatch.group!.createdAt); - - // Group Members-Checks - expect(match.group!.members.length, testMatch.group!.members.length); - for (int i = 0; i < testMatch.group!.members.length; i++) { - expect(match.group!.members[i].id, testMatch.group!.members[i].id); - expect( - match.group!.members[i].name, - testMatch.group!.members[i].name, - ); - expect( - match.group!.members[i].createdAt, - testMatch.group!.members[i].createdAt, - ); + for (int i = 0; i < testGroup1.members.length; i++) { + expect(result.group!.members[i].id, testGroup1.members[i].id); + expect(result.group!.members[i].name, testGroup1.members[i].name); } } else { - expect(match.group, null); + fail('Group is null'); } + expect(result.players.length, testMatch1.players.length); - // Players-Checks - expect(match.players.length, testMatch.players.length); - for (int i = 0; i < testMatch.players.length; i++) { - expect(match.players[i].id, testMatch.players[i].id); - expect(match.players[i].name, testMatch.players[i].name); - expect(match.players[i].createdAt, testMatch.players[i].createdAt); + for (int i = 0; i < testMatch1.players.length; i++) { + expect(result.players[i].id, testMatch1.players[i].id); + expect(result.players[i].name, testMatch1.players[i].name); + expect(result.players[i].createdAt, testMatch1.players[i].createdAt); } - } + }); + + test('Adding and fetching multiple matches works correctly', () async { + await database.matchDao.addMatchesAsList( + matches: [ + testMatch1, + testMatch2, + testMatchOnlyGroup, + testMatchOnlyPlayers, + ], + ); + + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches.length, 4); + + final testMatches = { + testMatch1.id: testMatch1, + testMatch2.id: testMatch2, + testMatchOnlyGroup.id: testMatchOnlyGroup, + testMatchOnlyPlayers.id: testMatchOnlyPlayers, + }; + + for (final match in allMatches) { + final testMatch = testMatches[match.id]!; + + // Match-Checks + expect(match.id, testMatch.id); + expect(match.name, testMatch.name); + expect(match.createdAt, testMatch.createdAt); + + // Group-Checks + if (testMatch.group != null) { + expect(match.group!.id, testMatch.group!.id); + expect(match.group!.name, testMatch.group!.name); + expect(match.group!.createdAt, testMatch.group!.createdAt); + + // Group Members-Checks + expect( + match.group!.members.length, + testMatch.group!.members.length, + ); + for (int i = 0; i < testMatch.group!.members.length; i++) { + expect( + match.group!.members[i].id, + testMatch.group!.members[i].id, + ); + expect( + match.group!.members[i].name, + testMatch.group!.members[i].name, + ); + expect( + match.group!.members[i].createdAt, + testMatch.group!.members[i].createdAt, + ); + } + } else { + expect(match.group, null); + } + + // Players-Checks + expect(match.players.length, testMatch.players.length); + for (int i = 0; i < testMatch.players.length; i++) { + expect(match.players[i].id, testMatch.players[i].id); + expect(match.players[i].name, testMatch.players[i].name); + expect(match.players[i].createdAt, testMatch.players[i].createdAt); + } + } + }); + + test('addMatch() ignores duplicate games', () async { + var added = await database.matchDao.addMatch(match: testMatch1); + expect(added, isTrue); + + added = await database.matchDao.addMatch(match: testMatch1); + expect(added, isFalse); + + final matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 1); + }); + + test('addMatchesAsList() returns isFalse for empty list', () async { + var added = await database.matchDao.addMatchesAsList(matches: []); + expect(added, isFalse); + }); + + test('addMatchesAsList() ignores duplicate games', () async { + final added = await database.matchDao.addMatchesAsList( + matches: [testMatch1, testMatch2, testMatch1], + ); + expect(added, isTrue); + + final matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 2); + }); }); - // Verifies that adding the same match twice does not create duplicates. - test('Adding the same match twice does not create duplicates', () async { - await database.matchDao.addMatch(match: testMatch1); - await database.matchDao.addMatch(match: testMatch1); + group('READ', () { + test('matchExists() works correctly', () async { + var matchExists = await database.matchDao.matchExists( + matchId: testMatch1.id, + ); + expect(matchExists, isFalse); - final matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); + await database.matchDao.addMatch(match: testMatch1); + + matchExists = await database.matchDao.matchExists( + matchId: testMatch1.id, + ); + expect(matchExists, isTrue); + }); + + test('getMatchesByGroup() works correctly', () async { + var matches = await database.matchDao.getMatchesByGroup( + groupId: 'non-existing-id', + ); + + expect(matches, isEmpty); + + await database.matchDao.addMatch(match: testMatch1); + matches = await database.matchDao.getMatchesByGroup( + groupId: testGroup1.id, + ); + expect(matches, isNotEmpty); + + final match = matches.first; + expect(match.id, testMatch1.id); + expect(match.group, isNotNull); + expect(match.group!.id, testGroup1.id); + }); + + test('getMatchCount() works correctly', () async { + var count = await database.matchDao.getMatchCount(); + expect(count, 0); + + await database.matchDao.addMatch(match: testMatch1); + + count = await database.matchDao.getMatchCount(); + expect(count, 1); + + await database.matchDao.addMatch(match: testMatch2); + + count = await database.matchDao.getMatchCount(); + expect(count, 2); + + await database.matchDao.deleteMatch(matchId: testMatch1.id); + + count = await database.matchDao.getMatchCount(); + expect(count, 1); + + await database.matchDao.deleteMatch(matchId: testMatch2.id); + + count = await database.matchDao.getMatchCount(); + expect(count, 0); + }); + + test('getMatchCountByGame() works correctly', () async { + var count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 0); + + await database.matchDao.addMatch(match: testMatch1); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 1); + + await database.matchDao.addMatch(match: testMatch2); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 2); + + await database.matchDao.deleteMatch(matchId: testMatch1.id); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 1); + + await database.matchDao.deleteMatch(matchId: testMatch2.id); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 0); + }); + + test('getMatchCountByGame() returns 0 for non-existent game', () async { + final count = await database.matchDao.getMatchCountByGame( + gameId: 'non-existent-game-id', + ); + expect(count, 0); + }); }); - // Verifies that matchExists returns correct boolean based on match presence. - test('Match existence check works correctly', () async { - var matchExists = await database.matchDao.matchExists( - matchId: testMatch1.id, - ); - expect(matchExists, false); + group('UPDATE', () { + test('updateMatchName() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); - await database.matchDao.addMatch(match: testMatch1); + const newName = 'New name'; + await database.matchDao.updateMatchName( + matchId: testMatch1.id, + name: newName, + ); - matchExists = await database.matchDao.matchExists(matchId: testMatch1.id); - expect(matchExists, true); - }); + final fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(fetchedMatch.name, newName); + }); - // Verifies that deleteMatch removes the match and returns true. - test('Deleting a match works correctly', () async { - await database.matchDao.addMatch(match: testMatch1); + test('updateMatchName() does nothing for non-existent match', () async { + final updated = await database.matchDao.updateMatchName( + matchId: 'non-existing-id', + name: 'New Name', + ); + expect(updated, isFalse); - final matchDeleted = await database.matchDao.deleteMatch( - matchId: testMatch1.id, - ); - expect(matchDeleted, true); + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); - final matchExists = await database.matchDao.matchExists( - matchId: testMatch1.id, - ); - expect(matchExists, false); - }); + test('updateMatchGroup() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.groupDao.addGroup(group: testGroup2); - // Verifies that getMatchCount returns correct count through add/delete operations. - test('Getting the match count works correctly', () async { - var matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); + await database.matchDao.updateMatchGroup( + matchId: testMatch1.id, + groupId: testGroup2.id, + ); - await database.matchDao.addMatch(match: testMatch1); + final fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(fetchedMatch.group?.id, testGroup2.id); + }); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); + test('updateMatchGroup() does nothing for non-existent match', () async { + final updated = await database.matchDao.updateMatchGroup( + matchId: 'non-existing-id', + groupId: 'group-id', + ); + expect(updated, isFalse); - await database.matchDao.addMatch(match: testMatch2); + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 2); - - await database.matchDao.deleteMatch(matchId: testMatch1.id); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); - - await database.matchDao.deleteMatch(matchId: testMatch2.id); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); - }); - - // Verifies that updateMatchName correctly updates only the name field. - test('Renaming a match works correctly', () async { - await database.matchDao.addMatch(match: testMatch1); - - var fetchedMatch = await database.matchDao.getMatchById( - matchId: testMatch1.id, - ); - expect(fetchedMatch.name, testMatch1.name); - - const newName = 'Updated Match Name'; - await database.matchDao.updateMatchName( - matchId: testMatch1.id, - newName: newName, - ); - - fetchedMatch = await database.matchDao.getMatchById( - matchId: testMatch1.id, - ); - expect(fetchedMatch.name, newName); - }); - - test('Fetching a winner works correctly', () async { - await database.matchDao.addMatch(match: testMatch1); - - var fetchedMatch = await database.matchDao.getMatchById( - matchId: testMatch1.id, - ); - - expect(fetchedMatch.mvp, isNotNull); - expect(fetchedMatch.mvp.first.id, testPlayer4.id); - }); - - test('Setting a winner works correctly', () async { - await database.matchDao.addMatch(match: testMatch1); - - await database.scoreEntryDao.setWinner( - matchId: testMatch1.id, - playerId: testPlayer5.id, - ); - - final fetchedMatch = await database.matchDao.getMatchById( - matchId: testMatch1.id, - ); - expect(fetchedMatch.mvp, isNotNull); - expect(fetchedMatch.mvp.first.id, testPlayer5.id); - }); - - test( - 'removeMatchGroup removes group from match with existing group', - () async { + test('removeMatchGroup() works correctly', () async { + expect(testMatch1.group, isNotNull); await database.matchDao.addMatch(match: testMatch1); final removed = await database.matchDao.removeMatchGroup( @@ -318,53 +390,151 @@ void main() { matchId: testMatch1.id, ); expect(updatedMatch.group, null); - expect(updatedMatch.game.id, testMatch1.game.id); - expect(updatedMatch.name, testMatch1.name); - expect(updatedMatch.notes, testMatch1.notes); - }, - ); + }); - test( - 'removeMatchGroup on match that already has no group still succeeds', - () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); + test( + 'removeMatchGroup() on match that already has no group still succeeds', + () async { + await database.matchDao.addMatch(match: testMatchOnlyPlayers); - final removed = await database.matchDao.removeMatchGroup( - matchId: testMatchOnlyPlayers.id, - ); - expect(removed, isTrue); + final removed = await database.matchDao.removeMatchGroup( + matchId: testMatchOnlyPlayers.id, + ); + expect(removed, isTrue); - final updatedMatch = await database.matchDao.getMatchById( - matchId: testMatchOnlyPlayers.id, - ); - expect(updatedMatch.group, null); - }, - ); - - test('removeMatchGroup on non-existing match returns false', () async { - final removed = await database.matchDao.removeMatchGroup( - matchId: 'non-existing-id', + final updatedMatch = await database.matchDao.getMatchById( + matchId: testMatchOnlyPlayers.id, + ); + expect(updatedMatch.group, null); + }, ); - expect(removed, isFalse); + + test( + 'removeMatchGroup() on non-existing match returns isFalse', + () async { + final removed = await database.matchDao.removeMatchGroup( + matchId: 'non-existing-id', + ); + expect(removed, isFalse); + }, + ); + + test('updateMatchNotes() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + const newName = 'New name'; + await database.matchDao.updateMatchName( + matchId: testMatch1.id, + name: newName, + ); + + final fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(fetchedMatch.name, newName); + }); + + test('updateMatchNotes() does nothing for non-existent game', () async { + final updated = await database.matchDao.updateMatchNotes( + matchId: 'non-existing-id', + notes: 'New notes', + ); + expect(updated, isFalse); + + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); + + test('updateMatchEndedAt() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + DateTime newEndedAt = DateTime(2030, 1, 1, 12, 0, 0); + await database.matchDao.updateMatchEndedAt( + matchId: testMatch1.id, + endedAt: newEndedAt, + ); + + final fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(fetchedMatch.endedAt, newEndedAt); + }); + + test('updateMatchEndedAt() does nothing for non-existent game', () async { + final updated = await database.matchDao.updateMatchEndedAt( + matchId: 'non-existing-id', + endedAt: DateTime.now(), + ); + expect(updated, isFalse); + + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); }); - test('Fetching all matches related to a group', () async { - var matches = await database.matchDao.getGroupMatches( - groupId: 'non-existing-id', - ); + group('DELETE', () { + test('deleteMatch() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); - expect(matches, isEmpty); + var deleted = await database.matchDao.deleteMatch( + matchId: testMatch1.id, + ); + expect(deleted, isTrue); + final matchExists = await database.matchDao.matchExists( + matchId: testMatch1.id, + ); + expect(matchExists, isFalse); + + deleted = await database.matchDao.deleteMatch(matchId: testMatch1.id); + expect(deleted, isFalse); + }); + + test('deleteAllMatches() works correctly', () async { + await database.matchDao.addMatchesAsList( + matches: [testMatch1, testMatch2, testMatchOnlyPlayers], + ); + + var count = await database.matchDao.getMatchCount(); + expect(count, 3); + + var deleted = await database.matchDao.deleteAllMatches(); + expect(deleted, isTrue); + + count = await database.matchDao.getMatchCount(); + expect(count, 0); + + deleted = await database.matchDao.deleteAllMatches(); + expect(deleted, isFalse); + }); + }); + + test('deleteMatchesByGame() deletes all matches for a game', () async { await database.matchDao.addMatch(match: testMatch1); + await database.matchDao.addMatch(match: testMatch2); - matches = await database.matchDao.getGroupMatches(groupId: testGroup1.id); + var count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 2); - expect(matches, isNotEmpty); + final deletedCount = await database.matchDao.deleteMatchesByGame( + gameId: testGame.id, + ); + expect(deletedCount, 2); - final match = matches.first; - expect(match.id, testMatch1.id); - expect(match.group, isNotNull); - expect(match.group!.id, testGroup1.id); + count = await database.matchDao.getMatchCountByGame(gameId: testGame.id); + expect(count, 0); + + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); + + test('deleteMatchesByGame() returns 0 for non-existent game', () async { + final deletedCount = await database.matchDao.deleteMatchesByGame( + gameId: 'non-existent-game-id', + ); + expect(deletedCount, 0); }); }); } diff --git a/test/db_tests/aggregates/team_test.dart b/test/db_tests/aggregates/team_test.dart index 39c5be5..fefdcc5 100644 --- a/test/db_tests/aggregates/team_test.dart +++ b/test/db_tests/aggregates/team_test.dart @@ -1,3 +1,5 @@ +import 'dart:core' hide Match; + import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; @@ -18,8 +20,11 @@ void main() { late Team testTeam1; late Team testTeam2; late Team testTeam3; - late Game testGame1; - late Game testGame2; + late Team testTeam4; + late Game testGame; + late Match testMatch1; + late Match testMatch2; + late Match matchWithNoTeams; final fixedDate = DateTime(2025, 11, 19, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -40,27 +45,35 @@ void main() { testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]); testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]); testTeam3 = Team(name: 'Team Gamma', members: [testPlayer1, testPlayer3]); - testGame1 = Game( - name: 'Game 1', - ruleset: Ruleset.singleWinner, - description: 'Test game 1', + testTeam4 = Team(name: 'Team Omega', members: [testPlayer2, testPlayer4]); + testGame = Game( + name: 'Test Game', + ruleset: Ruleset.highestScore, color: GameColor.blue, icon: '', ); - testGame2 = Game( - name: 'Game 2', - ruleset: Ruleset.highestScore, - description: 'Test game 2', - color: GameColor.red, - icon: '', + testMatch1 = Match( + name: 'Match 1', + game: testGame, + players: [], + teams: [testTeam1, testTeam2], + ); + testMatch2 = Match( + name: 'Match 2', + game: testGame, + players: [], + teams: [testTeam3, testTeam4], + ); + matchWithNoTeams = Match( + name: 'Match with no teams', + game: testGame, + players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); }); - + await database.gameDao.addGame(game: testGame); await database.playerDao.addPlayersAsList( players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); - await database.gameDao.addGame(game: testGame1); - await database.gameDao.addGame(game: testGame2); }); tearDown(() async { @@ -68,460 +81,251 @@ void main() { }); group('Team Tests', () { - // Verifies that a single team can be added and retrieved with all fields intact. - test('Adding and fetching a single team works correctly', () async { - final added = await database.teamDao.addTeam(team: testTeam1); - expect(added, true); - - final fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(fetchedTeam.id, testTeam1.id); - expect(fetchedTeam.name, testTeam1.name); - expect(fetchedTeam.createdAt, testTeam1.createdAt); - }); - - // Verifies that multiple teams can be added at once and retrieved correctly. - test('Adding and fetching multiple teams works correctly', () async { - await database.teamDao.addTeamsAsList( - teams: [testTeam1, testTeam2, testTeam3], - ); - - final allTeams = await database.teamDao.getAllTeams(); - expect(allTeams.length, 3); - - final testTeams = { - testTeam1.id: testTeam1, - testTeam2.id: testTeam2, - testTeam3.id: testTeam3, - }; - - for (final team in allTeams) { - final testTeam = testTeams[team.id]!; - - expect(team.id, testTeam.id); - expect(team.name, testTeam.name); - expect(team.createdAt, testTeam.createdAt); - } - }); - - // Verifies that adding the same team twice does not create duplicates and returns false. - test('Adding the same team twice does not create duplicates', () async { - await database.teamDao.addTeam(team: testTeam1); - final addedAgain = await database.teamDao.addTeam(team: testTeam1); - - expect(addedAgain, false); - - final teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 1); - }); - - // Verifies that teamExists returns correct boolean based on team presence. - test('Team existence check works correctly', () async { - var teamExists = await database.teamDao.teamExists(teamId: testTeam1.id); - expect(teamExists, false); - - await database.teamDao.addTeam(team: testTeam1); - - teamExists = await database.teamDao.teamExists(teamId: testTeam1.id); - expect(teamExists, true); - }); - - // Verifies that deleteTeam removes the team and returns true. - test('Deleting a team works correctly', () async { - await database.teamDao.addTeam(team: testTeam1); - - final teamDeleted = await database.teamDao.deleteTeam( - teamId: testTeam1.id, - ); - expect(teamDeleted, true); - - final teamExists = await database.teamDao.teamExists( - teamId: testTeam1.id, - ); - expect(teamExists, false); - }); - - // Verifies that deleteTeam returns false for a non-existent team ID. - test('Deleting a non-existent team returns false', () async { - final teamDeleted = await database.teamDao.deleteTeam( - teamId: 'non-existent-id', - ); - expect(teamDeleted, false); - }); - - // Verifies that getTeamCount returns correct count through add/delete operations. - test('Getting the team count works correctly', () async { - var teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 0); - - await database.teamDao.addTeam(team: testTeam1); - - teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 1); - - await database.teamDao.addTeam(team: testTeam2); - - teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 2); - - await database.teamDao.deleteTeam(teamId: testTeam1.id); - - teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 1); - - await database.teamDao.deleteTeam(teamId: testTeam2.id); - - teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 0); - }); - - // Verifies that updateTeamName correctly updates only the name field. - test('Updating team name works correctly', () async { - await database.teamDao.addTeam(team: testTeam1); - - var fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - expect(fetchedTeam.name, testTeam1.name); - - const newName = 'Updated Team Name'; - await database.teamDao.updateTeamName( - teamId: testTeam1.id, - newName: newName, - ); - - fetchedTeam = await database.teamDao.getTeamById(teamId: testTeam1.id); - expect(fetchedTeam.name, newName); - }); - - // Verifies that deleteAllTeams removes all teams from the database. - test('Deleting all teams works correctly', () async { - await database.teamDao.addTeamsAsList( - teams: [testTeam1, testTeam2, testTeam3], - ); - - var teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 3); - - final deleted = await database.teamDao.deleteAllTeams(); - expect(deleted, true); - - teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 0); - }); - - // Verifies that deleteAllTeams returns false when no teams exist. - test('Deleting all teams when empty returns false', () async { - final deleted = await database.teamDao.deleteAllTeams(); - expect(deleted, false); - }); - - // Verifies that addTeamsAsList returns false when given an empty list. - test('Adding teams as list with empty list returns false', () async { - final added = await database.teamDao.addTeamsAsList(teams: []); - expect(added, false); - }); - - // Verifies that addTeamsAsList with duplicate IDs ignores duplicates and keeps the first. - test('Adding teams with duplicate IDs ignores duplicates', () async { - final duplicateTeam = Team( - id: testTeam1.id, - name: 'Duplicate Team', - members: [testPlayer4], - ); - - await database.teamDao.addTeamsAsList( - teams: [testTeam1, duplicateTeam, testTeam2], - ); - - final teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 2); - - // The first one should be kept (insertOrIgnore) - final fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - expect(fetchedTeam.name, testTeam1.name); - }); - - // Verifies that getAllTeams returns empty list when no teams exist. - test('Getting all teams when empty returns empty list', () async { - final allTeams = await database.teamDao.getAllTeams(); - expect(allTeams.isEmpty, true); - }); - - // Verifies that getTeamById throws exception for non-existent team. - test('Getting non-existent team throws exception', () async { - expect( - () => database.teamDao.getTeamById(teamId: 'non-existent-id'), - throwsA(isA()), - ); - }); - - // Verifies that updating team name preserves other fields. - test('Updating team name preserves other team fields', () async { - await database.teamDao.addTeam(team: testTeam1); - final originalTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - final originalCreatedAt = originalTeam.createdAt; - - const newName = 'Brand New Team Name'; - await database.teamDao.updateTeamName( - teamId: testTeam1.id, - newName: newName, - ); - - final updatedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(updatedTeam.name, newName); - expect(updatedTeam.id, testTeam1.id); - expect(updatedTeam.createdAt, originalCreatedAt); - }); - - // Verifies that team name can be updated to an empty string. - test('Updating team name to empty string works', () async { - await database.teamDao.addTeam(team: testTeam1); - - await database.teamDao.updateTeamName(teamId: testTeam1.id, newName: ''); - - final updatedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(updatedTeam.name, ''); - }); - - // Verifies that team name can be updated to a very long string. - test('Updating team name to long string works', () async { - await database.teamDao.addTeam(team: testTeam1); - final longName = 'A' * 500; // 500 character name - - await database.teamDao.updateTeamName( - teamId: testTeam1.id, - newName: longName, - ); - - final updatedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(updatedTeam.name, longName); - expect(updatedTeam.name.length, 500); - }); - - // Verifies that updating non-existent team name doesn't throw error. - test('Updating non-existent team name completes without error', () async { - expect( - () => database.teamDao.updateTeamName( - teamId: 'non-existent-id', - newName: 'New Name', - ), - returnsNormally, - ); - }); - - // Verifies that deleteTeam only affects the specified team. - test('Deleting one team does not affect other teams', () async { - await database.teamDao.addTeamsAsList( - teams: [testTeam1, testTeam2, testTeam3], - ); - - await database.teamDao.deleteTeam(teamId: testTeam2.id); - - final allTeams = await database.teamDao.getAllTeams(); - expect(allTeams.length, 2); - expect(allTeams.any((t) => t.id == testTeam1.id), true); - expect(allTeams.any((t) => t.id == testTeam2.id), false); - expect(allTeams.any((t) => t.id == testTeam3.id), true); - }); - - // Verifies that teams with overlapping members are independent. - test('Teams with overlapping members are independent', () async { - // Create two matches since player_match has primary key {playerId, matchId} - final match1 = Match( - name: 'Match 1', - game: testGame1, - players: [testPlayer1, testPlayer2], - ); - final match2 = Match( - name: 'Match 2', - game: testGame2, - players: [testPlayer1, testPlayer2], - ); - await database.matchDao.addMatch(match: match1); - await database.matchDao.addMatch(match: match2); - - // Add teams to database - await database.teamDao.addTeamsAsList(teams: [testTeam1, testTeam3]); - - // Associate players with teams through match1 - // testTeam1: player1, player2 - await database.playerMatchDao.addPlayerToMatch( - playerId: testPlayer1.id, - matchId: match1.id, - teamId: testTeam1.id, - ); - await database.playerMatchDao.addPlayerToMatch( - playerId: testPlayer2.id, - matchId: match1.id, - teamId: testTeam1.id, - ); - - // Associate players with teams through match2 - // testTeam3: player1, player3 (overlapping player1) - await database.playerMatchDao.addPlayerToMatch( - playerId: testPlayer1.id, - matchId: match2.id, - teamId: testTeam3.id, - ); - await database.playerMatchDao.addPlayerToMatch( - playerId: testPlayer3.id, - matchId: match2.id, - teamId: testTeam3.id, - ); - - final team1 = await database.teamDao.getTeamById(teamId: testTeam1.id); - final team3 = await database.teamDao.getTeamById(teamId: testTeam3.id); - - expect(team1.members.length, 2); - expect(team3.members.length, 2); - expect(team1.members.any((p) => p.id == testPlayer1.id), true); - expect(team3.members.any((p) => p.id == testPlayer1.id), true); - }); - - // Verifies that adding teams sequentially works correctly. - test('Adding teams sequentially maintains correct count', () async { - var count = await database.teamDao.getTeamCount(); - expect(count, 0); - - await database.teamDao.addTeam(team: testTeam1); - count = await database.teamDao.getTeamCount(); - expect(count, 1); - - await database.teamDao.addTeam(team: testTeam2); - count = await database.teamDao.getTeamCount(); - expect(count, 2); - - await database.teamDao.addTeam(team: testTeam3); - count = await database.teamDao.getTeamCount(); - expect(count, 3); - }); - - // Verifies that getAllTeams returns all teams with correct data. - test('Getting all teams returns all teams with correct data', () async { - await database.teamDao.addTeamsAsList( - teams: [testTeam1, testTeam2, testTeam3], - ); - - final allTeams = await database.teamDao.getAllTeams(); - - expect(allTeams.length, 3); - expect(allTeams.map((t) => t.id).toSet(), { - testTeam1.id, - testTeam2.id, - testTeam3.id, + group('CREATE', () { + test('Adding and fetching a single team works correctly', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); + final added = await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); + expect(added, isTrue); + + final fetchedTeam = await database.teamDao.getTeamById( + teamId: testTeam1.id, + ); + + expect(fetchedTeam.id, testTeam1.id); + expect(fetchedTeam.name, testTeam1.name); + expect(fetchedTeam.createdAt, testTeam1.createdAt); + expect(fetchedTeam.members.length, testTeam1.members.length); + for (int i = 0; i < fetchedTeam.members.length; i++) { + expect(fetchedTeam.members[i].id, testTeam1.members[i].id); + expect(fetchedTeam.members[i].name, testTeam1.members[i].name); + } + }); + + test('Adding and fetching multiple teams works correctly', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); + await database.teamDao.addTeamsAsList( + teams: [testTeam1, testTeam2, testTeam3], + matchId: matchWithNoTeams.id, + ); + + final allTeams = await database.teamDao.getAllTeams(); + expect(allTeams.length, 3); + + final testTeams = { + testTeam1.id: testTeam1, + testTeam2.id: testTeam2, + testTeam3.id: testTeam3, + }; + + for (final team in allTeams) { + final testTeam = testTeams[team.id]!; + + expect(team.id, testTeam.id); + expect(team.name, testTeam.name); + expect(team.createdAt, testTeam.createdAt); + } + }); + + test('addTeam() ignores duplicates', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); + var added = await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); + expect(added, isTrue); + + added = await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); + expect(added, isFalse); + + final teamCount = await database.teamDao.getTeamCount(); + expect(teamCount, 1); + }); + + test('addTeamsAsList() with empty list returns isFalse', () async { + final added = await database.teamDao.addTeamsAsList( + teams: [], + matchId: matchWithNoTeams.id, + ); + expect(added, isFalse); + }); + + test('addTeamsAsList() ignores duplicates', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); + final added = await database.teamDao.addTeamsAsList( + teams: [testTeam1, testTeam2, testTeam1], + matchId: matchWithNoTeams.id, + ); + expect(added, isTrue); + + final teamCount = await database.teamDao.getTeamCount(); + expect(teamCount, 2); }); }); - // Verifies that teamExists returns false for deleted teams. - test('Team existence returns false after deletion', () async { - await database.teamDao.addTeam(team: testTeam1); - expect(await database.teamDao.teamExists(teamId: testTeam1.id), true); + group('READ', () { + test('getTeamCount works correctly', () async { + var count = await database.teamDao.getTeamCount(); + expect(count, 0); - await database.teamDao.deleteTeam(teamId: testTeam1.id); - expect(await database.teamDao.teamExists(teamId: testTeam1.id), false); + await database.matchDao.addMatch(match: testMatch1); + + count = await database.teamDao.getTeamCount(); + expect(count, 2); + + await database.teamDao.addTeam( + team: testTeam2, + matchId: matchWithNoTeams.id, + ); + + count = await database.teamDao.getTeamCount(); + expect(count, 2); + + await database.teamDao.deleteTeam(teamId: testTeam1.id); + + count = await database.teamDao.getTeamCount(); + expect(count, 1); + + await database.teamDao.deleteTeam(teamId: testTeam2.id); + + count = await database.teamDao.getTeamCount(); + expect(count, 0); + }); + + test('teamExists() works correctly', () async { + var teamExists = await database.teamDao.teamExists( + teamId: testTeam1.id, + ); + expect(teamExists, isFalse); + + await database.matchDao.addMatch(match: matchWithNoTeams); + + await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); + + teamExists = await database.teamDao.teamExists(teamId: testTeam1.id); + expect(teamExists, isTrue); + }); + + test('getAllTeams() with no teams returns empty list', () async { + final allTeams = await database.teamDao.getAllTeams(); + expect(allTeams, isA>()); + expect(allTeams.isEmpty, isTrue); + }); + + test('getAllTeams() works correctly', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); + + await database.teamDao.addTeamsAsList( + teams: [testTeam1, testTeam2, testTeam3], + matchId: matchWithNoTeams.id, + ); + + final allTeams = await database.teamDao.getAllTeams(); + + expect(allTeams.length, 3); + expect(allTeams.map((t) => t.id).toSet(), { + testTeam1.id, + testTeam2.id, + testTeam3.id, + }); + }); + + test('Getting non-existent team throws exception', () async { + expect( + () => database.teamDao.getTeamById(teamId: 'non-existent-id'), + throwsA(isA()), + ); + }); }); - // Verifies that adding multiple teams in batch then deleting returns correct count. - test('Batch add then partial delete maintains correct count', () async { - await database.teamDao.addTeamsAsList( - teams: [testTeam1, testTeam2, testTeam3], - ); + group('UPDATED', () { + test('updateTeamName() works correctly', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); - expect(await database.teamDao.getTeamCount(), 3); + await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); - await database.teamDao.deleteTeam(teamId: testTeam1.id); - expect(await database.teamDao.getTeamCount(), 2); + var fetchedTeam = await database.teamDao.getTeamById( + teamId: testTeam1.id, + ); + expect(fetchedTeam.name, testTeam1.name); - await database.teamDao.deleteTeam(teamId: testTeam3.id); - expect(await database.teamDao.getTeamCount(), 1); + const newName = 'New name'; + await database.teamDao.updateTeamName( + teamId: testTeam1.id, + name: newName, + ); + + fetchedTeam = await database.teamDao.getTeamById(teamId: testTeam1.id); + expect(fetchedTeam.name, newName); + }); + + test('updateTeamName() does nothing for non-existent team', () async { + final updated = await database.teamDao.updateTeamName( + teamId: 'non-existing-id', + name: 'New Name', + ); + expect(updated, isFalse); + + final allTeams = await database.teamDao.getAllTeams(); + expect(allTeams, isEmpty); + }); }); - // Verifies that deleteAllTeams with single team works. - test('Deleting all teams with single team returns true', () async { - await database.teamDao.addTeam(team: testTeam1); - expect(await database.teamDao.getTeamCount(), 1); + group('DELETE', () { + test('deleteTeam() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.matchDao.addMatch(match: matchWithNoTeams); - final deleted = await database.teamDao.deleteAllTeams(); - expect(deleted, true); - expect(await database.teamDao.getTeamCount(), 0); - }); + await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); - // Verifies that addTeam after deleteAllTeams works correctly. - test('Adding team after deleteAllTeams works correctly', () async { - await database.teamDao.addTeamsAsList(teams: [testTeam1, testTeam2]); - expect(await database.teamDao.getTeamCount(), 2); + final deleted = await database.teamDao.deleteTeam(teamId: testTeam1.id); + expect(deleted, isTrue); - await database.teamDao.deleteAllTeams(); - expect(await database.teamDao.getTeamCount(), 0); + final teamExists = await database.teamDao.teamExists( + teamId: testTeam1.id, + ); + expect(teamExists, isFalse); + }); - final added = await database.teamDao.addTeam(team: testTeam3); - expect(added, true); - expect(await database.teamDao.getTeamCount(), 1); + test('Deleting a non-existent team returns isFalse', () async { + final deleted = await database.teamDao.deleteTeam( + teamId: 'non-existent-id', + ); + expect(deleted, isFalse); + }); - final fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam3.id, - ); - expect(fetchedTeam.name, testTeam3.name); - }); + test('deleteAllTeams() works correctly', () async { + await database.matchDao.addMatchesAsList( + matches: [testMatch1, testMatch2], + ); + var teamCount = await database.teamDao.getTeamCount(); + expect(teamCount, 4); - // Verifies that addTeamsAsList with partial duplicates ignores duplicates. - test('Adding teams with some duplicates ignores only duplicates', () async { - await database.teamDao.addTeam(team: testTeam1); + final deleted = await database.teamDao.deleteAllTeams(); + expect(deleted, isTrue); - final duplicateTeam1 = Team( - id: testTeam1.id, - name: 'Different Name', - members: [testPlayer3], - ); + teamCount = await database.teamDao.getTeamCount(); + expect(teamCount, 0); + }); - await database.teamDao.addTeamsAsList( - teams: [duplicateTeam1, testTeam2, testTeam3], - ); - - final allTeams = await database.teamDao.getAllTeams(); - expect(allTeams.length, 3); - - // Verify testTeam1 retained original name (was inserted first) - final team1 = await database.teamDao.getTeamById(teamId: testTeam1.id); - expect(team1.name, testTeam1.name); - }); - - // Verifies that team IDs are preserved correctly. - test('Team IDs are preserved through add and retrieve', () async { - await database.teamDao.addTeam(team: testTeam1); - - final fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(fetchedTeam.id, testTeam1.id); - }); - - // Verifies that createdAt timestamps are preserved. - test('Team createdAt timestamps are preserved', () async { - await database.teamDao.addTeam(team: testTeam1); - - final fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(fetchedTeam.createdAt, testTeam1.createdAt); + test('deleteAllTeams() with empty list returns false', () async { + final deleted = await database.teamDao.deleteAllTeams(); + expect(deleted, isFalse); + }); }); }); } diff --git a/test/db_tests/entities/game_test.dart b/test/db_tests/entities/game_test.dart index b00dba1..778d43b 100644 --- a/test/db_tests/entities/game_test.dart +++ b/test/db_tests/entities/game_test.dart @@ -24,6 +24,7 @@ void main() { withClock(fakeClock, () { testGame1 = Game( + id: 'game1', name: 'Chess', ruleset: Ruleset.singleWinner, description: 'A classic strategy game', @@ -54,492 +55,350 @@ void main() { }); group('Game Tests', () { - // Verifies that getAllGames returns an empty list when the database has no games. - test('getAllGames returns empty list when no games exist', () async { - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); + group('CREATE', () { + test('Adding and fetching a single game works correctly', () async { + final added = await database.gameDao.addGame(game: testGame1); + expect(added, isTrue); - // Verifies that a single game can be added and retrieved with all fields intact. - test('Adding and fetching a single game works correctly', () async { - await database.gameDao.addGame(game: testGame1); + final game = await database.gameDao.getGameById(gameId: testGame1.id); + expect(game.id, testGame1.id); + expect(game.name, testGame1.name); + expect(game.ruleset, testGame1.ruleset); + expect(game.description, testGame1.description); + expect(game.color, testGame1.color); + expect(game.icon, testGame1.icon); + expect(game.createdAt, testGame1.createdAt); + }); - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 1); - expect(allGames.first.id, testGame1.id); - expect(allGames.first.name, testGame1.name); - expect(allGames.first.ruleset, testGame1.ruleset); - expect(allGames.first.description, testGame1.description); - expect(allGames.first.color, testGame1.color); - expect(allGames.first.icon, testGame1.icon); - expect(allGames.first.createdAt, testGame1.createdAt); - }); + test('Adding and fetching multiple games works correctly', () async { + final added = await database.gameDao.addGamesAsList( + games: [testGame1, testGame2, testGame3], + ); + expect(added, isTrue); - // Verifies that multiple games can be added and retrieved correctly. - test('Adding and fetching multiple games works correctly', () async { - await database.gameDao.addGame(game: testGame1); - await database.gameDao.addGame(game: testGame2); - await database.gameDao.addGame(game: testGame3); + final allGames = await database.gameDao.getAllGames(); + expect(allGames.length, 3); - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 3); + // Map for connecting fetched games with expected games + final testGames = { + testGame1.id: testGame1, + testGame2.id: testGame2, + testGame3.id: testGame3, + }; - final names = allGames.map((g) => g.name).toList(); - expect(names, containsAll(['Chess', 'Poker', 'Monopoly'])); - }); + for (final game in allGames) { + final testGame = testGames[game.id]!; - // Verifies that getGameById returns the correct game with all properties. - test('getGameById returns correct game', () async { - await database.gameDao.addGame(game: testGame1); - await database.gameDao.addGame(game: testGame2); + expect(game.id, testGame.id); + expect(game.name, testGame.name); + expect(game.createdAt, testGame.createdAt); + expect(game.description, testGame.description); + expect(game.ruleset, testGame.ruleset); + expect(game.color, testGame.color); + expect(game.icon, testGame.icon); + } + }); - final game = await database.gameDao.getGameById(gameId: testGame2.id); - expect(game.id, testGame2.id); - expect(game.name, testGame2.name); - expect(game.ruleset, testGame2.ruleset); - expect(game.description, testGame2.description); - expect(game.color, testGame2.color); - expect(game.icon, testGame2.icon); - }); + test('addGamesAsList() returns false for empty list', () async { + final result = await database.gameDao.addGamesAsList(games: []); + expect(result, isFalse); - // Verifies that getGameById throws a StateError when the game doesn't exist. - test('getGameById throws exception for non-existent game', () async { - expect( - () => database.gameDao.getGameById(gameId: 'non-existent-id'), - throwsA(isA()), + final allGames = await database.gameDao.getAllGames(); + expect(allGames.length, 0); + }); + + test('addGamesAsList() ignores duplicate games', () async { + final added = await database.gameDao.addGamesAsList( + games: [testGame1, testGame2, testGame1], + ); + expect(added, isTrue); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames.length, 2); + }); + + test( + 'Game with special characters in name is stored correctly', + () async { + final specialGame = Game( + name: 'Game\'s & "Special" ', + ruleset: Ruleset.multipleWinners, + description: 'Description with émojis 🎮🎲', + color: GameColor.purple, + icon: '', + ); + await database.gameDao.addGame(game: specialGame); + + final fetchedGame = await database.gameDao.getGameById( + gameId: specialGame.id, + ); + expect(fetchedGame.name, 'Game\'s & "Special" '); + expect(fetchedGame.description, 'Description with émojis 🎮🎲'); + }, ); }); - // Verifies that addGame returns true when a game is successfully added. - test('addGame returns true when game is added successfully', () async { - final result = await database.gameDao.addGame(game: testGame1); - expect(result, true); + group('READ', () { + test('getGameById() works correctly', () async { + await database.gameDao.addGame(game: testGame1); - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 1); + final game = await database.gameDao.getGameById(gameId: testGame1.id); + expect(game.id, testGame1.id); + expect(game.name, testGame1.name); + expect(game.ruleset, testGame1.ruleset); + expect(game.description, testGame1.description); + expect(game.color, testGame1.color); + expect(game.icon, testGame1.icon); + }); + + test('getGameById() throws exception for non-existent game', () async { + expect( + () => database.gameDao.getGameById(gameId: 'non-existent-id'), + throwsA(isA()), + ); + }); + + test('gameExists() works correctly', () async { + var exists = await database.gameDao.gameExists(gameId: testGame1.id); + expect(exists, isFalse); + + await database.gameDao.addGame(game: testGame1); + exists = await database.gameDao.gameExists(gameId: testGame1.id); + expect(exists, isTrue); + }); + + test('getAllGames() returns empty list when no games exist', () async { + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test('getGameCount() works correctly', () async { + var count = await database.gameDao.getGameCount(); + expect(count, 0); + + await database.gameDao.addGame(game: testGame1); + count = await database.gameDao.getGameCount(); + expect(count, 1); + + await database.gameDao.addGame(game: testGame2); + count = await database.gameDao.getGameCount(); + expect(count, 2); + + await database.gameDao.deleteGame(gameId: testGame1.id); + count = await database.gameDao.getGameCount(); + expect(count, 1); + }); }); - // Verifies that addGame returns false when trying to add a duplicate game. - test('addGame returns false when game already exists', () async { - final firstAdd = await database.gameDao.addGame(game: testGame1); - expect(firstAdd, true); + group('UPDATE', () { + test('updateGameName() works correctly', () async { + await database.gameDao.addGame(game: testGame1); + const newName = 'New name'; - final secondAdd = await database.gameDao.addGame(game: testGame1); - expect(secondAdd, false); + final updated = await database.gameDao.updateGameName( + gameId: testGame1.id, + name: newName, + ); + expect(updated, isTrue); - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 1); + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + expect(updatedGame.name, newName); + }); + + test('updateGameName() does nothing for non-existent game', () async { + final updated = await database.gameDao.updateGameName( + gameId: 'non-existent-id', + name: 'New name', + ); + expect(updated, isFalse); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test('updateGameRuleset() works correctly', () async { + await database.gameDao.addGame(game: testGame1); + const ruleset = Ruleset.highestScore; + + final updated = await database.gameDao.updateGameRuleset( + gameId: testGame1.id, + ruleset: ruleset, + ); + expect(updated, isTrue); + + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + expect(updatedGame.ruleset, ruleset); + }); + + test('updateGameRuleset() does nothing for non-existent game', () async { + final updated = await database.gameDao.updateGameRuleset( + gameId: 'non-existent-id', + ruleset: Ruleset.lowestScore, + ); + expect(updated, isFalse); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test('updateGameDescription() works correctly', () async { + await database.gameDao.addGame(game: testGame1); + const newDescription = 'New description'; + + final updated = await database.gameDao.updateGameDescription( + gameId: testGame1.id, + description: newDescription, + ); + expect(updated, isTrue); + + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + expect(updatedGame.description, newDescription); + }); + + test( + 'updateGameDescription() does nothing for non-existent game', + () async { + final updated = await database.gameDao.updateGameDescription( + gameId: 'non-existent-id', + description: 'New description', + ); + expect(updated, isFalse); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }, + ); + + test('updateGameColor() works correctly', () async { + await database.gameDao.addGame(game: testGame1); + + await database.gameDao.updateGameColor( + gameId: testGame1.id, + color: GameColor.green, + ); + + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + expect(updatedGame.color, GameColor.green); + }); + + test('updateGameColor() does nothing for non-existent game', () async { + final updated = await database.gameDao.updateGameColor( + gameId: 'non-existent-id', + color: GameColor.green, + ); + expect(updated, isFalse); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test('updateGameIcon() works correctly', () async { + await database.gameDao.addGame(game: testGame1); + const newIcon = 'new_chess_icon'; + + final updated = await database.gameDao.updateGameIcon( + gameId: testGame1.id, + icon: newIcon, + ); + expect(updated, isTrue); + + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + expect(updatedGame.icon, newIcon); + }); + + test('updateGameIcon() does nothing for non-existent game', () async { + final updated = await database.gameDao.updateGameIcon( + gameId: 'non-existent-id', + icon: 'New icon', + ); + expect(updated, isFalse); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test('Multiple updates to the same game work correctly', () async { + await database.gameDao.addGame(game: testGame1); + + const newName = 'New name'; + await database.gameDao.updateGameName( + gameId: testGame1.id, + name: newName, + ); + + const newGameColor = GameColor.teal; + await database.gameDao.updateGameColor( + gameId: testGame1.id, + color: newGameColor, + ); + + const newDescription = 'New description'; + await database.gameDao.updateGameDescription( + gameId: testGame1.id, + description: newDescription, + ); + + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + + // Changed values + expect(updatedGame.name, newName); + expect(updatedGame.color, newGameColor); + expect(updatedGame.description, newDescription); + + // Staying the same + expect(updatedGame.ruleset, testGame1.ruleset); + expect(updatedGame.icon, testGame1.icon); + }); }); - - // Verifies that a game with empty optional fields can be added and retrieved. - test('addGame handles game with null optional fields', () async { - final gameWithNulls = Game( - name: 'Simple Game', - ruleset: Ruleset.lowestScore, - description: 'A simple game', - color: GameColor.green, - icon: '', - ); - final result = await database.gameDao.addGame(game: gameWithNulls); - expect(result, true); - - final fetchedGame = await database.gameDao.getGameById( - gameId: gameWithNulls.id, - ); - expect(fetchedGame.name, 'Simple Game'); - expect(fetchedGame.description, 'A simple game'); - expect(fetchedGame.color, GameColor.green); - expect(fetchedGame.icon, ''); - }); - - // Verifies that multiple games can be added at once using addGamesAsList. - test('addGamesAsList adds multiple games correctly', () async { - final result = await database.gameDao.addGamesAsList( - games: [testGame1, testGame2, testGame3], - ); - expect(result, true); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 3); - }); - - // Verifies that addGamesAsList returns false when given an empty list. - test('addGamesAsList returns false for empty list', () async { - final result = await database.gameDao.addGamesAsList(games: []); - expect(result, false); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 0); - }); - - // Verifies that addGamesAsList ignores duplicate games when adding. - test('addGamesAsList ignores duplicate games', () async { - await database.gameDao.addGame(game: testGame1); - - final result = await database.gameDao.addGamesAsList( - games: [testGame1, testGame2], - ); - expect(result, true); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 2); - }); - - // Verifies that deleteGame returns true and removes the game from database. - test('deleteGame returns true when game is deleted', () async { - await database.gameDao.addGame(game: testGame1); - - final result = await database.gameDao.deleteGame(gameId: testGame1.id); - expect(result, true); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that deleteGame returns false for a non-existent game ID. - test('deleteGame returns false for non-existent game', () async { - final result = await database.gameDao.deleteGame( - gameId: 'non-existent-id', - ); - expect(result, false); - }); - - // Verifies that deleteGame only removes the specified game, leaving others intact. - test('deleteGame only deletes the specified game', () async { - await database.gameDao.addGamesAsList( - games: [testGame1, testGame2, testGame3], - ); - - await database.gameDao.deleteGame(gameId: testGame2.id); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 2); - expect(allGames.any((g) => g.id == testGame2.id), false); - expect(allGames.any((g) => g.id == testGame1.id), true); - expect(allGames.any((g) => g.id == testGame3.id), true); - }); - - // Verifies that gameExists returns true when the game exists in database. - test('gameExists returns true for existing game', () async { - await database.gameDao.addGame(game: testGame1); - - final exists = await database.gameDao.gameExists(gameId: testGame1.id); - expect(exists, true); - }); - - // Verifies that gameExists returns false for a non-existent game ID. - test('gameExists returns false for non-existent game', () async { - final exists = await database.gameDao.gameExists( - gameId: 'non-existent-id', - ); - expect(exists, false); - }); - - // Verifies that gameExists returns false after a game has been deleted. - test('gameExists returns false after game is deleted', () async { - await database.gameDao.addGame(game: testGame1); - await database.gameDao.deleteGame(gameId: testGame1.id); - - final exists = await database.gameDao.gameExists(gameId: testGame1.id); - expect(exists, false); - }); - - // Verifies that updateGameName correctly updates only the name field. - test('updateGameName updates the name correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameName( - gameId: testGame1.id, - newName: 'Updated Chess', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.name, 'Updated Chess'); - expect(updatedGame.ruleset, testGame1.ruleset); - }); - - // Verifies that updateGameName does nothing when game doesn't exist. - test('updateGameName does nothing for non-existent game', () async { - await database.gameDao.updateGameName( - gameId: 'non-existent-id', - newName: 'New Name', - ); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that updateGameRuleset correctly updates only the ruleset field. - test('updateGameRuleset updates the ruleset correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameRuleset( - gameId: testGame1.id, - newRuleset: Ruleset.highestScore, - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.ruleset, Ruleset.highestScore); - expect(updatedGame.name, testGame1.name); - }); - - // Verifies that updateGameRuleset does nothing when game doesn't exist. - test('updateGameRuleset does nothing for non-existent game', () async { - await database.gameDao.updateGameRuleset( - gameId: 'non-existent-id', - newRuleset: Ruleset.lowestScore, - ); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that updateGameDescription correctly updates the description. - test('updateGameDescription updates the description correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameDescription( - gameId: testGame1.id, - newDescription: 'An updated description', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.description, 'An updated description'); - }); - - // Verifies that updateGameDescription can set the description to an empty string. - test('updateGameDescription can set description to empty string', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameDescription( - gameId: testGame1.id, - newDescription: '', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.description, ''); - }); - - // Verifies that updateGameDescription does nothing when game doesn't exist. - test('updateGameDescription does nothing for non-existent game', () async { - await database.gameDao.updateGameDescription( - gameId: 'non-existent-id', - newDescription: 'New Description', - ); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that updateGameColor correctly updates the color value. - test('updateGameColor updates the color correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameColor( - gameId: testGame1.id, - newColor: GameColor.green, - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.color, GameColor.green); - }); - - // Verifies that updateGameColor does nothing when game doesn't exist. - test('updateGameColor does nothing for non-existent game', () async { - await database.gameDao.updateGameColor( - gameId: 'non-existent-id', - newColor: GameColor.green, - ); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that updateGameIcon correctly updates the icon value. - test('updateGameIcon updates the icon correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameIcon( - gameId: testGame1.id, - newIcon: 'new_chess_icon', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.icon, 'new_chess_icon'); - }); - - // Verifies that updateGameIcon can update the icon. - test('updateGameIcon updates icon correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameIcon( - gameId: testGame1.id, - newIcon: 'new_icon', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.icon, 'new_icon'); - }); - - // Verifies that updateGameIcon does nothing when game doesn't exist. - test('updateGameIcon does nothing for non-existent game', () async { - await database.gameDao.updateGameIcon( - gameId: 'non-existent-id', - newIcon: 'some_icon', - ); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that getGameCount returns 0 when no games exist. - test('getGameCount returns 0 when no games exist', () async { - final count = await database.gameDao.getGameCount(); - expect(count, 0); - }); - - // Verifies that getGameCount returns the correct count after adding games. - test('getGameCount returns correct count after adding games', () async { - await database.gameDao.addGamesAsList( - games: [testGame1, testGame2, testGame3], - ); - - final count = await database.gameDao.getGameCount(); - expect(count, 3); - }); - - // Verifies that getGameCount updates correctly after deleting a game. - test('getGameCount updates correctly after deletion', () async { - await database.gameDao.addGamesAsList(games: [testGame1, testGame2]); - - final countBefore = await database.gameDao.getGameCount(); - expect(countBefore, 2); - - await database.gameDao.deleteGame(gameId: testGame1.id); - - final countAfter = await database.gameDao.getGameCount(); - expect(countAfter, 1); - }); - - // Verifies that deleteAllGames removes all games from the database. - test('deleteAllGames removes all games', () async { - await database.gameDao.addGamesAsList( - games: [testGame1, testGame2, testGame3], - ); - - final countBefore = await database.gameDao.getGameCount(); - expect(countBefore, 3); - - final result = await database.gameDao.deleteAllGames(); - expect(result, true); - - final countAfter = await database.gameDao.getGameCount(); - expect(countAfter, 0); - }); - - // Verifies that deleteAllGames returns false when no games exist. - test('deleteAllGames returns false when no games exist', () async { - final result = await database.gameDao.deleteAllGames(); - expect(result, false); - }); - - // Verifies that games with special characters (quotes, emojis) are stored correctly. - test('Game with special characters in name is stored correctly', () async { - final specialGame = Game( - name: 'Game\'s & "Special" ', - ruleset: Ruleset.multipleWinners, - description: 'Description with émojis 🎮🎲', - color: GameColor.purple, - icon: '', - ); - await database.gameDao.addGame(game: specialGame); - - final fetchedGame = await database.gameDao.getGameById( - gameId: specialGame.id, - ); - expect(fetchedGame.name, 'Game\'s & "Special" '); - expect(fetchedGame.description, 'Description with émojis 🎮🎲'); - }); - - // Verifies that games with empty string fields are stored and retrieved correctly. - test('Game with empty string fields is stored correctly', () async { - final emptyGame = Game( - name: '', - ruleset: Ruleset.singleWinner, - description: '', - icon: '', - color: GameColor.red, - ); - await database.gameDao.addGame(game: emptyGame); - - final fetchedGame = await database.gameDao.getGameById( - gameId: emptyGame.id, - ); - expect(fetchedGame.name, ''); - expect(fetchedGame.ruleset, Ruleset.singleWinner); - expect(fetchedGame.description, ''); - expect(fetchedGame.icon, ''); - }); - - // Verifies that games with very long strings (10000 chars) are handled correctly. - test('Game with very long strings is stored correctly', () async { - final longString = 'A' * 10000; - final longGame = Game( - name: longString, - description: longString, - ruleset: Ruleset.multipleWinners, - color: GameColor.yellow, - icon: '', - ); - await database.gameDao.addGame(game: longGame); - - final fetchedGame = await database.gameDao.getGameById( - gameId: longGame.id, - ); - expect(fetchedGame.name.length, 10000); - expect(fetchedGame.description.length, 10000); - expect(fetchedGame.ruleset, Ruleset.multipleWinners); - }); - - // Verifies that multiple sequential updates to the same game work correctly. - test('Multiple updates to the same game work correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameName( - gameId: testGame1.id, - newName: 'Updated Name', - ); - await database.gameDao.updateGameColor( - gameId: testGame1.id, - newColor: GameColor.teal, - ); - await database.gameDao.updateGameDescription( - gameId: testGame1.id, - newDescription: 'Updated Description', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.name, 'Updated Name'); - expect(updatedGame.color, GameColor.teal); - expect(updatedGame.description, 'Updated Description'); - expect(updatedGame.ruleset, testGame1.ruleset); - expect(updatedGame.icon, testGame1.icon); + group('DELETE', () { + test('deleteGame() works correctly', () async { + await database.gameDao.addGame(game: testGame1); + + final deleted = await database.gameDao.deleteGame(gameId: testGame1.id); + expect(deleted, isTrue); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test('deleteGame() returns false for non-existent game', () async { + final deleted = await database.gameDao.deleteGame( + gameId: 'non-existent-id', + ); + expect(deleted, isFalse); + }); + + test('deleteAllGames() removes all games', () async { + await database.gameDao.addGamesAsList( + games: [testGame1, testGame2, testGame3], + ); + + var count = await database.gameDao.getGameCount(); + expect(count, 3); + + final deleted = await database.gameDao.deleteAllGames(); + expect(deleted, isTrue); + + count = await database.gameDao.getGameCount(); + expect(count, 0); + }); + + test('deleteAllGames() returns false when no games exist', () async { + final deleted = await database.gameDao.deleteAllGames(); + expect(deleted, isFalse); + }); }); }); } diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index 1aab348..bfcced4 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -24,8 +24,8 @@ void main() { ); withClock(fakeClock, () { - testPlayer1 = Player(name: 'Test Player'); - testPlayer2 = Player(name: 'Second Player'); + testPlayer1 = Player(name: 'Anna', description: 'First test player'); + testPlayer2 = Player(name: 'Bob', description: 'Second test player'); testPlayer3 = Player(name: 'Charlie'); testPlayer4 = Player(name: 'Diana'); }); @@ -35,355 +35,314 @@ void main() { }); group('Player Tests', () { - // Verifies that players can be added and retrieved with all fields intact. - test('Adding and fetching single player works correctly', () async { - await database.playerDao.addPlayer(player: testPlayer1); - await database.playerDao.addPlayer(player: testPlayer2); + group('CREATE', () { + test('Adding and fetching single player works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer2); - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers.length, 2); + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 2); - final fetchedPlayer1 = allPlayers.firstWhere( - (g) => g.id == testPlayer1.id, - ); - expect(fetchedPlayer1.name, testPlayer1.name); - expect(fetchedPlayer1.createdAt, testPlayer1.createdAt); + final fetchedPlayer1 = allPlayers.firstWhere( + (g) => g.id == testPlayer1.id, + ); + expect(fetchedPlayer1.name, testPlayer1.name); + expect(fetchedPlayer1.createdAt, testPlayer1.createdAt); + expect(fetchedPlayer1.description, testPlayer1.description); - final fetchedPlayer2 = allPlayers.firstWhere( - (g) => g.id == testPlayer2.id, - ); - expect(fetchedPlayer2.name, testPlayer2.name); - expect(fetchedPlayer2.createdAt, testPlayer2.createdAt); - }); + final fetchedPlayer2 = allPlayers.firstWhere( + (g) => g.id == testPlayer2.id, + ); + expect(fetchedPlayer2.name, testPlayer2.name); + expect(fetchedPlayer2.createdAt, testPlayer2.createdAt); + expect(fetchedPlayer2.description, testPlayer2.description); + }); - // Verifies that multiple players can be added at once and retrieved correctly. - test('Adding and fetching multiple players works correctly', () async { - await database.playerDao.addPlayersAsList( - players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], - ); - - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers.length, 4); - - // Map for connecting fetched players with expected players - final testPlayers = { - testPlayer1.id: testPlayer1, - testPlayer2.id: testPlayer2, - testPlayer3.id: testPlayer3, - testPlayer4.id: testPlayer4, - }; - - for (final player in allPlayers) { - final testPlayer = testPlayers[player.id]!; - - expect(player.id, testPlayer.id); - expect(player.name, testPlayer.name); - expect(player.createdAt, testPlayer.createdAt); - } - }); - - // Verifies that adding the same player twice does not create duplicates. - test('Adding the same player twice does not create duplicates', () async { - await database.playerDao.addPlayer(player: testPlayer1); - await database.playerDao.addPlayer(player: testPlayer1); - - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers.length, 1); - }); - - // Verifies that playerExists returns correct boolean based on player presence. - test('Player existence check works correctly', () async { - var playerExists = await database.playerDao.playerExists( - playerId: testPlayer1.id, - ); - expect(playerExists, false); - - await database.playerDao.addPlayer(player: testPlayer1); - - playerExists = await database.playerDao.playerExists( - playerId: testPlayer1.id, - ); - expect(playerExists, true); - }); - - // Verifies that deletePlayer removes the player and returns true. - test('Deleting a player works correctly', () async { - await database.playerDao.addPlayer(player: testPlayer1); - final playerDeleted = await database.playerDao.deletePlayer( - playerId: testPlayer1.id, - ); - expect(playerDeleted, true); - - final playerExists = await database.playerDao.playerExists( - playerId: testPlayer1.id, - ); - expect(playerExists, false); - }); - - // Verifies that updatePlayerName correctly updates only the name field. - test('Updating a player name works correctly', () async { - await database.playerDao.addPlayer(player: testPlayer1); - - const newPlayerName = 'new player name'; - - await database.playerDao.updatePlayerName( - playerId: testPlayer1.id, - newName: newPlayerName, - ); - - final result = await database.playerDao.getPlayerById( - playerId: testPlayer1.id, - ); - expect(result.name, newPlayerName); - }); - - // Verifies that getPlayerCount returns correct count through add/delete operations. - test('Getting the player count works correctly', () async { - var playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 0); - - await database.playerDao.addPlayer(player: testPlayer1); - - playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 1); - - await database.playerDao.addPlayer(player: testPlayer2); - - playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 2); - - await database.playerDao.deletePlayer(playerId: testPlayer1.id); - - playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 1); - - await database.playerDao.deletePlayer(playerId: testPlayer2.id); - - playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 0); - }); - - // Verifies that getAllPlayers returns an empty list when no players exist. - test('getAllPlayers returns empty list when no players exist', () async { - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers, isEmpty); - }); - - // Verifies that getPlayerById returns the correct player. - test('getPlayerById returns correct player', () async { - await database.playerDao.addPlayer(player: testPlayer1); - await database.playerDao.addPlayer(player: testPlayer2); - - final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: testPlayer1.id, - ); - - expect(fetchedPlayer.id, testPlayer1.id); - expect(fetchedPlayer.name, testPlayer1.name); - expect(fetchedPlayer.createdAt, testPlayer1.createdAt); - expect(fetchedPlayer.description, testPlayer1.description); - }); - - // Verifies that getPlayerById throws StateError for non-existent player ID. - test('getPlayerById throws exception for non-existent player', () async { - expect( - () => database.playerDao.getPlayerById(playerId: 'non-existent-id'), - throwsA(isA()), - ); - }); - - // Verifies that addPlayer returns false when trying to add a duplicate player. - test('addPlayer returns false when player already exists', () async { - final firstAdd = await database.playerDao.addPlayer(player: testPlayer1); - expect(firstAdd, true); - - final secondAdd = await database.playerDao.addPlayer(player: testPlayer1); - expect(secondAdd, false); - }); - - // Verifies that addPlayersAsList handles empty list correctly. - test('addPlayersAsList handles empty list correctly', () async { - final result = await database.playerDao.addPlayersAsList(players: []); - expect(result, false); - - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers, isEmpty); - }); - - // Verifies that addPlayersAsList ignores duplicate player IDs. - test('addPlayersAsList with duplicate IDs ignores duplicates', () async { - await database.playerDao.addPlayersAsList( - players: [testPlayer1, testPlayer1, testPlayer2], - ); - - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers.length, 2); - }); - - // Verifies that deletePlayer returns false for non-existent player. - test('deletePlayer returns false for non-existent player', () async { - final result = await database.playerDao.deletePlayer( - playerId: 'non-existent-id', - ); - expect(result, false); - }); - - // Verifies that updatePlayerName does nothing for non-existent player (no exception). - test('updatePlayerName does nothing for non-existent player', () async { - // Should not throw, just do nothing - await database.playerDao.updatePlayerName( - playerId: 'non-existent-id', - newName: 'New Name', - ); - - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers, isEmpty); - }); - - // Verifies that deleteAllPlayers removes all players. - test('deleteAllPlayers removes all players', () async { - await database.playerDao.addPlayersAsList( - players: [testPlayer1, testPlayer2, testPlayer3], - ); - - var playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 3); - - final result = await database.playerDao.deleteAllPlayers(); - expect(result, true); - - playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 0); - }); - - // Verifies that deleteAllPlayers returns false when no players exist. - test('deleteAllPlayers returns false when no players exist', () async { - final result = await database.playerDao.deleteAllPlayers(); - expect(result, false); - }); - - // Verifies that a player with special characters in name is stored correctly. - test( - 'Player with special characters in name is stored correctly', - () async { - final specialPlayer = Player( - name: 'Test!@#\$%^&*()_+-=[]{}|;\':",.<>?/`~', - description: '', + test('Adding and fetching multiple players works correctly', () async { + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); - await database.playerDao.addPlayer(player: specialPlayer); + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 4); + + // Map for connecting fetched players with expected players + final testPlayers = { + testPlayer1.id: testPlayer1, + testPlayer2.id: testPlayer2, + testPlayer3.id: testPlayer3, + testPlayer4.id: testPlayer4, + }; + + for (final player in allPlayers) { + final testPlayer = testPlayers[player.id]!; + + expect(player.id, testPlayer.id); + expect(player.name, testPlayer.name); + expect(player.createdAt, testPlayer.createdAt); + expect(player.description, testPlayer.description); + } + }); + + test('Adding the same player twice does not create duplicates', () async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer1); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 1); + }); + + test('addPlayer() returns false when player already exists', () async { + var added = await database.playerDao.addPlayer(player: testPlayer1); + expect(added, isTrue); + + added = await database.playerDao.addPlayer(player: testPlayer1); + expect(added, isFalse); + }); + + test('addPlayersAsList() handles empty list correctly', () async { + final added = await database.playerDao.addPlayersAsList(players: []); + expect(added, isFalse); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers, isEmpty); + }); + + test( + 'addPlayersAsList() with duplicate IDs ignores duplicates', + () async { + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer1, testPlayer2], + ); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 2); + }, + ); + + test( + 'Player with special characters in name is stored correctly', + () async { + final specialPlayer = Player( + name: 'Test!@#\$%^&*()_+-=[]{}|;\':"😎,.<>?/`~', + ); + + await database.playerDao.addPlayer(player: specialPlayer); + + final fetchedPlayer = await database.playerDao.getPlayerById( + playerId: specialPlayer.id, + ); + expect(fetchedPlayer.name, specialPlayer.name); + }, + ); + }); + + group('READ', () { + test('getPlayerCount() works correctly', () async { + var playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 0); + + await database.playerDao.addPlayer(player: testPlayer1); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 1); + + await database.playerDao.addPlayer(player: testPlayer2); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 2); + + await database.playerDao.deletePlayer(playerId: testPlayer1.id); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 1); + + await database.playerDao.deletePlayer(playerId: testPlayer2.id); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 0); + }); + + test('playerExists() works correctly', () async { + var playerExists = await database.playerDao.playerExists( + playerId: testPlayer1.id, + ); + expect(playerExists, isFalse); + + await database.playerDao.addPlayer(player: testPlayer1); + playerExists = await database.playerDao.playerExists( + playerId: testPlayer1.id, + ); + expect(playerExists, isTrue); + }); + + test( + 'getAllPlayers() returns empty list when no players exist', + () async { + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers, isEmpty); + }, + ); + + test('getPlayerById() returns correct player', () async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer2); final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: specialPlayer.id, + playerId: testPlayer1.id, ); - expect(fetchedPlayer.name, specialPlayer.name); - }, - ); - // Verifies that a player with description is stored correctly. - test('Player with description is stored correctly', () async { - final playerWithDescription = Player( - name: 'Described Player', - description: 'This is a test description', + expect(fetchedPlayer.id, testPlayer1.id); + expect(fetchedPlayer.name, testPlayer1.name); + expect(fetchedPlayer.createdAt, testPlayer1.createdAt); + expect(fetchedPlayer.description, testPlayer1.description); + }); + + test( + 'getPlayerById() throws exception for non-existent player', + () async { + expect( + () => database.playerDao.getPlayerById(playerId: 'non-existent-id'), + throwsA(isA()), + ); + }, ); - - await database.playerDao.addPlayer(player: playerWithDescription); - - final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: playerWithDescription.id, - ); - expect(fetchedPlayer.name, playerWithDescription.name); - expect(fetchedPlayer.description, playerWithDescription.description); }); - // Verifies that a player with null description is stored correctly. - test('Player with null description is stored correctly', () async { - final playerWithoutDescription = Player( - name: 'No Description Player', - description: '', + group('UPDATE', () { + test('updatePlayerName() works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + + const newName = 'New name'; + + await database.playerDao.updatePlayerName( + playerId: testPlayer1.id, + name: newName, + ); + + final player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.name, newName); + }); + + test('updatePlayerName() does nothing for non-existent player', () async { + final updated = await database.playerDao.updatePlayerName( + playerId: 'non-existent-id', + name: 'New name', + ); + expect(updated, isFalse); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers, isEmpty); + }); + + test('updatePlayerDescription() works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + + const newDescription = 'New description'; + + final updated = await database.playerDao.updatePlayerDescription( + playerId: testPlayer1.id, + description: newDescription, + ); + expect(updated, isTrue); + + final player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.description, newDescription); + }); + + test( + 'updatePlayerDescription() does nothing for non-existent player', + () async { + final updated = await database.playerDao.updatePlayerDescription( + playerId: 'non-existent-id', + description: 'New description', + ); + expect(updated, isFalse); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers, isEmpty); + }, ); - await database.playerDao.addPlayer(player: playerWithoutDescription); + test('Multiple updates to the same player work correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); - final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: playerWithoutDescription.id, - ); - expect(fetchedPlayer.description, ''); + await database.playerDao.updatePlayerName( + playerId: testPlayer1.id, + name: 'First Update', + ); + + var fetchedPlayer = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(fetchedPlayer.name, 'First Update'); + + await database.playerDao.updatePlayerName( + playerId: testPlayer1.id, + name: 'Second Update', + ); + + fetchedPlayer = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(fetchedPlayer.name, 'Second Update'); + + await database.playerDao.updatePlayerDescription( + playerId: testPlayer1.id, + description: 'Third Update', + ); + + fetchedPlayer = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(fetchedPlayer.description, 'Third Update'); + }); }); - // Verifies that multiple updates to the same player work correctly. - test('Multiple updates to the same player work correctly', () async { - await database.playerDao.addPlayer(player: testPlayer1); + group('DELETE', () { + test('deletePlayer() works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + final playerDeleted = await database.playerDao.deletePlayer( + playerId: testPlayer1.id, + ); + expect(playerDeleted, isTrue); - await database.playerDao.updatePlayerName( - playerId: testPlayer1.id, - newName: 'First Update', - ); + final playerExists = await database.playerDao.playerExists( + playerId: testPlayer1.id, + ); + expect(playerExists, isFalse); + }); - var fetchedPlayer = await database.playerDao.getPlayerById( - playerId: testPlayer1.id, - ); - expect(fetchedPlayer.name, 'First Update'); + test('deletePlayer() returns false for non-existent player', () async { + final deleted = await database.playerDao.deletePlayer( + playerId: 'non-existent-id', + ); + expect(deleted, isFalse); + }); - await database.playerDao.updatePlayerName( - playerId: testPlayer1.id, - newName: 'Second Update', - ); + test('deleteAllPlayers() removes all players', () async { + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2, testPlayer3], + ); - fetchedPlayer = await database.playerDao.getPlayerById( - playerId: testPlayer1.id, - ); - expect(fetchedPlayer.name, 'Second Update'); + var playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 3); - await database.playerDao.updatePlayerName( - playerId: testPlayer1.id, - newName: 'Third Update', - ); + final deleted = await database.playerDao.deleteAllPlayers(); + expect(deleted, isTrue); - fetchedPlayer = await database.playerDao.getPlayerById( - playerId: testPlayer1.id, - ); - expect(fetchedPlayer.name, 'Third Update'); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 0); + }); + + test('deleteAllPlayers() returns false when no players exist', () async { + final deleted = await database.playerDao.deleteAllPlayers(); + expect(deleted, isFalse); + }); }); - // Verifies that a player with empty string name is stored correctly. - test('Player with empty string name is stored correctly', () async { - final emptyNamePlayer = Player(name: ''); - - await database.playerDao.addPlayer(player: emptyNamePlayer); - - final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: emptyNamePlayer.id, - ); - expect(fetchedPlayer.name, ''); - }); - - // Verifies that a player with very long name is stored correctly. - test('Player with very long name is stored correctly', () async { - final longName = 'A' * 1000; - final longNamePlayer = Player(name: longName); - - await database.playerDao.addPlayer(player: longNamePlayer); - - final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: longNamePlayer.id, - ); - expect(fetchedPlayer.name, longName); - }); - - // Verifies that addPlayer returns true on first add. - test('addPlayer returns true when player is added successfully', () async { - final result = await database.playerDao.addPlayer(player: testPlayer1); - expect(result, true); - - final playerExists = await database.playerDao.playerExists( - playerId: testPlayer1.id, - ); - expect(playerExists, true); - }); - - group('Name Count Tests', () { - test('Single player gets initialized wih name count 0', () async { + group('NAME COUNT', () { + test('Single player gets initialized wih name count 0', () async { await database.playerDao.addPlayer(player: testPlayer1); final player = await database.playerDao.getPlayerById( @@ -392,7 +351,7 @@ void main() { expect(player.nameCount, 0); }); - test('Multiple players get initialized wih name count 0', () async { + test('Multiple players get initialized wih name count 0', () async { await database.playerDao.addPlayersAsList( players: [testPlayer1, testPlayer2], ); @@ -470,7 +429,7 @@ void main() { playerId: testPlayer1.id, nameCount: 2, ); - expect(success, true); + expect(success, isTrue); final player = await database.playerDao.getPlayerById( playerId: testPlayer1.id, diff --git a/test/db_tests/relationships/player_group_test.dart b/test/db_tests/relationships/player_group_test.dart index f687b1c..70e3d30 100644 --- a/test/db_tests/relationships/player_group_test.dart +++ b/test/db_tests/relationships/player_group_test.dart @@ -42,189 +42,162 @@ void main() { }); group('Player-Group Tests', () { - // Verifies that a player can be added to an existing group and isPlayerInGroup returns true. - test('Adding a player to a group works correctly', () async { - await database.groupDao.addGroup(group: testGroup); - await database.playerDao.addPlayer(player: testPlayer4); - await database.playerGroupDao.addPlayerToGroup( - groupId: testGroup.id, - player: testPlayer4, - ); - - var playerAdded = await database.playerGroupDao.isPlayerInGroup( - groupId: testGroup.id, - playerId: testPlayer4.id, - ); - - expect(playerAdded, true); - - playerAdded = await database.playerGroupDao.isPlayerInGroup( - groupId: testGroup.id, - playerId: '', - ); - - expect(playerAdded, false); - }); - - // Verifies that a player can be removed from a group and the group's member count decreases. - test('Removing player from group works correctly', () async { - await database.groupDao.addGroup(group: testGroup); - - final playerToRemove = testGroup.members[0]; - - final removed = await database.playerGroupDao.removePlayerFromGroup( - playerId: playerToRemove.id, - groupId: testGroup.id, - ); - expect(removed, true); - - final result = await database.groupDao.getGroupById( - groupId: testGroup.id, - ); - expect(result.members.length, testGroup.members.length - 1); - - final playerExists = result.members.any((p) => p.id == playerToRemove.id); - expect(playerExists, false); - }); - - // Verifies that getPlayersOfGroup returns all members of a group with correct data. - test('Retrieving players of a group works correctly', () async { - await database.groupDao.addGroup(group: testGroup); - final players = await database.playerGroupDao.getPlayersOfGroup( - groupId: testGroup.id, - ); - - for (int i = 0; i < players.length; i++) { - expect(players[i].id, testGroup.members[i].id); - expect(players[i].name, testGroup.members[i].name); - expect(players[i].createdAt, testGroup.members[i].createdAt); - } - }); - - // Verifies that isPlayerInGroup returns false for non-existent player. - test('isPlayerInGroup returns false for non-existent player', () async { - await database.groupDao.addGroup(group: testGroup); - - final result = await database.playerGroupDao.isPlayerInGroup( - playerId: 'non-existent-player-id', - groupId: testGroup.id, - ); - - expect(result, false); - }); - - // Verifies that isPlayerInGroup returns false for non-existent group. - test('isPlayerInGroup returns false for non-existent group', () async { - await database.playerDao.addPlayer(player: testPlayer1); - - final result = await database.playerGroupDao.isPlayerInGroup( - playerId: testPlayer1.id, - groupId: 'non-existent-group-id', - ); - - expect(result, false); - }); - - // Verifies that addPlayerToGroup returns false when player already in group. - test( - 'addPlayerToGroup returns false when player already in group', - () async { + group('CREATE', () { + test('addPlayerToGroup() works correctly', () async { await database.groupDao.addGroup(group: testGroup); - - // testPlayer1 is already in testGroup via group creation - final result = await database.playerGroupDao.addPlayerToGroup( - player: testPlayer1, - groupId: testGroup.id, - ); - - expect(result, false); - }, - ); - - // Verifies that addPlayerToGroup adds player to player table if not exists. - test( - 'addPlayerToGroup adds player to player table if not exists', - () async { - await database.groupDao.addGroup(group: testGroup); - - // testPlayer4 is not in the database yet - var playerExists = await database.playerDao.playerExists( - playerId: testPlayer4.id, - ); - expect(playerExists, false); - + await database.playerDao.addPlayer(player: testPlayer4); await database.playerGroupDao.addPlayerToGroup( - player: testPlayer4, groupId: testGroup.id, + player: testPlayer4, ); - // Now player should exist in player table - playerExists = await database.playerDao.playerExists( + var playerAdded = await database.playerGroupDao.isPlayerInGroup( + groupId: testGroup.id, playerId: testPlayer4.id, ); - expect(playerExists, true); - }, - ); - // Verifies that removePlayerFromGroup returns false for non-existent player. - test( - 'removePlayerFromGroup returns false for non-existent player', - () async { + expect(playerAdded, isTrue); + }); + + test( + 'addPlayerToGroup() returns false when player already in group', + () async { + await database.groupDao.addGroup(group: testGroup); + + final added = await database.playerGroupDao.addPlayerToGroup( + player: testPlayer1, + groupId: testGroup.id, + ); + expect(added, isFalse); + }, + ); + + test( + 'addPlayerToGroup() adds player to player table if not exists', + () async { + await database.groupDao.addGroup(group: testGroup); + + var playerExists = await database.playerDao.playerExists( + playerId: testPlayer4.id, + ); + expect(playerExists, isFalse); + + await database.playerGroupDao.addPlayerToGroup( + player: testPlayer4, + groupId: testGroup.id, + ); + + playerExists = await database.playerDao.playerExists( + playerId: testPlayer4.id, + ); + expect(playerExists, isTrue); + }, + ); + }); + group('READ', () { + test( + 'isPlayerInGroup() returns false for non-existent player or group', + () async { + await database.groupDao.addGroup(group: testGroup); + + var isInGroup = await database.playerGroupDao.isPlayerInGroup( + playerId: 'non-existent-player-id', + groupId: testGroup.id, + ); + expect(isInGroup, isFalse); + + isInGroup = await database.playerGroupDao.isPlayerInGroup( + playerId: testPlayer1.id, + groupId: 'non-existent-group-id', + ); + expect(isInGroup, isFalse); + + isInGroup = await database.playerGroupDao.isPlayerInGroup( + playerId: 'non-existent-player-id', + groupId: 'non-existent-group-id', + ); + expect(isInGroup, isFalse); + }, + ); + + test('getPlayersOfGroup() works correctly', () async { await database.groupDao.addGroup(group: testGroup); - - final result = await database.playerGroupDao.removePlayerFromGroup( - playerId: 'non-existent-player-id', + final players = await database.playerGroupDao.getPlayersOfGroup( groupId: testGroup.id, ); - expect(result, false); - }, - ); + for (int i = 0; i < players.length; i++) { + expect(players[i].id, testGroup.members[i].id); + expect(players[i].name, testGroup.members[i].name); + expect(players[i].createdAt, testGroup.members[i].createdAt); + } + }); - // Verifies that removePlayerFromGroup returns false for non-existent group. - test( - 'removePlayerFromGroup returns false for non-existent group', - () async { - await database.playerDao.addPlayer(player: testPlayer1); + test('getPlayersOfGroup() returns empty list for empty group', () async { + final emptyGroup = Group(name: 'Empty Group', members: []); + await database.groupDao.addGroup(group: emptyGroup); - final result = await database.playerGroupDao.removePlayerFromGroup( - playerId: testPlayer1.id, - groupId: 'non-existent-group-id', + final players = await database.playerGroupDao.getPlayersOfGroup( + groupId: emptyGroup.id, ); + expect(players, isEmpty); + }); - expect(result, false); - }, - ); - - // Verifies that getPlayersOfGroup returns empty list for group with no members. - test('getPlayersOfGroup returns empty list for empty group', () async { - final emptyGroup = Group( - name: 'Empty Group', - description: '', - members: [], + test( + 'getPlayersOfGroup() returns empty list for non-existent group', + () async { + final players = await database.playerGroupDao.getPlayersOfGroup( + groupId: 'non-existent-group-id', + ); + expect(players, isEmpty); + }, ); - await database.groupDao.addGroup(group: emptyGroup); + }); + group('UPDATE', () { + test('replaceGroupPlayers() works correctly ', () async { + await database.groupDao.addGroup(group: testGroup); - final players = await database.playerGroupDao.getPlayersOfGroup( - groupId: emptyGroup.id, - ); + var groupMembers = await database.groupDao.getGroupById( + groupId: testGroup.id, + ); + expect(groupMembers.members.length, testGroup.members.length); - expect(players, isEmpty); + final newPlayersList = [testPlayer3, testPlayer4]; + + final replaced = await database.playerGroupDao.replaceGroupPlayers( + groupId: testGroup.id, + newPlayers: newPlayersList, + ); + expect(replaced, isTrue); + + groupMembers = await database.groupDao.getGroupById( + groupId: testGroup.id, + ); + expect(groupMembers.members.length, 2); + expect(groupMembers.members.any((p) => p.id == testPlayer3.id), isTrue); + expect(groupMembers.members.any((p) => p.id == testPlayer4.id), isTrue); + }); + }); + group('DELETE', () { + test('removePlayerFromGroup() works correctly', () async { + await database.groupDao.addGroup(group: testGroup); + + final removed = await database.playerGroupDao.removePlayerFromGroup( + playerId: testPlayer1.id, + groupId: testGroup.id, + ); + expect(removed, isTrue); + + final result = await database.groupDao.getGroupById( + groupId: testGroup.id, + ); + expect(result.members.length, testGroup.members.length - 1); + + final playerExists = result.members.any((p) => p.id == testPlayer1.id); + expect(playerExists, isFalse); + }); }); - // Verifies that getPlayersOfGroup returns empty list for non-existent group. - test( - 'getPlayersOfGroup returns empty list for non-existent group', - () async { - final players = await database.playerGroupDao.getPlayersOfGroup( - groupId: 'non-existent-group-id', - ); - - expect(players, isEmpty); - }, - ); - - // Verifies that removing all players from a group leaves the group empty. test('Removing all players from a group leaves group empty', () async { await database.groupDao.addGroup(group: testGroup); @@ -240,137 +213,53 @@ void main() { ); expect(players, isEmpty); - // Group should still exist final groupExists = await database.groupDao.groupExists( groupId: testGroup.id, ); - expect(groupExists, true); + expect(groupExists, isTrue); }); - // Verifies that a player can be in multiple groups. - test('Player can be in multiple groups', () async { - final secondGroup = Group( - name: 'Second Group', - description: '', - members: [], - ); + test('removePlayerFromGroup() works correctly', () async { await database.groupDao.addGroup(group: testGroup); - await database.groupDao.addGroup(group: secondGroup); - // Add testPlayer1 to second group (already in testGroup) - await database.playerGroupDao.addPlayerToGroup( - player: testPlayer1, - groupId: secondGroup.id, - ); - - final inFirstGroup = await database.playerGroupDao.isPlayerInGroup( + var removed = await database.playerGroupDao.removePlayerFromGroup( playerId: testPlayer1.id, groupId: testGroup.id, ); - final inSecondGroup = await database.playerGroupDao.isPlayerInGroup( + expect(removed, isTrue); + + removed = await database.playerGroupDao.removePlayerFromGroup( playerId: testPlayer1.id, - groupId: secondGroup.id, - ); - - expect(inFirstGroup, true); - expect(inSecondGroup, true); - }); - - // Verifies that removing player from one group doesn't affect other groups. - test( - 'Removing player from one group does not affect other groups', - () async { - final secondGroup = Group( - name: 'Second Group', - description: '', - members: [testPlayer1], - ); - await database.groupDao.addGroup(group: testGroup); - await database.groupDao.addGroup(group: secondGroup); - - // Remove testPlayer1 from testGroup - await database.playerGroupDao.removePlayerFromGroup( - playerId: testPlayer1.id, - groupId: testGroup.id, - ); - - final inFirstGroup = await database.playerGroupDao.isPlayerInGroup( - playerId: testPlayer1.id, - groupId: testGroup.id, - ); - final inSecondGroup = await database.playerGroupDao.isPlayerInGroup( - playerId: testPlayer1.id, - groupId: secondGroup.id, - ); - - expect(inFirstGroup, false); - expect(inSecondGroup, true); - }, - ); - - // Verifies that addPlayerToGroup returns true on successful addition. - test('addPlayerToGroup returns true on successful addition', () async { - await database.groupDao.addGroup(group: testGroup); - await database.playerDao.addPlayer(player: testPlayer4); - - final result = await database.playerGroupDao.addPlayerToGroup( - player: testPlayer4, groupId: testGroup.id, ); - - expect(result, true); + expect(removed, isFalse); }); - // Verifies that removing the same player twice returns false on second attempt. test( - 'Removing same player twice returns false on second attempt', + 'removePlayerFromGroup() returns false for non-existent player or group', () async { await database.groupDao.addGroup(group: testGroup); - final firstRemoval = await database.playerGroupDao - .removePlayerFromGroup( - playerId: testPlayer1.id, - groupId: testGroup.id, - ); - expect(firstRemoval, true); + await database.groupDao.addGroup(group: testGroup); - final secondRemoval = await database.playerGroupDao - .removePlayerFromGroup( - playerId: testPlayer1.id, - groupId: testGroup.id, - ); - expect(secondRemoval, false); + var removed = await database.playerGroupDao.removePlayerFromGroup( + playerId: 'non-existent-player-id', + groupId: testGroup.id, + ); + expect(removed, isFalse); + + removed = await database.playerGroupDao.removePlayerFromGroup( + playerId: testPlayer1.id, + groupId: 'non-existent-group-id', + ); + expect(removed, isFalse); + + removed = await database.playerGroupDao.removePlayerFromGroup( + playerId: 'non-existent-player-id', + groupId: 'non-existent-group-id', + ); + expect(removed, isFalse); }, ); - - // Verifies that replaceGroupPlayers removes all existing players and replaces with new list. - test('replaceGroupPlayers replaces all group members correctly', () async { - // Create initial group with 3 players - await database.groupDao.addGroup(group: testGroup); - - // Verify initial members - var groupMembers = await database.groupDao.getGroupById( - groupId: testGroup.id, - ); - expect(groupMembers.members.length, 3); - - // Replace with new list containing 2 different players - final newPlayersList = [testPlayer3, testPlayer4]; - await database.groupDao.replaceGroupPlayers( - groupId: testGroup.id, - newPlayers: newPlayersList, - ); - - // Get updated group and verify members - groupMembers = await database.groupDao.getGroupById( - groupId: testGroup.id, - ); - - expect(groupMembers.members.length, 2); - expect(groupMembers.members.any((p) => p.id == testPlayer3.id), true); - expect(groupMembers.members.any((p) => p.id == testPlayer4.id), true); - expect(groupMembers.members.any((p) => p.id == testPlayer1.id), false); - expect(groupMembers.members.any((p) => p.id == testPlayer2.id), false); - }); }); } diff --git a/test/db_tests/relationships/player_match_test.dart b/test/db_tests/relationships/player_match_test.dart index 92601f0..6d879c3 100644 --- a/test/db_tests/relationships/player_match_test.dart +++ b/test/db_tests/relationships/player_match_test.dart @@ -5,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/game.dart'; -import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/team.dart'; @@ -17,11 +16,8 @@ void main() { late Player testPlayer3; late Player testPlayer4; late Player testPlayer5; - late Player testPlayer6; - late Group testGroup; late Game testGame; - late Match testMatchOnlyGroup; - late Match testMatchOnlyPlayers; + late Match testMatch1; late Team testTeam1; late Team testTeam2; final fixedDate = DateTime(2025, 11, 19, 00, 11, 23); @@ -42,12 +38,6 @@ void main() { testPlayer3 = Player(name: 'Charlie'); testPlayer4 = Player(name: 'Diana'); testPlayer5 = Player(name: 'Eve'); - testPlayer6 = Player(name: 'Frank'); - testGroup = Group( - name: 'Test Group', - description: '', - members: [testPlayer1, testPlayer2, testPlayer3], - ); testGame = Game( name: 'Test Game', ruleset: Ruleset.singleWinner, @@ -55,31 +45,17 @@ void main() { color: GameColor.blue, icon: '', ); - testMatchOnlyGroup = Match( - name: 'Test Match with Group', - game: testGame, - players: testGroup.members, - group: testGroup, - ); - testMatchOnlyPlayers = Match( + testMatch1 = Match( name: 'Test Match with Players', game: testGame, - players: [testPlayer4, testPlayer5, testPlayer6], + players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]); testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]); }); await database.playerDao.addPlayersAsList( - players: [ - testPlayer1, - testPlayer2, - testPlayer3, - testPlayer4, - testPlayer5, - testPlayer6, - ], + players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); - await database.groupDao.addGroup(group: testGroup); await database.gameDao.addGame(game: testGame); }); tearDown(() async { @@ -87,603 +63,361 @@ void main() { }); group('Player-Match Tests', () { - test('Match has player works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.playerDao.addPlayer(player: testPlayer1); + group('CREATE', () { + test('addPlayerToMatch() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.playerDao.addPlayer(player: testPlayer1); - var matchHasPlayers = await database.playerMatchDao.matchHasPlayers( - matchId: testMatchOnlyGroup.id, - ); - - expect(matchHasPlayers, true); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - ); - - matchHasPlayers = await database.playerMatchDao.matchHasPlayers( - matchId: testMatchOnlyGroup.id, - ); - - expect(matchHasPlayers, true); - }); - - test('Adding a player to a match works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.playerDao.addPlayer(player: testPlayer5); - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer5.id, - ); - - var playerAdded = await database.playerMatchDao.isPlayerInMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer5.id, - ); - - expect(playerAdded, true); - - playerAdded = await database.playerMatchDao.isPlayerInMatch( - matchId: testMatchOnlyGroup.id, - playerId: '', - ); - - expect(playerAdded, false); - }); - - test('Removing player from match works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - - final playerToRemove = testMatchOnlyPlayers.players[0]; - - final removed = await database.playerMatchDao.removePlayerFromMatch( - playerId: playerToRemove.id, - matchId: testMatchOnlyPlayers.id, - ); - expect(removed, true); - - final result = await database.matchDao.getMatchById( - matchId: testMatchOnlyPlayers.id, - ); - expect(result.players.length, testMatchOnlyPlayers.players.length - 1); - - final playerExists = result.players.any((p) => p.id == playerToRemove.id); - expect(playerExists, false); - }); - - test('Retrieving players of a match works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - final players = - await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, - ) ?? - []; - - for (int i = 0; i < players.length; i++) { - expect(players[i].id, testMatchOnlyPlayers.players[i].id); - expect(players[i].name, testMatchOnlyPlayers.players[i].name); - expect(players[i].createdAt, testMatchOnlyPlayers.players[i].createdAt); - } - }); - - test('Updating the match players works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - - final newPlayers = [testPlayer1, testPlayer2, testPlayer4]; - await database.playerDao.addPlayersAsList(players: newPlayers); - - // First, remove all existing players - final existingPlayers = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, - ); - - if (existingPlayers == null || existingPlayers.isEmpty) { - fail('Existing players should not be null or empty'); - } - - await database.playerMatchDao.updatePlayersFromMatch( - matchId: testMatchOnlyPlayers.id, - newPlayer: newPlayers, - ); - - final updatedPlayers = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, - ); - - if (updatedPlayers == null) { - fail('Updated players should not be null'); - } - - expect(updatedPlayers.length, newPlayers.length); - - /// Create a map of new players for easy lookup - final testPlayers = {for (var p in newPlayers) p.id: p}; - - /// Verify each updated player matches the new players - for (final player in updatedPlayers) { - final testPlayer = testPlayers[player.id]!; - - expect(player.id, testPlayer.id); - expect(player.name, testPlayer.name); - expect(player.createdAt, testPlayer.createdAt); - } - }); - - test( - 'Adding the same player to separate matches works correctly', - () async { - final playersList = [testPlayer1, testPlayer2, testPlayer3]; - final match1 = Match( - name: 'Match 1', - game: testGame, - players: playersList, - notes: '', + var added = await database.playerMatchDao.addPlayerToMatch( + matchId: testMatch1.id, + playerId: testPlayer1.id, ); - final match2 = Match( - name: 'Match 2', - game: testGame, - players: playersList, - notes: '', + expect(added, isTrue); + + added = await database.playerMatchDao.isPlayerInMatch( + matchId: testMatch1.id, + playerId: testPlayer1.id, ); + expect(added, isTrue); + }); - await Future.wait([ - database.matchDao.addMatch(match: match1), - database.matchDao.addMatch(match: match2), - ]); + test('addPlayerToMatch() with team works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.teamDao.addTeam(team: testTeam1, matchId: testMatch1.id); - 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()), - ); - }, - ); - - // Verifies that getPlayersOfMatch returns null for a non-existent match. - test('getPlayersOfMatch returns null for non-existent match', () async { - final players = await database.playerMatchDao.getPlayersOfMatch( - matchId: 'non-existent-match-id', - ); - - expect(players, isNull); - }); - - test('Adding player with teamId works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam1.id, - ); - - final playersInTeam = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, - ); - - expect(playersInTeam.length, 1); - expect(playersInTeam[0].id, testPlayer1.id); - }); - - test('updatePlayerTeam updates team correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); - await database.teamDao.addTeam(team: testTeam2); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam1.id, - ); - - // Update player's team - final updated = await database.playerMatchDao.updatePlayerTeam( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam2.id, - ); - - expect(updated, true); - - // Verify player is now in testTeam2 - final playersInTeam2 = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam2.id, - ); - - expect(playersInTeam2.length, 1); - expect(playersInTeam2[0].id, testPlayer1.id); - - // Verify player is no longer in testTeam1 - final playersInTeam1 = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, - ); - - expect(playersInTeam1.isEmpty, true); - }); - - test('updatePlayerTeam can remove player from team', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam1.id, - ); - - // Remove player from team by setting teamId to null - final updated = await database.playerMatchDao.updatePlayerTeam( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: null, - ); - - expect(updated, true); - - final playersInTeam = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, - ); - - expect(playersInTeam.isEmpty, true); - }); - - test( - 'updatePlayerTeam returns false for non-existent player-match', - () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - - final updated = await database.playerMatchDao.updatePlayerTeam( - matchId: testMatchOnlyGroup.id, - playerId: 'non-existent-player-id', + await database.playerMatchDao.addPlayerToMatch( + matchId: testMatch1.id, + playerId: testPlayer3.id, teamId: testTeam1.id, ); - expect(updated, false); - }, - ); + final playersInTeam = await database.playerMatchDao + .getPlayersOfTeamInMatch( + matchId: testMatch1.id, + teamId: testTeam1.id, + ); - // Verifies that getPlayersInTeam returns empty list for non-existent team. - test('getPlayersInTeam returns empty list for non-existent team', () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); + expect(playersInTeam, isNotEmpty); + expect(playersInTeam.length, 3); + }); - final players = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyPlayers.id, - teamId: 'non-existent-team-id', - ); + test('addPlayerToMatch() ignores duplicates', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.playerDao.addPlayer(player: testPlayer5); - expect(players.isEmpty, true); + final isInMatch = await database.playerMatchDao.isPlayerInMatch( + matchId: testMatch1.id, + playerId: testPlayer5.id, + ); + expect(isInMatch, isFalse); + + var players = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatch1.id, + ); + expect(players.length, testMatch1.players.length); + + await database.playerMatchDao.addPlayerToMatch( + matchId: testMatch1.id, + playerId: testPlayer5.id, + ); + await database.playerMatchDao.addPlayerToMatch( + matchId: testMatch1.id, + playerId: testPlayer5.id, + ); + + players = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatch1.id, + ); + + expect(players.length, testMatch1.players.length + 1); + }); }); - test('getPlayersInTeam returns all players of a team', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); + group('READ', () { + test('hasMatchPlayers() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + var matchHasPlayers = await database.playerMatchDao.hasMatchPlayers( + matchId: testMatch1.id, + ); + expect(matchHasPlayers, isTrue); + }); - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam1.id, - ); - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer2.id, - teamId: testTeam1.id, + test('hasMatchPlayers() returns false for non-existent match', () async { + final hasPlayers = await database.playerMatchDao.hasMatchPlayers( + matchId: 'non-existent-match-id', + ); + expect(hasPlayers, isFalse); + }); + + test('isPlayerInMatch() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + final isInMatch = await database.playerMatchDao.isPlayerInMatch( + matchId: testMatch1.id, + playerId: testPlayer1.id, + ); + expect(isInMatch, isTrue); + }); + + test('isPlayerInMatch() returns false for non-existent match', () async { + final isInMatch = await database.playerMatchDao.isPlayerInMatch( + matchId: 'non-existent-match-id', + playerId: testPlayer1.id, + ); + expect(isInMatch, isFalse); + }); + + test('getPlayersOfMatch() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final players = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatch1.id, + ); + expect(players, isNotEmpty); + + for (int i = 0; i < players.length; i++) { + expect(players[i].id, testMatch1.players[i].id); + expect(players[i].name, testMatch1.players[i].name); + expect(players[i].createdAt, testMatch1.players[i].createdAt); + } + }); + + test( + 'getPlayersOfMatch() returns empty list for non-existent match', + () async { + final players = await database.playerMatchDao.getPlayersOfMatch( + matchId: 'non-existent-match-id', + ); + expect(players, isEmpty); + }, ); - final playersInTeam = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, + test('getPlayersInTeam() works correctly', () async { + // Create a match with teams + final matchWithTeams = Match( + name: 'Match with teams', + game: testGame, + players: [], + teams: [testTeam1, testTeam2], + ); + await database.matchDao.addMatch(match: matchWithTeams); + + var playersInTeam = await database.playerMatchDao + .getPlayersOfTeamInMatch( + matchId: matchWithTeams.id, + teamId: testTeam1.id, + ); + + expect(playersInTeam, isNotEmpty); + expect(playersInTeam.length, 2); + + var playerIds = playersInTeam.map((p) => p.id).toSet(); + expect(playerIds.contains(testPlayer1.id), isTrue); + expect(playerIds.contains(testPlayer2.id), isTrue); + + playersInTeam = await database.playerMatchDao.getPlayersOfTeamInMatch( + matchId: matchWithTeams.id, + teamId: testTeam2.id, + ); + + expect(playersInTeam, isNotEmpty); + expect(playersInTeam.length, 2); + + playerIds = playersInTeam.map((p) => p.id).toSet(); + expect(playerIds.contains(testPlayer3.id), isTrue); + expect(playerIds.contains(testPlayer4.id), isTrue); + }); + + test( + 'getPlayersInTeam() returns empty list for non-existent match', + () async { + final players = await database.playerMatchDao.getPlayersOfTeamInMatch( + matchId: 'non-existent-match-id', + teamId: testTeam1.id, + ); + expect(players, isEmpty); + }, ); - expect(playersInTeam.length, 2); - final playerIds = playersInTeam.map((p) => p.id).toSet(); - expect(playerIds.contains(testPlayer1.id), true); - expect(playerIds.contains(testPlayer2.id), true); }); - test( - 'removePlayerFromMatch returns false for non-existent player', - () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); + group('UPDATE', () { + test('updateMatchPlayers() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final newPlayers = [testPlayer1, testPlayer2, testPlayer4]; + await database.playerDao.addPlayersAsList(players: newPlayers); + + final existingPlayers = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatch1.id, + ); + expect(existingPlayers, isNotEmpty); + + await database.playerMatchDao.updateMatchPlayers( + matchId: testMatch1.id, + player: newPlayers, + ); + + final updatedPlayers = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatch1.id, + ); + expect(updatedPlayers, isNotEmpty); + expect(updatedPlayers.length, newPlayers.length); + + /// Create a map of new players for easy lookup + final testPlayers = {for (var p in newPlayers) p.id: p}; + + for (final player in updatedPlayers) { + final testPlayer = testPlayers[player.id]!; + + expect(player.id, testPlayer.id); + expect(player.name, testPlayer.name); + expect(player.createdAt, testPlayer.createdAt); + } + }); + + test('updatePlayersTeam() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.teamDao.addTeam(team: testTeam1, matchId: testMatch1.id); + await database.teamDao.addTeam(team: testTeam2, matchId: testMatch1.id); + + await database.playerMatchDao.addPlayerToMatch( + matchId: testMatch1.id, + playerId: testPlayer1.id, + teamId: testTeam1.id, + ); + + final updated = await database.playerMatchDao.updatePlayersTeam( + matchId: testMatch1.id, + playerId: testPlayer1.id, + teamId: testTeam2.id, + ); + + expect(updated, isTrue); + + final playersInTeam2 = await database.playerMatchDao + .getPlayersOfTeamInMatch( + matchId: testMatch1.id, + teamId: testTeam2.id, + ); + expect(playersInTeam2, isNotEmpty); + expect(playersInTeam2.length, 3); + + final playersInTeam1 = await database.playerMatchDao + .getPlayersOfTeamInMatch( + matchId: testMatch1.id, + teamId: testTeam1.id, + ); + + expect(playersInTeam1, isNotEmpty); + expect(playersInTeam1.length, 1); + expect(playersInTeam1[0].id, testPlayer2.id); + }); + + test( + 'updatePlayersTeam() returns false for non-existent player-match', + () async { + await database.matchDao.addMatch(match: testMatch1); + + final updated = await database.playerMatchDao.updatePlayersTeam( + matchId: testMatch1.id, + playerId: 'non-existent-player-id', + teamId: testTeam1.id, + ); + expect(updated, isFalse); + }, + ); + + test('updateMatchPlayers() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + var matchPlayers = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(matchPlayers.players.length, testMatch1.players.length); + + final newPlayersList = [testPlayer1, testPlayer2]; + await database.playerMatchDao.updateMatchPlayers( + matchId: testMatch1.id, + player: newPlayersList, + ); + + matchPlayers = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + + expect(matchPlayers.players.length, 2); + expect(matchPlayers.players.any((p) => p.id == testPlayer1.id), isTrue); + expect(matchPlayers.players.any((p) => p.id == testPlayer2.id), isTrue); + }); + + test('updateMatchPlayers() with same players makes is ignored', () async { + await database.matchDao.addMatch(match: testMatch1); + + final originalPlayers = testMatch1.players; + + final updated = await database.playerMatchDao.updateMatchPlayers( + matchId: testMatch1.id, + player: originalPlayers, + ); + expect(updated, isFalse); + + final players = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatch1.id, + ); + + expect(players.length, originalPlayers.length); + final playerIds = players.map((p) => p.id).toSet(); + for (final originalPlayer in originalPlayers) { + expect(playerIds.contains(originalPlayer.id), isTrue); + } + }); + + test('updateMatchPlayers() with empty list returns false', () async { + await database.matchDao.addMatch(match: testMatch1); + final updated = await database.playerMatchDao.updateMatchPlayers( + matchId: testMatch1.id, + player: [], + ); + + expect(updated, isFalse); + }); + }); + + group('DELETE', () { + test('removePlayerFromMatch() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final playerToRemove = testMatch1.players[0]; final removed = await database.playerMatchDao.removePlayerFromMatch( - playerId: 'non-existent-player-id', - matchId: testMatchOnlyPlayers.id, + playerId: playerToRemove.id, + matchId: testMatch1.id, ); + expect(removed, isTrue); - expect(removed, false); - }, - ); - - test('Adding same player twice to same match is ignored', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - ); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - ); - - final players = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyGroup.id, - ); - - expect(players?.length, 3); - }); - - test( - 'updatePlayersFromMatch with empty list removes all players', - () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - - // Verify players exist initially - var players = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, + final result = await database.matchDao.getMatchById( + matchId: testMatch1.id, ); - expect(players?.length, 3); + expect(result.players.length, testMatch1.players.length - 1); - // Update with empty list - await database.playerMatchDao.updatePlayersFromMatch( - matchId: testMatchOnlyPlayers.id, - newPlayer: [], + final playerExists = result.players.any( + (p) => p.id == playerToRemove.id, ); + expect(playerExists, isFalse); + }); - // Verify all players are removed - players = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, - ); - expect(players, isNull); - }, - ); + test( + 'removePlayerFromMatch() returns false for non-existent player', + () async { + await database.matchDao.addMatch(match: testMatch1); - test('updatePlayersFromMatch with same players makes no changes', () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); + final removed = await database.playerMatchDao.removePlayerFromMatch( + playerId: 'non-existent-player-id', + matchId: testMatch1.id, + ); - final originalPlayers = [testPlayer4, testPlayer5, testPlayer6]; - - await database.playerMatchDao.updatePlayersFromMatch( - matchId: testMatchOnlyPlayers.id, - newPlayer: originalPlayers, + expect(removed, isFalse); + }, ); - - final players = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, - ); - - expect(players?.length, originalPlayers.length); - final playerIds = players!.map((p) => p.id).toSet(); - for (final originalPlayer in originalPlayers) { - expect(playerIds.contains(originalPlayer.id), true); - } - }); - - test('matchHasPlayers returns false for non-existent match', () async { - final hasPlayers = await database.playerMatchDao.matchHasPlayers( - matchId: 'non-existent-match-id', - ); - - expect(hasPlayers, false); - }); - - test('isPlayerInMatch returns false for non-existent match', () async { - final isInMatch = await database.playerMatchDao.isPlayerInMatch( - matchId: 'non-existent-match-id', - playerId: testPlayer1.id, - ); - - expect(isInMatch, false); - }); - - // Verifies that getPlayersInTeam returns empty list for non-existent match. - test( - 'getPlayersInTeam returns empty list for non-existent match', - () async { - await database.teamDao.addTeam(team: testTeam1); - - final players = await database.playerMatchDao.getPlayersInTeam( - matchId: 'non-existent-match-id', - teamId: testTeam1.id, - ); - - expect(players.isEmpty, true); - }, - ); - - // Verifies that players in different teams within the same match are returned correctly. - test('Players in different teams within same match are separate', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); - await database.teamDao.addTeam(team: testTeam2); - - // Add players to different teams - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam1.id, - ); - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer2.id, - teamId: testTeam1.id, - ); - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer3.id, - teamId: testTeam2.id, - ); - - // Verify team 1 players - final playersInTeam1 = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, - ); - - expect(playersInTeam1.length, 2); - final team1Ids = playersInTeam1.map((p) => p.id).toSet(); - expect(team1Ids.contains(testPlayer1.id), true); - expect(team1Ids.contains(testPlayer2.id), true); - expect(team1Ids.contains(testPlayer3.id), false); - - // Verify team 2 players - final playersInTeam2 = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam2.id, - ); - expect(playersInTeam2.length, 1); - expect(playersInTeam2[0].id, testPlayer3.id); - }); - - // Verifies that removePlayerFromMatch does not affect other matches. - test('removePlayerFromMatch does not affect other matches', () async { - final playersList = [testPlayer1, testPlayer2]; - final match1 = Match( - name: 'Match 1', - game: testGame, - players: playersList, - ); - final match2 = Match( - name: 'Match 2', - game: testGame, - players: playersList, - ); - - await Future.wait([ - database.matchDao.addMatch(match: match1), - database.matchDao.addMatch(match: match2), - ]); - - // Remove player from match1 - final removed = await database.playerMatchDao.removePlayerFromMatch( - playerId: testPlayer1.id, - matchId: match1.id, - ); - expect(removed, true); - - // Verify player is removed from match1 - final isInMatch1 = await database.playerMatchDao.isPlayerInMatch( - matchId: match1.id, - playerId: testPlayer1.id, - ); - expect(isInMatch1, false); - - // Verify player still exists in match2 - final isInMatch2 = await database.playerMatchDao.isPlayerInMatch( - matchId: match2.id, - playerId: testPlayer1.id, - ); - expect(isInMatch2, true); - }); - - // Verifies that updatePlayersFromMatch on non-existent match fails with constraint error. - test( - 'updatePlayersFromMatch on non-existent match fails with foreign key constraint', - () async { - // Should throw due to foreign key constraint - match doesn't exist - await expectLater( - database.playerMatchDao.updatePlayersFromMatch( - matchId: 'non-existent-match-id', - newPlayer: [testPlayer1, testPlayer2], - ), - throwsA(anything), - ); - }, - ); - - // Verifies that a player can be in a match without being assigned to a team. - test('Player can exist in match without team assignment', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); - - // Add player to match without team - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - ); - - // Add another player to match with team - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer2.id, - teamId: testTeam1.id, - ); - - // Verify both players are in the match - final isPlayer1InMatch = await database.playerMatchDao.isPlayerInMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - ); - final isPlayer2InMatch = await database.playerMatchDao.isPlayerInMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer2.id, - ); - - expect(isPlayer1InMatch, true); - expect(isPlayer2InMatch, true); - - // Verify only player2 is in the team - final playersInTeam = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, - ); - - expect(playersInTeam.length, 1); - expect(playersInTeam[0].id, testPlayer2.id); - }); - - // Verifies that replaceMatchPlayers removes all existing players and replaces with new list. - test('replaceMatchPlayers replaces all match players correctly', () async { - // Create initial match with 3 players - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - - // Verify initial players - var matchPlayers = await database.matchDao.getMatchById( - matchId: testMatchOnlyPlayers.id, - ); - expect(matchPlayers.players.length, 3); - - // Replace with new list containing 2 different players - final newPlayersList = [testPlayer1, testPlayer2]; - await database.matchDao.replaceMatchPlayers( - matchId: testMatchOnlyPlayers.id, - newPlayers: newPlayersList, - ); - - // Get updated match and verify players - matchPlayers = await database.matchDao.getMatchById( - matchId: testMatchOnlyPlayers.id, - ); - - expect(matchPlayers.players.length, 2); - expect(matchPlayers.players.any((p) => p.id == testPlayer1.id), true); - expect(matchPlayers.players.any((p) => p.id == testPlayer2.id), true); - expect(matchPlayers.players.any((p) => p.id == testPlayer4.id), false); - expect(matchPlayers.players.any((p) => p.id == testPlayer5.id), false); - expect(matchPlayers.players.any((p) => p.id == testPlayer6.id), false); }); }); } diff --git a/test/db_tests/values/score_test.dart b/test/db_tests/values/score_entry_test.dart similarity index 73% rename from test/db_tests/values/score_test.dart rename to test/db_tests/values/score_entry_test.dart index d550995..f6cc292 100644 --- a/test/db_tests/values/score_test.dart +++ b/test/db_tests/values/score_entry_test.dart @@ -17,6 +17,9 @@ void main() { late Game testGame; late Match testMatch1; late Match testMatch2; + ScoreEntry entryRound1 = ScoreEntry(roundNumber: 1, score: 10, change: 10); + ScoreEntry entryRound2 = ScoreEntry(roundNumber: 2, score: 25, change: 15); + ScoreEntry entryRound3 = ScoreEntry(roundNumber: 3, score: 30, change: 5); final fixedDate = DateTime(2025, 11, 19, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -65,13 +68,12 @@ void main() { }); group('Score Tests', () { - group('Adding and Fetching scores', () { - test('Single Score', () async { - ScoreEntry entry = ScoreEntry(roundNumber: 1, score: 10, change: 10); + group('CREATE', () { + test('Adding and fetching single score works correctly', () async { await database.scoreEntryDao.addScore( playerId: testPlayer1.id, matchId: testMatch1.id, - entry: entry, + entry: entryRound1, ); final score = await database.scoreEntryDao.getScore( @@ -81,41 +83,37 @@ void main() { ); expect(score, isNotNull); - expect(score!.roundNumber, 1); - expect(score.score, 10); - expect(score.change, 10); + expect(score!.roundNumber, entryRound1.roundNumber); + expect(score.score, entryRound1.score); + expect(score.change, entryRound1.change); }); - test('Multiple Scores', () async { - final entryList = [ - ScoreEntry(roundNumber: 1, score: 5, change: 5), - ScoreEntry(roundNumber: 2, score: 12, change: 7), - ScoreEntry(roundNumber: 3, score: 18, change: 6), - ]; - + test('Adding and fetching single score works correctly', () async { await database.scoreEntryDao.addScoresAsList( - entrys: entryList, + entrys: [entryRound1, entryRound2, entryRound3], playerId: testPlayer1.id, matchId: testMatch1.id, ); - final scores = await database.scoreEntryDao.getAllPlayerScoresInMatch( + final entrys = await database.scoreEntryDao.getAllPlayerScoresInMatch( playerId: testPlayer1.id, matchId: testMatch1.id, ); - expect(scores, isNotNull); + expect(entrys, isNotEmpty); - // Scores should be returned in order of round number - for (int i = 0; i < entryList.length; i++) { - expect(scores[i].roundNumber, entryList[i].roundNumber); - expect(scores[i].score, entryList[i].score); - expect(scores[i].change, entryList[i].change); + // Map for connecting fetched entry with expected entrys + final testScores = {1: entryRound1, 2: entryRound2, 3: entryRound3}; + + for (final entry in entrys) { + final testEntry = testScores[entry.roundNumber]!; + + expect(entry.roundNumber, testEntry.roundNumber); + expect(entry.score, testEntry.score); + expect(entry.change, testEntry.change); } }); - }); - group('Undesirable values', () { test('Score & Round can have negative values', () async { ScoreEntry entry = ScoreEntry(roundNumber: -2, score: -10, change: -10); await database.scoreEntryDao.addScore( @@ -155,6 +153,31 @@ void main() { expect(score.change, 0); }); + test('Adding the same score twice replaces the existing one', () async { + await database.scoreEntryDao.addScore( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entry: entryRound1, + ); + await database.scoreEntryDao.addScore( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entry: entryRound1, + ); + + final score = await database.scoreEntryDao.getScore( + playerId: testPlayer1.id, + matchId: testMatch1.id, + roundNumber: 1, + ); + + expect(score, isNotNull); + expect(score!.score, entryRound1.score); + expect(score.change, entryRound1.change); + }); + }); + + group('READ', () { test('Getting score for a non-existent entities returns null', () async { var score = await database.scoreEntryDao.getScore( playerId: testPlayer1.id, @@ -201,10 +224,8 @@ void main() { expect(score, isNull); }); - }); - group('Scores in matches', () { - test('getAllMatchScores()', () async { + test('getAllMatchScores() works correctly', () async { ScoreEntry entry1 = ScoreEntry(roundNumber: 1, score: 10, change: 10); ScoreEntry entry2 = ScoreEntry(roundNumber: 1, score: 20, change: 20); ScoreEntry entry3 = ScoreEntry(roundNumber: 2, score: 25, change: 15); @@ -238,10 +259,10 @@ void main() { matchId: testMatch1.id, ); - expect(scores.isEmpty, true); + expect(scores.isEmpty, isTrue); }); - test('getAllPlayerScoresInMatch()', () async { + test('getAllPlayerScoresInMatch() works correctly', () async { ScoreEntry entry1 = ScoreEntry(roundNumber: 1, score: 10, change: 10); ScoreEntry entry2 = ScoreEntry(roundNumber: 2, score: 25, change: 15); ScoreEntry entry3 = ScoreEntry(roundNumber: 1, score: 30, change: 30); @@ -278,7 +299,7 @@ void main() { matchId: testMatch1.id, ); - expect(playerScores.isEmpty, true); + expect(playerScores.isEmpty, isTrue); }); test('Scores are isolated across different matches', () async { @@ -315,31 +336,109 @@ void main() { expect(match2Scores[0].score, 50); expect(match2Scores[0].change, 50); }); + + test('getLatestRoundNumber() works correctly', () async { + var latestRound = await database.scoreEntryDao.getLatestRoundNumber( + matchId: testMatch1.id, + ); + expect(latestRound, isNull); + + await database.scoreEntryDao.addScore( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entry: entryRound1, + ); + + latestRound = await database.scoreEntryDao.getLatestRoundNumber( + matchId: testMatch1.id, + ); + expect(latestRound, 1); + + await database.scoreEntryDao.addScore( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entry: entryRound2, + ); + + latestRound = await database.scoreEntryDao.getLatestRoundNumber( + matchId: testMatch1.id, + ); + expect(latestRound, 2); + }); + + test('getLatestRoundNumber() with non-consecutive rounds', () async { + await database.scoreEntryDao.addScoresAsList( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entrys: [entryRound1, entryRound3], + ); + + final latestRound = await database.scoreEntryDao.getLatestRoundNumber( + matchId: testMatch1.id, + ); + + expect(latestRound, 3); + }); + + test('getTotalScoreForPlayer() works correctly', () async { + var totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( + playerId: testPlayer1.id, + matchId: testMatch1.id, + ); + expect(totalScore, 0); + + await database.scoreEntryDao.addScoresAsList( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entrys: [entryRound1, entryRound2, entryRound3], + ); + + totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( + playerId: testPlayer1.id, + matchId: testMatch1.id, + ); + final expectedTotal = + entryRound1.change + entryRound2.change + entryRound3.change; + expect(totalScore, expectedTotal); + }); + + test('getTotalScoreForPlayer() ignores round score', () async { + await database.scoreEntryDao.addScoresAsList( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entrys: [ + ScoreEntry(roundNumber: 2, score: 25, change: 25), + ScoreEntry(roundNumber: 1, score: 25, change: 10), + ScoreEntry(roundNumber: 3, score: 25, change: 25), + ], + ); + + final totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( + playerId: testPlayer1.id, + matchId: testMatch1.id, + ); + + // Should return the sum of all changes + expect(totalScore, 60); + }); }); - group('Updating scores', () { - test('updateScore()', () async { - ScoreEntry entry1 = ScoreEntry(roundNumber: 1, score: 10, change: 10); - ScoreEntry entry2 = ScoreEntry(roundNumber: 2, score: 15, change: 5); - await database.scoreEntryDao.addScore( + group('UPDATE', () { + test('updateScore() works correctly', () async { + await database.scoreEntryDao.addScoresAsList( playerId: testPlayer1.id, matchId: testMatch1.id, - entry: entry1, - ); - - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: entry2, + entrys: [entryRound1, entryRound2], ); + final newEntry = ScoreEntry(roundNumber: 2, score: 50, change: 40); final updated = await database.scoreEntryDao.updateScore( playerId: testPlayer1.id, matchId: testMatch1.id, - newEntry: ScoreEntry(roundNumber: 2, score: 50, change: 40), + entry: newEntry, ); - expect(updated, true); + expect(updated, isTrue); final score = await database.scoreEntryDao.getScore( playerId: testPlayer1.id, @@ -348,23 +447,23 @@ void main() { ); expect(score, isNotNull); - expect(score!.score, 50); - expect(score.change, 40); + expect(score!.score, newEntry.score); + expect(score.change, newEntry.change); }); test('Updating a non-existent score returns false', () async { final updated = await database.scoreEntryDao.updateScore( playerId: testPlayer1.id, matchId: testMatch1.id, - newEntry: ScoreEntry(roundNumber: 1, score: 20, change: 20), + entry: entryRound1, ); - expect(updated, false); + expect(updated, isFalse); }); }); - group('Deleting scores', () { - test('deleteScore() ', () async { + group('DELETE', () { + test('deleteScore() works correctly', () async { await database.scoreEntryDao.addScore( playerId: testPlayer1.id, matchId: testMatch1.id, @@ -377,7 +476,7 @@ void main() { roundNumber: 1, ); - expect(deleted, true); + expect(deleted, isTrue); final score = await database.scoreEntryDao.getScore( playerId: testPlayer1.id, @@ -395,31 +494,36 @@ void main() { roundNumber: 1, ); - expect(deleted, false); + expect(deleted, isFalse); }); test('deleteAllScoresForMatch() works correctly', () async { + final score1 = ScoreEntry(roundNumber: 1, score: 10, change: 10); await database.scoreEntryDao.addScore( playerId: testPlayer1.id, matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), + entry: score1, ); + + final score2 = ScoreEntry(roundNumber: 1, score: 20, change: 20); await database.scoreEntryDao.addScore( playerId: testPlayer2.id, matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 20, change: 20), + entry: score2, ); + + final score3 = ScoreEntry(roundNumber: 1, score: 15, change: 15); await database.scoreEntryDao.addScore( playerId: testPlayer1.id, matchId: testMatch2.id, - entry: ScoreEntry(roundNumber: 1, score: 15, change: 15), + entry: score3, ); final deleted = await database.scoreEntryDao.deleteAllScoresForMatch( matchId: testMatch1.id, ); - expect(deleted, true); + expect(deleted, isTrue); final match1Scores = await database.scoreEntryDao.getAllMatchScores( matchId: testMatch1.id, @@ -433,22 +537,16 @@ void main() { }); test('deleteAllScoresForPlayerInMatch() works correctly', () async { - await database.scoreEntryDao.addScore( + await database.scoreEntryDao.addScoresAsList( playerId: testPlayer1.id, matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), - ); - - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 2, score: 15, change: 5), + entrys: [entryRound1, entryRound2], ); await database.scoreEntryDao.addScore( playerId: testPlayer2.id, matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 6, change: 6), + entry: entryRound1, ); final deleted = await database.scoreEntryDao @@ -457,7 +555,7 @@ void main() { matchId: testMatch1.id, ); - expect(deleted, true); + expect(deleted, isTrue); final player1Scores = await database.scoreEntryDao .getAllPlayerScoresInMatch( @@ -475,146 +573,12 @@ void main() { }); }); - group('Score Aggregations & Edge Cases', () { - test('getLatestRoundNumber()', () async { - var latestRound = await database.scoreEntryDao.getLatestRoundNumber( - matchId: testMatch1.id, - ); - expect(latestRound, isNull); - - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), - ); - - latestRound = await database.scoreEntryDao.getLatestRoundNumber( - matchId: testMatch1.id, - ); - expect(latestRound, 1); - - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 5, score: 50, change: 40), - ); - - latestRound = await database.scoreEntryDao.getLatestRoundNumber( - matchId: testMatch1.id, - ); - expect(latestRound, 5); - }); - - test('getLatestRoundNumber() with non-consecutive rounds', () async { - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 5, score: 50, change: 40), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 3, score: 30, change: 20), - ); - - final latestRound = await database.scoreEntryDao.getLatestRoundNumber( - matchId: testMatch1.id, - ); - - expect(latestRound, 5); - }); - - test('getTotalScoreForPlayer()', () async { - var totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( - playerId: testPlayer1.id, - matchId: testMatch1.id, - ); - expect(totalScore, 0); - - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 2, score: 25, change: 15), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 3, score: 40, change: 15), - ); - - totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( - playerId: testPlayer1.id, - matchId: testMatch1.id, - ); - expect(totalScore, 40); - }); - - test('getTotalScoreForPlayer() ignores round score', () async { - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 2, score: 25, change: 25), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 25, change: 10), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 3, score: 25, change: 25), - ); - - final totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( - playerId: testPlayer1.id, - matchId: testMatch1.id, - ); - - // Should return the sum of all changes - expect(totalScore, 60); - }); - - test('Adding the same score twice replaces the existing one', () async { - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 20, change: 20), - ); - - final score = await database.scoreEntryDao.getScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - roundNumber: 1, - ); - - expect(score, isNotNull); - expect(score!.score, 20); - expect(score.change, 20); - }); - }); - - group('Handling Winner', () { + group('WINNER', () { test('hasWinner() works correctly', () async { var hasWinner = await database.scoreEntryDao.hasWinner( matchId: testMatch1.id, ); - expect(hasWinner, false); + expect(hasWinner, isFalse); await database.scoreEntryDao.setWinner( playerId: testPlayer1.id, @@ -624,7 +588,7 @@ void main() { hasWinner = await database.scoreEntryDao.hasWinner( matchId: testMatch1.id, ); - expect(hasWinner, true); + expect(hasWinner, isTrue); }); test('getWinnersForMatch() returns correct winner', () async { @@ -648,7 +612,7 @@ void main() { var removed = await database.scoreEntryDao.removeWinner( matchId: testMatch1.id, ); - expect(removed, false); + expect(removed, isFalse); await database.scoreEntryDao.setWinner( playerId: testPlayer1.id, @@ -658,7 +622,7 @@ void main() { removed = await database.scoreEntryDao.removeWinner( matchId: testMatch1.id, ); - expect(removed, true); + expect(removed, isTrue); var winner = await database.scoreEntryDao.getWinner( matchId: testMatch1.id, @@ -667,58 +631,58 @@ void main() { }); }); - group('Handling Looser', () { - test('hasLooser() works correctly', () async { - var hasLooser = await database.scoreEntryDao.hasLooser( + group('LOSER', () { + test('hasLoser() works correctly', () async { + var hasLooser = await database.scoreEntryDao.hasLoser( matchId: testMatch1.id, ); - expect(hasLooser, false); + expect(hasLooser, isFalse); - await database.scoreEntryDao.setLooser( + await database.scoreEntryDao.setLoser( playerId: testPlayer1.id, matchId: testMatch1.id, ); - hasLooser = await database.scoreEntryDao.hasLooser( + hasLooser = await database.scoreEntryDao.hasLoser( matchId: testMatch1.id, ); - expect(hasLooser, true); + expect(hasLooser, isTrue); }); - test('getLooser() returns correct winner', () async { - var looser = await database.scoreEntryDao.getLooser( + test('getLoser() returns correct winner', () async { + var looser = await database.scoreEntryDao.getLoser( matchId: testMatch1.id, ); expect(looser, isNull); - await database.scoreEntryDao.setLooser( + await database.scoreEntryDao.setLoser( playerId: testPlayer1.id, matchId: testMatch1.id, ); - looser = await database.scoreEntryDao.getLooser(matchId: testMatch1.id); + looser = await database.scoreEntryDao.getLoser(matchId: testMatch1.id); expect(looser, isNotNull); expect(looser!.id, testPlayer1.id); }); - test('removeLooser() works correctly', () async { - var removed = await database.scoreEntryDao.removeLooser( + test('removeLoser() works correctly', () async { + var removed = await database.scoreEntryDao.removeLoser( matchId: testMatch1.id, ); - expect(removed, false); + expect(removed, isFalse); - await database.scoreEntryDao.setLooser( + await database.scoreEntryDao.setLoser( playerId: testPlayer1.id, matchId: testMatch1.id, ); - removed = await database.scoreEntryDao.removeLooser( + removed = await database.scoreEntryDao.removeLoser( matchId: testMatch1.id, ); - expect(removed, true); + expect(removed, isTrue); - var looser = await database.scoreEntryDao.getLooser( + var looser = await database.scoreEntryDao.getLoser( matchId: testMatch1.id, ); expect(looser, isNull); diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index e863629..586138a 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -99,8 +99,8 @@ void main() { await database.playerDao.addPlayer(player: testPlayer1); await database.gameDao.addGame(game: testGame); await database.groupDao.addGroup(group: testGroup); - await database.teamDao.addTeam(team: testTeam); await database.matchDao.addMatch(match: testMatch); + await database.teamDao.addTeam(team: testTeam, matchId: testMatch.id); var playerCount = await database.playerDao.getPlayerCount(); var gameCount = await database.gameDao.getGameCount(); @@ -137,7 +137,9 @@ void main() { await database.playerDao.addPlayer(player: testPlayer2); await database.gameDao.addGame(game: testGame); await database.groupDao.addGroup(group: testGroup); + /* await database.teamDao.addTeam(team: testTeam); +*/ await database.matchDao.addMatch(match: testMatch); final ctx = await getContext(tester); @@ -147,22 +149,19 @@ void main() { final decoded = json.decode(jsonString) as Map; - expect(decoded.containsKey('players'), true); - expect(decoded.containsKey('games'), true); - expect(decoded.containsKey('groups'), true); - expect(decoded.containsKey('teams'), true); - expect(decoded.containsKey('matches'), true); + expect(decoded.containsKey('players'), isTrue); + expect(decoded.containsKey('games'), isTrue); + expect(decoded.containsKey('groups'), isTrue); + expect(decoded.containsKey('matches'), isTrue); final players = decoded['players'] as List; final games = decoded['games'] as List; final groups = decoded['groups'] as List; - final teams = decoded['teams'] as List; final matches = decoded['matches'] as List; expect(players.length, 2); expect(games.length, 1); expect(groups.length, 1); - expect(teams.length, 1); expect(matches.length, 1); }); @@ -175,13 +174,11 @@ void main() { final players = decoded['players'] as List; final games = decoded['games'] as List; final groups = decoded['groups'] as List; - final teams = decoded['teams'] as List; final matches = decoded['matches'] as List; expect(players, isEmpty); expect(games, isEmpty); expect(groups, isEmpty); - expect(teams, isEmpty); expect(matches, isEmpty); }); }); @@ -243,29 +240,6 @@ void main() { expect(memberIds, containsAll([testPlayer1.id, testPlayer2.id])); }); - testWidgets('Team data is correct', (tester) async { - await database.teamDao.addTeam(team: testTeam); - - final ctx = await getContext(tester); - final jsonString = await DataTransferService.getAppDataAsJson(ctx); - final decoded = json.decode(jsonString) as Map; - final teams = decoded['teams'] as List; - - expect(teams.length, 1); - - final teamData = teams[0] as Map; - - expect(teamData['id'], testTeam.id); - expect(teamData['name'], testTeam.name); - expect(teamData['memberIds'], isA()); - - // Note: In this system, teams don't have independent members. - // Team members are only tracked through matches via PlayerMatchTable. - // Therefore, memberIds will be empty for standalone teams. - final memberIds = teamData['memberIds'] as List; - expect(memberIds, isEmpty); - }); - testWidgets('Match data is correct', (tester) async { await database.playerDao.addPlayersAsList( players: [testPlayer1, testPlayer2], @@ -317,6 +291,51 @@ void main() { expect(player2Score.change, 15); }); + testWidgets('Match with teams is handled correctly', (tester) async { + final matchWithTeams = Match( + name: 'Match with Teams', + game: testGame, + players: [testPlayer1, testPlayer2], + teams: [testTeam], + notes: 'Team match', + ); + + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2], + ); + await database.gameDao.addGame(game: testGame); + await database.matchDao.addMatch(match: matchWithTeams); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final matches = decoded['matches'] as List; + + expect(matches.length, 1); + + final matchData = matches[0] as Map; + expect(matchData['id'], matchWithTeams.id); + expect(matchData['name'], matchWithTeams.name); + expect( + matchData['teams'], + isNotNull, + reason: 'teams should not be null', + ); + expect(matchData['teams'], isA()); + + final teamsInMatch = matchData['teams'] as List; + expect(teamsInMatch.length, 1); + + final teamData = teamsInMatch[0] as Map; + expect(teamData['id'], testTeam.id); + expect(teamData['name'], testTeam.name); + expect(teamData['memberIds'], isA()); + + final memberIds = teamData['memberIds'] as List; + expect(memberIds.length, 2); + expect(memberIds, containsAll([testPlayer1.id, testPlayer2.id])); + }); + testWidgets('Match without group is handled correctly', (tester) async { final matchWithoutGroup = Match( name: 'No Group Match', @@ -644,19 +663,17 @@ void main() { test('parseTeamsFromJson()', () { final playerById = {testPlayer1.id: testPlayer1}; - final jsonMap = { - 'teams': [ - { - 'id': testTeam.id, - 'name': testTeam.name, - 'memberIds': [testPlayer1.id], - 'createdAt': testTeam.createdAt.toIso8601String(), - }, - ], - }; + final teamsJson = [ + { + 'id': testTeam.id, + 'name': testTeam.name, + 'memberIds': [testPlayer1.id], + 'createdAt': testTeam.createdAt.toIso8601String(), + }, + ]; final teams = DataTransferService.parseTeamsFromJson( - jsonMap, + teamsJson, playerById, ); @@ -668,15 +685,21 @@ void main() { }); test('parseTeamsFromJson() empty list', () { - final jsonMap = {'teams': []}; - final teams = DataTransferService.parseTeamsFromJson(jsonMap, {}); + final teams = DataTransferService.parseTeamsFromJson([], {}); expect(teams, isEmpty); }); - test('parseTeamsFromJson() missing key', () { - final jsonMap = {}; - final teams = DataTransferService.parseTeamsFromJson(jsonMap, {}); - expect(teams, isEmpty); + test('parseTeamsFromJson() missing memberIds', () { + final teamsJson = [ + { + 'id': testTeam.id, + 'name': testTeam.name, + 'createdAt': testTeam.createdAt.toIso8601String(), + }, + ]; + final teams = DataTransferService.parseTeamsFromJson(teamsJson, {}); + expect(teams.length, 1); + expect(teams[0].members, isEmpty); }); test('parseMatchesFromJson()', () { @@ -860,14 +883,6 @@ void main() { 'createdAt': testGroup.createdAt.toIso8601String(), }, ], - 'teams': [ - { - 'id': testTeam.id, - 'name': testTeam.name, - 'memberIds': [testPlayer1.id, testPlayer2.id], - 'createdAt': testTeam.createdAt.toIso8601String(), - }, - ], 'matches': [ { 'id': testMatch.id,