Revert "Merge branch 'feature/193-statisticsview-rework' into development"
All checks were successful
Push Pipeline / update_version (push) Successful in 6s
Push Pipeline / generate_licenses (push) Successful in 38s
Push Pipeline / generate_localizations (push) Successful in 29s
Push Pipeline / test (push) Successful in 1m35s
Push Pipeline / sort_arb_files (push) Successful in 31s
Push Pipeline / format (push) Successful in 55s
Push Pipeline / build (push) Successful in 4m58s

This reverts commit 24f49e17b9, reversing
changes made to dba6c218d6.

# Conflicts:
#	pubspec.yaml
This commit is contained in:
2026-05-25 14:55:19 +02:00
parent 5659dc36c2
commit 9b208f4780
69 changed files with 834 additions and 6965 deletions

View File

@@ -24,48 +24,48 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) {
} }
} }
/// Translates a [AppColor] enum value to its corresponding localized string. /// Translates a [GameColor] enum value to its corresponding localized string.
String translateAppColorToString(AppColor color, BuildContext context) { String translateGameColorToString(GameColor color, BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
switch (color) { switch (color) {
case AppColor.red: case GameColor.red:
return loc.color_red; return loc.color_red;
case AppColor.blue: case GameColor.blue:
return loc.color_blue; return loc.color_blue;
case AppColor.green: case GameColor.green:
return loc.color_green; return loc.color_green;
case AppColor.yellow: case GameColor.yellow:
return loc.color_yellow; return loc.color_yellow;
case AppColor.purple: case GameColor.purple:
return loc.color_purple; return loc.color_purple;
case AppColor.orange: case GameColor.orange:
return loc.color_orange; return loc.color_orange;
case AppColor.pink: case GameColor.pink:
return loc.color_pink; return loc.color_pink;
case AppColor.teal: case GameColor.teal:
return loc.color_teal; return loc.color_teal;
} }
} }
/// Returns the [Color] object corresponding to a [AppColor] enum value. /// Returns the [Color] object corresponding to a [GameColor] enum value.
Color getColorFromAppColor(AppColor color) { Color getColorFromGameColor(GameColor color) {
switch (color) { switch (color) {
case AppColor.red: case GameColor.red:
return Colors.red; return Colors.red;
case AppColor.blue: case GameColor.blue:
return Colors.blue; return Colors.blue;
case AppColor.green: case GameColor.green:
return Colors.green; return Colors.green;
case AppColor.yellow: case GameColor.yellow:
return const Color(0xFFF7CA28); return const Color(0xFFF7CA28);
case AppColor.purple: case GameColor.purple:
return Colors.purple; return Colors.purple;
case AppColor.orange: case GameColor.orange:
return const Color(0xFFef681f); return const Color(0xFFef681f);
case AppColor.pink: case GameColor.pink:
return const Color(0xFFE91E63); return Colors.pink;
case AppColor.teal: case GameColor.teal:
return const Color(0xFF00BCD4); return Colors.teal;
} }
} }

View File

@@ -43,32 +43,4 @@ enum Ruleset {
} }
/// Different colors for highlighting games /// Different colors for highlighting games
enum AppColor { red, orange, yellow, green, teal, blue, purple, pink } enum GameColor { red, orange, yellow, green, teal, blue, purple, pink }
enum StatisticType {
totalMatches,
totalWins,
totalScore,
totalLosses,
averageScore,
bestScore,
worstScore,
winrate,
}
enum StatisticScope {
allPlayers,
//selectedPlayer,
selectedGroups,
selectedGames,
timeframe,
}
enum Timeframe {
last7Days,
last30Days,
last90Days,
last180Days,
lastYear,
allTime,
}

View File

