Compare commits
1 Commits
developmen
...
feature/19
| Author | SHA1 | Date | |
|---|---|---|---|
| eaf7822732 |
@@ -24,48 +24,48 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Translates a [GameColor] enum value to its corresponding localized string.
|
/// Translates a [AppColor] enum value to its corresponding localized string.
|
||||||
String translateGameColorToString(GameColor color, BuildContext context) {
|
String translateAppColorToString(AppColor color, BuildContext context) {
|
||||||
final loc = AppLocalizations.of(context);
|
final loc = AppLocalizations.of(context);
|
||||||
switch (color) {
|
switch (color) {
|
||||||
case GameColor.red:
|
case AppColor.red:
|
||||||
return loc.color_red;
|
return loc.color_red;
|
||||||
case GameColor.blue:
|
case AppColor.blue:
|
||||||
return loc.color_blue;
|
return loc.color_blue;
|
||||||
case GameColor.green:
|
case AppColor.green:
|
||||||
return loc.color_green;
|
return loc.color_green;
|
||||||
case GameColor.yellow:
|
case AppColor.yellow:
|
||||||
return loc.color_yellow;
|
return loc.color_yellow;
|
||||||
case GameColor.purple:
|
case AppColor.purple:
|
||||||
return loc.color_purple;
|
return loc.color_purple;
|
||||||
case GameColor.orange:
|
case AppColor.orange:
|
||||||
return loc.color_orange;
|
return loc.color_orange;
|
||||||
case GameColor.pink:
|
case AppColor.pink:
|
||||||
return loc.color_pink;
|
return loc.color_pink;
|
||||||
case GameColor.teal:
|
case AppColor.teal:
|
||||||
return loc.color_teal;
|
return loc.color_teal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the [Color] object corresponding to a [GameColor] enum value.
|
/// Returns the [Color] object corresponding to a [AppColor] enum value.
|
||||||
Color getColorFromGameColor(GameColor color) {
|
Color getColorFromAppColor(AppColor color) {
|
||||||
switch (color) {
|
switch (color) {
|
||||||
case GameColor.red:
|
case AppColor.red:
|
||||||
return Colors.red;
|
return Colors.red;
|
||||||
case GameColor.blue:
|
case AppColor.blue:
|
||||||
return Colors.blue;
|
return Colors.blue;
|
||||||
case GameColor.green:
|
case AppColor.green:
|
||||||
return Colors.green;
|
return Colors.green;
|
||||||
case GameColor.yellow:
|
case AppColor.yellow:
|
||||||
return const Color(0xFFF7CA28);
|
return const Color(0xFFF7CA28);
|
||||||
case GameColor.purple:
|
case AppColor.purple:
|
||||||
return Colors.purple;
|
return Colors.purple;
|
||||||
case GameColor.orange:
|
case AppColor.orange:
|
||||||
return const Color(0xFFef681f);
|
return const Color(0xFFef681f);
|
||||||
case GameColor.pink:
|
case AppColor.pink:
|
||||||
return Colors.pink;
|
return const Color(0xFFE91E63);
|
||||||
case GameColor.teal:
|
case AppColor.teal:
|
||||||
return Colors.teal;
|
return const Color(0xFF00BCD4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,4 +43,32 @@ enum Ruleset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Different colors for highlighting games
|
/// Different colors for highlighting games
|
||||||
enum GameColor { red, orange, yellow, green, teal, blue, purple, pink }
|
enum AppColor { 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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 result = await query.getSingleOrNull();
|
final row = await query.getSingleOrNull();
|
||||||
return result != null;
|
return row != 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: GameColor.values.firstWhere((e) => e.name == row.color),
|
color: AppColor.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 result = await query.getSingle();
|
final row = await query.getSingle();
|
||||||
return Game(
|
return Game(
|
||||||
id: result.id,
|
id: row.id,
|
||||||
name: result.name,
|
name: row.name,
|
||||||
ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset),
|
ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset),
|
||||||
description: result.description,
|
description: row.description,
|
||||||
color: GameColor.values.firstWhere((e) => e.name == result.color),
|
color: AppColor.values.firstWhere((e) => e.name == row.color),
|
||||||
icon: result.icon,
|
icon: row.icon,
|
||||||
createdAt: result.createdAt,
|
createdAt: row.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((g) => g.id.equals(gameId))).write(
|
await (update(gameTable)..where((tbl) => tbl.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((g) => g.id.equals(gameId))).write(
|
await (update(gameTable)..where((tbl) => tbl.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((g) => g.id.equals(gameId))).write(
|
await (update(gameTable)..where((tbl) => tbl.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 GameColor color,
|
required AppColor color,
|
||||||
}) async {
|
}) async {
|
||||||
final rowsAffected =
|
final rowsAffected =
|
||||||
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
|
await (update(gameTable)..where((tbl) => tbl.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((g) => g.id.equals(gameId))).write(
|
await (update(gameTable)..where((tbl) => tbl.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((g) => g.id.equals(gameId));
|
final query = delete(gameTable)..where((tbl) => tbl.id.equals(gameId));
|
||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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((groupData) async {
|
result.map((row) async {
|
||||||
final members = await db.playerGroupDao.getPlayersOfGroup(
|
final members = await db.playerGroupDao.getPlayersOfGroup(
|
||||||
groupId: groupData.id,
|
groupId: row.id,
|
||||||
);
|
);
|
||||||
return Group(
|
return Group(
|
||||||
id: groupData.id,
|
id: row.id,
|
||||||
name: groupData.name,
|
name: row.name,
|
||||||
description: groupData.description,
|
description: row.description,
|
||||||
members: members,
|
members: members,
|
||||||
createdAt: groupData.createdAt,
|
createdAt: row.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 result = await query.getSingle();
|
final row = 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: result.id,
|
id: row.id,
|
||||||
name: result.name,
|
name: row.name,
|
||||||
description: result.description,
|
description: row.description,
|
||||||
members: members,
|
members: members,
|
||||||
createdAt: result.createdAt,
|
createdAt: row.createdAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,17 +180,49 @@ 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((row) => row.read(groupTable.id.count()))
|
.map((tbl) => tbl.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 result = await query.getSingleOrNull();
|
final row = await query.getSingleOrNull();
|
||||||
return result != null;
|
return row != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Delete */
|
/* Delete */
|
||||||
@@ -220,9 +252,8 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
|||||||
required String name,
|
required String name,
|
||||||
}) async {
|
}) async {
|
||||||
final rowsAffected =
|
final rowsAffected =
|
||||||
await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
|
await (update(groupTable)..where((tbl) => tbl.id.equals(groupId)))
|
||||||
GroupTableCompanion(name: Value(name)),
|
.write(GroupTableCompanion(name: Value(name)));
|
||||||
);
|
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,9 +264,8 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
|||||||
required String description,
|
required String description,
|
||||||
}) async {
|
}) async {
|
||||||
final rowsAffected =
|
final rowsAffected =
|
||||||
await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
|
await (update(groupTable)..where((tbl) => tbl.id.equals(groupId)))
|
||||||
GroupTableCompanion(description: Value(description)),
|
.write(GroupTableCompanion(description: Value(description)));
|
||||||
);
|
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 result = await query.getSingleOrNull();
|
final row = await query.getSingleOrNull();
|
||||||
return result != null;
|
return row != 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((row) => row.read(matchTable.id.count()))
|
.map((tbl) => tbl.read(matchTable.id.count()))
|
||||||
.getSingle();
|
.getSingle();
|
||||||
return count ?? 0;
|
return count ?? 0;
|
||||||
}
|
}
|
||||||
@@ -279,10 +279,12 @@ 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,
|
||||||
);
|
);
|
||||||
@@ -312,13 +314,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 result = await query.getSingle();
|
final row = await query.getSingle();
|
||||||
|
|
||||||
final game = await db.gameDao.getGameById(gameId: result.gameId);
|
final game = await db.gameDao.getGameById(gameId: row.gameId);
|
||||||
|
|
||||||
Group? group;
|
Group? group;
|
||||||
if (result.groupId != null) {
|
if (row.groupId != null) {
|
||||||
group = await db.groupDao.getGroupById(groupId: result.groupId!);
|
group = await db.groupDao.getGroupById(groupId: row.groupId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
|
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
|
||||||
@@ -328,15 +330,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: result.id,
|
id: row.id,
|
||||||
name: result.name,
|
name: row.name,
|
||||||
game: game,
|
game: game,
|
||||||
group: group,
|
group: group,
|
||||||
players: players,
|
players: players,
|
||||||
teams: teams.isEmpty ? null : teams,
|
teams: teams.isEmpty ? null : teams,
|
||||||
notes: result.notes,
|
notes: row.notes,
|
||||||
createdAt: result.createdAt,
|
createdAt: row.createdAt,
|
||||||
endedAt: result.endedAt,
|
endedAt: row.endedAt,
|
||||||
scores: scores,
|
scores: scores,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -347,25 +349,73 @@ 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((row) => row.read(matchTable.id.count()))
|
.map((tbl) => tbl.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 rows = await query.get();
|
final result = await query.get();
|
||||||
|
|
||||||
return Future.wait(
|
return Future.wait(
|
||||||
rows.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);
|
||||||
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,
|
||||||
@@ -385,7 +435,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((pm) => pm.matchId.equals(matchId) & pm.teamId.isNotNull());
|
..where((tbl) => tbl.matchId.equals(matchId) & tbl.teamId.isNotNull());
|
||||||
final playerMatches = await playerMatchQuery.get();
|
final playerMatches = await playerMatchQuery.get();
|
||||||
|
|
||||||
if (playerMatches.isEmpty) return [];
|
if (playerMatches.isEmpty) return [];
|
||||||
@@ -412,7 +462,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((g) => g.id.equals(matchId));
|
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||||
final rowsAffected = await query.write(
|
final rowsAffected = await query.write(
|
||||||
MatchTableCompanion(name: Value(name)),
|
MatchTableCompanion(name: Value(name)),
|
||||||
);
|
);
|
||||||
@@ -427,7 +477,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((g) => g.id.equals(matchId));
|
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||||
final rowsAffected = await query.write(
|
final rowsAffected = await query.write(
|
||||||
MatchTableCompanion(groupId: Value(groupId)),
|
MatchTableCompanion(groupId: Value(groupId)),
|
||||||
);
|
);
|
||||||
@@ -440,7 +490,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((g) => g.id.equals(matchId));
|
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||||
final rowsAffected = await query.write(
|
final rowsAffected = await query.write(
|
||||||
MatchTableCompanion(notes: Value(notes)),
|
MatchTableCompanion(notes: Value(notes)),
|
||||||
);
|
);
|
||||||
@@ -451,7 +501,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((g) => g.id.equals(matchId));
|
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||||
final rowsAffected = await query.write(
|
final rowsAffected = await query.write(
|
||||||
const MatchTableCompanion(groupId: Value(null)),
|
const MatchTableCompanion(groupId: Value(null)),
|
||||||
);
|
);
|
||||||
@@ -465,7 +515,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((g) => g.id.equals(matchId));
|
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||||
final rowsAffected = await query.write(
|
final rowsAffected = await query.write(
|
||||||
MatchTableCompanion(endedAt: Value(endedAt)),
|
MatchTableCompanion(endedAt: Value(endedAt)),
|
||||||
);
|
);
|
||||||
@@ -477,7 +527,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((g) => g.id.equals(matchId));
|
final query = delete(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
@@ -493,7 +543,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((m) => m.gameId.equals(gameId));
|
final query = delete(matchTable)..where((tbl) => tbl.gameId.equals(gameId));
|
||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected;
|
return rowsAffected;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 calculateNameCount(name: player.name);
|
final int nameCount = await _processNameCount(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 calculateNameCount(name: name);
|
var nameCount = await _processNameCount(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((row) => row.read(playerTable.id.count()))
|
.map((tbl) => tbl.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 result = await query.getSingleOrNull();
|
final row = await query.getSingleOrNull();
|
||||||
return result != null;
|
return row != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves all players from the database.
|
/// Retrieves all players from the database.
|
||||||
@@ -146,57 +146,76 @@ 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 result = await query.getSingle();
|
final row = await query.getSingle();
|
||||||
return Player(
|
return Player(
|
||||||
id: result.id,
|
id: row.id,
|
||||||
name: result.name,
|
name: row.name,
|
||||||
description: result.description,
|
description: row.description,
|
||||||
createdAt: result.createdAt,
|
createdAt: row.createdAt,
|
||||||
nameCount: result.nameCount,
|
nameCount: row.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 {
|
||||||
// Get previous name and name count for the player before updating
|
return transaction(() async {
|
||||||
final previousPlayerName =
|
final previousPlayer = await (select(
|
||||||
await (select(playerTable)..where((p) => p.id.equals(playerId)))
|
playerTable,
|
||||||
.map((row) => row.name)
|
)..where((tbl) => tbl.id.equals(playerId))).getSingleOrNull();
|
||||||
.getSingleOrNull() ??
|
if (previousPlayer == null) return false;
|
||||||
'';
|
|
||||||
final previousNameCount = await getNameCount(name: previousPlayerName);
|
|
||||||
|
|
||||||
final rowsAffected =
|
final previousName = previousPlayer.name;
|
||||||
await (update(playerTable)..where((p) => p.id.equals(playerId))).write(
|
final previousCount = previousPlayer.nameCount;
|
||||||
PlayerTableCompanion(name: Value(name)),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update name count for the new name
|
// Determine the nameCount for the renamed player in the new group.
|
||||||
final count = await calculateNameCount(name: name);
|
final newNameCount = await _processNameCount(name: name);
|
||||||
if (count > 0) {
|
|
||||||
await (update(playerTable)..where((p) => p.name.equals(name))).write(
|
|
||||||
PlayerTableCompanion(nameCount: Value(count)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousNameCount > 0) {
|
final rowsAffected =
|
||||||
// Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount
|
await (update(
|
||||||
final player = await getPlayerWithHighestNameCount(
|
playerTable,
|
||||||
name: previousPlayerName,
|
)..where((tbl) => tbl.id.equals(playerId))).write(
|
||||||
);
|
PlayerTableCompanion(
|
||||||
if (player != null) {
|
name: Value(name),
|
||||||
await updateNameCount(
|
nameCount: Value(newNameCount),
|
||||||
playerId: player.id,
|
),
|
||||||
nameCount: previousNameCount,
|
);
|
||||||
);
|
|
||||||
|
// Consolidate the previous name group.
|
||||||
|
final remainingCount = await getNameCount(name: previousName);
|
||||||
|
|
||||||
|
if (remainingCount == 1) {
|
||||||
|
// Only one player left
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
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
|
||||||
@@ -207,9 +226,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
|||||||
required String description,
|
required String description,
|
||||||
}) async {
|
}) async {
|
||||||
final rowsAffected =
|
final rowsAffected =
|
||||||
await (update(playerTable)..where((g) => g.id.equals(playerId))).write(
|
await (update(playerTable)..where((tbl) => tbl.id.equals(playerId)))
|
||||||
PlayerTableCompanion(description: Value(description)),
|
.write(PlayerTableCompanion(description: Value(description)));
|
||||||
);
|
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +236,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((p) => p.id.equals(playerId));
|
final query = delete(playerTable)..where((tbl) => tbl.id.equals(playerId));
|
||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
@@ -226,8 +244,10 @@ 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((p) => p.name.equals(name));
|
final query = select(playerTable)..where((tbl) => tbl.name.equals(name));
|
||||||
final result = await query.get();
|
final result = await query.get();
|
||||||
return result.length;
|
return result.length;
|
||||||
}
|
}
|
||||||
@@ -238,7 +258,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((p) => p.id.equals(playerId));
|
final query = update(playerTable)..where((tbl) => tbl.id.equals(playerId));
|
||||||
final rowsAffected = await query.write(
|
final rowsAffected = await query.write(
|
||||||
PlayerTableCompanion(nameCount: Value(nameCount)),
|
PlayerTableCompanion(nameCount: Value(nameCount)),
|
||||||
);
|
);
|
||||||
@@ -248,8 +268,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((p) => p.name.equals(name))
|
..where((tbl) => tbl.name.equals(name))
|
||||||
..orderBy([(p) => OrderingTerm.desc(p.nameCount)])
|
..orderBy([(tbl) => OrderingTerm.desc(tbl.nameCount)])
|
||||||
..limit(1);
|
..limit(1);
|
||||||
final result = await query.getSingleOrNull();
|
final result = await query.getSingleOrNull();
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
@@ -264,34 +284,47 @@ 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 == 1) {
|
if (count == 0) {
|
||||||
// If one other player exists with the same name, initialize the nameCount
|
// If no other players exist with the same name, the returned nameCount is 0
|
||||||
await initializeNameCount(name: name);
|
nameCount = 0;
|
||||||
// And for the new player, set nameCount to 2
|
} else if (count == 1) {
|
||||||
|
// If one other player with the name count exists, the returned name count is 2
|
||||||
nameCount = 2;
|
nameCount = 2;
|
||||||
} else if (count > 1) {
|
} else {
|
||||||
// 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((p) => p.name.equals(name))).write(
|
await (update(playerTable)..where((tbl) => tbl.name.equals(name)))
|
||||||
const PlayerTableCompanion(nameCount: Value(1)),
|
.write(const PlayerTableCompanion(nameCount: Value(1)));
|
||||||
);
|
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,18 +39,25 @@ 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)
|
final query = select(playerGroupTable).join([
|
||||||
..where((pG) => pG.groupId.equals(groupId));
|
innerJoin(
|
||||||
final result = await query.get();
|
playerTable,
|
||||||
|
playerTable.id.equalsExp(playerGroupTable.playerId),
|
||||||
|
),
|
||||||
|
])..where(playerGroupTable.groupId.equals(groupId));
|
||||||
|
|
||||||
List<Player> groupMembers = List.empty(growable: true);
|
final result = await query.map((row) => row.readTable(playerTable)).get();
|
||||||
|
return result
|
||||||
for (var entry in result) {
|
.map(
|
||||||
final player = await db.playerDao.getPlayerById(playerId: entry.playerId);
|
(row) => Player(
|
||||||
groupMembers.add(player);
|
id: row.id,
|
||||||
}
|
createdAt: row.createdAt,
|
||||||
|
name: row.name,
|
||||||
return groupMembers;
|
nameCount: row.nameCount,
|
||||||
|
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].
|
||||||
@@ -60,7 +67,9 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
|
|||||||
required String groupId,
|
required String groupId,
|
||||||
}) async {
|
}) async {
|
||||||
final query = select(playerGroupTable)
|
final query = select(playerGroupTable)
|
||||||
..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId));
|
..where(
|
||||||
|
(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;
|
||||||
}
|
}
|
||||||
@@ -81,7 +90,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((p) => p.groupId.equals(groupId));
|
..where((tbl) => tbl.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
|
||||||
@@ -121,7 +130,9 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
|
|||||||
required String groupId,
|
required String groupId,
|
||||||
}) async {
|
}) async {
|
||||||
final query = delete(playerGroupTable)
|
final query = delete(playerGroupTable)
|
||||||
..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId));
|
..where(
|
||||||
|
(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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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((row) => row.read(playerMatchTable.playerId.count()))
|
.map((tbl) => tbl.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((row) => row.read(playerMatchTable.playerId.count()))
|
.map((tbl) => tbl.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((p) => p.matchId.equals(matchId))).get();
|
)..where((tbl) => tbl.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((p) => p.matchId.equals(matchId))
|
..where((tbl) => tbl.matchId.equals(matchId))
|
||||||
..where((p) => p.teamId.equals(teamId)))
|
..where((tbl) => tbl.teamId.equals(teamId)))
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (result.isEmpty) return [];
|
if (result.isEmpty) return [];
|
||||||
@@ -109,7 +109,8 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
|||||||
}) async {
|
}) async {
|
||||||
final rowsAffected =
|
final rowsAffected =
|
||||||
await (update(playerMatchTable)..where(
|
await (update(playerMatchTable)..where(
|
||||||
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
|
(tbl) =>
|
||||||
|
tbl.matchId.equals(matchId) & tbl.playerId.equals(playerId),
|
||||||
))
|
))
|
||||||
.write(PlayerMatchTableCompanion(teamId: Value(teamId)));
|
.write(PlayerMatchTableCompanion(teamId: Value(teamId)));
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
@@ -143,9 +144,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(
|
||||||
(pg) =>
|
(tbl) =>
|
||||||
pg.matchId.equals(matchId) &
|
tbl.matchId.equals(matchId) &
|
||||||
pg.playerId.isIn(playersToRemove.toList()),
|
tbl.playerId.isIn(playersToRemove.toList()),
|
||||||
))
|
))
|
||||||
.go();
|
.go();
|
||||||
}
|
}
|
||||||
@@ -182,8 +183,8 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
|||||||
required String playerId,
|
required String playerId,
|
||||||
}) async {
|
}) async {
|
||||||
final query = delete(playerMatchTable)
|
final query = delete(playerMatchTable)
|
||||||
..where((pg) => pg.matchId.equals(matchId))
|
..where((tbl) => tbl.matchId.equals(matchId))
|
||||||
..where((pg) => pg.playerId.equals(playerId));
|
..where((tbl) => tbl.playerId.equals(playerId));
|
||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
|||||||
}) async {
|
}) async {
|
||||||
final query = select(scoreEntryTable)
|
final query = select(scoreEntryTable)
|
||||||
..where(
|
..where(
|
||||||
(s) =>
|
(tbl) =>
|
||||||
s.playerId.equals(playerId) &
|
tbl.playerId.equals(playerId) &
|
||||||
s.matchId.equals(matchId) &
|
tbl.matchId.equals(matchId) &
|
||||||
s.roundNumber.equals(roundNumber),
|
tbl.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((s) => s.matchId.equals(matchId));
|
..where((tbl) => tbl.matchId.equals(matchId));
|
||||||
final result = await query.get();
|
final result = await query.get();
|
||||||
|
|
||||||
final Map<String, ScoreEntry?> scoresByPlayer = {};
|
final Map<String, ScoreEntry?> scoresByPlayer = {};
|
||||||
@@ -113,8 +113,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
|||||||
required String matchId,
|
required String matchId,
|
||||||
}) async {
|
}) async {
|
||||||
final query = select(scoreEntryTable)
|
final query = select(scoreEntryTable)
|
||||||
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId))
|
..where(
|
||||||
..orderBy([(s) => OrderingTerm.asc(s.roundNumber)]);
|
(tbl) => tbl.playerId.equals(playerId) & tbl.matchId.equals(matchId),
|
||||||
|
)
|
||||||
|
..orderBy([(tbl) => OrderingTerm.asc(tbl.roundNumber)]);
|
||||||
final result = await query.get();
|
final result = await query.get();
|
||||||
return result
|
return result
|
||||||
.map(
|
.map(
|
||||||
@@ -136,8 +138,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 result = await query.getSingle();
|
final row = await query.getSingle();
|
||||||
return result.read(scoreEntryTable.roundNumber.max());
|
return row.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
|
||||||
@@ -166,10 +168,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
|||||||
}) async {
|
}) async {
|
||||||
final rowsAffected =
|
final rowsAffected =
|
||||||
await (update(scoreEntryTable)..where(
|
await (update(scoreEntryTable)..where(
|
||||||
(s) =>
|
(tbl) =>
|
||||||
s.playerId.equals(playerId) &
|
tbl.playerId.equals(playerId) &
|
||||||
s.matchId.equals(matchId) &
|
tbl.matchId.equals(matchId) &
|
||||||
s.roundNumber.equals(entry.roundNumber),
|
tbl.roundNumber.equals(entry.roundNumber),
|
||||||
))
|
))
|
||||||
.write(
|
.write(
|
||||||
ScoreEntryTableCompanion(
|
ScoreEntryTableCompanion(
|
||||||
@@ -190,10 +192,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
|||||||
}) async {
|
}) async {
|
||||||
final query = delete(scoreEntryTable)
|
final query = delete(scoreEntryTable)
|
||||||
..where(
|
..where(
|
||||||
(s) =>
|
(tbl) =>
|
||||||
s.playerId.equals(playerId) &
|
tbl.playerId.equals(playerId) &
|
||||||
s.matchId.equals(matchId) &
|
tbl.matchId.equals(matchId) &
|
||||||
s.roundNumber.equals(roundNumber),
|
tbl.roundNumber.equals(roundNumber),
|
||||||
);
|
);
|
||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
@@ -201,7 +203,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((s) => s.matchId.equals(matchId));
|
..where((tbl) => tbl.matchId.equals(matchId));
|
||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
@@ -211,7 +213,9 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
|||||||
required String playerId,
|
required String playerId,
|
||||||
}) async {
|
}) async {
|
||||||
final query = delete(scoreEntryTable)
|
final query = delete(scoreEntryTable)
|
||||||
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId));
|
..where(
|
||||||
|
(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;
|
||||||
}
|
}
|
||||||
|
|||||||
127
lib/data/dao/statistic_dao.dart
Normal file
127
lib/data/dao/statistic_dao.dart
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/data/dao/statistic_dao.g.dart
Normal file
19
lib/data/dao/statistic_dao.g.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
61
lib/data/dao/statistic_game_dao.dart
Normal file
61
lib/data/dao/statistic_game_dao.dart
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/data/dao/statistic_game_dao.g.dart
Normal file
29
lib/data/dao/statistic_game_dao.g.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
67
lib/data/dao/statistic_group_dao.dart
Normal file
67
lib/data/dao/statistic_group_dao.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/data/dao/statistic_group_dao.g.dart
Normal file
29
lib/data/dao/statistic_group_dao.g.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
55
lib/data/dao/statistic_scope_dao.dart
Normal file
55
lib/data/dao/statistic_scope_dao.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/data/dao/statistic_scope_dao.g.dart
Normal file
26
lib/data/dao/statistic_scope_dao.g.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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((row) => row.read(teamTable.id.count()))
|
.map((tbl) => tbl.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 result = await query.getSingleOrNull();
|
final row = await query.getSingleOrNull();
|
||||||
return result != null;
|
return row != 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 result = await query.getSingle();
|
final row = await query.getSingle();
|
||||||
final members = await _getTeamMembers(teamId: teamId);
|
final members = await _getTeamMembers(teamId: teamId);
|
||||||
return Team(
|
return Team(
|
||||||
id: result.id,
|
id: row.id,
|
||||||
name: result.name,
|
name: row.name,
|
||||||
createdAt: result.createdAt,
|
createdAt: row.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((pm) => pm.teamId.equals(teamId));
|
..where((tbl) => tbl.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((pm) => pm.playerId).toSet();
|
final playerIds = playerMatches.map((tbl) => tbl.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((t) => t.id.equals(teamId))).write(
|
await (update(teamTable)..where((tbl) => tbl.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((t) => t.id.equals(teamId));
|
final query = delete(teamTable)..where((tbl) => tbl.id.equals(teamId));
|
||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ 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';
|
||||||
@@ -16,6 +20,10 @@ 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';
|
||||||
@@ -30,6 +38,10 @@ part 'database.g.dart';
|
|||||||
GameTable,
|
GameTable,
|
||||||
TeamTable,
|
TeamTable,
|
||||||
ScoreEntryTable,
|
ScoreEntryTable,
|
||||||
|
StatisticTable,
|
||||||
|
StatisticScopeTable,
|
||||||
|
StatisticGameTable,
|
||||||
|
StatisticGroupTable,
|
||||||
],
|
],
|
||||||
daos: [
|
daos: [
|
||||||
PlayerDao,
|
PlayerDao,
|
||||||
@@ -40,6 +52,10 @@ 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
13
lib/data/db/tables/statistic_game_table.dart
Normal file
13
lib/data/db/tables/statistic_game_table.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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};
|
||||||
|
}
|
||||||
13
lib/data/db/tables/statistic_group_table.dart
Normal file
13
lib/data/db/tables/statistic_group_table.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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};
|
||||||
|
}
|
||||||
11
lib/data/db/tables/statistic_scope_table.dart
Normal file
11
lib/data/db/tables/statistic_scope_table.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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};
|
||||||
|
}
|
||||||
11
lib/data/db/tables/statistic_table.dart
Normal file
11
lib/data/db/tables/statistic_table.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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};
|
||||||
|
}
|
||||||
@@ -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 GameColor color;
|
final AppColor color;
|
||||||
final String icon;
|
final String icon;
|
||||||
|
|
||||||
Game({
|
Game({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.ruleset,
|
required this.ruleset,
|
||||||
this.color = GameColor.orange,
|
this.color = AppColor.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,
|
||||||
GameColor? color,
|
AppColor? 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 = GameColor.values.firstWhere((e) => e.name == json['color']),
|
color = AppColor.values.firstWhere((e) => e.name == json['color']),
|
||||||
icon = json['icon'];
|
icon = json['icon'];
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ import 'package:uuid/uuid.dart';
|
|||||||
|
|
||||||
class Group {
|
class Group {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
|
||||||
final String description;
|
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
|
final String name;
|
||||||
final List<Player> members;
|
final List<Player> members;
|
||||||
|
final String description;
|
||||||
|
|
||||||
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 ?? '';
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class Match {
|
|||||||
name: '',
|
name: '',
|
||||||
ruleset: Ruleset.singleWinner,
|
ruleset: Ruleset.singleWinner,
|
||||||
description: '',
|
description: '',
|
||||||
color: GameColor.blue,
|
color: AppColor.blue,
|
||||||
icon: '',
|
icon: '',
|
||||||
),
|
),
|
||||||
group = null,
|
group = null,
|
||||||
|
|||||||
48
lib/data/models/statistic.dart
Normal file
48
lib/data/models/statistic.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,18 @@
|
|||||||
"@@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",
|
||||||
@@ -19,12 +23,31 @@
|
|||||||
"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",
|
||||||
@@ -44,11 +67,15 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -59,30 +86,42 @@
|
|||||||
"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",
|
||||||
@@ -92,6 +131,7 @@
|
|||||||
"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",
|
||||||
@@ -99,13 +139,16 @@
|
|||||||
"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",
|
||||||
@@ -121,15 +164,36 @@
|
|||||||
"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",
|
||||||
@@ -137,12 +201,18 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -18,13 +18,30 @@
|
|||||||
"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",
|
||||||
"could_not_add_player": "Could not add player",
|
"confirm": "Confirm",
|
||||||
|
"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",
|
||||||
@@ -42,13 +59,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
@@ -66,6 +87,7 @@
|
|||||||
"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",
|
||||||
@@ -77,12 +99,16 @@
|
|||||||
"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",
|
||||||
@@ -92,6 +118,7 @@
|
|||||||
"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",
|
||||||
@@ -99,13 +126,16 @@
|
|||||||
"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",
|
||||||
@@ -126,11 +156,26 @@
|
|||||||
"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",
|
||||||
@@ -145,6 +190,12 @@
|
|||||||
"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)",
|
||||||
|
|||||||
@@ -206,17 +206,29 @@ 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'**
|
/// **'Could not add player {playerName}'**
|
||||||
String could_not_add_player(Object playerName);
|
String could_not_add_player(String playerName);
|
||||||
|
|
||||||
/// No description provided for @create_game.
|
/// No description provided for @create_game.
|
||||||
///
|
///
|
||||||
@@ -248,6 +260,54 @@ 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:
|
||||||
@@ -308,6 +368,12 @@ 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:
|
||||||
@@ -320,6 +386,12 @@ 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:
|
||||||
@@ -350,6 +422,18 @@ 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:
|
||||||
@@ -452,6 +536,12 @@ 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:
|
||||||
@@ -518,6 +608,12 @@ 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:
|
||||||
@@ -554,6 +650,24 @@ 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:
|
||||||
@@ -608,6 +722,12 @@ 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:
|
||||||
@@ -650,6 +770,12 @@ 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:
|
||||||
@@ -668,6 +794,12 @@ 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:
|
||||||
@@ -692,6 +824,12 @@ 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:
|
||||||
@@ -812,12 +950,48 @@ 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:
|
||||||
@@ -842,6 +1016,60 @@ 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:
|
||||||
@@ -872,6 +1100,42 @@ 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:
|
||||||
|
|||||||
@@ -62,11 +62,17 @@ 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 could_not_add_player(Object playerName) {
|
String get confirm => 'Bestätigen';
|
||||||
|
|
||||||
|
@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';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +91,33 @@ 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';
|
||||||
|
|
||||||
@@ -125,12 +158,18 @@ 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';
|
||||||
|
|
||||||
@@ -146,6 +185,12 @@ 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';
|
||||||
|
|
||||||
@@ -201,6 +246,9 @@ 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';
|
||||||
|
|
||||||
@@ -234,6 +282,9 @@ 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';
|
||||||
|
|
||||||
@@ -252,6 +303,15 @@ 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';
|
||||||
|
|
||||||
@@ -279,6 +339,9 @@ 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';
|
||||||
|
|
||||||
@@ -301,6 +364,9 @@ 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';
|
||||||
|
|
||||||
@@ -310,6 +376,9 @@ 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';
|
||||||
|
|
||||||
@@ -322,6 +391,9 @@ 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';
|
||||||
|
|
||||||
@@ -387,9 +459,27 @@ 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';
|
||||||
|
|
||||||
@@ -402,6 +492,33 @@ 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';
|
||||||
@@ -422,6 +539,24 @@ 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';
|
||||||
|
|
||||||
|
|||||||
@@ -62,12 +62,18 @@ 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 could_not_add_player(Object playerName) {
|
String get confirm => 'Confirm';
|
||||||
return 'Could not add player';
|
|
||||||
|
@override
|
||||||
|
String could_not_add_player(String playerName) {
|
||||||
|
return 'Could not add player $playerName';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -85,6 +91,33 @@ 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';
|
||||||
|
|
||||||
@@ -125,12 +158,18 @@ 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';
|
||||||
|
|
||||||
@@ -146,6 +185,12 @@ 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';
|
||||||
|
|
||||||
@@ -201,6 +246,9 @@ 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';
|
||||||
|
|
||||||
@@ -234,6 +282,9 @@ 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';
|
||||||
|
|
||||||
@@ -252,6 +303,15 @@ 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';
|
||||||
|
|
||||||
@@ -279,6 +339,9 @@ 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';
|
||||||
|
|
||||||
@@ -301,6 +364,9 @@ 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';
|
||||||
|
|
||||||
@@ -310,6 +376,9 @@ 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';
|
||||||
|
|
||||||
@@ -322,6 +391,9 @@ 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';
|
||||||
|
|
||||||
@@ -387,9 +459,27 @@ 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';
|
||||||
|
|
||||||
@@ -402,6 +492,33 @@ 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';
|
||||||
@@ -421,6 +538,24 @@ 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';
|
||||||
|
|
||||||
|
|||||||
@@ -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.dart';
|
import 'package:tallee/presentation/views/main_menu/statistics_view/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';
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ 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];
|
||||||
@@ -134,6 +135,7 @@ 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);
|
||||||
@@ -157,7 +159,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ 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(
|
||||||
@@ -106,13 +107,10 @@ class _GroupViewState extends State<GroupView> {
|
|||||||
context,
|
context,
|
||||||
adaptivePageRoute(
|
adaptivePageRoute(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return const CreateGroupView();
|
return CreateGroupView(onMembersChanged: loadGroups);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setState(() {
|
|
||||||
loadGroups();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
|||||||
game.ruleset,
|
game.ruleset,
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
badgeColor: getColorFromGameColor(game.color),
|
badgeColor: getColorFromAppColor(game.color),
|
||||||
isHighlighted: selectedGameId == game.id,
|
isHighlighted: selectedGameId == game.id,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
@@ -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<(GameColor, String)> _colors;
|
late List<(AppColor, String)> _colors;
|
||||||
|
|
||||||
Ruleset? selectedRuleset = Ruleset.singleWinner;
|
Ruleset? selectedRuleset = Ruleset.singleWinner;
|
||||||
GameColor? selectedColor = GameColor.orange;
|
AppColor? selectedColor = AppColor.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(
|
||||||
GameColor.values.length,
|
AppColor.values.length,
|
||||||
(index) => (
|
(index) => (
|
||||||
GameColor.values[index],
|
AppColor.values[index],
|
||||||
translateGameColorToString(GameColor.values[index], context),
|
translateAppColorToString(AppColor.values[index], context),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -117,7 +117,6 @@ 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: [
|
||||||
@@ -468,7 +467,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: getColorFromGameColor(
|
color: getColorFromAppColor(
|
||||||
_colors[index].$1,
|
_colors[index].$1,
|
||||||
),
|
),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
@@ -502,13 +501,13 @@ class _CreateGameViewState extends State<CreateGameView> {
|
|||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: getColorFromGameColor(selectedColor!),
|
color: getColorFromAppColor(selectedColor!),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 5),
|
padding: const EdgeInsets.only(right: 5),
|
||||||
child: Text(translateGameColorToString(selectedColor!, context)),
|
child: Text(translateAppColorToString(selectedColor!, context)),
|
||||||
),
|
),
|
||||||
Transform.rotate(
|
Transform.rotate(
|
||||||
angle: pi / 2,
|
angle: pi / 2,
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ 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;
|
||||||
|
|||||||
@@ -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: GameColor.blue,
|
color: AppColor.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.report,
|
icon: Icons.info,
|
||||||
title: loc.info,
|
title: loc.info,
|
||||||
message: loc.no_matches_created_yet,
|
message: loc.no_matches_created_yet,
|
||||||
),
|
),
|
||||||
@@ -97,6 +97,7 @@ 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(
|
||||||
|
|||||||
394
lib/presentation/views/main_menu/player_detail_view.dart
Normal file
394
lib/presentation/views/main_menu/player_detail_view.dart
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39620,12 +39620,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||||||
SOFTWARE.''',
|
SOFTWARE.''',
|
||||||
);
|
);
|
||||||
|
|
||||||
/// tallee 0.0.35+283
|
/// tallee 0.0.35+277
|
||||||
const _tallee = Package(
|
const _tallee = Package(
|
||||||
name: 'tallee',
|
name: 'tallee',
|
||||||
description: 'Tracking App for Card Games',
|
description: 'Tracking App for Card Games',
|
||||||
authors: [],
|
authors: [],
|
||||||
version: '0.0.35+283',
|
version: '0.0.35+277',
|
||||||
spdxIdentifiers: ['LGPL-3.0'],
|
spdxIdentifiers: ['LGPL-3.0'],
|
||||||
isMarkdown: false,
|
isMarkdown: false,
|
||||||
isSdk: false,
|
isSdk: false,
|
||||||
|
|||||||
@@ -1,311 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,635 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,13 @@ 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.
|
||||||
@@ -22,6 +24,9 @@ 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();
|
||||||
}
|
}
|
||||||
@@ -45,13 +50,14 @@ 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) {
|
||||||
return Stack(
|
final children = <Widget>[...previousChildren];
|
||||||
alignment: Alignment.topCenter,
|
if (currentChild != null) children.add(currentChild);
|
||||||
children: [...previousChildren, ?currentChild],
|
return Stack(alignment: widget.alignment, children: children);
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
child: widget.child,
|
child: widget.fixLayoutBuilder
|
||||||
|
? Align(alignment: widget.alignment, child: widget.child)
|
||||||
|
: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class AnimatedDialogButton extends StatefulWidget {
|
|||||||
const AnimatedDialogButton({
|
const AnimatedDialogButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.buttonText,
|
required this.buttonText,
|
||||||
required this.onPressed,
|
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,28 +38,38 @@ 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 GestureDetector(
|
return IgnorePointer(
|
||||||
onTapDown: (_) => setState(() => _isPressed = true),
|
ignoring: isDisabled,
|
||||||
onTapUp: (_) => setState(() => _isPressed = false),
|
child: Opacity(
|
||||||
onTapCancel: () => setState(() => _isPressed = false),
|
opacity: isDisabled ? 0.5 : 1.0,
|
||||||
onTap: widget.onPressed,
|
child: GestureDetector(
|
||||||
child: AnimatedScale(
|
onTapDown: (_) => setState(() => _isPressed = true),
|
||||||
scale: _isPressed ? 0.95 : 1.0,
|
onTapUp: (_) => setState(() => _isPressed = false),
|
||||||
duration: const Duration(milliseconds: 100),
|
onTapCancel: () => setState(() => _isPressed = false),
|
||||||
child: AnimatedOpacity(
|
onTap: widget.onPressed,
|
||||||
opacity: _isPressed ? 0.6 : 1.0,
|
child: AnimatedScale(
|
||||||
duration: const Duration(milliseconds: 100),
|
scale: _isPressed ? 0.95 : 1.0,
|
||||||
child: Center(
|
duration: const Duration(milliseconds: 100),
|
||||||
child: Container(
|
child: AnimatedOpacity(
|
||||||
constraints: widget.buttonConstraints,
|
opacity: _isPressed ? 0.6 : 1.0,
|
||||||
decoration: buttonDecoration,
|
duration: const Duration(milliseconds: 100),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
child: Center(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
child: Container(
|
||||||
child: Text(
|
constraints: widget.buttonConstraints,
|
||||||
widget.buttonText,
|
decoration: buttonDecoration,
|
||||||
style: textStyling,
|
padding: const EdgeInsets.symmetric(
|
||||||
textAlign: TextAlign.center,
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
widget.buttonText,
|
||||||
|
style: textStyling,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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(
|
||||||
|
|||||||
@@ -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,
|
||||||
required this.onPressed,
|
this.onPressed,
|
||||||
required this.text,
|
required this.text,
|
||||||
this.buttonType = ButtonType.primary,
|
this.buttonType = ButtonType.primary,
|
||||||
this.isDestructive = false,
|
this.isDestructive = false,
|
||||||
@@ -20,17 +20,18 @@ 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: () async {
|
onPressed: onPressed != null
|
||||||
await HapticFeedback.selectionClick();
|
? () async {
|
||||||
onPressed.call();
|
await HapticFeedback.selectionClick();
|
||||||
},
|
onPressed?.call();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
buttonText: text,
|
buttonText: text,
|
||||||
buttonType: buttonType,
|
buttonType: buttonType,
|
||||||
isDescructive: isDestructive,
|
isDescructive: isDestructive,
|
||||||
|
|||||||
@@ -12,41 +12,44 @@ class GameLabel extends StatelessWidget {
|
|||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final String description;
|
final String description;
|
||||||
final GameColor color;
|
final AppColor color;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final backgroundColor = getColorFromGameColor(color);
|
final backgroundColor = getColorFromAppColor(color);
|
||||||
final fontColor = backgroundColor.computeLuminance() > 0.5
|
final fontColor = backgroundColor.computeLuminance() > 0.5
|
||||||
? Colors.black
|
? Colors.black
|
||||||
: Colors.white;
|
: Colors.white;
|
||||||
|
|
||||||
return IntrinsicHeight(
|
return Row(
|
||||||
child: Row(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
// Title
|
||||||
// Title
|
Container(
|
||||||
Container(
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: backgroundColor.withAlpha(230),
|
||||||
color: backgroundColor.withAlpha(230),
|
borderRadius: const BorderRadius.only(
|
||||||
borderRadius: const BorderRadius.only(
|
topLeft: Radius.circular(8),
|
||||||
topLeft: Radius.circular(8),
|
bottomLeft: Radius.circular(8),
|
||||||
bottomLeft: Radius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: fontColor,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
softWrap: false,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: fontColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Description
|
// Description
|
||||||
Container(
|
Flexible(
|
||||||
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor.withAlpha(140),
|
color: backgroundColor.withAlpha(140),
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
@@ -57,6 +60,9 @@ 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,
|
||||||
@@ -64,8 +70,8 @@ class GameLabel extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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.
|
||||||
@@ -37,6 +38,9 @@ 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();
|
||||||
}
|
}
|
||||||
@@ -323,6 +327,7 @@ 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);
|
||||||
|
|||||||
@@ -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 ?? getColorFromGameColor(GameColor.orange);
|
final gameColor = badgeColor ?? getColorFromAppColor(AppColor.orange);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
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 {
|
||||||
@@ -15,6 +17,7 @@ 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.
|
||||||
@@ -26,6 +29,9 @@ 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();
|
||||||
}
|
}
|
||||||
@@ -92,6 +98,19 @@ 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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ 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';
|
||||||
|
|
||||||
@@ -24,6 +26,7 @@ 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.
|
||||||
@@ -32,6 +35,9 @@ 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;
|
||||||
|
|
||||||
@@ -224,6 +230,19 @@ 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(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
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';
|
||||||
@@ -21,8 +25,11 @@ 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.
|
||||||
@@ -37,12 +44,16 @@ 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);
|
||||||
@@ -52,91 +63,202 @@ class StatisticsTile extends StatelessWidget {
|
|||||||
title: title,
|
title: title,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
content: Padding(
|
content: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.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.65;
|
final maxBarWidth = constraints.maxWidth * 0.8;
|
||||||
|
|
||||||
|
// 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: List.generate(min(values.length, itemCount), (index) {
|
children: [
|
||||||
/// The maximum wins among all players
|
// Bars
|
||||||
final maxMatches = values.isNotEmpty ? values[0].$2 : 0;
|
...List.generate(valuesShown, (index) {
|
||||||
|
/// Fraction of wins
|
||||||
|
final double fraction = (maxVal > 0)
|
||||||
|
? (displayValues[index].$2 / maxVal)
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
/// Fraction of wins
|
/// Calculated width for current the bar
|
||||||
final double fraction = (maxMatches > 0)
|
final double barWidth = (maxBarWidth * fraction).clamp(
|
||||||
? (values[index].$2 / maxMatches)
|
0.0,
|
||||||
: 0.0;
|
maxBarWidth,
|
||||||
|
);
|
||||||
|
|
||||||
/// Calculated width for current the bar
|
final barClr = index >= displayCount
|
||||||
final double barWidth = maxBarWidth * fraction;
|
? barColor.withAlpha(150)
|
||||||
|
: barColor;
|
||||||
|
|
||||||
return Padding(
|
var textClr = barColor.computeLuminance() > 0.5
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
? const Color(0xFF101010)
|
||||||
child: Row(
|
: CustomTheme.textColor;
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
textClr = textClr.withAlpha(
|
||||||
children: [
|
index >= displayCount ? 220 : 255,
|
||||||
Stack(
|
);
|
||||||
children: [
|
|
||||||
Container(
|
return Padding(
|
||||||
height: 24,
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
width: barWidth,
|
child: Row(
|
||||||
decoration: BoxDecoration(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
borderRadius: BorderRadius.circular(4),
|
children: [
|
||||||
color: barColor,
|
SizedBox(
|
||||||
),
|
width: maxBarWidth,
|
||||||
),
|
child: Stack(
|
||||||
Padding(
|
clipBehavior: Clip.hardEdge,
|
||||||
padding: const EdgeInsets.only(left: 4.0),
|
children: [
|
||||||
child: RichText(
|
// Bar
|
||||||
overflow: TextOverflow.ellipsis,
|
Container(
|
||||||
text: TextSpan(
|
height: 24,
|
||||||
style: DefaultTextStyle.of(context).style,
|
width: barWidth,
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
TextSpan(
|
borderRadius: BorderRadius.circular(4),
|
||||||
text: values[index].$1.name,
|
color: barClr,
|
||||||
style: const TextStyle(
|
),
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: getNameCountText(values[index].$1),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: CustomTheme.textColor.withAlpha(
|
|
||||||
150,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
// Player
|
||||||
],
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
const Spacer(),
|
child: RichText(
|
||||||
Center(
|
maxLines: 1,
|
||||||
child: Text(
|
softWrap: false,
|
||||||
values[index].$2 <= 1 && values[index].$2 is double
|
overflow: TextOverflow.ellipsis,
|
||||||
? values[index].$2.toStringAsFixed(2)
|
text: TextSpan(
|
||||||
: values[index].$2.toString(),
|
style: DefaultTextStyle.of(context).style,
|
||||||
textAlign: TextAlign.center,
|
children: [
|
||||||
style: const TextStyle(
|
TextSpan(
|
||||||
fontSize: 16,
|
text: displayValues[index].$1.name,
|
||||||
fontWeight: FontWeight.bold,
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textClr,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: getNameCountText(
|
||||||
|
displayValues[index].$1,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color:
|
||||||
|
(barColor ==
|
||||||
|
getColorFromAppColor(
|
||||||
|
AppColor.yellow,
|
||||||
|
)
|
||||||
|
? const Color(
|
||||||
|
0xFF101010,
|
||||||
|
)
|
||||||
|
: CustomTheme.textColor)
|
||||||
|
.withAlpha(150),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Spacer(),
|
||||||
],
|
|
||||||
|
// Value
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
displayValues[index].$2 <= 1 &&
|
||||||
|
displayValues[index].$2 is double
|
||||||
|
? displayValues[index].$2.toStringAsFixed(2)
|
||||||
|
: displayValues[index].$2.toString(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -144,4 +266,24 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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.
|
||||||
@@ -25,52 +26,58 @@ 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 Container(
|
return GestureDetector(
|
||||||
padding: const EdgeInsets.all(5),
|
onTap: onTileTap,
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
color: CustomTheme.onBoxColor,
|
padding: const EdgeInsets.all(5),
|
||||||
borderRadius: BorderRadius.circular(12),
|
decoration: BoxDecoration(
|
||||||
),
|
color: CustomTheme.onBoxColor,
|
||||||
child: Row(
|
borderRadius: BorderRadius.circular(12),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
if (iconEnabled) const SizedBox(width: 3),
|
mainAxisSize: MainAxisSize.min,
|
||||||
Flexible(
|
children: [
|
||||||
child: RichText(
|
if (iconEnabled) const SizedBox(width: 3),
|
||||||
overflow: TextOverflow.ellipsis,
|
Flexible(
|
||||||
text: TextSpan(
|
child: RichText(
|
||||||
style: DefaultTextStyle.of(context).style,
|
overflow: TextOverflow.ellipsis,
|
||||||
children: [
|
text: TextSpan(
|
||||||
TextSpan(
|
style: DefaultTextStyle.of(context).style,
|
||||||
text: text,
|
children: [
|
||||||
style: const TextStyle(
|
TextSpan(
|
||||||
fontSize: 14,
|
text: text,
|
||||||
fontWeight: FontWeight.w500,
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
TextSpan(
|
||||||
TextSpan(
|
text: suffixText,
|
||||||
text: suffixText,
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 13,
|
||||||
fontSize: 13,
|
fontWeight: FontWeight.w500,
|
||||||
fontWeight: FontWeight.w500,
|
color: CustomTheme.textColor.withAlpha(120),
|
||||||
color: CustomTheme.textColor.withAlpha(120),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (iconEnabled) ...<Widget>[
|
||||||
if (iconEnabled) ...<Widget>[
|
const SizedBox(width: 3),
|
||||||
const SizedBox(width: 3),
|
GestureDetector(
|
||||||
GestureDetector(
|
onTap: onIconTap,
|
||||||
onTap: onIconTap,
|
child: const Icon(Icons.close, size: 20),
|
||||||
child: const Icon(Icons.close, size: 20),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ 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();
|
||||||
@@ -278,7 +279,7 @@ class DataTransferService {
|
|||||||
name: 'Unknown',
|
name: 'Unknown',
|
||||||
ruleset: Ruleset.singleWinner,
|
ruleset: Ruleset.singleWinner,
|
||||||
description: '',
|
description: '',
|
||||||
color: GameColor.blue,
|
color: AppColor.blue,
|
||||||
icon: '',
|
icon: '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
68
pubspec.lock
68
pubspec.lock
@@ -17,6 +17,14 @@ 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:
|
||||||
@@ -85,10 +93,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
|
sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.15.0"
|
version: "2.13.1"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -177,6 +185,14 @@ 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:
|
||||||
@@ -325,26 +341,34 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: drift
|
name: drift
|
||||||
sha256: "8033500116b24398fba0cca0369cc31678cd627c01e41753a61186911cea743e"
|
sha256: "970cd188fddb111b26ea6a9b07a62bf5c2432d74147b8122c67044ae3b97e99e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.33.0"
|
version: "2.31.0"
|
||||||
drift_dev:
|
drift_dev:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: drift_dev
|
name: drift_dev
|
||||||
sha256: b3dd5b75e30522a91da8abda9f5bb17230cb038097f6d15fa75d42bb563428aa
|
sha256: "917184b2fb867b70a548a83bf0d36268423b38d39968c06cce4905683da49587"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.33.0"
|
version: "2.31.0"
|
||||||
drift_flutter:
|
drift_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: drift_flutter
|
name: drift_flutter
|
||||||
sha256: "887fdec622174dc7eaefd0048403e34ee07cc18626ac8a7544cc3b8a4a172166"
|
sha256: c07120854742a0cae2f7501a0da02493addde550db6641d284983c08762e60a7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.0"
|
version: "0.2.8"
|
||||||
|
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:
|
||||||
@@ -389,10 +413,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_saver
|
name: file_saver
|
||||||
sha256: "68c9a085d9bb4546e0a31d1e583a48d7c17a6987d538788ea064f0043b1fc02d"
|
sha256: "9d93db09bd4da9e43238f9dd485360fc51a5c138eea5ef5f407ec56e58079ac0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.0"
|
version: "0.3.1"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1114,38 +1138,30 @@ 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: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5"
|
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.1"
|
version: "2.9.4"
|
||||||
sqlite3_flutter_libs:
|
sqlite3_flutter_libs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlite3_flutter_libs
|
name: sqlite3_flutter_libs
|
||||||
sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454"
|
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0+eol"
|
version: "0.5.42"
|
||||||
sqlparser:
|
sqlparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlparser
|
name: sqlparser
|
||||||
sha256: ecdc06d4a7d79dcbc928d99afd2f7f5b0f98a637c46f89be83d911617f759978
|
sha256: "337e9997f7141ffdd054259128553c348635fa318f7ca492f07a4ab76f850d19"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.44.4"
|
version: "0.43.1"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1427,5 +1443,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.12.0 <4.0.0"
|
dart: ">=3.10.3 <4.0.0"
|
||||||
flutter: ">=3.41.0"
|
flutter: ">=3.38.4"
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
name: tallee
|
name: tallee
|
||||||
description: "Tracking App for Card Games"
|
description: "Tracking App for Card Games"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.0.35+283
|
version: 0.0.33+281
|
||||||
|
|
||||||
environment:
|
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
|
||||||
|
|||||||
@@ -194,6 +194,31 @@ 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],
|
||||||
|
|||||||
@@ -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: GameColor.blue,
|
color: AppColor.blue,
|
||||||
icon: '',
|
icon: '',
|
||||||
);
|
);
|
||||||
testMatch1 = Match(
|
testMatch1 = Match(
|
||||||
@@ -260,6 +260,34 @@ 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);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ void main() {
|
|||||||
testGame = Game(
|
testGame = Game(
|
||||||
name: 'Test Game',
|
name: 'Test Game',
|
||||||
ruleset: Ruleset.highestScore,
|
ruleset: Ruleset.highestScore,
|
||||||
color: GameColor.blue,
|
color: AppColor.blue,
|
||||||
icon: '',
|
icon: '',
|
||||||
);
|
);
|
||||||
testMatch1 = Match(
|
testMatch1 = Match(
|
||||||
|
|||||||
@@ -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: GameColor.blue,
|
color: AppColor.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: GameColor.red,
|
color: AppColor.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: GameColor.orange,
|
color: AppColor.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: GameColor.purple,
|
color: AppColor.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: GameColor.green,
|
color: AppColor.green,
|
||||||
);
|
);
|
||||||
|
|
||||||
final updatedGame = await database.gameDao.getGameById(
|
final updatedGame = await database.gameDao.getGameById(
|
||||||
gameId: testGame1.id,
|
gameId: testGame1.id,
|
||||||
);
|
);
|
||||||
expect(updatedGame.color, GameColor.green);
|
expect(updatedGame.color, AppColor.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: GameColor.green,
|
color: AppColor.green,
|
||||||
);
|
);
|
||||||
expect(updated, isFalse);
|
expect(updated, isFalse);
|
||||||
|
|
||||||
@@ -336,7 +336,7 @@ void main() {
|
|||||||
name: newName,
|
name: newName,
|
||||||
);
|
);
|
||||||
|
|
||||||
const newGameColor = GameColor.teal;
|
const newGameColor = AppColor.teal;
|
||||||
await database.gameDao.updateGameColor(
|
await database.gameDao.updateGameColor(
|
||||||
gameId: testGame1.id,
|
gameId: testGame1.id,
|
||||||
color: newGameColor,
|
color: newGameColor,
|
||||||
|
|||||||
@@ -233,6 +233,95 @@ 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);
|
||||||
|
|
||||||
@@ -372,14 +461,22 @@ 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, 2);
|
expect(players.length, 3);
|
||||||
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);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -404,24 +501,62 @@ 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.addPlayersAsList(
|
await database.playerDao.addPlayer(player: testPlayer1);
|
||||||
players: [testPlayer1, player2, player3],
|
|
||||||
|
var nameCount = await database.playerDao.getNameCount(
|
||||||
|
name: testPlayer1.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
final nameCount = await database.playerDao.getNameCount(
|
expect(nameCount, 1);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
@@ -441,14 +576,24 @@ 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.addPlayersAsList(
|
await database.playerDao.addPlayer(player: testPlayer1);
|
||||||
players: [testPlayer1, player2, player3],
|
var player = await database.playerDao.getPlayerWithHighestNameCount(
|
||||||
);
|
|
||||||
|
|
||||||
final player = await database.playerDao.getPlayerWithHighestNameCount(
|
|
||||||
name: testPlayer1.name,
|
name: testPlayer1.name,
|
||||||
);
|
);
|
||||||
|
expect(player, isNotNull);
|
||||||
|
expect(player!.nameCount, 0);
|
||||||
|
|
||||||
|
await database.playerDao.addPlayer(player: player2);
|
||||||
|
player = await database.playerDao.getPlayerWithHighestNameCount(
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
@@ -460,32 +605,6 @@ 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);
|
||||||
|
|||||||
@@ -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: GameColor.blue,
|
color: AppColor.blue,
|
||||||
icon: '',
|
icon: '',
|
||||||
);
|
);
|
||||||
testMatch1 = Match(
|
testMatch1 = Match(
|
||||||
|
|||||||
122
test/db_tests/statistics/statistic_test.dart
Normal file
122
test/db_tests/statistics/statistic_test.dart
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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: GameColor.blue,
|
color: AppColor.blue,
|
||||||
icon: '',
|
icon: '',
|
||||||
);
|
);
|
||||||
testMatch1 = Match(
|
testMatch1 = Match(
|
||||||
|
|||||||
@@ -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: GameColor.blue,
|
color: AppColor.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: GameColor.red,
|
color: AppColor.red,
|
||||||
icon: 'icon',
|
icon: 'icon',
|
||||||
),
|
),
|
||||||
Game(
|
Game(
|
||||||
name: 'Blue Game',
|
name: 'Blue Game',
|
||||||
ruleset: Ruleset.singleWinner,
|
ruleset: Ruleset.singleWinner,
|
||||||
color: GameColor.blue,
|
color: AppColor.blue,
|
||||||
icon: 'icon',
|
icon: 'icon',
|
||||||
),
|
),
|
||||||
Game(
|
Game(
|
||||||
name: 'Green Game',
|
name: 'Green Game',
|
||||||
ruleset: Ruleset.singleWinner,
|
ruleset: Ruleset.singleWinner,
|
||||||
color: GameColor.green,
|
color: AppColor.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: GameColor.blue,
|
color: AppColor.blue,
|
||||||
icon: 'icon',
|
icon: 'icon',
|
||||||
),
|
),
|
||||||
Game(
|
Game(
|
||||||
name: 'Lowest Score Game',
|
name: 'Lowest Score Game',
|
||||||
ruleset: Ruleset.lowestScore,
|
ruleset: Ruleset.lowestScore,
|
||||||
color: GameColor.blue,
|
color: AppColor.blue,
|
||||||
icon: 'icon',
|
icon: 'icon',
|
||||||
),
|
),
|
||||||
Game(
|
Game(
|
||||||
name: 'Single Winner',
|
name: 'Single Winner',
|
||||||
ruleset: Ruleset.singleWinner,
|
ruleset: Ruleset.singleWinner,
|
||||||
color: GameColor.blue,
|
color: AppColor.blue,
|
||||||
icon: 'icon',
|
icon: 'icon',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user