@@ -77,8 +77,8 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Returns `true` if the game exists, `false` otherwise. /// Returns `true` if the game exists, `false` otherwise.
Future<bool> gameExists({required String gameId}) async { Future<bool> gameExists({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId)); final query = select(gameTable)..where((g) => g.id.equals(gameId));
final row = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return row != null; return result != null;
} }
/// Retrieves all games from the database. /// Retrieves all games from the database.
@@ -92,7 +92,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
name: row.name, name: row.name,
ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset),
description: row.description, description: row.description,
color: AppColor.values.firstWhere((e) => e.name == row.color), color: GameColor.values.firstWhere((e) => e.name == row.color),
icon: row.icon, icon: row.icon,
createdAt: row.createdAt, createdAt: row.createdAt,
), ),
@@ -103,15 +103,15 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Retrieves a [Game] by its [gameId]. /// Retrieves a [Game] by its [gameId].
Future<Game> getGameById({required String gameId}) async { Future<Game> getGameById({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId)); final query = select(gameTable)..where((g) => g.id.equals(gameId));
final row = await query.getSingle(); final result = await query.getSingle();
return Game( return Game(
id: row.id, id: result.id,
name: row.name, name: result.name,
ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset),
description: row.description, description: result.description,
color: AppColor.values.firstWhere((e) => e.name == row.color), color: GameColor.values.firstWhere((e) => e.name == result.color),
icon: row.icon, icon: result.icon,
createdAt: row.createdAt, createdAt: result.createdAt,
); );
} }
@@ -123,7 +123,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
required String name, required String name,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write( await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(name: Value(name)), GameTableCompanion(name: Value(name)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -135,7 +135,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
required Ruleset ruleset, required Ruleset ruleset,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write( await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(ruleset: Value(ruleset.name)), GameTableCompanion(ruleset: Value(ruleset.name)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -147,7 +147,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
required String description, required String description,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write( await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(description: Value(description)), GameTableCompanion(description: Value(description)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -156,10 +156,10 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Updates the color of the game with the given [gameId]. /// Updates the color of the game with the given [gameId].
Future<bool> updateGameColor({ Future<bool> updateGameColor({
required String gameId, required String gameId,
required AppColor color, required GameColor color,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write( await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(color: Value(color.name)), GameTableCompanion(color: Value(color.name)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -171,7 +171,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
required String icon, required String icon,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write( await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(icon: Value(icon)), GameTableCompanion(icon: Value(icon)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -182,7 +182,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Deletes the game with the given [gameId] from the database. /// Deletes the game with the given [gameId] from the database.
/// Returns `true` if the game was deleted, `false` if the game did not exist. /// Returns `true` if the game was deleted, `false` if the game did not exist.
Future<bool> deleteGame({required String gameId}) async { Future<bool> deleteGame({required String gameId}) async {
final query = delete(gameTable)..where((tbl) => tbl.id.equals(gameId)); final query = delete(gameTable)..where((g) => g.id.equals(gameId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -143,16 +143,16 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
final query = select(groupTable); final query = select(groupTable);
final result = await query.get(); final result = await query.get();
return Future.wait( return Future.wait(
result.map((row) async { result.map((groupData) async {
final members = await db.playerGroupDao.getPlayersOfGroup( final members = await db.playerGroupDao.getPlayersOfGroup(
groupId: row.id, groupId: groupData.id,
); );
return Group( return Group(
id: row.id, id: groupData.id,
name: row.name, name: groupData.name,
description: row.description, description: groupData.description,
members: members, members: members,
createdAt: row.createdAt, createdAt: groupData.createdAt,
); );
}), }),
); );
@@ -161,18 +161,18 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
/// Retrieves a [Group] by its [groupId], including its members. /// Retrieves a [Group] by its [groupId], including its members.
Future<Group> getGroupById({required String groupId}) async { Future<Group> getGroupById({required String groupId}) async {
final query = select(groupTable)..where((g) => g.id.equals(groupId)); final query = select(groupTable)..where((g) => g.id.equals(groupId));
final row = await query.getSingle(); final result = await query.getSingle();
List<Player> members = await db.playerGroupDao.getPlayersOfGroup( List<Player> members = await db.playerGroupDao.getPlayersOfGroup(
groupId: groupId, groupId: groupId,
); );
return Group( return Group(
id: row.id, id: result.id,
name: row.name, name: result.name,
description: row.description, description: result.description,
members: members, members: members,
createdAt: row.createdAt, createdAt: result.createdAt,
); );
} }
@@ -180,49 +180,17 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
Future<int> getGroupCount() async { Future<int> getGroupCount() async {
final count = final count =
await (selectOnly(groupTable)..addColumns([groupTable.id.count()])) await (selectOnly(groupTable)..addColumns([groupTable.id.count()]))
.map((tbl) => tbl.read(groupTable.id.count())) .map((row) => row.read(groupTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
/// Retrieves all groups a specific player belongs to.
/// Returns an empty list if the player is not part of any group.
Future<List<Group>> getGroupsByPlayer({required String playerId}) async {
final playerGroups = await (select(
playerGroupTable,
)..where((tbl) => tbl.playerId.equals(playerId))).get();
if (playerGroups.isEmpty) return [];
final groupIds = playerGroups.map((pg) => pg.groupId).toSet().toList();
final result =
await (select(groupTable)
..where((tbl) => tbl.id.isIn(groupIds))
..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)]))
.get();
return Future.wait(
result.map((row) async {
final members = await db.playerGroupDao.getPlayersOfGroup(
groupId: row.id,
);
return Group(
id: row.id,
name: row.name,
description: row.description,
members: members,
createdAt: row.createdAt,
);
}),
);
}
/// Checks if a group with the given [groupId] exists in the database. /// Checks if a group with the given [groupId] exists in the database.
/// Returns `true` if the group exists, `false` otherwise. /// Returns `true` if the group exists, `false` otherwise.
Future<bool> groupExists({required String groupId}) async { Future<bool> groupExists({required String groupId}) async {
final query = select(groupTable)..where((g) => g.id.equals(groupId)); final query = select(groupTable)..where((g) => g.id.equals(groupId));
final row = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return row != null; return result != null;
} }
/* Delete */ /* Delete */
@@ -252,8 +220,9 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
required String name, required String name,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(groupTable)..where((tbl) => tbl.id.equals(groupId))) await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
.write(GroupTableCompanion(name: Value(name))); GroupTableCompanion(name: Value(name)),
);
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -264,8 +233,9 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
required String description, required String description,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(groupTable)..where((tbl) => tbl.id.equals(groupId))) await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
.write(GroupTableCompanion(description: Value(description))); GroupTableCompanion(description: Value(description)),
);
return rowsAffected > 0; return rowsAffected > 0;
} }
} }

View File

@@ -258,15 +258,15 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Returns `true` if the match exists, otherwise `false`. /// Returns `true` if the match exists, otherwise `false`.
Future<bool> matchExists({required String matchId}) async { Future<bool> matchExists({required String matchId}) async {
final query = select(matchTable)..where((g) => g.id.equals(matchId)); final query = select(matchTable)..where((g) => g.id.equals(matchId));
final row = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return row != null; return result != null;
} }
/// Retrieves the number of matches in the database. /// Retrieves the number of matches in the database.
Future<int> getMatchCount() async { Future<int> getMatchCount() async {
final count = final count =
await (selectOnly(matchTable)..addColumns([matchTable.id.count()])) await (selectOnly(matchTable)..addColumns([matchTable.id.count()]))
.map((tbl) => tbl.read(matchTable.id.count())) .map((row) => row.read(matchTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
@@ -279,12 +279,10 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return Future.wait( return Future.wait(
result.map((row) async { result.map((row) async {
final game = await db.gameDao.getGameById(gameId: row.gameId); final game = await db.gameDao.getGameById(gameId: row.gameId);
Group? group; Group? group;
if (row.groupId != null) { if (row.groupId != null) {
group = await db.groupDao.getGroupById(groupId: row.groupId!); group = await db.groupDao.getGroupById(groupId: row.groupId!);
} }
final players = await db.playerMatchDao.getPlayersOfMatch( final players = await db.playerMatchDao.getPlayersOfMatch(
matchId: row.id, matchId: row.id,
); );
@@ -314,13 +312,13 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Retrieves a [Match] by its [matchId]. /// Retrieves a [Match] by its [matchId].
Future<Match> getMatchById({required String matchId}) async { Future<Match> getMatchById({required String matchId}) async {
final query = select(matchTable)..where((g) => g.id.equals(matchId)); final query = select(matchTable)..where((g) => g.id.equals(matchId));
final row = await query.getSingle(); final result = await query.getSingle();
final game = await db.gameDao.getGameById(gameId: row.gameId); final game = await db.gameDao.getGameById(gameId: result.gameId);
Group? group; Group? group;
if (row.groupId != null) { if (result.groupId != null) {
group = await db.groupDao.getGroupById(groupId: row.groupId!); group = await db.groupDao.getGroupById(groupId: result.groupId!);
} }
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId); final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
@@ -330,15 +328,15 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
final teams = await _getMatchTeams(matchId: matchId); final teams = await _getMatchTeams(matchId: matchId);
return Match( return Match(
id: row.id, id: result.id,
name: row.name, name: result.name,
game: game, game: game,
group: group, group: group,
players: players, players: players,
teams: teams.isEmpty ? null : teams, teams: teams.isEmpty ? null : teams,
notes: row.notes, notes: result.notes,
createdAt: row.createdAt, createdAt: result.createdAt,
endedAt: row.endedAt, endedAt: result.endedAt,
scores: scores, scores: scores,
); );
} }
@@ -349,73 +347,25 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
await (selectOnly(matchTable) await (selectOnly(matchTable)
..where(matchTable.gameId.equals(gameId)) ..where(matchTable.gameId.equals(gameId))
..addColumns([matchTable.id.count()])) ..addColumns([matchTable.id.count()]))
.map((tbl) => tbl.read(matchTable.id.count())) .map((row) => row.read(matchTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
Future<List<Match>> getMatchesByPlayer({required String playerId}) async {
final playerMatches = await (select(
playerMatchTable,
)..where((tbl) => tbl.playerId.equals(playerId))).get();
if (playerMatches.isEmpty) return [];
final matchIds = playerMatches.map((tbl) => tbl.matchId).toSet().toList();
final result =
await (select(matchTable)
..where((tbl) => tbl.id.isIn(matchIds))
..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)]))
.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 all matches associated with the given [groupId]. /// Retrieves all matches associated with the given [groupId].
/// Queries the database directly, filtering by [groupId]. /// Queries the database directly, filtering by [groupId].
Future<List<Match>> getMatchesByGroup({required String groupId}) async { Future<List<Match>> getMatchesByGroup({required String groupId}) async {
final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); final query = select(matchTable)..where((m) => m.groupId.equals(groupId));
final result = await query.get(); final rows = await query.get();
return Future.wait( return Future.wait(
result.map((row) async { rows.map((row) async {
final game = await db.gameDao.getGameById(gameId: row.gameId); final game = await db.gameDao.getGameById(gameId: row.gameId);
final group = await db.groupDao.getGroupById(groupId: groupId); final group = await db.groupDao.getGroupById(groupId: groupId);
final players = await db.playerMatchDao.getPlayersOfMatch( final players = await db.playerMatchDao.getPlayersOfMatch(
matchId: row.id, matchId: row.id,
); );
final teams = await _getMatchTeams(matchId: row.id); final teams = await _getMatchTeams(matchId: row.id);
return Match( return Match(
id: row.id, id: row.id,
name: row.name, name: row.name,
@@ -435,7 +385,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
Future<List<Team>> _getMatchTeams({required String matchId}) async { Future<List<Team>> _getMatchTeams({required String matchId}) async {
// Get all unique team IDs from PlayerMatchTable for this match // Get all unique team IDs from PlayerMatchTable for this match
final playerMatchQuery = select(db.playerMatchTable) final playerMatchQuery = select(db.playerMatchTable)
..where((tbl) => tbl.matchId.equals(matchId) & tbl.teamId.isNotNull()); ..where((pm) => pm.matchId.equals(matchId) & pm.teamId.isNotNull());
final playerMatches = await playerMatchQuery.get(); final playerMatches = await playerMatchQuery.get();
if (playerMatches.isEmpty) return []; if (playerMatches.isEmpty) return [];
@@ -462,7 +412,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
required String matchId, required String matchId,
required String name, required String name,
}) async { }) async {
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId)); final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
MatchTableCompanion(name: Value(name)), MatchTableCompanion(name: Value(name)),
); );
@@ -477,7 +427,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
required String matchId, required String matchId,
required String? groupId, required String? groupId,
}) async { }) async {
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId)); final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
MatchTableCompanion(groupId: Value(groupId)), MatchTableCompanion(groupId: Value(groupId)),
); );
@@ -490,7 +440,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
required String matchId, required String matchId,
required String notes, required String notes,
}) async { }) async {
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId)); final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
MatchTableCompanion(notes: Value(notes)), MatchTableCompanion(notes: Value(notes)),
); );
@@ -501,7 +451,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Sets the groupId to null. /// Sets the groupId to null.
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeMatchGroup({required String matchId}) async { Future<bool> removeMatchGroup({required String matchId}) async {
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId)); final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
const MatchTableCompanion(groupId: Value(null)), const MatchTableCompanion(groupId: Value(null)),
); );
@@ -515,7 +465,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
required String matchId, required String matchId,
required DateTime endedAt, required DateTime endedAt,
}) async { }) async {
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId)); final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
MatchTableCompanion(endedAt: Value(endedAt)), MatchTableCompanion(endedAt: Value(endedAt)),
); );
@@ -527,7 +477,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Deletes the match with the given [matchId] from the database. /// Deletes the match with the given [matchId] from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteMatch({required String matchId}) async { Future<bool> deleteMatch({required String matchId}) async {
final query = delete(matchTable)..where((tbl) => tbl.id.equals(matchId)); final query = delete(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -543,7 +493,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Deletes all matches associated with a specific game. /// Deletes all matches associated with a specific game.
/// Returns the number of matches deleted. /// Returns the number of matches deleted.
Future<int> deleteMatchesByGame({required String gameId}) async { Future<int> deleteMatchesByGame({required String gameId}) async {
final query = delete(matchTable)..where((tbl) => tbl.gameId.equals(gameId)); final query = delete(matchTable)..where((m) => m.gameId.equals(gameId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected; return rowsAffected;
} }

View File

@@ -17,7 +17,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// the new one. /// the new one.
Future<bool> addPlayer({required Player player}) async { Future<bool> addPlayer({required Player player}) async {
if (!await playerExists(playerId: player.id)) { if (!await playerExists(playerId: player.id)) {
final int nameCount = await _processNameCount(name: player.name); final int nameCount = await calculateNameCount(name: player.name);
await into(playerTable).insert( await into(playerTable).insert(
PlayerTableCompanion.insert( PlayerTableCompanion.insert(
@@ -64,7 +64,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
final playersWithName = entry.value; final playersWithName = entry.value;
// Get the current nameCount // Get the current nameCount
var nameCount = await _processNameCount(name: name); var nameCount = await calculateNameCount(name: name);
// One player with the same name // One player with the same name
if (playersWithName.length == 1) { if (playersWithName.length == 1) {
@@ -113,7 +113,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
Future<int> getPlayerCount() async { Future<int> getPlayerCount() async {
final count = final count =
await (selectOnly(playerTable)..addColumns([playerTable.id.count()])) await (selectOnly(playerTable)..addColumns([playerTable.id.count()]))
.map((tbl) => tbl.read(playerTable.id.count())) .map((row) => row.read(playerTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
@@ -122,8 +122,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// Returns `true` if the player exists, `false` otherwise. /// Returns `true` if the player exists, `false` otherwise.
Future<bool> playerExists({required String playerId}) async { Future<bool> playerExists({required String playerId}) async {
final query = select(playerTable)..where((p) => p.id.equals(playerId)); final query = select(playerTable)..where((p) => p.id.equals(playerId));
final row = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return row != null; return result != null;
} }
/// Retrieves all players from the database. /// Retrieves all players from the database.
@@ -146,76 +146,57 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// Retrieves a [Player] by their [id]. /// Retrieves a [Player] by their [id].
Future<Player> getPlayerById({required String playerId}) async { Future<Player> getPlayerById({required String playerId}) async {
final query = select(playerTable)..where((p) => p.id.equals(playerId)); final query = select(playerTable)..where((p) => p.id.equals(playerId));
final row = await query.getSingle(); final result = await query.getSingle();
return Player( return Player(
id: row.id, id: result.id,
name: row.name, name: result.name,
description: row.description, description: result.description,
createdAt: row.createdAt, createdAt: result.createdAt,
nameCount: row.nameCount, nameCount: result.nameCount,
); );
} }
/* Update */ /* Update */
/// Updates the name of the player with the given [playerId] to [name]. /// Updates the name of the player with the given [playerId] to [name].
///
/// Keeps the `nameCount` values of the affected name groups consistent:
/// - The renamed player gets a fresh `nameCount` for the new name group.
/// - All players in the previous name group whose `nameCount` was greater
/// than the removed one get decremented by 1, so the numbering stays
/// contiguous (1..N) in `createdAt` order.
/// - If only one player remains in the previous name group, their
/// `nameCount` is reset to 0.
Future<bool> updatePlayerName({ Future<bool> updatePlayerName({
required String playerId, required String playerId,
required String name, required String name,
}) async { }) async {
return transaction(() async { // Get previous name and name count for the player before updating
final previousPlayer = await (select( final previousPlayerName =
playerTable, await (select(playerTable)..where((p) => p.id.equals(playerId)))
)..where((tbl) => tbl.id.equals(playerId))).getSingleOrNull(); .map((row) => row.name)
if (previousPlayer == null) return false; .getSingleOrNull() ??
'';
final previousName = previousPlayer.name; final previousNameCount = await getNameCount(name: previousPlayerName);
final previousCount = previousPlayer.nameCount;
// Determine the nameCount for the renamed player in the new group.
final newNameCount = await _processNameCount(name: name);
final rowsAffected = final rowsAffected =
await (update( await (update(playerTable)..where((p) => p.id.equals(playerId))).write(
playerTable, PlayerTableCompanion(name: Value(name)),
)..where((tbl) => tbl.id.equals(playerId))).write(
PlayerTableCompanion(
name: Value(name),
nameCount: Value(newNameCount),
),
); );
// Consolidate the previous name group. // Update name count for the new name
final remainingCount = await getNameCount(name: previousName); final count = await calculateNameCount(name: name);
if (count > 0) {
if (remainingCount == 1) { await (update(playerTable)..where((p) => p.name.equals(name))).write(
// Only one player left PlayerTableCompanion(nameCount: Value(count)),
await (update(playerTable)..where((p) => p.name.equals(previousName)))
.write(const PlayerTableCompanion(nameCount: Value(0)));
} else if (remainingCount > 1 && previousCount > 0) {
// Shift every player above the gap down by one to keep numbering in order.
await (update(playerTable)..where(
(tbl) =>
tbl.name.equals(previousName) &
tbl.nameCount.isBiggerThanValue(previousCount),
))
.write(
PlayerTableCompanion.custom(
nameCount: playerTable.nameCount - const Constant(1),
),
); );
} }
if (previousNameCount > 0) {
// Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount
final player = await getPlayerWithHighestNameCount(
name: previousPlayerName,
);
if (player != null) {
await updateNameCount(
playerId: player.id,
nameCount: previousNameCount,
);
}
}
return rowsAffected > 0; return rowsAffected > 0;
});
} }
/// Updates the description of the player with the given [playerId] to /// Updates the description of the player with the given [playerId] to
@@ -226,8 +207,9 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
required String description, required String description,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(playerTable)..where((tbl) => tbl.id.equals(playerId))) await (update(playerTable)..where((g) => g.id.equals(playerId))).write(
.write(PlayerTableCompanion(description: Value(description))); PlayerTableCompanion(description: Value(description)),
);
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -236,7 +218,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// Deletes the player with the given [id] from the database. /// Deletes the player with the given [id] from the database.
/// Returns `true` if the player was deleted, `false` if the player did not exist. /// Returns `true` if the player was deleted, `false` if the player did not exist.
Future<bool> deletePlayer({required String playerId}) async { Future<bool> deletePlayer({required String playerId}) async {
final query = delete(playerTable)..where((tbl) => tbl.id.equals(playerId)); final query = delete(playerTable)..where((p) => p.id.equals(playerId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -244,10 +226,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/* Name count management */ /* Name count management */
/// Retrieves the count of players with the given [name]. /// Retrieves the count of players with the given [name].
/// Returns the highest name count if players with the same name exist,
/// otherwise `null`.
Future<int> getNameCount({required String name}) async { Future<int> getNameCount({required String name}) async {
final query = select(playerTable)..where((tbl) => tbl.name.equals(name)); final query = select(playerTable)..where((p) => p.name.equals(name));
final result = await query.get(); final result = await query.get();
return result.length; return result.length;
} }
@@ -258,7 +238,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
required String playerId, required String playerId,
required int nameCount, required int nameCount,
}) async { }) async {
final query = update(playerTable)..where((tbl) => tbl.id.equals(playerId)); final query = update(playerTable)..where((p) => p.id.equals(playerId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
PlayerTableCompanion(nameCount: Value(nameCount)), PlayerTableCompanion(nameCount: Value(nameCount)),
); );
@@ -268,8 +248,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
@visibleForTesting @visibleForTesting
Future<Player?> getPlayerWithHighestNameCount({required String name}) async { Future<Player?> getPlayerWithHighestNameCount({required String name}) async {
final query = select(playerTable) final query = select(playerTable)
..where((tbl) => tbl.name.equals(name)) ..where((p) => p.name.equals(name))
..orderBy([(tbl) => OrderingTerm.desc(tbl.nameCount)]) ..orderBy([(p) => OrderingTerm.desc(p.nameCount)])
..limit(1); ..limit(1);
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
if (result != null) { if (result != null) {
@@ -284,47 +264,34 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
return null; return null;
} }
/// Processes the name count for a new player with the given [name].
///- 0 Player: returning 0
///- 1 Player: returning 2, and initializes the nameCount for the existing player to 1
///- Other: returning the existing count + 1
Future<int> _processNameCount({required String name}) async {
final nameCount = await calculateNameCount(name: name);
if (nameCount == 2) {
// If one other player exists with the same name, initialize the nameCount
await initializeNameCount(name: name);
}
return nameCount;
}
@visibleForTesting @visibleForTesting
/// Calculates the name count for a new player with the given [name].
/// - 0 Players: Name count is 0
/// - 1 Player: Name count is 2 (since the existing player will be 1)
/// - Other: Name count is the existing count + 1
Future<int> calculateNameCount({required String name}) async { Future<int> calculateNameCount({required String name}) async {
final count = await getNameCount(name: name); final count = await getNameCount(name: name);
final int nameCount; final int nameCount;
if (count == 0) { if (count == 1) {
// If no other players exist with the same name, the returned nameCount is 0 // If one other player exists with the same name, initialize the nameCount
nameCount = 0; await initializeNameCount(name: name);
} else if (count == 1) { // And for the new player, set nameCount to 2
// If one other player with the name count exists, the returned name count is 2
nameCount = 2; nameCount = 2;
} else { } else if (count > 1) {
// If more than one player exists with the same name, just increment // If more than one player exists with the same name, just increment
// the nameCount for the new player // the nameCount for the new player
nameCount = count + 1; nameCount = count + 1;
} else {
// If no other players exist with the same name, set nameCount to 0
nameCount = 0;
} }
return nameCount; return nameCount;
} }
@visibleForTesting @visibleForTesting
Future<bool> initializeNameCount({required String name}) async { Future<bool> initializeNameCount({required String name}) async {
final rowsAffected = final rowsAffected =
await (update(playerTable)..where((tbl) => tbl.name.equals(name))) await (update(playerTable)..where((p) => p.name.equals(name))).write(
.write(const PlayerTableCompanion(nameCount: Value(1))); const PlayerTableCompanion(nameCount: Value(1)),
);
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -39,25 +39,18 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
/// Retrieves all players belonging to a specific group by [groupId]. /// Retrieves all players belonging to a specific group by [groupId].
Future<List<Player>> getPlayersOfGroup({required String groupId}) async { Future<List<Player>> getPlayersOfGroup({required String groupId}) async {
final query = select(playerGroupTable).join([ final query = select(playerGroupTable)
innerJoin( ..where((pG) => pG.groupId.equals(groupId));
playerTable, final result = await query.get();
playerTable.id.equalsExp(playerGroupTable.playerId),
),
])..where(playerGroupTable.groupId.equals(groupId));
final result = await query.map((row) => row.readTable(playerTable)).get(); List<Player> groupMembers = List.empty(growable: true);
return result
.map( for (var entry in result) {
(row) => Player( final player = await db.playerDao.getPlayerById(playerId: entry.playerId);
id: row.id, groupMembers.add(player);
createdAt: row.createdAt, }
name: row.name,
nameCount: row.nameCount, return groupMembers;
description: row.description,
),
)
.toList();
} }
/// Checks if a player with [playerId] is in the group with [groupId]. /// Checks if a player with [playerId] is in the group with [groupId].
@@ -67,9 +60,7 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
required String groupId, required String groupId,
}) async { }) async {
final query = select(playerGroupTable) final query = select(playerGroupTable)
..where( ..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId));
(tbl) => tbl.playerId.equals(playerId) & tbl.groupId.equals(groupId),
);
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return result != null; return result != null;
} }
@@ -90,7 +81,7 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
await db.transaction(() async { await db.transaction(() async {
// Remove all existing players from the group // Remove all existing players from the group
final deleteQuery = delete(db.playerGroupTable) final deleteQuery = delete(db.playerGroupTable)
..where((tbl) => tbl.groupId.equals(groupId)); ..where((p) => p.groupId.equals(groupId));
await deleteQuery.go(); await deleteQuery.go();
// Add new players to the player table if they don't exist // Add new players to the player table if they don't exist
@@ -130,9 +121,7 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
required String groupId, required String groupId,
}) async { }) async {
final query = delete(playerGroupTable) final query = delete(playerGroupTable)
..where( ..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId));
(tbl) => tbl.playerId.equals(playerId) & tbl.groupId.equals(groupId),
);
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -40,7 +40,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
await (selectOnly(playerMatchTable) await (selectOnly(playerMatchTable)
..where(playerMatchTable.matchId.equals(matchId)) ..where(playerMatchTable.matchId.equals(matchId))
..addColumns([playerMatchTable.playerId.count()])) ..addColumns([playerMatchTable.playerId.count()]))
.map((tbl) => tbl.read(playerMatchTable.playerId.count())) .map((row) => row.read(playerMatchTable.playerId.count()))
.getSingle(); .getSingle();
return (count ?? 0) > 0; return (count ?? 0) > 0;
} }
@@ -56,7 +56,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
..where(playerMatchTable.matchId.equals(matchId)) ..where(playerMatchTable.matchId.equals(matchId))
..where(playerMatchTable.playerId.equals(playerId)) ..where(playerMatchTable.playerId.equals(playerId))
..addColumns([playerMatchTable.playerId.count()])) ..addColumns([playerMatchTable.playerId.count()]))
.map((tbl) => tbl.read(playerMatchTable.playerId.count())) .map((row) => row.read(playerMatchTable.playerId.count()))
.getSingle(); .getSingle();
return (count ?? 0) > 0; return (count ?? 0) > 0;
} }
@@ -66,7 +66,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
Future<List<Player>> getPlayersOfMatch({required String matchId}) async { Future<List<Player>> getPlayersOfMatch({required String matchId}) async {
final result = await (select( final result = await (select(
playerMatchTable, playerMatchTable,
)..where((tbl) => tbl.matchId.equals(matchId))).get(); )..where((p) => p.matchId.equals(matchId))).get();
if (result.isEmpty) return []; if (result.isEmpty) return [];
@@ -85,8 +85,8 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
}) async { }) async {
final result = final result =
await (select(playerMatchTable) await (select(playerMatchTable)
..where((tbl) => tbl.matchId.equals(matchId)) ..where((p) => p.matchId.equals(matchId))
..where((tbl) => tbl.teamId.equals(teamId))) ..where((p) => p.teamId.equals(teamId)))
.get(); .get();
if (result.isEmpty) return []; if (result.isEmpty) return [];
@@ -109,8 +109,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(playerMatchTable)..where( await (update(playerMatchTable)..where(
(tbl) => (p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
tbl.matchId.equals(matchId) & tbl.playerId.equals(playerId),
)) ))
.write(PlayerMatchTableCompanion(teamId: Value(teamId))); .write(PlayerMatchTableCompanion(teamId: Value(teamId)));
return rowsAffected > 0; return rowsAffected > 0;
@@ -144,9 +143,9 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
// Remove old players // Remove old players
if (playersToRemove.isNotEmpty) { if (playersToRemove.isNotEmpty) {
await (delete(playerMatchTable)..where( await (delete(playerMatchTable)..where(
(tbl) => (pg) =>
tbl.matchId.equals(matchId) & pg.matchId.equals(matchId) &
tbl.playerId.isIn(playersToRemove.toList()), pg.playerId.isIn(playersToRemove.toList()),
)) ))
.go(); .go();
} }
@@ -183,8 +182,8 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
required String playerId, required String playerId,
}) async { }) async {
final query = delete(playerMatchTable) final query = delete(playerMatchTable)
..where((tbl) => tbl.matchId.equals(matchId)) ..where((pg) => pg.matchId.equals(matchId))
..where((tbl) => tbl.playerId.equals(playerId)); ..where((pg) => pg.playerId.equals(playerId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -70,10 +70,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
}) async { }) async {
final query = select(scoreEntryTable) final query = select(scoreEntryTable)
..where( ..where(
(tbl) => (s) =>
tbl.playerId.equals(playerId) & s.playerId.equals(playerId) &
tbl.matchId.equals(matchId) & s.matchId.equals(matchId) &
tbl.roundNumber.equals(roundNumber), s.roundNumber.equals(roundNumber),
); );
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
@@ -91,7 +91,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
required String matchId, required String matchId,
}) async { }) async {
final query = select(scoreEntryTable) final query = select(scoreEntryTable)
..where((tbl) => tbl.matchId.equals(matchId)); ..where((s) => s.matchId.equals(matchId));
final result = await query.get(); final result = await query.get();
final Map<String, ScoreEntry?> scoresByPlayer = {}; final Map<String, ScoreEntry?> scoresByPlayer = {};
@@ -113,10 +113,8 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
required String matchId, required String matchId,
}) async { }) async {
final query = select(scoreEntryTable) final query = select(scoreEntryTable)
..where( ..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId))
(tbl) => tbl.playerId.equals(playerId) & tbl.matchId.equals(matchId), ..orderBy([(s) => OrderingTerm.asc(s.roundNumber)]);
)
..orderBy([(tbl) => OrderingTerm.asc(tbl.roundNumber)]);
final result = await query.get(); final result = await query.get();
return result return result
.map( .map(
@@ -138,8 +136,8 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
final query = selectOnly(scoreEntryTable) final query = selectOnly(scoreEntryTable)
..where(scoreEntryTable.matchId.equals(matchId)) ..where(scoreEntryTable.matchId.equals(matchId))
..addColumns([scoreEntryTable.roundNumber.max()]); ..addColumns([scoreEntryTable.roundNumber.max()]);
final row = await query.getSingle(); final result = await query.getSingle();
return row.read(scoreEntryTable.roundNumber.max()); return result.read(scoreEntryTable.roundNumber.max());
} }
/// Aggregates the total score for a player in a match by summing all their /// Aggregates the total score for a player in a match by summing all their
@@ -168,10 +166,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(scoreEntryTable)..where( await (update(scoreEntryTable)..where(
(tbl) => (s) =>
tbl.playerId.equals(playerId) & s.playerId.equals(playerId) &
tbl.matchId.equals(matchId) & s.matchId.equals(matchId) &
tbl.roundNumber.equals(entry.roundNumber), s.roundNumber.equals(entry.roundNumber),
)) ))
.write( .write(
ScoreEntryTableCompanion( ScoreEntryTableCompanion(
@@ -192,10 +190,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
}) async { }) async {
final query = delete(scoreEntryTable) final query = delete(scoreEntryTable)
..where( ..where(
(tbl) => (s) =>
tbl.playerId.equals(playerId) & s.playerId.equals(playerId) &
tbl.matchId.equals(matchId) & s.matchId.equals(matchId) &
tbl.roundNumber.equals(roundNumber), s.roundNumber.equals(roundNumber),
); );
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
@@ -203,7 +201,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
Future<bool> deleteAllScoresForMatch({required String matchId}) async { Future<bool> deleteAllScoresForMatch({required String matchId}) async {
final query = delete(scoreEntryTable) final query = delete(scoreEntryTable)
..where((tbl) => tbl.matchId.equals(matchId)); ..where((s) => s.matchId.equals(matchId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -213,9 +211,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
required String playerId, required String playerId,
}) async { }) async {
final query = delete(scoreEntryTable) final query = delete(scoreEntryTable)
..where( ..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId));
(tbl) => tbl.playerId.equals(playerId) & tbl.matchId.equals(matchId),
);
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -1,127 +0,0 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/statistic_table.dart';
import 'package:tallee/data/models/statistic.dart';
part 'statistic_dao.g.dart';
@DriftAccessor(tables: [StatisticTable])
class StatisticDao extends DatabaseAccessor<AppDatabase>
with _$StatisticDaoMixin {
StatisticDao(super.db);
/* Create */
Future<bool> addStatistic({required Statistic statistic}) async {
await into(statisticTable).insert(
StatisticTableCompanion.insert(
id: statistic.id,
type: statistic.type.name,
timeframe: Value(statistic.timeframe?.name),
displayCount: Value(statistic.displayCount),
),
mode: InsertMode.insertOrReplace,
);
await db.statisticScopeDao.addStatisticScopes(
statisticId: statistic.id,
scopes: statistic.scopes,
);
if (statistic.selectedGroups != null) {
await db.statisticGroupDao.addStatisticGroups(
statisticId: statistic.id,
groups: statistic.selectedGroups!,
);
}
if (statistic.selectedGames != null) {
await db.statisticGameDao.addStatisticGames(
statisticId: statistic.id,
games: statistic.selectedGames!,
);
}
return true;
}
/* Read */
Future<Statistic?> getStatisticById(String statisticId) async {
final query = select(statisticTable);
final row = await query.getSingleOrNull();
if (row != null) {
final groups = await db.statisticGroupDao.getGroupsForStatistic(row.id);
final games = await db.statisticGameDao.getGamesForStatistic(row.id);
final scopes = await db.statisticScopeDao.getScopeForStatistic(row.id);
return Statistic(
type: StatisticType.values.firstWhere((type) => type.name == row.type),
scopes: scopes,
timeframe: Timeframe.values.firstWhereOrNull(
(t) => t.name == row.timeframe,
),
selectedGroups: groups,
selectedGames: games,
displayCount: row.displayCount,
id: row.id,
);
}
return null;
}
/// Retrieves all statistics from the database, including their associated groups and games.
Future<List<Statistic>> getAllStatistics() async {
final query = select(statisticTable);
final result = await query.get();
return Future.wait(
result.map((row) async {
final groups = await db.statisticGroupDao.getGroupsForStatistic(row.id);
final games = await db.statisticGameDao.getGamesForStatistic(row.id);
final scopes = await db.statisticScopeDao.getScopeForStatistic(row.id);
return Statistic(
type: StatisticType.values.firstWhere(
(type) => type.name == row.type,
),
scopes: scopes,
timeframe: Timeframe.values.firstWhereOrNull(
(t) => t.name == row.timeframe,
),
selectedGroups: groups,
selectedGames: games,
displayCount: row.displayCount,
id: row.id,
);
}),
);
}
/* Update */
Future<bool> updateDisplayCount(String statisticId, int displayCount) async {
final rowsUpdated =
await (update(statisticTable)
..where((tbl) => tbl.id.equals(statisticId)))
.write(StatisticTableCompanion(displayCount: Value(displayCount)));
return rowsUpdated > 0;
}
/* Delete */
Future<bool> deleteStatistic(String statisticId) async {
final rowsDeleted = await (delete(
statisticTable,
)..where((tbl) => tbl.id.equals(statisticId))).go();
return rowsDeleted > 0;
}
Future<bool> deleteAllStatistics() async {
final rowsDeleted = await delete(statisticTable).go();
return rowsDeleted > 0;
}
}

View File

@@ -1,19 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'statistic_dao.dart';
// ignore_for_file: type=lint
mixin _$StatisticDaoMixin on DatabaseAccessor<AppDatabase> {
$StatisticTableTable get statisticTable => attachedDatabase.statisticTable;
StatisticDaoManager get managers => StatisticDaoManager(this);
}
class StatisticDaoManager {
final _$StatisticDaoMixin _db;
StatisticDaoManager(this._db);
$$StatisticTableTableTableManager get statisticTable =>
$$StatisticTableTableTableManager(
_db.attachedDatabase,
_db.statisticTable,
);
}

View File

@@ -1,61 +0,0 @@
import 'package:drift/drift.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/statistic_game_table.dart';
import 'package:tallee/data/models/game.dart';
part 'statistic_game_dao.g.dart';
@DriftAccessor(tables: [StatisticGameTable])
class StatisticGameDao extends DatabaseAccessor<AppDatabase>
with _$StatisticGameDaoMixin {
StatisticGameDao(super.db);
/// Retrieves a list of games associated with a specific statistic.
Future<List<Game>?> getGamesForStatistic(String statisticId) async {
final query = select(statisticGameTable).join([
innerJoin(gameTable, gameTable.id.equalsExp(statisticGameTable.gameId)),
])..where(statisticGameTable.statisticId.equals(statisticId));
final results = await query.map((row) => row.readTable(gameTable)).get();
if (results.isEmpty) return null;
return results
.map(
(row) => Game(
id: row.id,
name: row.name,
ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset),
description: row.description,
color: AppColor.values.firstWhere((e) => e.name == row.color),
icon: row.icon,
createdAt: row.createdAt,
),
)
.toList();
}
Future<bool> addStatisticGames({
required String statisticId,
required List<Game> games,
}) {
final entries = games
.map(
(game) => StatisticGameTableCompanion.insert(
statisticId: statisticId,
gameId: game.id,
),
)
.toList();
return batch((batch) {
batch.insertAll(
statisticGameTable,
entries,
mode: InsertMode.insertOrReplace,
);
}).then((_) => true).catchError((error) {
print('Error adding statistic games: $error');
return false;
});
}
}

View File

@@ -1,29 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'statistic_game_dao.dart';
// ignore_for_file: type=lint
mixin _$StatisticGameDaoMixin on DatabaseAccessor<AppDatabase> {
$StatisticTableTable get statisticTable => attachedDatabase.statisticTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$StatisticGameTableTable get statisticGameTable =>
attachedDatabase.statisticGameTable;
StatisticGameDaoManager get managers => StatisticGameDaoManager(this);
}
class StatisticGameDaoManager {
final _$StatisticGameDaoMixin _db;
StatisticGameDaoManager(this._db);
$$StatisticTableTableTableManager get statisticTable =>
$$StatisticTableTableTableManager(
_db.attachedDatabase,
_db.statisticTable,
);
$$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
$$StatisticGameTableTableTableManager get statisticGameTable =>
$$StatisticGameTableTableTableManager(
_db.attachedDatabase,
_db.statisticGameTable,
);
}

View File

@@ -1,67 +0,0 @@
import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/group_table.dart';
import 'package:tallee/data/db/tables/statistic_group_table.dart';
import 'package:tallee/data/models/group.dart';
part 'statistic_group_dao.g.dart';
@DriftAccessor(tables: [StatisticGroupTable, GroupTable])
class StatisticGroupDao extends DatabaseAccessor<AppDatabase>
with _$StatisticGroupDaoMixin {
StatisticGroupDao(super.db);
/// Retrieves a list of groups associated with a specific statistic.
Future<List<Group>?> getGroupsForStatistic(String statisticId) async {
final query = select(statisticGroupTable).join([
innerJoin(
groupTable,
groupTable.id.equalsExp(statisticGroupTable.groupId),
),
])..where(statisticGroupTable.statisticId.equals(statisticId));
final results = await query.map((row) => row.readTable(groupTable)).get();
if (results.isEmpty) return null;
final groups = await Future.wait(
results.map((result) async {
final groupMembers = await db.playerGroupDao.getPlayersOfGroup(
groupId: result.id,
);
return Group(
id: result.id,
createdAt: result.createdAt,
name: result.name,
description: result.description,
members: groupMembers,
);
}),
);
return groups;
}
Future<bool> addStatisticGroups({
required String statisticId,
required List<Group> groups,
}) async {
final entries = groups
.map(
(group) => StatisticGroupTableCompanion.insert(
statisticId: statisticId,
groupId: group.id,
),
)
.toList();
return batch((batch) {
batch.insertAll(
statisticGroupTable,
entries,
mode: InsertMode.insertOrReplace,
);
}).then((_) => true).catchError((error) {
print('Error adding statistic groups: $error');
return false;
});
}
}

View File

@@ -1,29 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'statistic_group_dao.dart';
// ignore_for_file: type=lint
mixin _$StatisticGroupDaoMixin on DatabaseAccessor<AppDatabase> {
$StatisticTableTable get statisticTable => attachedDatabase.statisticTable;
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$StatisticGroupTableTable get statisticGroupTable =>
attachedDatabase.statisticGroupTable;
StatisticGroupDaoManager get managers => StatisticGroupDaoManager(this);
}
class StatisticGroupDaoManager {
final _$StatisticGroupDaoMixin _db;
StatisticGroupDaoManager(this._db);
$$StatisticTableTableTableManager get statisticTable =>
$$StatisticTableTableTableManager(
_db.attachedDatabase,
_db.statisticTable,
);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$StatisticGroupTableTableTableManager get statisticGroupTable =>
$$StatisticGroupTableTableTableManager(
_db.attachedDatabase,
_db.statisticGroupTable,
);
}

View File

@@ -1,55 +0,0 @@
import 'package:drift/drift.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/statistic_scope_table.dart';
part 'statistic_scope_dao.g.dart';
@DriftAccessor(tables: [StatisticScopeTable])
class StatisticScopeDao extends DatabaseAccessor<AppDatabase>
with _$StatisticScopeDaoMixin {
StatisticScopeDao(super.db);
/// Retrieves a list of statistic scopes associated with a specific statistic ID.
Future<List<StatisticScope>> getScopeForStatistic(String statisticId) async {
final query = select(statisticScopeTable)
..where((tbl) => tbl.statisticId.equals(statisticId));
final result = await query.get();
return result
.map(
(row) => StatisticScope.values.firstWhere(
(e) => e.name == row.scope,
orElse: () => throw Exception(
'Invalid scope value: ${row.scope} for statistic ID: $statisticId',
),
),
)
.toList();
}
Future<bool> addStatisticScopes({
required String statisticId,
required List<StatisticScope> scopes,
}) async {
final entries = scopes
.map(
(scope) => StatisticScopeTableCompanion.insert(
statisticId: statisticId,
scope: scope.name,
),
)
.toList();
return batch((batch) {
batch.insertAll(
statisticScopeTable,
entries,
mode: InsertMode.insertOrReplace,
);
}).then((_) => true).catchError((error) {
print('Error adding statistic scopes: $error');
return false;
});
}
}

View File

@@ -1,26 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'statistic_scope_dao.dart';
// ignore_for_file: type=lint
mixin _$StatisticScopeDaoMixin on DatabaseAccessor<AppDatabase> {
$StatisticTableTable get statisticTable => attachedDatabase.statisticTable;
$StatisticScopeTableTable get statisticScopeTable =>
attachedDatabase.statisticScopeTable;
StatisticScopeDaoManager get managers => StatisticScopeDaoManager(this);
}
class StatisticScopeDaoManager {
final _$StatisticScopeDaoMixin _db;
StatisticScopeDaoManager(this._db);
$$StatisticTableTableTableManager get statisticTable =>
$$StatisticTableTableTableManager(
_db.attachedDatabase,
_db.statisticTable,
);
$$StatisticScopeTableTableTableManager get statisticScopeTable =>
$$StatisticScopeTableTableTableManager(
_db.attachedDatabase,
_db.statisticScopeTable,
);
}

View File

@@ -86,7 +86,7 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
Future<int> getTeamCount() async { Future<int> getTeamCount() async {
final count = final count =
await (selectOnly(teamTable)..addColumns([teamTable.id.count()])) await (selectOnly(teamTable)..addColumns([teamTable.id.count()]))
.map((tbl) => tbl.read(teamTable.id.count())) .map((row) => row.read(teamTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
@@ -95,8 +95,8 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
/// Returns `true` if the team exists, `false` otherwise. /// Returns `true` if the team exists, `false` otherwise.
Future<bool> teamExists({required String teamId}) async { Future<bool> teamExists({required String teamId}) async {
final query = select(teamTable)..where((t) => t.id.equals(teamId)); final query = select(teamTable)..where((t) => t.id.equals(teamId));
final row = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return row != null; return result != null;
} }
/// Retrieves all teams from the database. /// Retrieves all teams from the database.
@@ -119,12 +119,12 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
/// Retrieves a [Team] by its [teamId], including its members. /// Retrieves a [Team] by its [teamId], including its members.
Future<Team> getTeamById({required String teamId}) async { Future<Team> getTeamById({required String teamId}) async {
final query = select(teamTable)..where((t) => t.id.equals(teamId)); final query = select(teamTable)..where((t) => t.id.equals(teamId));
final row = await query.getSingle(); final result = await query.getSingle();
final members = await _getTeamMembers(teamId: teamId); final members = await _getTeamMembers(teamId: teamId);
return Team( return Team(
id: row.id, id: result.id,
name: row.name, name: result.name,
createdAt: row.createdAt, createdAt: result.createdAt,
members: members, members: members,
); );
} }
@@ -133,13 +133,13 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
Future<List<Player>> _getTeamMembers({required String teamId}) async { Future<List<Player>> _getTeamMembers({required String teamId}) async {
// Get all player_match entries with this teamId // Get all player_match entries with this teamId
final playerMatchQuery = select(db.playerMatchTable) final playerMatchQuery = select(db.playerMatchTable)
..where((tbl) => tbl.teamId.equals(teamId)); ..where((pm) => pm.teamId.equals(teamId));
final playerMatches = await playerMatchQuery.get(); final playerMatches = await playerMatchQuery.get();
if (playerMatches.isEmpty) return []; if (playerMatches.isEmpty) return [];
// Get unique player IDs // Get unique player IDs
final playerIds = playerMatches.map((tbl) => tbl.playerId).toSet(); final playerIds = playerMatches.map((pm) => pm.playerId).toSet();
// Fetch all players // Fetch all players
final players = await Future.wait( final players = await Future.wait(
@@ -156,7 +156,7 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
required String name, required String name,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(teamTable)..where((tbl) => tbl.id.equals(teamId))).write( await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
TeamTableCompanion(name: Value(name)), TeamTableCompanion(name: Value(name)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -175,7 +175,7 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
/// Deletes the team with the given [teamId] from the database. /// Deletes the team with the given [teamId] from the database.
/// Returns `true` if the team was deleted, `false` otherwise. /// Returns `true` if the team was deleted, `false` otherwise.
Future<bool> deleteTeam({required String teamId}) async { Future<bool> deleteTeam({required String teamId}) async {
final query = delete(teamTable)..where((tbl) => tbl.id.equals(teamId)); final query = delete(teamTable)..where((t) => t.id.equals(teamId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -8,10 +8,6 @@ import 'package:tallee/data/dao/player_dao.dart';
import 'package:tallee/data/dao/player_group_dao.dart'; import 'package:tallee/data/dao/player_group_dao.dart';
import 'package:tallee/data/dao/player_match_dao.dart'; import 'package:tallee/data/dao/player_match_dao.dart';
import 'package:tallee/data/dao/score_entry_dao.dart'; import 'package:tallee/data/dao/score_entry_dao.dart';
import 'package:tallee/data/dao/statistic_dao.dart';
import 'package:tallee/data/dao/statistic_game_dao.dart';
import 'package:tallee/data/dao/statistic_group_dao.dart';
import 'package:tallee/data/dao/statistic_scope_dao.dart';
import 'package:tallee/data/dao/team_dao.dart'; import 'package:tallee/data/dao/team_dao.dart';
import 'package:tallee/data/db/tables/game_table.dart'; import 'package:tallee/data/db/tables/game_table.dart';
import 'package:tallee/data/db/tables/group_table.dart'; import 'package:tallee/data/db/tables/group_table.dart';
@@ -20,10 +16,6 @@ import 'package:tallee/data/db/tables/player_group_table.dart';
import 'package:tallee/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:tallee/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/player_table.dart';
import 'package:tallee/data/db/tables/score_entry_table.dart'; import 'package:tallee/data/db/tables/score_entry_table.dart';
import 'package:tallee/data/db/tables/statistic_game_table.dart';
import 'package:tallee/data/db/tables/statistic_group_table.dart';
import 'package:tallee/data/db/tables/statistic_scope_table.dart';
import 'package:tallee/data/db/tables/statistic_table.dart';
import 'package:tallee/data/db/tables/team_table.dart'; import 'package:tallee/data/db/tables/team_table.dart';
part 'database.g.dart'; part 'database.g.dart';
@@ -38,10 +30,6 @@ part 'database.g.dart';
GameTable, GameTable,
TeamTable, TeamTable,
ScoreEntryTable, ScoreEntryTable,
StatisticTable,
StatisticScopeTable,
StatisticGameTable,
StatisticGroupTable,
], ],
daos: [ daos: [
PlayerDao, PlayerDao,
@@ -52,10 +40,6 @@ part 'database.g.dart';
GameDao, GameDao,
ScoreEntryDao, ScoreEntryDao,
TeamDao, TeamDao,
StatisticDao,
StatisticScopeDao,
StatisticGameDao,
StatisticGroupDao,
], ],
) )
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
import 'package:drift/drift.dart';
import 'package:tallee/data/db/tables/statistic_table.dart';
class StatisticScopeTable extends Table {
TextColumn get statisticId =>
text().references(StatisticTable, #id, onDelete: KeyAction.cascade)();
TextColumn get scope => text()();
@override
Set<Column<Object>> get primaryKey => {statisticId, scope};
}

View File

@@ -1,11 +0,0 @@
import 'package:drift/drift.dart';
class StatisticTable extends Table {
TextColumn get id => text()();
TextColumn get type => text()();
TextColumn get timeframe => text().nullable()();
IntColumn get displayCount => integer().withDefault(const Constant(5))();
@override
Set<Column<Object>> get primaryKey => {id};
}

View File

@@ -8,13 +8,13 @@ class Game {
final String name; final String name;
final Ruleset ruleset; final Ruleset ruleset;
final String description; final String description;
final AppColor color; final GameColor color;
final String icon; final String icon;
Game({ Game({
required this.name, required this.name,
required this.ruleset, required this.ruleset,
this.color = AppColor.orange, this.color = GameColor.orange,
this.description = '', this.description = '',
this.icon = '', this.icon = '',
String? id, String? id,
@@ -33,7 +33,7 @@ class Game {
String? name, String? name,
Ruleset? ruleset, Ruleset? ruleset,
String? description, String? description,
AppColor? color, GameColor? color,
String? icon, String? icon,
}) { }) {
return Game( return Game(
@@ -73,7 +73,7 @@ class Game {
orElse: () => Ruleset.singleWinner, orElse: () => Ruleset.singleWinner,
), ),
description = json['description'], description = json['description'],
color = AppColor.values.firstWhere((e) => e.name == json['color']), color = GameColor.values.firstWhere((e) => e.name == json['color']),
icon = json['icon']; icon = json['icon'];
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {

View File

@@ -5,17 +5,17 @@ import 'package:uuid/uuid.dart';
class Group { class Group {
final String id; final String id;
final DateTime createdAt;
final String name; final String name;
final List<Player> members;
final String description; final String description;
final DateTime createdAt;
final List<Player> members;
Group({ Group({
required this.name,
required this.members,
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
required this.name,
String? description, String? description,
required this.members,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(), createdAt = createdAt ?? clock.now(),
description = description ?? ''; description = description ?? '';

View File

@@ -107,7 +107,7 @@ class Match {
name: '', name: '',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: '', description: '',
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
), ),
group = null, group = null,

View File

@@ -1,48 +0,0 @@
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:uuid/uuid.dart';
class Statistic {
final String id;
final StatisticType type;
final List<StatisticScope> scopes;
final Timeframe? timeframe;
final List<Group>? selectedGroups;
final List<Game>? selectedGames;
final int displayCount;
Statistic({
required this.type,
required this.scopes,
this.timeframe,
this.selectedGroups,
this.selectedGames,
this.displayCount = 5,
String? id,
}) : id = id ?? const Uuid().v4();
@override
String toString() {
return 'Statistic(id: $id, type: $type, scopes: $scopes, timeframe: $timeframe, selectedGroups: $selectedGroups, selectedGames: $selectedGames)';
}
Statistic copyWith({
StatisticType? type,
List<StatisticScope>? scopes,
Timeframe? timeframe,
List<Group>? selectedGroups,
List<Game>? selectedGames,
int? displayCount,
}) {
return Statistic(
id: id,
type: type ?? this.type,
scopes: scopes ?? this.scopes,
timeframe: timeframe ?? this.timeframe,
selectedGroups: selectedGroups ?? this.selectedGroups,
selectedGames: selectedGames ?? this.selectedGames,
displayCount: displayCount ?? this.displayCount,
);
}
}

View File

@@ -2,18 +2,14 @@
"@@locale": "de", "@@locale": "de",
"all_players": "Alle Spieler:innen", "all_players": "Alle Spieler:innen",
"all_players_selected": "Alle Spieler:innen ausgewählt", "all_players_selected": "Alle Spieler:innen ausgewählt",
"all_time": "Gesamter Zeitraum",
"amount_of_matches": "Anzahl der Spiele", "amount_of_matches": "Anzahl der Spiele",
"app_name": "Tallee", "app_name": "Tallee",
"average_score": "Durchschnittliche Punktzahl",
"best_player": "Beste:r Spieler:in", "best_player": "Beste:r Spieler:in",
"best_score": "Beste Punktzahl",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"choose_color": "Farbe wählen", "choose_color": "Farbe wählen",
"choose_game": "Spielvorlage wählen", "choose_game": "Spielvorlage wählen",
"choose_group": "Gruppe wählen", "choose_group": "Gruppe wählen",
"choose_ruleset": "Regelwerk wählen", "choose_ruleset": "Regelwerk wählen",
"classifier": "Klassifikator",
"color": "Farbe", "color": "Farbe",
"color_blue": "Blau", "color_blue": "Blau",
"color_green": "Grün", "color_green": "Grün",
@@ -23,31 +19,12 @@
"color_red": "Rot", "color_red": "Rot",
"color_teal": "Türkis", "color_teal": "Türkis",
"color_yellow": "Gelb", "color_yellow": "Gelb",
"confirm": "Bestätigen",
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
"@could_not_add_player": {
"placeholders": {
"playerName": {
"type": "String"
}
}
},
"create_game": "Spielvorlage erstellen", "create_game": "Spielvorlage erstellen",
"create_group": "Gruppe erstellen", "create_group": "Gruppe erstellen",
"create_match": "Spiel erstellen", "create_match": "Spiel erstellen",
"create_new_group": "Neue Gruppe erstellen", "create_new_group": "Neue Gruppe erstellen",
"create_new_match": "Neues Spiel erstellen", "create_new_match": "Neues Spiel erstellen",
"create_statistic": "Statistik erstellen",
"create_statistic_classifier_subtitle": "Wähle die anzuzeigende Hauptmetrik aus",
"create_statistic_classifier_title": "Klassifikator",
"create_statistic_games_subtitle": "Wähle die gefilterten Spielvorlagen",
"create_statistic_games_title": "Spielvorlagen",
"create_statistic_groups_subtitle": "Wähle die gefilterten Gruppen",
"create_statistic_groups_title": "Gruppen",
"create_statistic_scope_subtitle": "Wähle den Hauptfilter für deine Statistik. Er bestimmt, welche Daten zur Berechnung des Klassifikators verwendet werden.",
"create_statistic_scope_title": "Bereich",
"create_statistic_timeframe_subtitle": "Wähle einen Zeitraum, nach dem die Daten gefiltert werden. Nur Spiele, die innerhalb des Zeitraums beendet wurden, fließen in die Statistik ein.",
"create_statistic_timeframe_title": "Zeitraum",
"created_on": "Erstellt am", "created_on": "Erstellt am",
"data": "Daten", "data": "Daten",
"data_successfully_deleted": "Daten erfolgreich gelöscht", "data_successfully_deleted": "Daten erfolgreich gelöscht",
@@ -67,15 +44,11 @@
}, },
"delete_group": "Gruppe löschen", "delete_group": "Gruppe löschen",
"delete_match": "Spiel löschen", "delete_match": "Spiel löschen",
"delete_player": "Spieler:in löschen",
"description": "Beschreibung", "description": "Beschreibung",
"displayed_entries": "Angezeigte Einträge",
"drag_to_set_placement": "Ziehen um Platzierung zu setzen", "drag_to_set_placement": "Ziehen um Platzierung zu setzen",
"edit_game": "Spielvorlage bearbeiten", "edit_game": "Spielvorlage bearbeiten",
"edit_group": "Gruppe bearbeiten", "edit_group": "Gruppe bearbeiten",
"edit_match": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten",
"edit_name": "Name ändern",
"edit_player": "Spieler bearbeiten",
"enter_points": "Punkte eingeben", "enter_points": "Punkte eingeben",
"enter_results": "Ergebnisse eintragen", "enter_results": "Ergebnisse eintragen",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
@@ -86,42 +59,30 @@
"exit_view": "Ansicht verlassen", "exit_view": "Ansicht verlassen",
"export_canceled": "Export abgebrochen", "export_canceled": "Export abgebrochen",
"export_data": "Daten exportieren", "export_data": "Daten exportieren",
"filter": "Filter",
"format_exception": "Formatfehler (siehe Konsole)", "format_exception": "Formatfehler (siehe Konsole)",
"game": "Spielvorlage", "game": "Spielvorlage",
"game_name": "Spielvorlagenname", "game_name": "Spielvorlagenname",
"games": "Spielvorlagen",
"group": "Gruppe", "group": "Gruppe",
"group_name": "Gruppenname", "group_name": "Gruppenname",
"group_profile": "Gruppenprofil", "group_profile": "Gruppenprofil",
"groups": "Gruppen", "groups": "Gruppen",
"groups_part_of": "Gruppen Teil von",
"highest_score": "Höchste Punkte", "highest_score": "Höchste Punkte",
"home": "Startseite", "home": "Startseite",
"import_canceled": "Import abgebrochen", "import_canceled": "Import abgebrochen",
"import_data": "Daten importieren", "import_data": "Daten importieren",
"info": "Info", "info": "Info",
"invalid_schema": "Ungültiges Schema", "invalid_schema": "Ungültiges Schema",
"last_180_days": "Letzte 180 Tage",
"last_30_days": "Letzte 30 Tage",
"last_7_days": "Letzte 7 Tage",
"last_90_days": "Letzte 90 Tage",
"last_year": "Letztes Jahr",
"least_points": "Niedrigste Punkte", "least_points": "Niedrigste Punkte",
"legal": "Rechtliches", "legal": "Rechtliches",
"legal_notice": "Impressum", "legal_notice": "Impressum",
"licenses": "Lizenzen", "licenses": "Lizenzen",
"live_edit_mode": "Live-Bearbeitungsmodus", "live_edit_mode": "Live-Bearbeitungsmodus",
"loading": "Lädt...",
"loser": "Verlierer:in", "loser": "Verlierer:in",
"lowest_score": "Niedrigste Punkte", "lowest_score": "Niedrigste Punkte",
"match_in_progress": "Spiel läuft...", "match_in_progress": "Spiel läuft...",
"match_name": "Spieltitel", "match_name": "Spieltitel",
"match_profile": "Spielprofil", "match_profile": "Spielprofil",
"matches": "Spiele", "matches": "Spiele",
"matches_part_of": "Spiele Teil von",
"matches_played": "Spiele gespielt",
"matches_won": "Spiele gewonnen",
"members": "Mitglieder", "members": "Mitglieder",
"most_points": "Höchste Punkte", "most_points": "Höchste Punkte",
"multiple_winners": "Mehrere Gewinner:innen", "multiple_winners": "Mehrere Gewinner:innen",
@@ -131,7 +92,6 @@
"no_license_text_available": "Kein Lizenztext verfügbar", "no_license_text_available": "Kein Lizenztext verfügbar",
"no_licenses_found": "Keine Lizenzen gefunden", "no_licenses_found": "Keine Lizenzen gefunden",
"no_matches_created_yet": "Noch keine Spiele erstellt", "no_matches_created_yet": "Noch keine Spiele erstellt",
"no_matches_played_yet": "Noch kein Spiel gespielt",
"no_players_created_yet": "Noch keine Spieler:in erstellt", "no_players_created_yet": "Noch keine Spieler:in erstellt",
"no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden", "no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
"no_players_selected": "Keine Spieler:innen ausgewählt", "no_players_selected": "Keine Spieler:innen ausgewählt",
@@ -139,16 +99,13 @@
"no_results_entered_yet": "Noch keine Ergebnisse eingetragen", "no_results_entered_yet": "Noch keine Ergebnisse eingetragen",
"no_second_match_available": "Kein zweites Spiel verfügbar", "no_second_match_available": "Kein zweites Spiel verfügbar",
"no_statistics_available": "Keine Statistiken verfügbar", "no_statistics_available": "Keine Statistiken verfügbar",
"no_statistics_created_yet": "Noch keine Statistiken erstellt",
"none": "Kein", "none": "Kein",
"none_group": "Keine", "none_group": "Keine",
"not_available": "Nicht verfügbar", "not_available": "Nicht verfügbar",
"not_part_of_any_group": "Noch keiner Gruppe hinzugefügt",
"place": "Platz", "place": "Platz",
"placement": "Platzierung", "placement": "Platzierung",
"played_matches": "Gespielte Spiele", "played_matches": "Gespielte Spiele",
"player_name": "Spieler:innenname", "player_name": "Spieler:innenname",
"player_profile": "Spieler:in-Profil",
"players": "Spieler:innen", "players": "Spieler:innen",
"point": "Punkt", "point": "Punkt",
"points": "Punkte", "points": "Punkte",
@@ -164,36 +121,15 @@
"ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.", "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.", "ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.",
"save_changes": "Änderungen speichern", "save_changes": "Änderungen speichern",
"scope": "Bereich",
"search_for_groups": "Nach Gruppen suchen", "search_for_groups": "Nach Gruppen suchen",
"search_for_players": "Nach Spieler:innen suchen", "search_for_players": "Nach Spieler:innen suchen",
"select_a_classifier": "Klassifikator auswählen",
"select_a_game": "Spielvorlage auswählen",
"select_a_group": "Gruppe auswählen",
"select_a_scope": "Bereich auswählen",
"select_a_timeframe": "Zeitraum auswählen",
"select_a_timeframe_for_which_data_will_be_filtered": "Wähle einen Zeitraum, für den die Daten gefiltert werden sollen",
"select_loser": "Verlierer:in wählen", "select_loser": "Verlierer:in wählen",
"select_the_filtered_games": "Wähle Spiele, nach denen gefiltert werden soll.",
"select_the_filtered_groups": "Wähle Gruppen, nach denen gefiltert werden soll.",
"select_the_filtered_timeframe": "Wähle einen Zeitraum, nach dem gefiltert werden soll.",
"select_winner": "Gewinner:in wählen", "select_winner": "Gewinner:in wählen",
"select_winners": "Gewinner:innen wählen", "select_winners": "Gewinner:innen wählen",
"selected_games": "Ausgewählte Spielvorlagen",
"selected_groups": "Ausgewählte Gruppen",
"selected_players": "Ausgewählte Spieler:innen", "selected_players": "Ausgewählte Spieler:innen",
"set_name": "Name setzen",
"settings": "Einstellungen", "settings": "Einstellungen",
"single_loser": "Ein:e Verlierer:in", "single_loser": "Ein:e Verlierer:in",
"single_winner": "Ein:e Gewinner:in", "single_winner": "Ein:e Gewinner:in",
"statistic_type_average_score": "Durchschnittliche Punktzahl",
"statistic_type_best_score": "Beste Punktzahl",
"statistic_type_total_losses": "Niederlagen insgesamt",
"statistic_type_total_matches": "Spiele insgesamt",
"statistic_type_total_score": "Punktzahl insgesamt",
"statistic_type_total_wins": "Siege insgesamt",
"statistic_type_winrate": "Siegquote",
"statistic_type_worst_score": "Schlechteste Punktzahl",
"statistics": "Statistiken", "statistics": "Statistiken",
"stats": "Statistiken", "stats": "Statistiken",
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
@@ -201,18 +137,12 @@
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, 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.", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.",
"tie": "Unentschieden", "tie": "Unentschieden",
"timeframe": "Zeitraum",
"today_at": "Heute um", "today_at": "Heute um",
"total_losses": "Niederlagen insgesamt",
"total_matches": "Spiele insgesamt",
"total_score": "Punktzahl insgesamt",
"total_wins": "Siege insgesamt",
"undo": "Rückgängig", "undo": "Rückgängig",
"unknown_exception": "Unbekannter Fehler (siehe Konsole)", "unknown_exception": "Unbekannter Fehler (siehe Konsole)",
"winner": "Gewinner:in", "winner": "Gewinner:in",
"winners": "Gewinner:innen", "winners": "Gewinner:innen",
"winrate": "Siegquote", "winrate": "Siegquote",
"wins": "Siege", "wins": "Siege",
"worst_score": "Schlechteste Punktzahl",
"yesterday_at": "Gestern um" "yesterday_at": "Gestern um"
} }

View File

@@ -18,30 +18,13 @@
"color_purple": "Purple", "color_purple": "Purple",
"color_red": "Red", "color_red": "Red",
"color_teal": "Teal", "color_teal": "Teal",
"displayed_entries": "Displayed entries",
"color_yellow": "Yellow", "color_yellow": "Yellow",
"confirm": "Confirm", "could_not_add_player": "Could not add player",
"could_not_add_player": "Could not add player {playerName}",
"@could_not_add_player": {
"placeholders": {
"playerName": {
"type": "String"
}
}
},
"create_game": "Create Game", "create_game": "Create Game",
"create_group": "Create Group", "create_group": "Create Group",
"create_match": "Create match", "create_match": "Create match",
"create_new_group": "Create new group", "create_new_group": "Create new group",
"create_new_match": "Create new match", "create_new_match": "Create new match",
"create_statistic": "Create statistic",
"classifier": "Classifier",
"select_the_filtered_timeframe": "Select the timeframe you want to filter by.",
"select_the_filtered_games": "Select the games you want to filter by.",
"games": "Games",
"select_the_filtered_groups": "Select the groups you want to filter by.",
"scope": "Scope",
"timeframe": "Timeframe",
"created_on": "Created on", "created_on": "Created on",
"data": "Data", "data": "Data",
"data_successfully_deleted": "Data successfully deleted", "data_successfully_deleted": "Data successfully deleted",
@@ -59,17 +42,13 @@
} }
} }
}, },
"filter": "Filter",
"delete_group": "Delete Group", "delete_group": "Delete Group",
"delete_match": "Delete Match", "delete_match": "Delete Match",
"delete_player": "Delete player?",
"description": "Description", "description": "Description",
"drag_to_set_placement": "Drag to set placement", "drag_to_set_placement": "Drag to set placement",
"edit_game": "Edit Game", "edit_game": "Edit Game",
"edit_group": "Edit Group", "edit_group": "Edit Group",
"edit_match": "Edit Match", "edit_match": "Edit Match",
"edit_name": "Edit name",
"edit_player": "Edit player",
"enter_points": "Enter points", "enter_points": "Enter points",
"enter_results": "Enter Results", "enter_results": "Enter Results",
"error_creating_group": "Error while creating group, please try again", "error_creating_group": "Error while creating group, please try again",
@@ -87,7 +66,6 @@
"group_name": "Group name", "group_name": "Group name",
"group_profile": "Group Profile", "group_profile": "Group Profile",
"groups": "Groups", "groups": "Groups",
"groups_part_of": "Groups part of",
"highest_score": "Highest Score", "highest_score": "Highest Score",
"home": "Home", "home": "Home",
"import_canceled": "Import canceled", "import_canceled": "Import canceled",
@@ -99,16 +77,12 @@
"legal_notice": "Legal Notice", "legal_notice": "Legal Notice",
"licenses": "Licenses", "licenses": "Licenses",
"live_edit_mode": "Live Edit Mode", "live_edit_mode": "Live Edit Mode",
"loading": "Loading...",
"loser": "Loser", "loser": "Loser",
"lowest_score": "Lowest Score", "lowest_score": "Lowest Score",
"match_in_progress": "Match in progress...", "match_in_progress": "Match in progress...",
"match_name": "Match name", "match_name": "Match name",
"match_profile": "Match Profile", "match_profile": "Match Profile",
"matches": "Matches", "matches": "Matches",
"matches_part_of": "Matches part of",
"matches_played": "Matches played",
"matches_won": "Matches won",
"members": "Members", "members": "Members",
"most_points": "Most Points", "most_points": "Most Points",
"multiple_winners": "Multiple Winners", "multiple_winners": "Multiple Winners",
@@ -118,7 +92,6 @@
"no_license_text_available": "No license text available", "no_license_text_available": "No license text available",
"no_licenses_found": "No licenses found", "no_licenses_found": "No licenses found",
"no_matches_created_yet": "No matches created yet", "no_matches_created_yet": "No matches created yet",
"no_matches_played_yet": "No games played yet",
"no_players_created_yet": "No players created yet", "no_players_created_yet": "No players created yet",
"no_players_found_with_that_name": "No players found with that name", "no_players_found_with_that_name": "No players found with that name",
"no_players_selected": "No players selected", "no_players_selected": "No players selected",
@@ -126,16 +99,13 @@
"no_results_entered_yet": "No results entered yet", "no_results_entered_yet": "No results entered yet",
"no_second_match_available": "No second match available", "no_second_match_available": "No second match available",
"no_statistics_available": "No statistics available", "no_statistics_available": "No statistics available",
"no_statistics_created_yet": "No statistics created yet",
"none": "None", "none": "None",
"none_group": "None", "none_group": "None",
"not_available": "Not available", "not_available": "Not available",
"not_part_of_any_group": "Not part of any group yet",
"place": "place", "place": "place",
"placement": "Placement", "placement": "Placement",
"played_matches": "Played Matches", "played_matches": "Played Matches",
"player_name": "Player name", "player_name": "Player name",
"player_profile": "Player Profile",
"players": "Players", "players": "Players",
"point": "Point", "point": "Point",
"points": "Points", "points": "Points",
@@ -156,26 +126,11 @@
"select_winner": "Select Winner", "select_winner": "Select Winner",
"select_winners": "Select Winners", "select_winners": "Select Winners",
"selected_players": "Selected players", "selected_players": "Selected players",
"set_name": "Set name",
"settings": "Settings", "settings": "Settings",
"select_a_classifier": "Select a classifier",
"select_a_game": "Select a game",
"select_a_group": "Select a group",
"select_a_scope": "Select a scope",
"select_a_timeframe": "Select a timeframe",
"single_loser": "Single Loser", "single_loser": "Single Loser",
"single_winner": "Single Winner", "single_winner": "Single Winner",
"statistics": "Statistics", "statistics": "Statistics",
"stats": "Stats", "stats": "Stats",
"selected_games": "Selected games",
"selected_groups": "Selected groups",
"average_score": "Average score",
"best_score": "Best score",
"total_losses": "Total losses",
"total_matches": "Total matches",
"total_score": "Total score",
"total_wins": "Total wins",
"worst_score": "Worst score",
"successfully_added_player": "Successfully added player {playerName}", "successfully_added_player": "Successfully added player {playerName}",
"@successfully_added_player": { "@successfully_added_player": {
"description": "Success message when adding a player", "description": "Success message when adding a player",
@@ -190,12 +145,6 @@
"there_is_no_group_matching_your_search": "There is no group 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.", "this_cannot_be_undone": "This can't be undone.",
"tie": "Tie", "tie": "Tie",
"all_time": "All time",
"last_180_days": "Last 180 days",
"last_30_days": "Last 30 days",
"last_7_days": "Last 7 days",
"last_90_days": "Last 90 days",
"last_year": "Last year",
"today_at": "Today at", "today_at": "Today at",
"undo": "Undo", "undo": "Undo",
"unknown_exception": "Unknown Exception (see console)", "unknown_exception": "Unknown Exception (see console)",

View File

@@ -206,29 +206,17 @@ abstract class AppLocalizations {
/// **'Teal'** /// **'Teal'**
String get color_teal; String get color_teal;
/// No description provided for @displayed_entries.
///
/// In en, this message translates to:
/// **'Displayed entries'**
String get displayed_entries;
/// No description provided for @color_yellow. /// No description provided for @color_yellow.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Yellow'** /// **'Yellow'**
String get color_yellow; String get color_yellow;
/// No description provided for @confirm.
///
/// In en, this message translates to:
/// **'Confirm'**
String get confirm;
/// No description provided for @could_not_add_player. /// No description provided for @could_not_add_player.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Could not add player {playerName}'** /// **'Could not add player'**
String could_not_add_player(String playerName); String could_not_add_player(Object playerName);
/// No description provided for @create_game. /// No description provided for @create_game.
/// ///
@@ -260,54 +248,6 @@ abstract class AppLocalizations {
/// **'Create new match'** /// **'Create new match'**
String get create_new_match; String get create_new_match;
/// No description provided for @create_statistic.
///
/// In en, this message translates to:
/// **'Create statistic'**
String get create_statistic;
/// No description provided for @classifier.
///
/// In en, this message translates to:
/// **'Classifier'**
String get classifier;
/// No description provided for @select_the_filtered_timeframe.
///
/// In en, this message translates to:
/// **'Select the timeframe you want to filter by.'**
String get select_the_filtered_timeframe;
/// No description provided for @select_the_filtered_games.
///
/// In en, this message translates to:
/// **'Select the games you want to filter by.'**
String get select_the_filtered_games;
/// No description provided for @games.
///
/// In en, this message translates to:
/// **'Games'**
String get games;
/// No description provided for @select_the_filtered_groups.
///
/// In en, this message translates to:
/// **'Select the groups you want to filter by.'**
String get select_the_filtered_groups;
/// No description provided for @scope.
///
/// In en, this message translates to:
/// **'Scope'**
String get scope;
/// No description provided for @timeframe.
///
/// In en, this message translates to:
/// **'Timeframe'**
String get timeframe;
/// No description provided for @created_on. /// No description provided for @created_on.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -368,12 +308,6 @@ abstract class AppLocalizations {
/// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'** /// **'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); String delete_game_with_matches_warning(int count);
/// No description provided for @filter.
///
/// In en, this message translates to:
/// **'Filter'**
String get filter;
/// No description provided for @delete_group. /// No description provided for @delete_group.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -386,12 +320,6 @@ abstract class AppLocalizations {
/// **'Delete Match'** /// **'Delete Match'**
String get delete_match; String get delete_match;
/// No description provided for @delete_player.
///
/// In en, this message translates to:
/// **'Delete player?'**
String get delete_player;
/// No description provided for @description. /// No description provided for @description.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -422,18 +350,6 @@ abstract class AppLocalizations {
/// **'Edit Match'** /// **'Edit Match'**
String get edit_match; String get edit_match;
/// No description provided for @edit_name.
///
/// In en, this message translates to:
/// **'Edit name'**
String get edit_name;
/// No description provided for @edit_player.
///
/// In en, this message translates to:
/// **'Edit player'**
String get edit_player;
/// No description provided for @enter_points. /// No description provided for @enter_points.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -536,12 +452,6 @@ abstract class AppLocalizations {
/// **'Groups'** /// **'Groups'**
String get groups; String get groups;
/// No description provided for @groups_part_of.
///
/// In en, this message translates to:
/// **'Groups part of'**
String get groups_part_of;
/// No description provided for @highest_score. /// No description provided for @highest_score.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -608,12 +518,6 @@ abstract class AppLocalizations {
/// **'Live Edit Mode'** /// **'Live Edit Mode'**
String get live_edit_mode; String get live_edit_mode;
/// No description provided for @loading.
///
/// In en, this message translates to:
/// **'Loading...'**
String get loading;
/// No description provided for @loser. /// No description provided for @loser.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -650,24 +554,6 @@ abstract class AppLocalizations {
/// **'Matches'** /// **'Matches'**
String get matches; String get matches;
/// No description provided for @matches_part_of.
///
/// In en, this message translates to:
/// **'Matches part of'**
String get matches_part_of;
/// No description provided for @matches_played.
///
/// In en, this message translates to:
/// **'Matches played'**
String get matches_played;
/// No description provided for @matches_won.
///
/// In en, this message translates to:
/// **'Matches won'**
String get matches_won;
/// No description provided for @members. /// No description provided for @members.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -722,12 +608,6 @@ abstract class AppLocalizations {
/// **'No matches created yet'** /// **'No matches created yet'**
String get no_matches_created_yet; String get no_matches_created_yet;
/// No description provided for @no_matches_played_yet.
///
/// In en, this message translates to:
/// **'No games played yet'**
String get no_matches_played_yet;
/// No description provided for @no_players_created_yet. /// No description provided for @no_players_created_yet.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -770,12 +650,6 @@ abstract class AppLocalizations {
/// **'No statistics available'** /// **'No statistics available'**
String get no_statistics_available; String get no_statistics_available;
/// No description provided for @no_statistics_created_yet.
///
/// In en, this message translates to:
/// **'No statistics created yet'**
String get no_statistics_created_yet;
/// No description provided for @none. /// No description provided for @none.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -794,12 +668,6 @@ abstract class AppLocalizations {
/// **'Not available'** /// **'Not available'**
String get not_available; String get not_available;
/// No description provided for @not_part_of_any_group.
///
/// In en, this message translates to:
/// **'Not part of any group yet'**
String get not_part_of_any_group;
/// No description provided for @place. /// No description provided for @place.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -824,12 +692,6 @@ abstract class AppLocalizations {
/// **'Player name'** /// **'Player name'**
String get player_name; String get player_name;
/// No description provided for @player_profile.
///
/// In en, this message translates to:
/// **'Player Profile'**
String get player_profile;
/// No description provided for @players. /// No description provided for @players.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -950,48 +812,12 @@ abstract class AppLocalizations {
/// **'Selected players'** /// **'Selected players'**
String get selected_players; String get selected_players;
/// No description provided for @set_name.
///
/// In en, this message translates to:
/// **'Set name'**
String get set_name;
/// No description provided for @settings. /// No description provided for @settings.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Settings'** /// **'Settings'**
String get settings; String get settings;
/// No description provided for @select_a_classifier.
///
/// In en, this message translates to:
/// **'Select a classifier'**
String get select_a_classifier;
/// No description provided for @select_a_game.
///
/// In en, this message translates to:
/// **'Select a game'**
String get select_a_game;
/// No description provided for @select_a_group.
///
/// In en, this message translates to:
/// **'Select a group'**
String get select_a_group;
/// No description provided for @select_a_scope.
///
/// In en, this message translates to:
/// **'Select a scope'**
String get select_a_scope;
/// No description provided for @select_a_timeframe.
///
/// In en, this message translates to:
/// **'Select a timeframe'**
String get select_a_timeframe;
/// No description provided for @single_loser. /// No description provided for @single_loser.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1016,60 +842,6 @@ abstract class AppLocalizations {
/// **'Stats'** /// **'Stats'**
String get stats; String get stats;
/// No description provided for @selected_games.
///
/// In en, this message translates to:
/// **'Selected games'**
String get selected_games;
/// No description provided for @selected_groups.
///
/// In en, this message translates to:
/// **'Selected groups'**
String get selected_groups;
/// No description provided for @average_score.
///
/// In en, this message translates to:
/// **'Average score'**
String get average_score;
/// No description provided for @best_score.
///
/// In en, this message translates to:
/// **'Best score'**
String get best_score;
/// No description provided for @total_losses.
///
/// In en, this message translates to:
/// **'Total losses'**
String get total_losses;
/// No description provided for @total_matches.
///
/// In en, this message translates to:
/// **'Total matches'**
String get total_matches;
/// No description provided for @total_score.
///
/// In en, this message translates to:
/// **'Total score'**
String get total_score;
/// No description provided for @total_wins.
///
/// In en, this message translates to:
/// **'Total wins'**
String get total_wins;
/// No description provided for @worst_score.
///
/// In en, this message translates to:
/// **'Worst score'**
String get worst_score;
/// Success message when adding a player /// Success message when adding a player
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1100,42 +872,6 @@ abstract class AppLocalizations {
/// **'Tie'** /// **'Tie'**
String get tie; String get tie;
/// No description provided for @all_time.
///
/// In en, this message translates to:
/// **'All time'**
String get all_time;
/// No description provided for @last_180_days.
///
/// In en, this message translates to:
/// **'Last 180 days'**
String get last_180_days;
/// No description provided for @last_30_days.
///
/// In en, this message translates to:
/// **'Last 30 days'**
String get last_30_days;
/// No description provided for @last_7_days.
///
/// In en, this message translates to:
/// **'Last 7 days'**
String get last_7_days;
/// No description provided for @last_90_days.
///
/// In en, this message translates to:
/// **'Last 90 days'**
String get last_90_days;
/// No description provided for @last_year.
///
/// In en, this message translates to:
/// **'Last year'**
String get last_year;
/// No description provided for @today_at. /// No description provided for @today_at.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -62,17 +62,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get color_teal => 'Türkis'; String get color_teal => 'Türkis';
@override
String get displayed_entries => 'Angezeigte Einträge';
@override @override
String get color_yellow => 'Gelb'; String get color_yellow => 'Gelb';
@override @override
String get confirm => 'Bestätigen'; String could_not_add_player(Object playerName) {
@override
String could_not_add_player(String playerName) {
return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
} }
@@ -91,33 +85,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get create_new_match => 'Neues Spiel erstellen'; String get create_new_match => 'Neues Spiel erstellen';
@override
String get create_statistic => 'Statistik erstellen';
@override
String get classifier => 'Klassifikator';
@override
String get select_the_filtered_timeframe =>
'Wähle einen Zeitraum, nach dem gefiltert werden soll.';
@override
String get select_the_filtered_games =>
'Wähle Spiele, nach denen gefiltert werden soll.';
@override
String get games => 'Spielvorlagen';
@override
String get select_the_filtered_groups =>
'Wähle Gruppen, nach denen gefiltert werden soll.';
@override
String get scope => 'Bereich';
@override
String get timeframe => 'Zeitraum';
@override @override
String get created_on => 'Erstellt am'; String get created_on => 'Erstellt am';
@@ -158,18 +125,12 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.';
} }
@override
String get filter => 'Filter';
@override @override
String get delete_group => 'Gruppe löschen'; String get delete_group => 'Gruppe löschen';
@override @override
String get delete_match => 'Spiel löschen'; String get delete_match => 'Spiel löschen';
@override
String get delete_player => 'Spieler:in löschen';
@override @override
String get description => 'Beschreibung'; String get description => 'Beschreibung';
@@ -185,12 +146,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get edit_match => 'Gruppe bearbeiten'; String get edit_match => 'Gruppe bearbeiten';
@override
String get edit_name => 'Name ändern';
@override
String get edit_player => 'Spieler bearbeiten';
@override @override
String get enter_points => 'Punkte eingeben'; String get enter_points => 'Punkte eingeben';
@@ -246,9 +201,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get groups => 'Gruppen'; String get groups => 'Gruppen';
@override
String get groups_part_of => 'Gruppen Teil von';
@override @override
String get highest_score => 'Höchste Punkte'; String get highest_score => 'Höchste Punkte';
@@ -282,9 +234,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get live_edit_mode => 'Live-Bearbeitungsmodus'; String get live_edit_mode => 'Live-Bearbeitungsmodus';
@override
String get loading => 'Lädt...';
@override @override
String get loser => 'Verlierer:in'; String get loser => 'Verlierer:in';
@@ -303,15 +252,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get matches => 'Spiele'; String get matches => 'Spiele';
@override
String get matches_part_of => 'Spiele Teil von';
@override
String get matches_played => 'Spiele gespielt';
@override
String get matches_won => 'Spiele gewonnen';
@override @override
String get members => 'Mitglieder'; String get members => 'Mitglieder';
@@ -339,9 +279,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get no_matches_created_yet => 'Noch keine Spiele erstellt'; String get no_matches_created_yet => 'Noch keine Spiele erstellt';
@override
String get no_matches_played_yet => 'Noch kein Spiel gespielt';
@override @override
String get no_players_created_yet => 'Noch keine Spieler:in erstellt'; String get no_players_created_yet => 'Noch keine Spieler:in erstellt';
@@ -364,9 +301,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get no_statistics_available => 'Keine Statistiken verfügbar'; String get no_statistics_available => 'Keine Statistiken verfügbar';
@override
String get no_statistics_created_yet => 'Noch keine Statistiken erstellt';
@override @override
String get none => 'Kein'; String get none => 'Kein';
@@ -376,9 +310,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get not_available => 'Nicht verfügbar'; String get not_available => 'Nicht verfügbar';
@override
String get not_part_of_any_group => 'Noch keiner Gruppe hinzugefügt';
@override @override
String get place => 'Platz'; String get place => 'Platz';
@@ -391,9 +322,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get player_name => 'Spieler:innenname'; String get player_name => 'Spieler:innenname';
@override
String get player_profile => 'Spieler:in-Profil';
@override @override
String get players => 'Spieler:innen'; String get players => 'Spieler:innen';
@@ -459,27 +387,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get selected_players => 'Ausgewählte Spieler:innen'; String get selected_players => 'Ausgewählte Spieler:innen';
@override
String get set_name => 'Name setzen';
@override @override
String get settings => 'Einstellungen'; String get settings => 'Einstellungen';
@override
String get select_a_classifier => 'Klassifikator auswählen';
@override
String get select_a_game => 'Spielvorlage auswählen';
@override
String get select_a_group => 'Gruppe auswählen';
@override
String get select_a_scope => 'Bereich auswählen';
@override
String get select_a_timeframe => 'Zeitraum auswählen';
@override @override
String get single_loser => 'Ein:e Verlierer:in'; String get single_loser => 'Ein:e Verlierer:in';
@@ -492,33 +402,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get stats => 'Statistiken'; String get stats => 'Statistiken';
@override
String get selected_games => 'Ausgewählte Spielvorlagen';
@override
String get selected_groups => 'Ausgewählte Gruppen';
@override
String get average_score => 'Durchschnittliche Punktzahl';
@override
String get best_score => 'Beste Punktzahl';
@override
String get total_losses => 'Niederlagen insgesamt';
@override
String get total_matches => 'Spiele insgesamt';
@override
String get total_score => 'Punktzahl insgesamt';
@override
String get total_wins => 'Siege insgesamt';
@override
String get worst_score => 'Schlechteste Punktzahl';
@override @override
String successfully_added_player(String playerName) { String successfully_added_player(String playerName) {
return 'Spieler:in $playerName erfolgreich hinzugefügt'; return 'Spieler:in $playerName erfolgreich hinzugefügt';
@@ -539,24 +422,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get tie => 'Unentschieden'; String get tie => 'Unentschieden';
@override
String get all_time => 'Gesamter Zeitraum';
@override
String get last_180_days => 'Letzte 180 Tage';
@override
String get last_30_days => 'Letzte 30 Tage';
@override
String get last_7_days => 'Letzte 7 Tage';
@override
String get last_90_days => 'Letzte 90 Tage';
@override
String get last_year => 'Letztes Jahr';
@override @override
String get today_at => 'Heute um'; String get today_at => 'Heute um';

View File

@@ -62,18 +62,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get color_teal => 'Teal'; String get color_teal => 'Teal';
@override
String get displayed_entries => 'Displayed entries';
@override @override
String get color_yellow => 'Yellow'; String get color_yellow => 'Yellow';
@override @override
String get confirm => 'Confirm'; String could_not_add_player(Object playerName) {
return 'Could not add player';
@override
String could_not_add_player(String playerName) {
return 'Could not add player $playerName';
} }
@override @override
@@ -91,33 +85,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get create_new_match => 'Create new match'; String get create_new_match => 'Create new match';
@override
String get create_statistic => 'Create statistic';
@override
String get classifier => 'Classifier';
@override
String get select_the_filtered_timeframe =>
'Select the timeframe you want to filter by.';
@override
String get select_the_filtered_games =>
'Select the games you want to filter by.';
@override
String get games => 'Games';
@override
String get select_the_filtered_groups =>
'Select the groups you want to filter by.';
@override
String get scope => 'Scope';
@override
String get timeframe => 'Timeframe';
@override @override
String get created_on => 'Created on'; String get created_on => 'Created on';
@@ -158,18 +125,12 @@ class AppLocalizationsEn extends AppLocalizations {
return 'If you delete this game template, $_temp0 using this game template will also be deleted.'; return 'If you delete this game template, $_temp0 using this game template will also be deleted.';
} }
@override
String get filter => 'Filter';
@override @override
String get delete_group => 'Delete Group'; String get delete_group => 'Delete Group';
@override @override
String get delete_match => 'Delete Match'; String get delete_match => 'Delete Match';
@override
String get delete_player => 'Delete player?';
@override @override
String get description => 'Description'; String get description => 'Description';
@@ -185,12 +146,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get edit_match => 'Edit Match'; String get edit_match => 'Edit Match';
@override
String get edit_name => 'Edit name';
@override
String get edit_player => 'Edit player';
@override @override
String get enter_points => 'Enter points'; String get enter_points => 'Enter points';
@@ -246,9 +201,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get groups => 'Groups'; String get groups => 'Groups';
@override
String get groups_part_of => 'Groups part of';
@override @override
String get highest_score => 'Highest Score'; String get highest_score => 'Highest Score';
@@ -282,9 +234,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get live_edit_mode => 'Live Edit Mode'; String get live_edit_mode => 'Live Edit Mode';
@override
String get loading => 'Loading...';
@override @override
String get loser => 'Loser'; String get loser => 'Loser';
@@ -303,15 +252,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get matches => 'Matches'; String get matches => 'Matches';
@override
String get matches_part_of => 'Matches part of';
@override
String get matches_played => 'Matches played';
@override
String get matches_won => 'Matches won';
@override @override
String get members => 'Members'; String get members => 'Members';
@@ -339,9 +279,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get no_matches_created_yet => 'No matches created yet'; String get no_matches_created_yet => 'No matches created yet';
@override
String get no_matches_played_yet => 'No games played yet';
@override @override
String get no_players_created_yet => 'No players created yet'; String get no_players_created_yet => 'No players created yet';
@@ -364,9 +301,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get no_statistics_available => 'No statistics available'; String get no_statistics_available => 'No statistics available';
@override
String get no_statistics_created_yet => 'No statistics created yet';
@override @override
String get none => 'None'; String get none => 'None';
@@ -376,9 +310,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get not_available => 'Not available'; String get not_available => 'Not available';
@override
String get not_part_of_any_group => 'Not part of any group yet';
@override @override
String get place => 'place'; String get place => 'place';
@@ -391,9 +322,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get player_name => 'Player name'; String get player_name => 'Player name';
@override
String get player_profile => 'Player Profile';
@override @override
String get players => 'Players'; String get players => 'Players';
@@ -459,27 +387,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get selected_players => 'Selected players'; String get selected_players => 'Selected players';
@override
String get set_name => 'Set name';
@override @override
String get settings => 'Settings'; String get settings => 'Settings';
@override
String get select_a_classifier => 'Select a classifier';
@override
String get select_a_game => 'Select a game';
@override
String get select_a_group => 'Select a group';
@override
String get select_a_scope => 'Select a scope';
@override
String get select_a_timeframe => 'Select a timeframe';
@override @override
String get single_loser => 'Single Loser'; String get single_loser => 'Single Loser';
@@ -492,33 +402,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get stats => 'Stats'; String get stats => 'Stats';
@override
String get selected_games => 'Selected games';
@override
String get selected_groups => 'Selected groups';
@override
String get average_score => 'Average score';
@override
String get best_score => 'Best score';
@override
String get total_losses => 'Total losses';
@override
String get total_matches => 'Total matches';
@override
String get total_score => 'Total score';
@override
String get total_wins => 'Total wins';
@override
String get worst_score => 'Worst score';
@override @override
String successfully_added_player(String playerName) { String successfully_added_player(String playerName) {
return 'Successfully added player $playerName'; return 'Successfully added player $playerName';
@@ -538,24 +421,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get tie => 'Tie'; String get tie => 'Tie';
@override
String get all_time => 'All time';
@override
String get last_180_days => 'Last 180 days';
@override
String get last_30_days => 'Last 30 days';
@override
String get last_7_days => 'Last 7 days';
@override
String get last_90_days => 'Last 90 days';
@override
String get last_year => 'Last year';
@override @override
String get today_at => 'Today at'; String get today_at => 'Today at';

View File

@@ -6,7 +6,7 @@ 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/group_view/group_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_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/settings_view/settings_view.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view/statistics_view.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/navbar_item.dart'; import 'package:tallee/presentation/widgets/navbar_item.dart';

View File

@@ -89,7 +89,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
initialSelectedPlayers: initialSelectedPlayers, initialSelectedPlayers: initialSelectedPlayers,
onPlayerCreated: () => widget.onMembersChanged?.call(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectedPlayers = [...value]; selectedPlayers = [...value];
@@ -135,7 +134,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
if (!mounted) return; if (!mounted) return;
if (success) { if (success) {
widget.onMembersChanged?.call();
await HapticFeedback.successNotification(); await HapticFeedback.successNotification();
if (mounted) { if (mounted) {
Navigator.pop(context, updatedGroup); Navigator.pop(context, updatedGroup);
@@ -159,6 +157,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
final success = await db.groupDao.addGroup( final success = await db.groupDao.addGroup(
group: Group(name: groupName, members: selectedPlayers), group: Group(name: groupName, members: selectedPlayers),
); );
return success; return success;
} }

View File

@@ -77,7 +77,6 @@ class _GroupViewState extends State<GroupView> {
); );
} }
return GroupTile( return GroupTile(
onPlayerChanged: loadGroups,
group: groups[index], group: groups[index],
onTap: () async { onTap: () async {
await Navigator.push( await Navigator.push(
@@ -107,10 +106,13 @@ class _GroupViewState extends State<GroupView> {
context, context,
adaptivePageRoute( adaptivePageRoute(
builder: (context) { builder: (context) {
return CreateGroupView(onMembersChanged: loadGroups); return const CreateGroupView();
}, },
), ),
); );
setState(() {
loadGroups();
});
}, },
), ),
), ),

View File

@@ -164,7 +164,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
game.ruleset, game.ruleset,
context, context,
), ),
badgeColor: getColorFromAppColor(game.color), badgeColor: getColorFromGameColor(game.color),
isHighlighted: selectedGameId == game.id, isHighlighted: selectedGameId == game.id,
onTap: () async { onTap: () async {
setState(() { setState(() {

View File

@@ -49,10 +49,10 @@ class _CreateGameViewState extends State<CreateGameView> {
late final AppDatabase db; late final AppDatabase db;
late List<(Ruleset, String)> _rulesets; late List<(Ruleset, String)> _rulesets;
late List<(AppColor, String)> _colors; late List<(GameColor, String)> _colors;
Ruleset? selectedRuleset = Ruleset.singleWinner; Ruleset? selectedRuleset = Ruleset.singleWinner;
AppColor? selectedColor = AppColor.orange; GameColor? selectedColor = GameColor.orange;
/// Controller for the game name input field. /// Controller for the game name input field.
final _gameNameController = TextEditingController(); final _gameNameController = TextEditingController();
@@ -87,10 +87,10 @@ class _CreateGameViewState extends State<CreateGameView> {
), ),
); );
_colors = List.generate( _colors = List.generate(
AppColor.values.length, GameColor.values.length,
(index) => ( (index) => (
AppColor.values[index], GameColor.values[index],
translateAppColorToString(AppColor.values[index], context), translateGameColorToString(GameColor.values[index], context),
), ),
); );
@@ -117,6 +117,7 @@ class _CreateGameViewState extends State<CreateGameView> {
return ScaffoldMessenger( return ScaffoldMessenger(
child: Scaffold( child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text(isEditing ? loc.edit_game : loc.create_game), title: Text(isEditing ? loc.edit_game : loc.create_game),
actions: [ actions: [
@@ -467,7 +468,7 @@ class _CreateGameViewState extends State<CreateGameView> {
height: 16, height: 16,
margin: const EdgeInsets.only(left: 12), margin: const EdgeInsets.only(left: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: getColorFromAppColor( color: getColorFromGameColor(
_colors[index].$1, _colors[index].$1,
), ),
shape: BoxShape.circle, shape: BoxShape.circle,
@@ -501,13 +502,13 @@ class _CreateGameViewState extends State<CreateGameView> {
width: 16, width: 16,
height: 16, height: 16,
decoration: BoxDecoration( decoration: BoxDecoration(
color: getColorFromAppColor(selectedColor!), color: getColorFromGameColor(selectedColor!),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.only(right: 5), padding: const EdgeInsets.only(right: 5),
child: Text(translateAppColorToString(selectedColor!, context)), child: Text(translateGameColorToString(selectedColor!, context)),
), ),
Transform.rotate( Transform.rotate(
angle: pi / 2, angle: pi / 2,

View File

@@ -196,7 +196,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
child: PlayerSelection( child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'), key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialSelectedPlayers: selectedPlayers, initialSelectedPlayers: selectedPlayers,
onPlayerCreated: () => widget.onMatchesUpdated?.call(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectedPlayers = value; selectedPlayers = value;

View File

@@ -39,7 +39,7 @@ class _MatchViewState extends State<MatchView> {
game: Game( game: Game(
name: 'Game name', name: 'Game name',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
), ),
group: Group( group: Group(
@@ -79,7 +79,7 @@ class _MatchViewState extends State<MatchView> {
visible: matches.isNotEmpty, visible: matches.isNotEmpty,
replacement: Center( replacement: Center(
child: TopCenteredMessage( child: TopCenteredMessage(
icon: Icons.info, icon: Icons.report,
title: loc.info, title: loc.info,
message: loc.no_matches_created_yet, message: loc.no_matches_created_yet,
), ),
@@ -97,7 +97,6 @@ class _MatchViewState extends State<MatchView> {
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 12.0), padding: const EdgeInsets.only(bottom: 12.0),
child: MatchTile( child: MatchTile(
onPlayerEdited: loadMatches,
width: MediaQuery.sizeOf(context).width * 0.95, width: MediaQuery.sizeOf(context).width * 0.95,
onTap: () async { onTap: () async {
Navigator.push( Navigator.push(

View File

@@ -1,394 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/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/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
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/text_input/text_input_field.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class PlayerDetailView extends StatefulWidget {
const PlayerDetailView({
super.key,
required this.player,
required this.callback,
});
/// The player to display
final Player player;
final VoidCallback callback;
@override
State<PlayerDetailView> createState() => _PlayerDetailViewState();
}
class _PlayerDetailViewState extends State<PlayerDetailView> {
late final AppDatabase db;
late Player _player;
late String playerNameCount;
bool isLoading = true;
/// Total matches played by this player
int totalMatches = 0;
/// Total matches won by this player
int matchesWon = 0;
/// Total groups this player belongs to
int totalGroups = 0;
/// Full list of groups this player belongs to
List<Group> playerGroups = List.filled(
4,
Group(name: 'Skeleton group', members: []),
);
/// Full list of matches this player played in
List<Match> playerMatches = List.filled(
4,
Match(
name: 'Skeleton match',
game: Game(name: 'Game name', ruleset: Ruleset.singleWinner),
players: [],
),
);
TextEditingController nameController = TextEditingController();
@override
void initState() {
super.initState();
_player = widget.player;
db = Provider.of<AppDatabase>(context, listen: false);
playerNameCount = getNameCountText(_player);
_loadData();
}
@override
void dispose() {
nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(loc.player_profile),
actions: [
HapticIconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
showDialog<bool>(
context: context,
builder: (context) => CustomAlertDialog(
title: loc.delete_player,
content: Text(loc.this_cannot_be_undone),
actions: [
CustomDialogAction(
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! && context.mounted) {
//TODO: implement player deletion in db
if (!context.mounted) return;
Navigator.pop(context);
widget.callback();
}
});
},
),
],
),
body: SafeArea(
child: Stack(
alignment: Alignment.center,
children: [
ListView(
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 20,
bottom: 100,
),
children: [
const Center(
child: ColoredIconContainer(
icon: Icons.person,
containerSize: 55,
iconSize: 38,
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_player.name,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
textAlign: TextAlign.center,
),
Text(
playerNameCount,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor.withAlpha(120),
),
textAlign: TextAlign.center,
),
],
),
const SizedBox(height: 5),
Text(
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(_player.createdAt)}',
style: const TextStyle(
fontSize: 12,
color: CustomTheme.textColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
InfoTile(
title: '${loc.matches_part_of} ($totalMatches)',
icon: Icons.sports_esports,
horizontalAlignment: CrossAxisAlignment.start,
content: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
alignment: Alignment.topLeft,
child: playerMatches.isNotEmpty
? Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: playerMatches.map((match) {
return TextIconTile(
text: match.name,
iconEnabled: false,
);
}).toList(),
)
: Text(
loc.no_matches_played_yet,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
),
),
const SizedBox(height: 15),
InfoTile(
title: '${loc.groups_part_of} ($totalGroups)',
icon: Icons.people,
horizontalAlignment: CrossAxisAlignment.start,
content: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
alignment: Alignment.topLeft,
child: playerGroups.isNotEmpty
? Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: playerGroups.map((group) {
return TextIconTile(
text: group.name,
iconEnabled: false,
);
}).toList(),
)
: Text(
loc.not_part_of_any_group,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
),
),
const SizedBox(height: 15),
InfoTile(
title: loc.statistics,
icon: Icons.bar_chart,
content: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
child: Column(
children: [
_buildStatRow(
loc.matches_played,
totalMatches.toString(),
),
_buildStatRow(loc.matches_won, matchesWon.toString()),
_buildStatRow(
loc.winrate,
'${totalMatches == 0 ? 0 : ((matchesWon / totalMatches) * 100).round()}%',
),
],
),
),
),
],
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: MainMenuButton(
text: loc.edit_player,
icon: Icons.edit,
onPressed: () async {
nameController.text = _player.name;
showDialog<bool>(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) {
return CustomAlertDialog(
title: loc.edit_name,
content: TextInputField(
controller: nameController,
hintText: loc.set_name,
onChanged: (_) => setDialogState(() {}),
),
actions: [
CustomDialogAction(
onPressed: isConfirmButtonEnabled()
? () => Navigator.of(context).pop(true)
: null,
text: loc.confirm,
),
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
buttonType: ButtonType.secondary,
text: loc.cancel,
),
],
);
},
),
).then((confirmed) async {
if (confirmed! && context.mounted) {
final newName = nameController.text.trim();
if (newName != _player.name) {
final fetchedPlayerNameCount = await db.playerDao
.getNameCount(name: newName);
await db.playerDao.updatePlayerName(
playerId: _player.id,
name: newName,
);
widget.callback.call();
setState(() {
_player = Player(
name: newName,
createdAt: _player.createdAt,
id: _player.id,
nameCount: _player.nameCount,
description: _player.description,
);
// If there is already a player with the same name,
// the count of that player is 0, so we start counting from 2 to get the correct count for this player. If there are no players with the same name, we just show the name without a count.
playerNameCount = fetchedPlayerNameCount == 0
? ''
: ' #${fetchedPlayerNameCount + 1}';
});
}
}
});
},
),
),
],
),
),
);
}
/// Loads statistics for this player
Future<void> _loadData() async {
isLoading = true;
final fetchedMatches = await db.matchDao.getMatchesByPlayer(
playerId: _player.id,
);
final fetchedGroups = await db.groupDao.getGroupsByPlayer(
playerId: _player.id,
);
if (!mounted) return;
setState(() {
playerMatches = fetchedMatches;
totalMatches = fetchedMatches.length;
matchesWon = fetchedMatches
.where((match) => match.mvp.any((mvp) => mvp.id == _player.id))
.length;
playerGroups = fetchedGroups;
totalGroups = fetchedGroups.length;
isLoading = false;
});
}
/// Builds a single statistic row with a label and value
/// - [label]: The label of the statistic
/// - [value]: The value of the statistic
Widget _buildStatRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
label,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
],
),
Text(
value,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
);
}
bool isConfirmButtonEnabled() {
return nameController.text.trim().isNotEmpty;
}
}

View File

@@ -0,0 +1,311 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/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';
class StatisticsView extends StatefulWidget {
/// A view that displays player statistics
const StatisticsView({super.key});
@override
State<StatisticsView> createState() => _StatisticsViewState();
}
class _StatisticsViewState extends State<StatisticsView> {
int matchCount = 0;
int groupCount = 0;
List<(Player, int)> winCounts = List.filled(6, (
Player(name: 'Skeleton Player'),
1,
));
List<(Player, int)> matchCounts = List.filled(6, (
Player(name: 'Skeleton Player'),
1,
));
List<(Player, double)> winRates = List.filled(6, (
Player(name: 'Skeleton Player'),
1,
));
bool isLoading = true;
@override
void initState() {
super.initState();
loadStatisticData();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
child: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: constraints.maxWidth),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
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 &&
matchCounts.isEmpty &&
winRates.isEmpty,
replacement: Column(
children: [
StatisticsTile(
icon: Icons.sports_score,
title: loc.wins,
width: constraints.maxWidth * 0.95,
values: winCounts,
itemCount: 3,
barColor: Colors.green,
),
SizedBox(height: constraints.maxHeight * 0.02),
StatisticsTile(
icon: Icons.percent,
title: loc.winrate,
width: constraints.maxWidth * 0.95,
values: winRates,
itemCount: 5,
barColor: Colors.orange[700]!,
),
SizedBox(height: constraints.maxHeight * 0.02),
StatisticsTile(
icon: Icons.casino,
title: loc.amount_of_matches,
width: constraints.maxWidth * 0.95,
values: matchCounts,
itemCount: 10,
barColor: Colors.blue,
),
],
),
child: TopCenteredMessage(
icon: Icons.info,
title: loc.info,
message: AppLocalizations.of(
context,
).no_statistics_available,
),
),
SizedBox(height: MediaQuery.paddingOf(context).bottom),
],
),
),
),
);
},
);
}
/// Loads matches and players from the database
/// and calculates statistics for each player
void loadStatisticData() {
final db = Provider.of<AppDatabase>(context, listen: false);
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<Match>;
final players = results[1] as List<Player>;
matchCount = results[2] as int;
groupCount = results[3] as int;
winCounts = _calculateWinsForAllPlayers(
matches: matches,
players: players,
context: context,
);
matchCounts = _calculateMatchAmountsForAllPlayers(
matches: matches,
players: players,
context: context,
);
winRates = computeWinRatePercent(
winCounts: winCounts,
matchCounts: matchCounts,
);
setState(() {
isLoading = false;
});
});
}
/// Calculates the number of wins for each player
/// and returns a sorted list of tuples (playerName, winCount)
List<(Player, int)> _calculateWinsForAllPlayers({
required List<Match> matches,
required List<Player> players,
required BuildContext context,
}) {
List<(Player, int)> winCounts = [];
final loc = AppLocalizations.of(context);
// Getting the winners
for (var match in matches) {
final mvps = match.mvp;
for (var winner in mvps) {
final index = winCounts.indexWhere((entry) => entry.$1.id == winner.id);
// -1 means winner not found in winCounts
if (index != -1) {
final current = winCounts[index].$2;
winCounts[index] = (winner, current + 1);
} else {
winCounts.add((winner, 1));
}
}
}
// Adding all players with zero wins
for (var player in players) {
final index = winCounts.indexWhere((entry) => entry.$1.id == player.id);
// -1 means player not found in winCounts
if (index == -1) {
winCounts.add((player, 0));
}
}
// Replace player IDs with names
for (int i = 0; i < winCounts.length; i++) {
final playerId = winCounts[i].$1.id;
final player = players.firstWhere(
(p) => p.id == playerId,
orElse: () => Player(id: playerId, name: loc.not_available),
);
winCounts[i] = (player, winCounts[i].$2);
}
winCounts.sort((a, b) => b.$2.compareTo(a.$2));
return winCounts;
}
/// Calculates the number of matches played for each player
/// and returns a sorted list of tuples (playerName, matchCount)
List<(Player, int)> _calculateMatchAmountsForAllPlayers({
required List<Match> matches,
required List<Player> players,
required BuildContext context,
}) {
List<(Player, int)> matchCounts = [];
final loc = AppLocalizations.of(context);
// Counting matches for each player
for (var match in matches) {
for (Player player in match.players) {
// Check if the player is already in matchCounts
final index = matchCounts.indexWhere(
(entry) => entry.$1.id == player.id,
);
// -1 -> not found
if (index == -1) {
// Add new entry
matchCounts.add((player, 1));
} else {
// Update existing entry
final currentMatchAmount = matchCounts[index].$2;
matchCounts[index] = (player, currentMatchAmount + 1);
}
}
}
// Adding all players with zero matches
for (var player in players) {
final index = matchCounts.indexWhere((entry) => entry.$1.id == player.id);
// -1 means player not found in matchCounts
if (index == -1) {
matchCounts.add((player, 0));
}
}
// Replace player IDs with names
for (int i = 0; i < matchCounts.length; i++) {
final playerId = matchCounts[i].$1.id;
final player = players.firstWhere(
(p) => p.id == playerId,
orElse: () => Player(id: playerId, name: loc.not_available),
);
matchCounts[i] = (player, matchCounts[i].$2);
}
matchCounts.sort((a, b) => b.$2.compareTo(a.$2));
return matchCounts;
}
List<(Player, double)> computeWinRatePercent({
required List<(Player, int)> winCounts,
required List<(Player, int)> matchCounts,
}) {
final Map<Player, int> winsMap = {for (var e in winCounts) e.$1: e.$2};
final Map<Player, int> matchesMap = {for (var e in matchCounts) e.$1: e.$2};
// Get all unique player names
final player = {...matchesMap.keys};
// Calculate win rates
final result = player.map((name) {
final int w = winsMap[name] ?? 0;
final int m = matchesMap[name] ?? 0;
// Calculate percentage and round to 2 decimal places
// Avoid division by zero
final double percent = (m > 0)
? double.parse(((w / m)).toStringAsFixed(2))
: 0;
return (name, percent);
}).toList();
// Sort the result: first by winrate descending,
// then by wins descending in case of a tie
result.sort((a, b) {
final cmp = b.$2.compareTo(a.$2);
if (cmp != 0) return cmp;
final wa = winsMap[a.$1] ?? 0;
final wb = winsMap[b.$1] ?? 0;
return wb.compareTo(wa);
});
return result;
}
}

View File

@@ -1,635 +0,0 @@
import 'package:animated_custom_dropdown/custom_dropdown.dart';
import 'package:flutter/material.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/data/models/player.dart';
import 'package:tallee/data/models/statistic.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
class CreateStatisticView extends StatefulWidget {
const CreateStatisticView({super.key, required this.onStatisticCreated});
final void Function() onStatisticCreated;
@override
State<CreateStatisticView> createState() => _CreateStatisticViewState();
}
class _CreateStatisticViewState extends State<CreateStatisticView> {
bool isLoading = false;
/* Data loaded from the database */
List<Player> players = [];
List<Game> games = [];
List<Group> groups = [];
/* User selections */
StatisticType? selectedType;
List<StatisticScope> selectedScope = [];
List<Game> selectedGames = [];
List<Player> selectedPlayers = [];
List<Group> selectedGroups = [];
Timeframe? selectedTimeframe;
@override
void initState() {
loadAllData();
super.initState();
}
@override
Widget build(BuildContext context) {
var loc = AppLocalizations.of(context);
return ScaffoldMessenger(
child: Scaffold(
appBar: AppBar(title: Text(loc.create_statistic)),
body: Stack(
alignment: AlignmentDirectional.center,
children: [
SingleChildScrollView(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 80,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Classifier title
Padding(
padding: const EdgeInsetsGeometry.only(left: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.classifier,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Text(
'description',
textAlign: TextAlign.start,
style: TextStyle(
color: CustomTheme.textColor,
fontSize: 12,
),
softWrap: true,
),
],
),
),
// Classifier selection
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: CustomDropdown<StatisticType>(
closedHeaderPadding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
listItemBuilder:
(context, item, isSelected, onItemSelect) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
translateStatisticTypeToString(item, context),
style: itemStyle,
),
if (isSelected)
const Icon(
Icons.check,
color: CustomTheme.textColor,
),
],
),
headerBuilder: (context, selectedType, enabled) => Text(
translateStatisticTypeToString(selectedType, context),
style: headerStyle,
),
hintText: loc.select_a_classifier,
items: StatisticType.values,
decoration: decoration,
onChanged: (value) {
setState(() {
selectedType = value;
});
},
),
),
const SizedBox(height: 10),
// Scope title
Padding(
padding: const EdgeInsetsGeometry.only(left: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.scope,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Text(
'description',
textAlign: TextAlign.start,
style: TextStyle(
color: CustomTheme.textColor,
fontSize: 12,
),
),
],
),
),
// Scope selection
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: CustomDropdown<StatisticScope>.multiSelect(
closedHeaderPadding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
hintText: loc.select_a_scope,
items: StatisticScope.values,
decoration: decoration,
listItemBuilder:
(context, scope, isSelected, onItemSelect) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
translateScopeToString(scope, context),
style: itemStyle,
),
if (isSelected)
const Icon(
Icons.check,
color: CustomTheme.textColor,
),
],
),
headerListBuilder: (context, selectedItems, enabled) =>
Text(
selectedItems
.map((s) => translateScopeToString(s, context))
.join(', '),
style: headerStyle,
overflow: TextOverflow.ellipsis,
),
onListChanged: (List<StatisticScope> values) {
setState(() {
selectedScope = values;
});
},
),
),
if (selectedScope.contains(StatisticScope.selectedGames)) ...[
const SizedBox(height: 10),
// games title
Padding(
padding: const EdgeInsetsGeometry.only(left: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.games,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
loc.select_the_filtered_games,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
fontSize: 12,
),
),
],
),
),
// game selection
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: CustomDropdown<Game>.multiSelect(
enabled: !isLoading,
disabledDecoration: disabledDecoration,
closedHeaderPadding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
hintText: isLoading ? loc.loading : loc.select_a_game,
items: games,
decoration: decoration,
listItemBuilder:
(context, item, isSelected, onItemSelect) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Name
Text(item.name, style: itemStyle),
const SizedBox(width: 12),
// Ruleset
Text(
translateRulesetToString(
item.ruleset,
context,
),
style: hintStyle.copyWith(fontSize: 12),
),
],
),
// Check icon
if (isSelected)
const Icon(
Icons.check,
color: CustomTheme.textColor,
),
],
),
headerListBuilder: (context, selectedItems, enabled) =>
Text(
selectedItems.map((g) => g.name).join(', '),
style: const TextStyle(
color: CustomTheme.textColor,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
onListChanged: (List<Game> values) {
setState(() {
selectedGames = values;
});
},
),
),
],
if (selectedScope.contains(
StatisticScope.selectedGroups,
)) ...[
const SizedBox(height: 10),
// groups title
Padding(
padding: const EdgeInsetsGeometry.only(left: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.groups,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
loc.select_the_filtered_groups,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
fontSize: 12,
),
),
],
),
),
// groups selection
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: CustomDropdown<Group>.multiSelect(
enabled: !isLoading,
disabledDecoration: disabledDecoration,
closedHeaderPadding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
hintText: isLoading ? loc.loading : loc.select_a_group,
items: groups,
decoration: decoration,
listItemBuilder:
(context, item, isSelected, onItemSelect) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Name
Text(item.name, style: itemStyle),
const SizedBox(width: 12),
// Ruleset
Text(
' ${item.members.length.toString()} ${loc.members}',
style: hintStyle.copyWith(fontSize: 12),
),
],
),
if (isSelected)
const Icon(
Icons.check,
color: CustomTheme.textColor,
),
],
),
headerListBuilder: (context, selectedItems, enabled) =>
Text(
selectedItems.map((g) => g.name).join(', '),
style: headerStyle,
overflow: TextOverflow.ellipsis,
),
onListChanged: (List<Group> groups) {
setState(() {
selectedGroups = groups;
});
},
),
),
],
if (selectedScope.contains(StatisticScope.timeframe)) ...[
const SizedBox(height: 10),
// timeframe title
Padding(
padding: const EdgeInsetsGeometry.only(left: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.timeframe,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
loc.select_the_filtered_timeframe,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
fontSize: 12,
),
),
],
),
),
// groups selection
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: CustomDropdown<Timeframe>(
enabled: !isLoading,
excludeSelected: false,
disabledDecoration: disabledDecoration,
closedHeaderPadding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
hintText: isLoading
? loc.loading
: loc.select_a_timeframe,
items: Timeframe.values,
decoration: decoration,
listItemBuilder:
(context, timeframe, isSelected, onItemSelect) =>
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
translateTimeframeToString(
timeframe,
context,
),
style: itemStyle,
),
if (isSelected)
const Icon(
Icons.check,
color: CustomTheme.textColor,
),
],
),
headerBuilder: (context, selectedTimeframe, enabled) =>
Text(
translateTimeframeToString(
selectedTimeframe,
context,
),
style: headerStyle,
overflow: TextOverflow.ellipsis,
),
onChanged: (Timeframe? timeframe) {
setState(() {
selectedTimeframe = timeframe;
});
},
),
),
],
],
),
),
// Create statistic button
Positioned(
bottom: MediaQuery.of(context).padding.bottom,
child: AnimatedDialogButton(
buttonConstraints: const BoxConstraints(minWidth: 350),
buttonText: loc.create_statistic,
onPressed: selectedType != null && selectedScope.isNotEmpty
? () => submitStatistic()
: null,
),
),
],
),
),
);
}
CustomDropdownDecoration get decoration => CustomDropdownDecoration(
listItemDecoration: const ListItemDecoration(
selectedIconBorder: BorderSide(color: CustomTheme.primaryColor, width: 1),
selectedIconColor: CustomTheme.primaryColor,
highlightColor: CustomTheme.secondaryColor,
splashColor: Colors.transparent,
selectedColor: CustomTheme.onBoxColor,
),
listItemStyle: itemStyle,
headerStyle: headerStyle,
hintStyle: hintStyle,
closedFillColor: CustomTheme.boxColor,
closedBorder: Border.all(color: CustomTheme.boxBorderColor, width: 1),
expandedFillColor: CustomTheme.boxColor,
expandedBorder: Border.all(color: CustomTheme.boxBorderColor, width: 1),
);
CustomDropdownDisabledDecoration get disabledDecoration =>
CustomDropdownDisabledDecoration(
fillColor: CustomTheme.boxColor.withAlpha(125),
border: Border.all(
color: CustomTheme.boxBorderColor.withAlpha(125),
width: 1,
),
headerStyle: disabledHeaderStyle,
hintStyle: disabledHintStyle,
);
TextStyle get headerStyle => const TextStyle(
color: CustomTheme.textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
);
TextStyle get itemStyle =>
const TextStyle(color: CustomTheme.textColor, fontSize: 14);
TextStyle get hintStyle =>
const TextStyle(color: CustomTheme.hintColor, fontSize: 14);
TextStyle get disabledHeaderStyle => const TextStyle(
color: CustomTheme.hintColor,
fontSize: 14,
fontWeight: FontWeight.bold,
);
TextStyle get disabledHintStyle =>
const TextStyle(color: CustomTheme.hintColor, fontSize: 14);
Future<void> loadAllData() async {
isLoading = true;
final db = Provider.of<AppDatabase>(context, listen: false);
Future.wait([
db.playerDao.getAllPlayers(),
db.groupDao.getAllGroups(),
db.gameDao.getAllGames(),
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
])
.then((results) async {
players = results[0];
groups = results[1];
games = results[2];
isLoading = false;
})
.catchError((error) {
print('Error loading data: $error');
});
}
void submitStatistic() {
final newStatistic = Statistic(
type: selectedType!,
scopes: selectedScope,
timeframe: selectedTimeframe,
selectedGroups: selectedGroups,
selectedGames: selectedGames,
);
final db = Provider.of<AppDatabase>(context, listen: false);
db.statisticDao.addStatistic(statistic: newStatistic);
Navigator.of(context).pop(newStatistic);
}
}
String translateTimeframeToString(Timeframe timeframe, BuildContext context) {
final loc = AppLocalizations.of(context);
switch (timeframe) {
case Timeframe.last7Days:
return loc.last_7_days;
case Timeframe.last30Days:
return loc.last_30_days;
case Timeframe.last90Days:
return loc.last_90_days;
case Timeframe.last180Days:
return loc.last_180_days;
case Timeframe.lastYear:
return loc.last_year;
case Timeframe.allTime:
return loc.all_time;
}
}
String translateScopeToString(StatisticScope scope, BuildContext context) {
final loc = AppLocalizations.of(context);
switch (scope) {
case StatisticScope.allPlayers:
return loc.all_players;
case StatisticScope.selectedGroups:
return loc.selected_groups;
case StatisticScope.selectedGames:
return loc.selected_games;
case StatisticScope.timeframe:
return loc.timeframe;
}
}
String translateStatisticTypeToString(
StatisticType type,
BuildContext context,
) {
final loc = AppLocalizations.of(context);
switch (type) {
case StatisticType.totalMatches:
return loc.total_matches;
case StatisticType.totalWins:
return loc.total_wins;
case StatisticType.totalScore:
return loc.total_score;
case StatisticType.totalLosses:
return loc.total_losses;
case StatisticType.averageScore:
return loc.average_score;
case StatisticType.bestScore:
return loc.best_score;
case StatisticType.worstScore:
return loc.worst_score;
case StatisticType.winrate:
return loc.winrate;
}
}

View File

@@ -1,191 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/statistic.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart';
class StatisticDetailView extends StatefulWidget {
const StatisticDetailView({
super.key,
required this.statistic,
required this.values,
required this.icon,
required this.barColor,
});
final Statistic statistic;
final List<(Player, num)> values;
final IconData icon;
final Color barColor;
@override
State<StatisticDetailView> createState() => _StatisticDetailViewState();
}
class _StatisticDetailViewState extends State<StatisticDetailView> {
late int displayCount;
@override
void initState() {
super.initState();
displayCount = widget.statistic.displayCount;
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
final title = translateStatisticTypeToString(
widget.statistic.type,
context,
);
const style = TextStyle(fontWeight: FontWeight.bold);
return Scaffold(
appBar: AppBar(
title: Text(title),
leading: HapticIconButton(
icon: const Icon(Icons.arrow_back_ios_new),
onPressed: () => handleBack(context),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
StatisticsTile(
icon: widget.icon,
title: title,
width: MediaQuery.sizeOf(context).width * 0.95,
values: widget.values,
barColor: widget.barColor,
selectedGroups: widget.statistic.selectedGroups,
selectedGames: widget.statistic.selectedGames,
displayCount: displayCount,
showAllValues: true,
),
const SizedBox(height: 12),
InfoTile(
icon: Icons.filter_alt,
title: loc.filter,
content: Column(
spacing: 12,
children: [
// Scopes
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(loc.scope, style: style),
Text(
widget.statistic.scopes
.map(
(scope) => translateScopeToString(scope, context),
)
.join('\n'),
textAlign: TextAlign.end,
),
],
),
// Timeframe
if (widget.statistic.timeframe != null)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(loc.timeframe, style: style),
Text(
translateTimeframeToString(
widget.statistic.timeframe!,
context,
),
textAlign: TextAlign.end,
),
],
),
// Groups
if (widget.statistic.selectedGroups != null)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(loc.groups, style: style),
Text(
widget.statistic.selectedGroups!
.map((group) => group.name)
.join('\n'),
textAlign: TextAlign.end,
),
],
),
// Games
if (widget.statistic.selectedGames != null)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(loc.games, style: style),
Text(
widget.statistic.selectedGames!
.map((game) => game.name)
.join('\n'),
textAlign: TextAlign.end,
),
],
),
if (widget.values.isNotEmpty)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(loc.displayed_entries, style: style),
Row(
children: [
HapticIconButton(
icon: const Icon(Icons.remove),
onPressed: displayCount <= 1
? null
: () => setState(() => displayCount -= 1),
),
SizedBox(
width: 30,
child: Text(
'$displayCount',
textAlign: TextAlign.center,
),
),
HapticIconButton(
icon: const Icon(Icons.add),
onPressed: displayCount >= widget.values.length
? null
: () => setState(() => displayCount += 1),
),
],
),
],
),
],
),
),
],
),
),
);
}
// Handles saving the display count and giving it to statistics view
Future<void> handleBack(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false);
await db.statisticDao.updateDisplayCount(widget.statistic.id, displayCount);
if (context.mounted) Navigator.of(context).pop(displayCount);
}
}

View File

@@ -1,322 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:tallee/core/common.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/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/statistic.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart'
show translateStatisticTypeToString;
import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart';
List<Color> _colorPalette = AppColor.values
.map((c) => getColorFromAppColor(c))
.toList();
/// Returns the icon for the given statistic type.
IconData getStatisticIconForType(StatisticType type) =>
_getStatisticIcon(type: type);
/// Returns a color from the palette based on the statistic's ID.
Color getStatisticColorForStatistic(Statistic stat) => _getStatisticColor(stat);
/// Computes the statistic values for a given [Statistic].
List<(Player, num)> computeStatisticValues({
required Statistic statistic,
required List<Match> matches,
required List<Player> players,
}) {
final filteredMatches = _getFilterMatches(statistic, matches);
final filteredPlayers = _getFilteredPlayers(
statistic,
players,
filteredMatches,
);
return _computeValuesForType(
type: statistic.type,
matches: filteredMatches,
players: filteredPlayers,
);
}
/// Build the [StatisticsTile] for a given [Statistic].
Widget buildStatisticTile({
required Statistic statistic,
required List<Match> matches,
required List<Player> players,
required BuildContext context,
double? width,
}) {
final values = computeStatisticValues(
statistic: statistic,
matches: matches,
players: players,
);
return StatisticsTile(
icon: _getStatisticIcon(type: statistic.type),
title: translateStatisticTypeToString(statistic.type, context),
width: width ?? MediaQuery.sizeOf(context).width * 0.95,
values: values,
barColor: _getStatisticColor(statistic),
displayCount: statistic.displayCount,
selectedGroups: statistic.selectedGroups,
selectedGames: statistic.selectedGames,
);
}
List<Match> _getFilterMatches(Statistic statistic, List<Match> matches) {
List<Match> filteredMatches = matches;
// Filter timeframe
if (statistic.scopes.contains(StatisticScope.timeframe) &&
statistic.timeframe != null) {
final minDate = _getMinimumDate(timeframe: statistic.timeframe!);
print(
'Filtering matches by timeframe: ${statistic.timeframe}, minDate: $minDate',
);
if (minDate != null) {
filteredMatches = matches
.where((m) => m.endedAt != null && m.endedAt!.isAfter(minDate))
.toList();
}
}
// Filter games
if (statistic.scopes.contains(StatisticScope.selectedGames) &&
(statistic.selectedGames?.isNotEmpty ?? false)) {
final gameIds = statistic.selectedGames!.map((g) => g.id).toSet();
filteredMatches = filteredMatches
.where((match) => gameIds.contains(match.game.id))
.toList();
}
// Filter groups
if (statistic.scopes.contains(StatisticScope.selectedGroups) &&
(statistic.selectedGroups?.isNotEmpty ?? false)) {
final groupIds = statistic.selectedGroups!.map((g) => g.id).toSet();
filteredMatches = filteredMatches
.where((m) => m.group != null && groupIds.contains(m.group!.id))
.toList();
}
return filteredMatches;
}
/// Returns a [Player] List with the selected players depending on
List<Player> _getFilteredPlayers(
Statistic statistic,
List<Player> allPlayers,
List<Match> filteredMatches,
) {
// allPlayers
if (statistic.scopes.contains(StatisticScope.allPlayers)) {
return allPlayers;
}
// selectedGroups -> only members
if (statistic.scopes.contains(StatisticScope.selectedGroups) &&
(statistic.selectedGroups?.isNotEmpty ?? false)) {
final Set<String> ids = {
for (final g in statistic.selectedGroups!)
for (final p in g.members) p.id,
};
return allPlayers.where((p) => ids.contains(p.id)).toList();
}
// Else -> all players from filtered matches
final Set<String> ids = {
for (final m in filteredMatches)
for (final p in m.players) p.id,
};
return allPlayers.where((p) => ids.contains(p.id)).toList();
}
/// Returns a [DateTime] with the minimum time and date the [timeframe] allows
DateTime? _getMinimumDate({required Timeframe timeframe}) {
final now = DateTime.now();
switch (timeframe) {
case Timeframe.last7Days:
return now.subtract(const Duration(days: 7));
case Timeframe.last30Days:
return now.subtract(const Duration(days: 30));
case Timeframe.last90Days:
return now.subtract(const Duration(days: 90));
case Timeframe.last180Days:
return now.subtract(const Duration(days: 180));
case Timeframe.lastYear:
return now.subtract(const Duration(days: 365));
case Timeframe.allTime:
return null;
}
}
/// Computes the statistic values for each player based on the statistic type
/// and returns a list of (Player, value) tuples sorted descending by value.
List<(Player, num)> _computeValuesForType({
required StatisticType type,
required List<Match> matches,
required List<Player> players,
}) {
switch (type) {
case StatisticType.totalMatches:
return _sortDesc(
players.map((p) => (p, _matchesPlayed(p, matches) as num)).toList(),
);
case StatisticType.totalWins:
return _sortDesc(
players.map((p) => (p, _wins(p, matches) as num)).toList(),
);
case StatisticType.totalLosses:
return _sortDesc(
players
.map(
(p) =>
(p, (_matchesPlayed(p, matches) - _wins(p, matches)) as num),
)
.toList(),
);
case StatisticType.totalScore:
return _sortDesc(
players.map((p) => (p, _totalScore(p, matches) as num)).toList(),
);
case StatisticType.averageScore:
return _sortDesc(
players.map((p) {
final scores = _scoresOf(p, matches);
final avg = scores.isEmpty
? 0.0
: double.parse(
(scores.reduce((a, b) => a + b) / scores.length)
.toStringAsFixed(2),
);
return (p, avg as num);
}).toList(),
);
case StatisticType.bestScore:
return _sortDesc(
players.map((p) {
final scores = _scoresOf(p, matches);
final best = scores.isEmpty ? 0 : scores.reduce(max);
return (p, best as num);
}).toList(),
);
case StatisticType.worstScore:
// Ascending here is more meaningful for "worst", but keep the
// existing tile semantics (largest bar = top entry) by sorting
// descending on the inverse — i.e. show smallest score on top.
final entries = players.map((p) {
final scores = _scoresOf(p, matches);
final worst = scores.isEmpty ? 0 : scores.reduce(min);
return (p, worst as num);
}).toList();
entries.sort((a, b) => a.$2.compareTo(b.$2));
return entries;
case StatisticType.winrate:
return _sortDesc(
players.map((p) {
final played = _matchesPlayed(p, matches);
final wins = _wins(p, matches);
final rate = played == 0
? 0.0
: double.parse((wins / played).toStringAsFixed(2));
return (p, rate as num);
}).toList(),
);
}
}
/* Helper functions for different statistic types */
/// Detemerines how many matches the player has played in the given list of matches.
int _matchesPlayed(Player p, List<Match> matches) =>
matches.where((m) => m.players.any((mp) => mp.id == p.id)).length;
/// Determines how many matches the player has won in the given list of matches.
int _wins(Player p, List<Match> matches) =>
matches.where((m) => m.mvp.any((mp) => mp.id == p.id)).length;
/// Determines the total score of the player in the given list of matches.
int _totalScore(Player p, List<Match> matches) {
var total = 0;
for (final m in matches) {
final s = m.scores[p.id];
if (s != null) total += s.score;
}
return total;
}
/// Returns a list of all scores the player has achieved in the given list of matches.
List<int> _scoresOf(Player p, List<Match> matches) => [
for (final m in matches)
if (m.scores[p.id] != null) m.scores[p.id]!.score,
];
/// Returns the list of entries sorted descending by the statistic value.
List<(Player, num)> _sortDesc(List<(Player, num)> entries) {
entries.sort((a, b) => b.$2.compareTo(a.$2));
return entries;
}
/* Icon and color */
/// Returns the icon for the given statistic type.
IconData _getStatisticIcon({required StatisticType type}) {
switch (type) {
case StatisticType.totalMatches:
return Icons.casino;
case StatisticType.totalWins:
return Icons.emoji_events;
case StatisticType.totalLosses:
return Icons.sentiment_dissatisfied;
case StatisticType.totalScore:
return Icons.scoreboard;
case StatisticType.averageScore:
return Icons.show_chart;
case StatisticType.bestScore:
return Icons.trending_up;
case StatisticType.worstScore:
return Icons.trending_down;
case StatisticType.winrate:
return Icons.percent;
}
}
/// Returns a color from the palette based on the statistic's ID as random seed.
Color _getStatisticColor(Statistic stat) {
final seed = stat.id.hashCode;
return _colorPalette[seed.abs() % _colorPalette.length];
}
/* Skeleton data */
/// A placeholder tile with mock data for the loading state.
Widget buildSkeletonStatisticTile({required BuildContext context}) {
final count = 4 + Random().nextInt(5); // 4..8
final values = <(Player, num)>[
for (var i = 0; i < count; i++)
(Player(name: 'Player ${i + 1}'), count - i),
];
return StatisticsTile(
icon: Icons.bar_chart,
title: 'Skeleton title',
width: MediaQuery.sizeOf(context).width * 0.95,
values: values,
barColor: _colorPalette[Random().nextInt(_colorPalette.length)],
selectedGames: [Game(name: 'Game 1', ruleset: Ruleset.highestScore)],
selectedGroups: [Group(name: 'Group 1', members: [])],
displayCount: 5,
);
}

View File

@@ -1,190 +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/data/db/database.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/statistic.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_detail_view.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart';
class StatisticsView extends StatefulWidget {
/// A view that displays player statistics
const StatisticsView({super.key});
@override
State<StatisticsView> createState() => _StatisticsViewState();
}
class _StatisticsViewState extends State<StatisticsView> {
bool isLoading = true;
List<Match> _allMatches = const [];
List<Player> _allPlayers = const [];
List<Statistic> _statistics = const [];
List<Widget> statisticTiles = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
loadStatistics(context);
});
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Stack(
alignment: AlignmentDirectional.bottomCenter,
fit: StackFit.expand,
children: [
Visibility(
visible: statisticTiles.isNotEmpty,
replacement: Center(
child: TopCenteredMessage(
icon: Icons.info,
title: loc.info,
message: loc.no_statistics_created_yet,
),
),
child: SingleChildScrollView(
child: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
child: Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
...statisticTiles,
SizedBox(
height: MediaQuery.paddingOf(context).bottom + 80,
),
],
),
),
),
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom + 20,
child: MainMenuButton(
text: loc.create_statistic,
icon: Icons.bar_chart,
onPressed: () async {
Statistic newStatistic = await Navigator.push(
context,
adaptivePageRoute(
builder: (context) => CreateStatisticView(
onStatisticCreated: () => loadStatistics(context),
),
),
);
if (!context.mounted) return;
setState(() {
_statistics = [..._statistics, newStatistic];
statisticTiles = _statistics
.map((stat) => _buildStatisticTile(context, stat))
.toList();
});
},
),
),
],
);
},
);
}
Future<void> loadStatistics(BuildContext context) async {
setState(() {
isLoading = true;
statisticTiles = List.generate(
4,
(index) => Column(
children: [
buildSkeletonStatisticTile(context: context),
const SizedBox(height: 12),
],
),
);
});
final db = Provider.of<AppDatabase>(context, listen: false);
final results = await Future.wait([
db.statisticDao.getAllStatistics(),
db.matchDao.getAllMatches(),
db.playerDao.getAllPlayers(),
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
]);
if (!mounted) return;
final statistics = results[0] as List<Statistic>;
_allMatches = results[1] as List<Match>;
_allPlayers = results[2] as List<Player>;
_statistics = statistics;
setState(() {
statisticTiles = _statistics
.map((stat) => _buildStatisticTile(context, stat))
.toList();
isLoading = false;
});
}
Widget _buildStatisticTile(BuildContext context, Statistic statistic) {
final values = computeStatisticValues(
statistic: statistic,
matches: _allMatches,
players: _allPlayers,
);
return GestureDetector(
onTap: () async {
final newDisplayCount = await Navigator.push(
context,
adaptivePageRoute(
builder: (context) => StatisticDetailView(
statistic: statistic,
values: values,
icon: getStatisticIconForType(statistic.type),
barColor: getStatisticColorForStatistic(statistic),
),
),
);
if (newDisplayCount != null &&
newDisplayCount != statistic.displayCount) {
setState(() {
_statistics = _statistics
.map(
(stat) => stat.id == statistic.id
? stat.copyWith(displayCount: newDisplayCount)
: stat,
)
.toList();
statisticTiles = _statistics
.map((stat) => _buildStatisticTile(context, stat))
.toList();
});
}
},
child: buildStatisticTile(
statistic: statistic,
matches: _allMatches,
players: _allPlayers,
context: context,
),
);
}
}

View File

@@ -6,13 +6,11 @@ class AppSkeleton extends StatefulWidget {
/// - [child]: The widget tree to apply the skeleton effect to. /// - [child]: The widget tree to apply the skeleton effect to.
/// - [enabled]: A boolean to enable or disable the skeleton effect. /// - [enabled]: A boolean to enable or disable the skeleton effect.
/// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher. /// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher.
/// - [alignment]: The alignment used for the custom layout builder and optional Align wrapper. Defaults to [Alignment.center].
const AppSkeleton({ const AppSkeleton({
super.key, super.key,
required this.child, required this.child,
this.enabled = true, this.enabled = true,
this.fixLayoutBuilder = false, this.fixLayoutBuilder = false,
this.alignment = Alignment.center,
}); });
/// The widget tree to apply the skeleton effect to. /// The widget tree to apply the skeleton effect to.
@@ -24,9 +22,6 @@ class AppSkeleton extends StatefulWidget {
/// A boolean to fix the layout builder for AnimatedSwitcher. /// A boolean to fix the layout builder for AnimatedSwitcher.
final bool fixLayoutBuilder; final bool fixLayoutBuilder;
/// The alignment used for the custom layout builder and optional Align wrapper
final Alignment alignment;
@override @override
State<AppSkeleton> createState() => _AppSkeletonState(); State<AppSkeleton> createState() => _AppSkeletonState();
} }
@@ -50,14 +45,13 @@ class _AppSkeletonState extends State<AppSkeleton> {
layoutBuilder: !widget.fixLayoutBuilder layoutBuilder: !widget.fixLayoutBuilder
? AnimatedSwitcher.defaultLayoutBuilder ? AnimatedSwitcher.defaultLayoutBuilder
: (Widget? currentChild, List<Widget> previousChildren) { : (Widget? currentChild, List<Widget> previousChildren) {
final children = <Widget>[...previousChildren]; return Stack(
if (currentChild != null) children.add(currentChild); alignment: Alignment.topCenter,
return Stack(alignment: widget.alignment, children: children); children: [...previousChildren, ?currentChild],
);
}, },
), ),
child: widget.fixLayoutBuilder child: widget.child,
? Align(alignment: widget.alignment, child: widget.child)
: widget.child,
); );
} }
} }

View File

@@ -11,7 +11,7 @@ class AnimatedDialogButton extends StatefulWidget {
const AnimatedDialogButton({ const AnimatedDialogButton({
super.key, super.key,
required this.buttonText, required this.buttonText,
this.onPressed, required this.onPressed,
this.buttonConstraints, this.buttonConstraints,
this.buttonType = ButtonType.primary, this.buttonType = ButtonType.primary,
this.isDescructive = false, this.isDescructive = false,
@@ -19,7 +19,7 @@ class AnimatedDialogButton extends StatefulWidget {
final String buttonText; final String buttonText;
final VoidCallback? onPressed; final VoidCallback onPressed;
final BoxConstraints? buttonConstraints; final BoxConstraints? buttonConstraints;
@@ -38,13 +38,8 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textStyling = _getTextStyling(); final textStyling = _getTextStyling();
final buttonDecoration = _getButtonDecoration(); final buttonDecoration = _getButtonDecoration();
bool isDisabled = widget.onPressed == null;
return IgnorePointer( return GestureDetector(
ignoring: isDisabled,
child: Opacity(
opacity: isDisabled ? 0.5 : 1.0,
child: GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true), onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false), onTapUp: (_) => setState(() => _isPressed = false),
onTapCancel: () => setState(() => _isPressed = false), onTapCancel: () => setState(() => _isPressed = false),
@@ -59,10 +54,7 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
child: Container( child: Container(
constraints: widget.buttonConstraints, constraints: widget.buttonConstraints,
decoration: buttonDecoration, decoration: buttonDecoration,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
horizontal: 16,
vertical: 12,
),
margin: const EdgeInsets.symmetric(vertical: 8), margin: const EdgeInsets.symmetric(vertical: 8),
child: Text( child: Text(
widget.buttonText, widget.buttonText,
@@ -73,8 +65,6 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
), ),
), ),
), ),
),
),
); );
} }

View File

@@ -19,6 +19,7 @@ class CustomAlertDialog extends StatelessWidget {
final String title; final String title;
final Widget content; final Widget content;
final List<CustomDialogAction> actions; final List<CustomDialogAction> actions;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(

View File

@@ -10,7 +10,7 @@ class CustomDialogAction extends StatelessWidget {
/// - [onPressed]: Callback function that is triggered when the button is pressed. /// - [onPressed]: Callback function that is triggered when the button is pressed.
const CustomDialogAction({ const CustomDialogAction({
super.key, super.key,
this.onPressed, required this.onPressed,
required this.text, required this.text,
this.buttonType = ButtonType.primary, this.buttonType = ButtonType.primary,
this.isDestructive = false, this.isDestructive = false,
@@ -20,18 +20,17 @@ class CustomDialogAction extends StatelessWidget {
final ButtonType buttonType; final ButtonType buttonType;
final VoidCallback? onPressed; final VoidCallback onPressed;
final bool isDestructive; final bool isDestructive;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedDialogButton( return AnimatedDialogButton(
onPressed: onPressed != null onPressed: () async {
? () async {
await HapticFeedback.selectionClick(); await HapticFeedback.selectionClick();
onPressed?.call(); onPressed.call();
} },
: null,
buttonText: text, buttonText: text,
buttonType: buttonType, buttonType: buttonType,
isDescructive: isDestructive, isDescructive: isDestructive,

View File

@@ -12,16 +12,17 @@ class GameLabel extends StatelessWidget {
final String title; final String title;
final String description; final String description;
final AppColor color; final GameColor color;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final backgroundColor = getColorFromAppColor(color); final backgroundColor = getColorFromGameColor(color);
final fontColor = backgroundColor.computeLuminance() > 0.5 final fontColor = backgroundColor.computeLuminance() > 0.5
? Colors.black ? Colors.black
: Colors.white; : Colors.white;
return Row( return IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Title // Title
@@ -36,9 +37,6 @@ class GameLabel extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Text( child: Text(
title, title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: fontColor, color: fontColor,
@@ -48,8 +46,7 @@ class GameLabel extends StatelessWidget {
), ),
// Description // Description
Flexible( Container(
child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor.withAlpha(140), color: backgroundColor.withAlpha(140),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@@ -60,9 +57,6 @@ class GameLabel extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Text( child: Text(
description, description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: fontColor, color: fontColor,
@@ -70,8 +64,8 @@ class GameLabel extends StatelessWidget {
), ),
), ),
), ),
),
], ],
),
); );
} }
} }

View File

@@ -26,7 +26,6 @@ class PlayerSelection extends StatefulWidget {
this.availablePlayers, this.availablePlayers,
this.initialSelectedPlayers, this.initialSelectedPlayers,
required this.onChanged, required this.onChanged,
this.onPlayerCreated,
}); });
/// An optional list of players to choose from. If null, all players from the database are used. /// An optional list of players to choose from. If null, all players from the database are used.
@@ -38,9 +37,6 @@ class PlayerSelection extends StatefulWidget {
/// A callback function that is invoked whenever the selection changes, /// A callback function that is invoked whenever the selection changes,
final Function(List<Player> value) onChanged; final Function(List<Player> value) onChanged;
/// A callback function that is invoked when a player was created in this widget
final VoidCallback? onPlayerCreated;
@override @override
State<PlayerSelection> createState() => _PlayerSelectionState(); State<PlayerSelection> createState() => _PlayerSelectionState();
} }
@@ -327,7 +323,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
/// Updates the state after successfully adding a new player. /// Updates the state after successfully adding a new player.
void _handleSuccessfulPlayerCreation(Player player) { void _handleSuccessfulPlayerCreation(Player player) {
widget.onPlayerCreated?.call();
selectedPlayers.insert(0, player); selectedPlayers.insert(0, player);
widget.onChanged([...selectedPlayers]); widget.onChanged([...selectedPlayers]);
allPlayers.add(player); allPlayers.add(player);

View File

@@ -51,7 +51,7 @@ class GameTile extends StatelessWidget {
? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white) ? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white)
: Colors.white; : Colors.white;
final gameColor = badgeColor ?? getColorFromAppColor(AppColor.orange); final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange);
return GestureDetector( return GestureDetector(
onTap: () async { onTap: () async {

View File

@@ -1,10 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class GroupTile extends StatefulWidget { class GroupTile extends StatefulWidget {
@@ -17,7 +15,6 @@ class GroupTile extends StatefulWidget {
required this.group, required this.group,
this.isHighlighted = false, this.isHighlighted = false,
this.onTap, this.onTap,
this.onPlayerChanged,
}); });
/// The group data to be displayed. /// The group data to be displayed.
@@ -29,9 +26,6 @@ class GroupTile extends StatefulWidget {
/// Callback function to be executed when the tile is tapped. /// Callback function to be executed when the tile is tapped.
final VoidCallback? onTap; final VoidCallback? onTap;
/// Callback function to be executed when the players in the group are changed.
final VoidCallback? onPlayerChanged;
@override @override
State<GroupTile> createState() => _GroupTileState(); State<GroupTile> createState() => _GroupTileState();
} }
@@ -98,19 +92,6 @@ class _GroupTileState extends State<GroupTile> {
text: member.name, text: member.name,
suffixText: getNameCountText(member), suffixText: getNameCountText(member),
iconEnabled: false, iconEnabled: false,
onTileTap: () {
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => PlayerDetailView(
player: member,
callback: () {
widget.onPlayerChanged?.call();
},
),
),
);
},
), ),
], ],
), ),

View File

@@ -3,13 +3,11 @@ import 'dart:core' hide Match;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/game_label.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -26,7 +24,6 @@ class MatchTile extends StatefulWidget {
required this.onTap, required this.onTap,
this.width, this.width,
this.compact = false, this.compact = false,
this.onPlayerEdited,
}); });
/// The match data to be displayed. /// The match data to be displayed.
@@ -35,9 +32,6 @@ class MatchTile extends StatefulWidget {
/// The callback invoked when the tile is tapped. /// The callback invoked when the tile is tapped.
final VoidCallback onTap; final VoidCallback onTap;
/// The callback invoked when the players are edited
final VoidCallback? onPlayerEdited;
/// Optional width for the tile. /// Optional width for the tile.
final double? width; final double? width;
@@ -230,19 +224,6 @@ class _MatchTileState extends State<MatchTile> {
text: player.name, text: player.name,
suffixText: getNameCountText(player), suffixText: getNameCountText(player),
iconEnabled: false, iconEnabled: false,
onTileTap: () {
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => PlayerDetailView(
player: player,
callback: () {
widget.onPlayerEdited?.call();
},
),
),
);
},
); );
}).toList(), }).toList(),
), ),

View File

@@ -1,12 +1,8 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttericon/rpg_awesome_icons.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
@@ -25,11 +21,8 @@ class StatisticsTile extends StatelessWidget {
required this.title, required this.title,
required this.width, required this.width,
required this.values, required this.values,
required this.itemCount,
required this.barColor, required this.barColor,
required this.displayCount,
this.selectedGroups,
this.selectedGames,
this.showAllValues = false,
}); });
/// The icon displayed next to the title. /// The icon displayed next to the title.
@@ -44,16 +37,12 @@ class StatisticsTile extends StatelessWidget {
/// A list of tuples containing labels and their corresponding numeric values. /// A list of tuples containing labels and their corresponding numeric values.
final List<(Player, num)> values; final List<(Player, num)> values;
/// The maximum number of items to display.
final int itemCount;
/// The color of the bars representing the values. /// The color of the bars representing the values.
final Color barColor; final Color barColor;
// statistic data
final int displayCount;
final List<Group>? selectedGroups;
final List<Game>? selectedGames;
final bool showAllValues;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
@@ -63,115 +52,66 @@ class StatisticsTile extends StatelessWidget {
title: title, title: title,
icon: icon, icon: icon,
content: Padding( content: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Visibility( child: Visibility(
visible: values.isNotEmpty, visible: values.isNotEmpty,
// No data avaiable message
replacement: Center( replacement: Center(
heightFactor: 4, heightFactor: 4,
child: Text(loc.no_data_available), child: Text(loc.no_data_available),
), ),
// Bar chart
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final maxBarWidth = constraints.maxWidth * 0.8; final maxBarWidth = constraints.maxWidth * 0.65;
// If displayCount wasnt provided, take all values
final valuesShown = showAllValues
? values.length
: min(values.length, displayCount);
final displayValues = values.take(valuesShown).toList();
final maxVal = displayValues.isNotEmpty
? displayValues.fold<num>(
0,
(currentMax, entry) =>
entry.$2 > currentMax ? entry.$2 : currentMax,
)
: 0;
return Column( return Column(
children: [ children: List.generate(min(values.length, itemCount), (index) {
// Bars /// The maximum wins among all players
...List.generate(valuesShown, (index) { final maxMatches = values.isNotEmpty ? values[0].$2 : 0;
/// Fraction of wins /// Fraction of wins
final double fraction = (maxVal > 0) final double fraction = (maxMatches > 0)
? (displayValues[index].$2 / maxVal) ? (values[index].$2 / maxMatches)
: 0.0; : 0.0;
/// Calculated width for current the bar /// Calculated width for current the bar
final double barWidth = (maxBarWidth * fraction).clamp( final double barWidth = maxBarWidth * fraction;
0.0,
maxBarWidth,
);
final barClr = index >= displayCount
? barColor.withAlpha(150)
: barColor;
var textClr = barColor.computeLuminance() > 0.5
? const Color(0xFF101010)
: CustomTheme.textColor;
textClr = textClr.withAlpha(
index >= displayCount ? 220 : 255,
);
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0), padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
SizedBox( Stack(
width: maxBarWidth,
child: Stack(
clipBehavior: Clip.hardEdge,
children: [ children: [
// Bar
Container( Container(
height: 24, height: 24,
width: barWidth, width: barWidth,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
color: barClr, color: barColor,
), ),
), ),
// Player
Padding( Padding(
padding: const EdgeInsets.only(left: 4.0), padding: const EdgeInsets.only(left: 4.0),
child: RichText( child: RichText(
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
text: TextSpan( text: TextSpan(
style: DefaultTextStyle.of(context).style, style: DefaultTextStyle.of(context).style,
children: [ children: [
TextSpan( TextSpan(
text: displayValues[index].$1.name, text: values[index].$1.name,
style: TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: textClr,
), ),
), ),
TextSpan( TextSpan(
text: getNameCountText( text: getNameCountText(values[index].$1),
displayValues[index].$1,
),
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: color: CustomTheme.textColor.withAlpha(
(barColor == 150,
getColorFromAppColor( ),
AppColor.yellow,
)
? const Color(
0xFF101010,
)
: CustomTheme.textColor)
.withAlpha(150),
), ),
), ),
], ],
@@ -180,16 +120,12 @@ class StatisticsTile extends StatelessWidget {
), ),
], ],
), ),
),
const Spacer(), const Spacer(),
// Value
Center( Center(
child: Text( child: Text(
displayValues[index].$2 <= 1 && values[index].$2 <= 1 && values[index].$2 is double
displayValues[index].$2 is double ? values[index].$2.toStringAsFixed(2)
? displayValues[index].$2.toStringAsFixed(2) : values[index].$2.toString(),
: displayValues[index].$2.toString(),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
@@ -201,64 +137,6 @@ class StatisticsTile extends StatelessWidget {
), ),
); );
}), }),
// Group & Game info
if (hasGame || hasGroup)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4,
runSpacing: 4,
children: [
// Game
if (hasGroup)
Row(
spacing: 8,
children: [
const Icon(
RpgAwesome.clovers_card,
color: CustomTheme.hintColor,
size: 20,
),
Text(
getGameText(selectedGames!),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: CustomTheme.hintColor,
),
overflow: TextOverflow.ellipsis,
),
],
),
if (hasGroup && hasGame) const SizedBox(width: 20),
// Group
if (hasGroup)
Row(
spacing: 8,
children: [
const Icon(
Icons.groups,
color: CustomTheme.hintColor,
),
Text(
getGroupText(selectedGroups!),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: CustomTheme.hintColor,
),
overflow: TextOverflow.ellipsis,
),
],
),
],
),
),
],
); );
}, },
), ),
@@ -266,24 +144,4 @@ class StatisticsTile extends StatelessWidget {
), ),
); );
} }
String getGroupText(List<Group> groups) {
var text = groups[0].name;
if (groups.length > 1) {
return '$text + ${groups.length - 1}';
}
return text;
}
String getGameText(List<Game> games) {
var text = games[0].name;
if (games.length > 1) {
return '$text + ${games.length - 1}';
}
return text;
}
bool get hasGroup => selectedGroups != null && selectedGroups!.isNotEmpty;
bool get hasGame => selectedGames != null && selectedGames!.isNotEmpty;
} }

View File

@@ -12,7 +12,6 @@ class TextIconTile extends StatelessWidget {
this.suffixText = '', this.suffixText = '',
this.iconEnabled = true, this.iconEnabled = true,
this.onIconTap, this.onIconTap,
this.onTileTap,
}); });
/// The text to display in the tile. /// The text to display in the tile.
@@ -26,14 +25,9 @@ class TextIconTile extends StatelessWidget {
/// The callback to be invoked when the icon is tapped. /// The callback to be invoked when the icon is tapped.
final VoidCallback? onIconTap; final VoidCallback? onIconTap;
/// The callback to be invoked when the tile is tapped.
final VoidCallback? onTileTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return Container(
onTap: onTileTap,
child: Container(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: CustomTheme.onBoxColor, color: CustomTheme.onBoxColor,
@@ -78,7 +72,6 @@ class TextIconTile extends StatelessWidget {
], ],
], ],
), ),
),
); );
} }
} }

View File

@@ -20,7 +20,6 @@ class DataTransferService {
static Future<void> deleteAllData(BuildContext context) async { static Future<void> deleteAllData(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
await db.statisticDao.deleteAllStatistics();
await db.matchDao.deleteAllMatches(); await db.matchDao.deleteAllMatches();
await db.teamDao.deleteAllTeams(); await db.teamDao.deleteAllTeams();
await db.groupDao.deleteAllGroups(); await db.groupDao.deleteAllGroups();
@@ -279,7 +278,7 @@ class DataTransferService {
name: 'Unknown', name: 'Unknown',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: '', description: '',
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
); );
} }

View File

@@ -17,14 +17,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.1" version: "10.0.1"
animated_custom_dropdown:
dependency: "direct main"
description:
name: animated_custom_dropdown
sha256: "5a72dc209041bb53f6c7164bc2e366552d5197cdb032b1c9b2c36e3013024486"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
arb_utils: arb_utils:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -93,10 +85,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: build_runner name: build_runner
sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.1" version: "2.15.0"
built_collection: built_collection:
dependency: transitive dependency: transitive
description: description:
@@ -185,14 +177,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
url: "https://pub.dev"
source: hosted
version: "4.11.1"
collection: collection:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -341,34 +325,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: drift name: drift
sha256: "970cd188fddb111b26ea6a9b07a62bf5c2432d74147b8122c67044ae3b97e99e" sha256: "8033500116b24398fba0cca0369cc31678cd627c01e41753a61186911cea743e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.31.0" version: "2.33.0"
drift_dev: drift_dev:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: drift_dev name: drift_dev
sha256: "917184b2fb867b70a548a83bf0d36268423b38d39968c06cce4905683da49587" sha256: b3dd5b75e30522a91da8abda9f5bb17230cb038097f6d15fa75d42bb563428aa
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.31.0" version: "2.33.0"
drift_flutter: drift_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: drift_flutter name: drift_flutter
sha256: c07120854742a0cae2f7501a0da02493addde550db6641d284983c08762e60a7 sha256: "887fdec622174dc7eaefd0048403e34ee07cc18626ac8a7544cc3b8a4a172166"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.8" version: "0.3.0"
dropdown_flutter:
dependency: "direct main"
description:
name: dropdown_flutter
sha256: "5ae3d05d768d0bb6030ff735e6b4b93f7b29be3cf3bec7c86cd4f444c8f067ff"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
equatable: equatable:
dependency: transitive dependency: transitive
description: description:
@@ -413,10 +389,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_saver name: file_saver
sha256: "9d93db09bd4da9e43238f9dd485360fc51a5c138eea5ef5f407ec56e58079ac0" sha256: "68c9a085d9bb4546e0a31d1e583a48d7c17a6987d538788ea064f0043b1fc02d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.1" version: "0.4.0"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@@ -1138,30 +1114,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.2" version: "1.10.2"
sqlcipher_flutter_libs:
dependency: transitive
description:
name: sqlcipher_flutter_libs
sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929"
url: "https://pub.dev"
source: hosted
version: "0.7.0+eol"
sqlite3: sqlite3:
dependency: transitive dependency: transitive
description: description:
name: sqlite3 name: sqlite3
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.9.4" version: "3.3.1"
sqlite3_flutter_libs: sqlite3_flutter_libs:
dependency: transitive dependency: transitive
description: description:
name: sqlite3_flutter_libs name: sqlite3_flutter_libs
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.42" version: "0.6.0+eol"
sqlparser: sqlparser:
dependency: transitive dependency: transitive
description: description:
name: sqlparser name: sqlparser
sha256: "337e9997f7141ffdd054259128553c348635fa318f7ca492f07a4ab76f850d19" sha256: ecdc06d4a7d79dcbc928d99afd2f7f5b0f98a637c46f89be83d911617f759978
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.43.1" version: "0.44.4"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -1443,5 +1427,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.10.3 <4.0.0" dart: ">=3.12.0 <4.0.0"
flutter: ">=3.38.4" flutter: ">=3.41.0"

View File

@@ -7,10 +7,8 @@ environment:
sdk: ^3.12.0 sdk: ^3.12.0
dependencies: dependencies:
animated_custom_dropdown: ^3.1.1
clock: ^1.1.2 clock: ^1.1.2
collection: ^1.19.1 collection: ^1.19.1
dropdown_flutter: ^1.0.3
cupertino_icons: ^1.0.9 cupertino_icons: ^1.0.9
drift: ^2.33.0 drift: ^2.33.0
drift_flutter: ^0.3.0 drift_flutter: ^0.3.0

View File

@@ -194,31 +194,6 @@ void main() {
expect(allGroups, isEmpty); expect(allGroups, isEmpty);
}); });
test('getGroupsByPlayer() works correctly', () async {
await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup2],
);
final groups = await database.groupDao.getGroupsByPlayer(
playerId: testPlayer2.id,
);
expect(groups, hasLength(2));
expect(groups.any((group) => group.id == testGroup1.id), isTrue);
expect(groups.any((group) => group.id == testGroup2.id), isTrue);
});
test(
'getGroupsByPlayer() returns empty list for non-existent player',
() async {
final groups = await database.groupDao.getGroupsByPlayer(
playerId: 'non-existent-player-id',
);
expect(groups, isEmpty);
},
);
test('addGroupsAsList() with duplicate groups only adds once', () async { test('addGroupsAsList() with duplicate groups only adds once', () async {
await database.groupDao.addGroupsAsList( await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup1, testGroup1], groups: [testGroup1, testGroup1, testGroup1],

View File

@@ -56,7 +56,7 @@ void main() {
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A test game', description: 'A test game',
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(
@@ -260,34 +260,6 @@ void main() {
expect(match.group!.id, testGroup1.id); expect(match.group!.id, testGroup1.id);
}); });
test('getMatchesByPlayer() works correctly', () async {
await database.matchDao.addMatchesAsList(
matches: [testMatch1, testMatch2],
);
final matches = await database.matchDao.getMatchesByPlayer(
playerId: testPlayer1.id,
);
expect(matches, hasLength(1));
expect(matches.first.id, testMatch2.id);
expect(
matches.first.players.any((p) => p.id == testPlayer1.id),
isTrue,
);
});
test(
'getMatchesByPlayer() returns empty list for non-existent player',
() async {
final matches = await database.matchDao.getMatchesByPlayer(
playerId: 'non-existing-player-id',
);
expect(matches, isEmpty);
},
);
test('getMatchCount() works correctly', () async { test('getMatchCount() works correctly', () async {
var count = await database.matchDao.getMatchCount(); var count = await database.matchDao.getMatchCount();
expect(count, 0); expect(count, 0);

View File

@@ -49,7 +49,7 @@ void main() {
testGame = Game( testGame = Game(
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.highestScore, ruleset: Ruleset.highestScore,
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(

View File

@@ -28,7 +28,7 @@ void main() {
name: 'Chess', name: 'Chess',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A classic strategy game', description: 'A classic strategy game',
color: AppColor.blue, color: GameColor.blue,
icon: 'chess_icon', icon: 'chess_icon',
); );
testGame2 = Game( testGame2 = Game(
@@ -36,7 +36,7 @@ void main() {
name: 'Poker', name: 'Poker',
ruleset: Ruleset.multipleWinners, ruleset: Ruleset.multipleWinners,
description: 'Card game with multiple winners', description: 'Card game with multiple winners',
color: AppColor.red, color: GameColor.red,
icon: 'poker_icon', icon: 'poker_icon',
); );
testGame3 = Game( testGame3 = Game(
@@ -44,7 +44,7 @@ void main() {
name: 'Monopoly', name: 'Monopoly',
ruleset: Ruleset.highestScore, ruleset: Ruleset.highestScore,
description: 'A board game about real estate', description: 'A board game about real estate',
color: AppColor.orange, color: GameColor.orange,
icon: '', icon: '',
); );
}); });
@@ -124,7 +124,7 @@ void main() {
name: 'Game\'s & "Special" <Name>', name: 'Game\'s & "Special" <Name>',
ruleset: Ruleset.multipleWinners, ruleset: Ruleset.multipleWinners,
description: 'Description with émojis 🎮🎲', description: 'Description with émojis 🎮🎲',
color: AppColor.purple, color: GameColor.purple,
icon: '', icon: '',
); );
await database.gameDao.addGame(game: specialGame); await database.gameDao.addGame(game: specialGame);
@@ -280,19 +280,19 @@ void main() {
await database.gameDao.updateGameColor( await database.gameDao.updateGameColor(
gameId: testGame1.id, gameId: testGame1.id,
color: AppColor.green, color: GameColor.green,
); );
final updatedGame = await database.gameDao.getGameById( final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id, gameId: testGame1.id,
); );
expect(updatedGame.color, AppColor.green); expect(updatedGame.color, GameColor.green);
}); });
test('updateGameColor() does nothing for non-existent game', () async { test('updateGameColor() does nothing for non-existent game', () async {
final updated = await database.gameDao.updateGameColor( final updated = await database.gameDao.updateGameColor(
gameId: 'non-existent-id', gameId: 'non-existent-id',
color: AppColor.green, color: GameColor.green,
); );
expect(updated, isFalse); expect(updated, isFalse);
@@ -336,7 +336,7 @@ void main() {
name: newName, name: newName,
); );
const newGameColor = AppColor.teal; const newGameColor = GameColor.teal;
await database.gameDao.updateGameColor( await database.gameDao.updateGameColor(
gameId: testGame1.id, gameId: testGame1.id,
color: newGameColor, color: newGameColor,

View File

@@ -233,95 +233,6 @@ void main() {
expect(allPlayers, isEmpty); expect(allPlayers, isEmpty);
}); });
test('updatePlayerName() sets correct nameCount with 2 player', () async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer2);
final newName = testPlayer1.name;
await database.playerDao.updatePlayerName(
playerId: testPlayer2.id,
name: newName,
);
var player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 1);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 2);
await database.playerDao.updatePlayerName(
playerId: testPlayer1.id,
name: 'different name',
);
player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 0);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 0);
});
test('updatePlayerName() sets correct nameCount with 3 player', () async {
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2, testPlayer3],
);
// Changing both names to player 1's name
final newName = testPlayer1.name;
await database.playerDao.updatePlayerName(
playerId: testPlayer2.id,
name: newName,
);
await database.playerDao.updatePlayerName(
playerId: testPlayer3.id,
name: newName,
);
var player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 1);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 2);
player = await database.playerDao.getPlayerById(
playerId: testPlayer3.id,
);
expect(player.nameCount, 3);
// Changing the middle players name
await database.playerDao.updatePlayerName(
playerId: testPlayer2.id,
name: 'different name',
);
player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 1);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 0);
player = await database.playerDao.getPlayerById(
playerId: testPlayer3.id,
);
expect(player.nameCount, 2);
});
test('updatePlayerDescription() works correctly', () async { test('updatePlayerDescription() works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
@@ -461,22 +372,14 @@ void main() {
final player1 = Player(name: testPlayer1.name, description: ''); final player1 = Player(name: testPlayer1.name, description: '');
await database.playerDao.addPlayer(player: player1); await database.playerDao.addPlayer(player: player1);
final player2 = Player(name: testPlayer1.name, description: '');
await database.playerDao.addPlayer(player: player2);
var players = await database.playerDao.getAllPlayers(); var players = await database.playerDao.getAllPlayers();
expect(players.length, 3); expect(players.length, 2);
players.sort((a, b) => a.nameCount.compareTo(b.nameCount)); players.sort((a, b) => a.nameCount.compareTo(b.nameCount));
for (int i = 0; i < players.length - 1; i++) { for (int i = 0; i < players.length - 1; i++) {
expect(players[i].nameCount, i + 1); expect(players[i].nameCount, i + 1);
} }
// ids are correct in the right order
expect(players[0].id, testPlayer1.id);
expect(players[1].id, player1.id);
expect(players[2].id, player2.id);
}, },
); );
@@ -501,62 +404,24 @@ void main() {
for (int i = 0; i < players.length - 1; i++) { for (int i = 0; i < players.length - 1; i++) {
expect(players[i].nameCount, i + 1); expect(players[i].nameCount, i + 1);
} }
// ids are correct in the right order
expect(players[0].id, testPlayer1.id);
expect(players[1].id, player1.id);
expect(players[2].id, player2.id);
expect(players[3].id, player3.id);
}, },
); );
test('getNameCount works correctly', () async { test('getNameCount works correctly', () async {
final player1 = Player(name: testPlayer1.name);
final player2 = Player(name: testPlayer1.name); final player2 = Player(name: testPlayer1.name);
final player3 = Player(name: testPlayer1.name);
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayersAsList(
players: [testPlayer1, player2, player3],
var nameCount = await database.playerDao.getNameCount(
name: testPlayer1.name,
); );
expect(nameCount, 1); final nameCount = await database.playerDao.getNameCount(
await database.playerDao.addPlayersAsList(players: [player1, player2]);
nameCount = await database.playerDao.getNameCount(
name: testPlayer1.name, name: testPlayer1.name,
); );
expect(nameCount, 3); expect(nameCount, 3);
}); });
test('calculateNameCount works correctly', () async {
final player1 = Player(name: testPlayer1.name);
final player2 = Player(name: testPlayer1.name);
// Case 1: No existing players with the name
var nameCount = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(nameCount, 0);
// Case 2: One existing player with the name. Should return 2 for
// the new player
await database.playerDao.addPlayer(player: testPlayer1);
nameCount = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(nameCount, 2);
// Case 3: Multiple existing players with the name. Should return count + 1
await database.playerDao.addPlayersAsList(players: [player1, player2]);
nameCount = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(nameCount, 4);
});
test('updateNameCount works correctly', () async { test('updateNameCount works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
@@ -576,24 +441,14 @@ void main() {
final player2 = Player(name: testPlayer1.name, description: ''); final player2 = Player(name: testPlayer1.name, description: '');
final player3 = Player(name: testPlayer1.name, description: ''); final player3 = Player(name: testPlayer1.name, description: '');
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayersAsList(
var player = await database.playerDao.getPlayerWithHighestNameCount( players: [testPlayer1, player2, player3],
name: testPlayer1.name,
); );
expect(player, isNotNull);
expect(player!.nameCount, 0);
await database.playerDao.addPlayer(player: player2); final player = await database.playerDao.getPlayerWithHighestNameCount(
player = await database.playerDao.getPlayerWithHighestNameCount(
name: testPlayer1.name, name: testPlayer1.name,
); );
expect(player, isNotNull);
expect(player!.nameCount, 2);
await database.playerDao.addPlayer(player: player3);
player = await database.playerDao.getPlayerWithHighestNameCount(
name: testPlayer1.name,
);
expect(player, isNotNull); expect(player, isNotNull);
expect(player!.nameCount, 3); expect(player!.nameCount, 3);
}); });
@@ -605,6 +460,32 @@ void main() {
expect(player, isNull); expect(player, isNull);
}); });
test('calculateNameCount works correctly', () async {
// Case 1: No existing players with the name
var count = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(count, 0);
// Case 2: One existing player with the name. Should update that
// player's nameCount to 1 and return 2 for the new player
await database.playerDao.addPlayer(player: testPlayer1);
count = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(count, 2);
// Case 3: Multiple existing players with the name.
final player2 = Player(name: testPlayer1.name, nameCount: count);
await database.playerDao.addPlayer(player: player2);
count = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(count, 3);
});
test('getPlayerWithHighestNameCount with non existing player', () async { test('getPlayerWithHighestNameCount with non existing player', () async {
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.initializeNameCount(name: testPlayer1.name); await database.playerDao.initializeNameCount(name: testPlayer1.name);

View File

@@ -42,7 +42,7 @@ void main() {
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A test game', description: 'A test game',
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(

View File

@@ -1,122 +0,0 @@
import 'dart:core';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' hide isNotNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/statistic.dart';
void main() {
late AppDatabase database;
late Player testPlayer1;
late Player testPlayer2;
late Player testPlayer3;
late Player testPlayer4;
late Player testPlayer5;
late Group testGroup1;
late Group testGroup2;
late Game testGame;
/*late Match testMatch1;
late Match testMatch2;
late Match testMatchOnlyPlayers;
late Match testMatchOnlyGroup;*/
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
final fakeClock = Clock(() => fixedDate);
setUp(() async {
database = AppDatabase(
DatabaseConnection(
NativeDatabase.memory(),
// Recommended for widget tests to avoid test errors.
closeStreamsSynchronously: true,
),
);
withClock(fakeClock, () {
testPlayer1 = Player(name: 'Alice');
testPlayer2 = Player(name: 'Bob');
testPlayer3 = Player(name: 'Charlie');
testPlayer4 = Player(name: 'Diana');
testPlayer5 = Player(name: 'Eve');
testGroup1 = Group(
name: 'Test Group 1',
description: '',
members: [testPlayer1, testPlayer2, testPlayer3],
);
testGroup2 = Group(
name: 'Test Group 2',
description: '',
members: [testPlayer4, testPlayer5],
);
testGame = Game(
name: 'Test Game',
ruleset: Ruleset.singleWinner,
description: 'A test game',
color: AppColor.blue,
icon: '',
);
/*testMatch1 = Match(
name: 'First Test Match',
game: testGame,
group: testGroup1,
players: [testPlayer4, testPlayer5],
scores: {testPlayer4.id: ScoreEntry(score: 1)},
);
testMatch2 = Match(
name: 'Second Test Match',
game: testGame,
group: testGroup2,
players: [testPlayer1, testPlayer2, testPlayer3],
);
testMatchOnlyPlayers = Match(
name: 'Test Match with Players',
game: testGame,
players: [testPlayer1, testPlayer2, testPlayer3],
);
testMatchOnlyGroup = Match(
name: 'Test Match with Group',
game: testGame,
group: testGroup2,
players: testGroup2.members,
);*/
});
await database.playerDao.addPlayersAsList(
players: [
testPlayer1,
testPlayer2,
testPlayer3,
testPlayer4,
testPlayer5,
],
);
await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]);
await database.gameDao.addGame(game: testGame);
});
tearDown(() async {
await database.close();
});
test('Adding/fetching statistic works correclty', () async {
final statistic = Statistic(
type: StatisticType.averageScore,
scopes: [StatisticScope.allPlayers, StatisticScope.selectedGames],
timeframe: Timeframe.allTime,
selectedGames: [testGame],
selectedGroups: [testGroup1],
);
final added = await database.statisticDao.addStatistic(
statistic: statistic,
);
expect(added, isTrue);
final fetched = await database.statisticDao.getStatisticById(statistic.id);
expect(fetched, isNotNull);
expect(fetched!.type, statistic.type);
});
}

View File

@@ -40,7 +40,7 @@ void main() {
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A test game', description: 'A test game',
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(

View File

@@ -45,7 +45,7 @@ void main() {
name: 'Chess', name: 'Chess',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'Strategic board game', description: 'Strategic board game',
color: AppColor.blue, color: GameColor.blue,
icon: 'chess_icon', icon: 'chess_icon',
); );
@@ -448,19 +448,19 @@ void main() {
Game( Game(
name: 'Red Game', name: 'Red Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: AppColor.red, color: GameColor.red,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Blue Game', name: 'Blue Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: AppColor.blue, color: GameColor.blue,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Green Game', name: 'Green Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: AppColor.green, color: GameColor.green,
icon: 'icon', icon: 'icon',
), ),
]; ];
@@ -484,19 +484,19 @@ void main() {
Game( Game(
name: 'Highest Score Game', name: 'Highest Score Game',
ruleset: Ruleset.highestScore, ruleset: Ruleset.highestScore,
color: AppColor.blue, color: GameColor.blue,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Lowest Score Game', name: 'Lowest Score Game',
ruleset: Ruleset.lowestScore, ruleset: Ruleset.lowestScore,
color: AppColor.blue, color: GameColor.blue,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Single Winner', name: 'Single Winner',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: AppColor.blue, color: GameColor.blue,
icon: 'icon', icon: 'icon',
), ),
]; ];