Merge remote-tracking branch 'origin/development' into bug/195-datenbank-onDelete-ueberpruefen
# Conflicts: # lib/data/db/tables/player_table.dart
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
|
||||
/// Translates a [Ruleset] enum value to its corresponding localized string.
|
||||
@@ -43,3 +44,18 @@ String getExtraPlayerCount(Match match) {
|
||||
}
|
||||
return ' + ${count.toString()}';
|
||||
}
|
||||
|
||||
String getNameCountText(Player player) {
|
||||
if (player.nameCount >= 1) {
|
||||
return ' #${player.nameCount}';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
String getPointLabel(AppLocalizations loc, int points) {
|
||||
if (points == 1) {
|
||||
return '$points ${loc.point}';
|
||||
} else {
|
||||
return '$points ${loc.points}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,21 +85,21 @@ class CustomTheme {
|
||||
);
|
||||
|
||||
static const SearchBarThemeData searchBarTheme = SearchBarThemeData(
|
||||
textStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.textColor)),
|
||||
hintStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.hintColor)),
|
||||
textStyle: WidgetStatePropertyAll(TextStyle(color: textColor)),
|
||||
hintStyle: WidgetStatePropertyAll(TextStyle(color: hintColor)),
|
||||
);
|
||||
|
||||
static final RadioThemeData radioTheme = RadioThemeData(
|
||||
fillColor: WidgetStateProperty.resolveWith<Color>((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return CustomTheme.primaryColor;
|
||||
return primaryColor;
|
||||
}
|
||||
return CustomTheme.textColor;
|
||||
return textColor;
|
||||
}),
|
||||
);
|
||||
|
||||
static const InputDecorationTheme inputDecorationTheme = InputDecorationTheme(
|
||||
labelStyle: TextStyle(color: CustomTheme.textColor),
|
||||
hintStyle: TextStyle(color: CustomTheme.hintColor),
|
||||
labelStyle: TextStyle(color: textColor),
|
||||
hintStyle: TextStyle(color: hintColor),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
matchId: row.id,
|
||||
);
|
||||
|
||||
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
|
||||
return Match(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -45,7 +44,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
createdAt: row.createdAt,
|
||||
endedAt: row.endedAt,
|
||||
scores: scores,
|
||||
winner: winner,
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -68,8 +66,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
|
||||
final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId);
|
||||
|
||||
final winner = await db.scoreEntryDao.getWinner(matchId: matchId);
|
||||
|
||||
return Match(
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
@@ -80,7 +76,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
createdAt: result.createdAt,
|
||||
endedAt: result.endedAt,
|
||||
scores: scores,
|
||||
winner: winner,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,19 +105,14 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
}
|
||||
|
||||
for (final pid in match.scores.keys) {
|
||||
final playerScores = match.scores[pid]!;
|
||||
await db.scoreEntryDao.addScoresAsList(
|
||||
entrys: playerScores,
|
||||
playerId: pid,
|
||||
matchId: match.id,
|
||||
);
|
||||
}
|
||||
|
||||
if (match.winner != null) {
|
||||
await db.scoreEntryDao.setWinner(
|
||||
matchId: match.id,
|
||||
playerId: match.winner!.id,
|
||||
);
|
||||
final playerScores = match.scores[pid];
|
||||
if (playerScores != null) {
|
||||
await db.scoreEntryDao.addScore(
|
||||
entry: playerScores,
|
||||
playerId: pid,
|
||||
matchId: match.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -140,6 +130,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
uniqueGames[match.game.id] = match.game;
|
||||
}
|
||||
|
||||
// Add games
|
||||
if (uniqueGames.isNotEmpty) {
|
||||
await db.batch(
|
||||
(b) => b.insertAll(
|
||||
@@ -162,7 +153,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
);
|
||||
}
|
||||
|
||||
// Add all groups of the matches in batch
|
||||
// Add groups
|
||||
await db.batch(
|
||||
(b) => b.insertAll(
|
||||
db.groupTable,
|
||||
@@ -181,7 +172,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
),
|
||||
);
|
||||
|
||||
// Add all matches in batch
|
||||
// Add matches
|
||||
await db.batch(
|
||||
(b) => b.insertAll(
|
||||
matchTable,
|
||||
@@ -202,7 +193,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
),
|
||||
);
|
||||
|
||||
// Add all players of the matches in batch (unique)
|
||||
// Add players
|
||||
final uniquePlayers = <String, Player>{};
|
||||
for (final match in matches) {
|
||||
for (final p in match.players) {
|
||||
@@ -235,7 +226,27 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
);
|
||||
}
|
||||
|
||||
// Add all player-match associations in batch
|
||||
await db.batch((b) {
|
||||
for (final match in matches) {
|
||||
for (final entry in match.scores.entries) {
|
||||
if (entry.value != null) {
|
||||
b.insert(
|
||||
db.scoreEntryTable,
|
||||
ScoreEntryTableCompanion.insert(
|
||||
matchId: match.id,
|
||||
playerId: entry.key,
|
||||
score: entry.value!.score,
|
||||
roundNumber: entry.value!.roundNumber,
|
||||
change: entry.value!.change,
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add player-match associations
|
||||
await db.batch((b) {
|
||||
for (final match in matches) {
|
||||
for (final p in match.players) {
|
||||
@@ -251,7 +262,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
}
|
||||
});
|
||||
|
||||
// Add all player-group associations in batch
|
||||
// Add player-group associations
|
||||
await db.batch((b) {
|
||||
for (final match in matches) {
|
||||
if (match.group != null) {
|
||||
@@ -300,7 +311,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
final group = await db.groupDao.getGroupById(groupId: groupId);
|
||||
final players =
|
||||
await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? [];
|
||||
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
|
||||
return Match(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -310,7 +320,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
notes: row.notes ?? '',
|
||||
createdAt: row.createdAt,
|
||||
endedAt: row.endedAt,
|
||||
winner: winner,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/db/tables/player_table.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
@@ -20,6 +21,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
createdAt: row.createdAt,
|
||||
nameCount: row.nameCount,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
@@ -34,6 +36,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
name: result.name,
|
||||
description: result.description,
|
||||
createdAt: result.createdAt,
|
||||
nameCount: result.nameCount,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,12 +45,15 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
/// the new one.
|
||||
Future<bool> addPlayer({required Player player}) async {
|
||||
if (!await playerExists(playerId: player.id)) {
|
||||
final int nameCount = await calculateNameCount(name: player.name);
|
||||
|
||||
await into(playerTable).insert(
|
||||
PlayerTableCompanion.insert(
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
description: player.description,
|
||||
createdAt: player.createdAt,
|
||||
nameCount: Value(nameCount),
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
@@ -62,20 +68,67 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
Future<bool> addPlayersAsList({required List<Player> players}) async {
|
||||
if (players.isEmpty) return false;
|
||||
|
||||
// Filter out players that already exist
|
||||
final newPlayers = <Player>[];
|
||||
for (final player in players) {
|
||||
if (!await playerExists(playerId: player.id)) {
|
||||
newPlayers.add(player);
|
||||
}
|
||||
}
|
||||
|
||||
if (newPlayers.isEmpty) return false;
|
||||
|
||||
// Group players by name
|
||||
final nameGroups = <String, List<Player>>{};
|
||||
for (final player in newPlayers) {
|
||||
nameGroups.putIfAbsent(player.name, () => []).add(player);
|
||||
}
|
||||
|
||||
final playersToInsert = <PlayerTableCompanion>[];
|
||||
|
||||
// Process each group of players with the same name
|
||||
for (final entry in nameGroups.entries) {
|
||||
final name = entry.key;
|
||||
final playersWithName = entry.value;
|
||||
|
||||
// Get the current nameCount
|
||||
var nameCount = await calculateNameCount(name: name);
|
||||
|
||||
// One player with the same name
|
||||
if (playersWithName.length == 1) {
|
||||
final player = playersWithName[0];
|
||||
playersToInsert.add(
|
||||
PlayerTableCompanion.insert(
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
description: player.description,
|
||||
createdAt: player.createdAt,
|
||||
nameCount: Value(nameCount),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (nameCount == 0) nameCount++;
|
||||
// Multiple players with the same name
|
||||
for (var i = 0; i < playersWithName.length; i++) {
|
||||
final player = playersWithName[i];
|
||||
playersToInsert.add(
|
||||
PlayerTableCompanion.insert(
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
description: player.description,
|
||||
createdAt: player.createdAt,
|
||||
nameCount: Value(nameCount + i),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db.batch(
|
||||
(b) => b.insertAll(
|
||||
playerTable,
|
||||
players
|
||||
.map(
|
||||
(player) => PlayerTableCompanion.insert(
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
description: player.description,
|
||||
createdAt: player.createdAt,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
playersToInsert,
|
||||
mode: InsertMode.insertOrReplace,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -90,7 +143,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Checks if a player with the given [id] exists in the database.
|
||||
/// Checks if a player with the given [playerId] exists in the database.
|
||||
/// Returns `true` if the player exists, `false` otherwise.
|
||||
Future<bool> playerExists({required String playerId}) async {
|
||||
final query = select(playerTable)..where((p) => p.id.equals(playerId));
|
||||
@@ -103,9 +156,38 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
required String playerId,
|
||||
required String newName,
|
||||
}) async {
|
||||
// Get previous name and name count for the player before updating
|
||||
final previousPlayerName =
|
||||
await (select(playerTable)..where((p) => p.id.equals(playerId)))
|
||||
.map((row) => row.name)
|
||||
.getSingleOrNull() ??
|
||||
'';
|
||||
final previousNameCount = await getNameCount(name: previousPlayerName);
|
||||
|
||||
await (update(playerTable)..where((p) => p.id.equals(playerId))).write(
|
||||
PlayerTableCompanion(name: Value(newName)),
|
||||
);
|
||||
|
||||
// Update name count for the new name
|
||||
final count = await calculateNameCount(name: newName);
|
||||
if (count > 0) {
|
||||
await (update(playerTable)..where((p) => p.name.equals(newName))).write(
|
||||
PlayerTableCompanion(nameCount: Value(count)),
|
||||
);
|
||||
}
|
||||
|
||||
if (previousNameCount > 0) {
|
||||
// Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount
|
||||
final player = await getPlayerWithHighestNameCount(
|
||||
name: previousPlayerName,
|
||||
);
|
||||
if (player != null) {
|
||||
await updateNameCount(
|
||||
playerId: player.id,
|
||||
nameCount: previousNameCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the total count of players in the database.
|
||||
@@ -117,6 +199,76 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
/// Retrieves the count of players with the given [name].
|
||||
Future<int> getNameCount({required String name}) async {
|
||||
final query = select(playerTable)..where((p) => p.name.equals(name));
|
||||
final result = await query.get();
|
||||
return result.length;
|
||||
}
|
||||
|
||||
/// Updates the nameCount for the player with the given [playerId] to [nameCount].
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> updateNameCount({
|
||||
required String playerId,
|
||||
required int nameCount,
|
||||
}) async {
|
||||
final query = update(playerTable)..where((p) => p.id.equals(playerId));
|
||||
final rowsAffected = await query.write(
|
||||
PlayerTableCompanion(nameCount: Value(nameCount)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<Player?> getPlayerWithHighestNameCount({required String name}) async {
|
||||
final query = select(playerTable)
|
||||
..where((p) => p.name.equals(name))
|
||||
..orderBy([(p) => OrderingTerm.desc(p.nameCount)])
|
||||
..limit(1);
|
||||
final result = await query.getSingleOrNull();
|
||||
if (result != null) {
|
||||
return Player(
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
description: result.description,
|
||||
createdAt: result.createdAt,
|
||||
nameCount: result.nameCount,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<int> calculateNameCount({required String name}) async {
|
||||
final count = await getNameCount(name: name);
|
||||
final int nameCount;
|
||||
|
||||
if (count == 1) {
|
||||
// If one other player exists with the same name, initialize the nameCount
|
||||
await initializeNameCount(name: name);
|
||||
// And for the new player, set nameCount to 2
|
||||
nameCount = 2;
|
||||
} else if (count > 1) {
|
||||
// If more than one player exists with the same name, just increment
|
||||
// the nameCount for the new player
|
||||
nameCount = count + 1;
|
||||
} else {
|
||||
// If no other players exist with the same name, set nameCount to 0
|
||||
nameCount = 0;
|
||||
}
|
||||
|
||||
return nameCount;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<bool> initializeNameCount({required String name}) async {
|
||||
final rowsAffected =
|
||||
await (update(playerTable)..where((p) => p.name.equals(name))).write(
|
||||
const PlayerTableCompanion(nameCount: Value(1)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Deletes all players from the database.
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> deleteAllPlayers() async {
|
||||
|
||||
@@ -24,7 +24,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
||||
matchId: matchId,
|
||||
teamId: Value(teamId),
|
||||
),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -83,21 +83,21 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
}
|
||||
|
||||
/// Retrieves all scores for a specific match.
|
||||
Future<Map<String, List<ScoreEntry>>> getAllMatchScores({
|
||||
Future<Map<String, ScoreEntry?>> getAllMatchScores({
|
||||
required String matchId,
|
||||
}) async {
|
||||
final query = select(scoreEntryTable)
|
||||
..where((s) => s.matchId.equals(matchId));
|
||||
final result = await query.get();
|
||||
|
||||
final Map<String, List<ScoreEntry>> scoresByPlayer = {};
|
||||
final Map<String, ScoreEntry?> scoresByPlayer = {};
|
||||
for (final row in result) {
|
||||
final score = ScoreEntry(
|
||||
roundNumber: row.roundNumber,
|
||||
score: row.score,
|
||||
change: row.change,
|
||||
);
|
||||
scoresByPlayer.putIfAbsent(row.playerId, () => []).add(score);
|
||||
scoresByPlayer[row.playerId] = score;
|
||||
}
|
||||
|
||||
return scoresByPlayer;
|
||||
@@ -235,22 +235,29 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
// Retrieves the winner of a match based on the highest score.
|
||||
// Retrieves the winner of a match by looking for a score entry where score
|
||||
/// is 1. Returns `null` if no player found, else the first with the score.
|
||||
Future<Player?> getWinner({required String matchId}) async {
|
||||
final query = select(scoreEntryTable)
|
||||
..where((s) => s.matchId.equals(matchId))
|
||||
..orderBy([(s) => OrderingTerm.desc(s.score)])
|
||||
..limit(1);
|
||||
final result = await query.getSingleOrNull();
|
||||
final query =
|
||||
select(scoreEntryTable).join([
|
||||
innerJoin(
|
||||
db.playerTable,
|
||||
db.playerTable.id.equalsExp(scoreEntryTable.playerId),
|
||||
),
|
||||
])..where(
|
||||
scoreEntryTable.matchId.equals(matchId) &
|
||||
scoreEntryTable.score.equals(1),
|
||||
);
|
||||
|
||||
if (result == null) return null;
|
||||
final result = await query.get();
|
||||
if (result.isEmpty) return null;
|
||||
|
||||
final player = await db.playerDao.getPlayerById(playerId: result.playerId);
|
||||
final playerData = result.first.readTable(db.playerTable);
|
||||
return Player(
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
createdAt: player.createdAt,
|
||||
description: player.description,
|
||||
id: playerData.id,
|
||||
name: playerData.name,
|
||||
createdAt: playerData.createdAt,
|
||||
description: playerData.description,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -295,20 +302,29 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Retrieves the looser of a match based on the score 0.
|
||||
/// Retrieves the looser of a match by looking for a score entry where score
|
||||
/// is 0. Returns `null` if no player found, else the first with the score.
|
||||
Future<Player?> getLooser({required String matchId}) async {
|
||||
final query = select(scoreEntryTable)
|
||||
..where((s) => s.matchId.equals(matchId) & s.score.equals(0));
|
||||
final result = await query.getSingleOrNull();
|
||||
final query =
|
||||
select(scoreEntryTable).join([
|
||||
innerJoin(
|
||||
db.playerTable,
|
||||
db.playerTable.id.equalsExp(scoreEntryTable.playerId),
|
||||
),
|
||||
])..where(
|
||||
scoreEntryTable.matchId.equals(matchId) &
|
||||
scoreEntryTable.score.equals(0),
|
||||
);
|
||||
|
||||
if (result == null) return null;
|
||||
final result = await query.get();
|
||||
if (result.isEmpty) return null;
|
||||
|
||||
final player = await db.playerDao.getPlayerById(playerId: result.playerId);
|
||||
final playerData = result.first.readTable(db.playerTable);
|
||||
return Player(
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
createdAt: player.createdAt,
|
||||
description: player.description,
|
||||
id: playerData.id,
|
||||
name: playerData.name,
|
||||
createdAt: playerData.createdAt,
|
||||
description: playerData.description,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,17 @@ class $PlayerTableTable extends PlayerTable
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
||||
'createdAt',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||
'created_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _nameMeta = const VerificationMeta('name');
|
||||
@override
|
||||
late final GeneratedColumn<String> name = GeneratedColumn<String>(
|
||||
@@ -27,6 +38,18 @@ class $PlayerTableTable extends PlayerTable
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _nameCountMeta = const VerificationMeta(
|
||||
'nameCount',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<int> nameCount = GeneratedColumn<int>(
|
||||
'name_count',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const Constant(0),
|
||||
);
|
||||
static const VerificationMeta _descriptionMeta = const VerificationMeta(
|
||||
'description',
|
||||
);
|
||||
@@ -38,19 +61,14 @@ class $PlayerTableTable extends PlayerTable
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
||||
'createdAt',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||
'created_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id, name, description, createdAt];
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
createdAt,
|
||||
name,
|
||||
nameCount,
|
||||
description,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
@@ -68,6 +86,14 @@ class $PlayerTableTable extends PlayerTable
|
||||
} else if (isInserting) {
|
||||
context.missing(_idMeta);
|
||||
}
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(
|
||||
_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_createdAtMeta);
|
||||
}
|
||||
if (data.containsKey('name')) {
|
||||
context.handle(
|
||||
_nameMeta,
|
||||
@@ -76,6 +102,12 @@ class $PlayerTableTable extends PlayerTable
|
||||
} else if (isInserting) {
|
||||
context.missing(_nameMeta);
|
||||
}
|
||||
if (data.containsKey('name_count')) {
|
||||
context.handle(
|
||||
_nameCountMeta,
|
||||
nameCount.isAcceptableOrUnknown(data['name_count']!, _nameCountMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('description')) {
|
||||
context.handle(
|
||||
_descriptionMeta,
|
||||
@@ -87,14 +119,6 @@ class $PlayerTableTable extends PlayerTable
|
||||
} else if (isInserting) {
|
||||
context.missing(_descriptionMeta);
|
||||
}
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(
|
||||
_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_createdAtMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -108,18 +132,22 @@ class $PlayerTableTable extends PlayerTable
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}id'],
|
||||
)!,
|
||||
createdAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}created_at'],
|
||||
)!,
|
||||
name: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}name'],
|
||||
)!,
|
||||
nameCount: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}name_count'],
|
||||
)!,
|
||||
description: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}description'],
|
||||
)!,
|
||||
createdAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}created_at'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,31 +159,35 @@ class $PlayerTableTable extends PlayerTable
|
||||
|
||||
class PlayerTableData extends DataClass implements Insertable<PlayerTableData> {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final DateTime createdAt;
|
||||
final String name;
|
||||
final int nameCount;
|
||||
final String description;
|
||||
const PlayerTableData({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.createdAt,
|
||||
required this.name,
|
||||
required this.nameCount,
|
||||
required this.description,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<String>(id);
|
||||
map['name'] = Variable<String>(name);
|
||||
map['description'] = Variable<String>(description);
|
||||
map['created_at'] = Variable<DateTime>(createdAt);
|
||||
map['name'] = Variable<String>(name);
|
||||
map['name_count'] = Variable<int>(nameCount);
|
||||
map['description'] = Variable<String>(description);
|
||||
return map;
|
||||
}
|
||||
|
||||
PlayerTableCompanion toCompanion(bool nullToAbsent) {
|
||||
return PlayerTableCompanion(
|
||||
id: Value(id),
|
||||
name: Value(name),
|
||||
description: Value(description),
|
||||
createdAt: Value(createdAt),
|
||||
name: Value(name),
|
||||
nameCount: Value(nameCount),
|
||||
description: Value(description),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,9 +198,10 @@ class PlayerTableData extends DataClass implements Insertable<PlayerTableData> {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return PlayerTableData(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
description: serializer.fromJson<String>(json['description']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
nameCount: serializer.fromJson<int>(json['nameCount']),
|
||||
description: serializer.fromJson<String>(json['description']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -176,31 +209,35 @@ class PlayerTableData extends DataClass implements Insertable<PlayerTableData> {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'description': serializer.toJson<String>(description),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'nameCount': serializer.toJson<int>(nameCount),
|
||||
'description': serializer.toJson<String>(description),
|
||||
};
|
||||
}
|
||||
|
||||
PlayerTableData copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? description,
|
||||
DateTime? createdAt,
|
||||
String? name,
|
||||
int? nameCount,
|
||||
String? description,
|
||||
}) => PlayerTableData(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
name: name ?? this.name,
|
||||
nameCount: nameCount ?? this.nameCount,
|
||||
description: description ?? this.description,
|
||||
);
|
||||
PlayerTableData copyWithCompanion(PlayerTableCompanion data) {
|
||||
return PlayerTableData(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
name: data.name.present ? data.name.value : this.name,
|
||||
nameCount: data.nameCount.present ? data.nameCount.value : this.nameCount,
|
||||
description: data.description.present
|
||||
? data.description.value
|
||||
: this.description,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -208,76 +245,85 @@ class PlayerTableData extends DataClass implements Insertable<PlayerTableData> {
|
||||
String toString() {
|
||||
return (StringBuffer('PlayerTableData(')
|
||||
..write('id: $id, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('name: $name, ')
|
||||
..write('description: $description, ')
|
||||
..write('createdAt: $createdAt')
|
||||
..write('nameCount: $nameCount, ')
|
||||
..write('description: $description')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, name, description, createdAt);
|
||||
int get hashCode => Object.hash(id, createdAt, name, nameCount, description);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is PlayerTableData &&
|
||||
other.id == this.id &&
|
||||
other.createdAt == this.createdAt &&
|
||||
other.name == this.name &&
|
||||
other.description == this.description &&
|
||||
other.createdAt == this.createdAt);
|
||||
other.nameCount == this.nameCount &&
|
||||
other.description == this.description);
|
||||
}
|
||||
|
||||
class PlayerTableCompanion extends UpdateCompanion<PlayerTableData> {
|
||||
final Value<String> id;
|
||||
final Value<String> name;
|
||||
final Value<String> description;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<String> name;
|
||||
final Value<int> nameCount;
|
||||
final Value<String> description;
|
||||
final Value<int> rowid;
|
||||
const PlayerTableCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.name = const Value.absent(),
|
||||
this.description = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.name = const Value.absent(),
|
||||
this.nameCount = const Value.absent(),
|
||||
this.description = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
PlayerTableCompanion.insert({
|
||||
required String id,
|
||||
required String name,
|
||||
required String description,
|
||||
required DateTime createdAt,
|
||||
required String name,
|
||||
this.nameCount = const Value.absent(),
|
||||
required String description,
|
||||
this.rowid = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
createdAt = Value(createdAt),
|
||||
name = Value(name),
|
||||
description = Value(description),
|
||||
createdAt = Value(createdAt);
|
||||
description = Value(description);
|
||||
static Insertable<PlayerTableData> custom({
|
||||
Expression<String>? id,
|
||||
Expression<String>? name,
|
||||
Expression<String>? description,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<String>? name,
|
||||
Expression<int>? nameCount,
|
||||
Expression<String>? description,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (name != null) 'name': name,
|
||||
if (description != null) 'description': description,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
if (name != null) 'name': name,
|
||||
if (nameCount != null) 'name_count': nameCount,
|
||||
if (description != null) 'description': description,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
PlayerTableCompanion copyWith({
|
||||
Value<String>? id,
|
||||
Value<String>? name,
|
||||
Value<String>? description,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<String>? name,
|
||||
Value<int>? nameCount,
|
||||
Value<String>? description,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return PlayerTableCompanion(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
name: name ?? this.name,
|
||||
nameCount: nameCount ?? this.nameCount,
|
||||
description: description ?? this.description,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
@@ -288,15 +334,18 @@ class PlayerTableCompanion extends UpdateCompanion<PlayerTableData> {
|
||||
if (id.present) {
|
||||
map['id'] = Variable<String>(id.value);
|
||||
}
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
if (name.present) {
|
||||
map['name'] = Variable<String>(name.value);
|
||||
}
|
||||
if (nameCount.present) {
|
||||
map['name_count'] = Variable<int>(nameCount.value);
|
||||
}
|
||||
if (description.present) {
|
||||
map['description'] = Variable<String>(description.value);
|
||||
}
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
@@ -307,9 +356,10 @@ class PlayerTableCompanion extends UpdateCompanion<PlayerTableData> {
|
||||
String toString() {
|
||||
return (StringBuffer('PlayerTableCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('name: $name, ')
|
||||
..write('description: $description, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('name: $name, ')
|
||||
..write('nameCount: $nameCount, ')
|
||||
..write('description: $description, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
@@ -2790,17 +2840,19 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
typedef $$PlayerTableTableCreateCompanionBuilder =
|
||||
PlayerTableCompanion Function({
|
||||
required String id,
|
||||
required String name,
|
||||
required String description,
|
||||
required DateTime createdAt,
|
||||
required String name,
|
||||
Value<int> nameCount,
|
||||
required String description,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$PlayerTableTableUpdateCompanionBuilder =
|
||||
PlayerTableCompanion Function({
|
||||
Value<String> id,
|
||||
Value<String> name,
|
||||
Value<String> description,
|
||||
Value<DateTime> createdAt,
|
||||
Value<String> name,
|
||||
Value<int> nameCount,
|
||||
Value<String> description,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
@@ -2892,18 +2944,23 @@ class $$PlayerTableTableFilterComposer
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get name => $composableBuilder(
|
||||
column: $table.name,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get description => $composableBuilder(
|
||||
column: $table.description,
|
||||
ColumnFilters<int> get nameCount => $composableBuilder(
|
||||
column: $table.nameCount,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
ColumnFilters<String> get description => $composableBuilder(
|
||||
column: $table.description,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
@@ -2997,18 +3054,23 @@ class $$PlayerTableTableOrderingComposer
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get name => $composableBuilder(
|
||||
column: $table.name,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get description => $composableBuilder(
|
||||
column: $table.description,
|
||||
ColumnOrderings<int> get nameCount => $composableBuilder(
|
||||
column: $table.nameCount,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
ColumnOrderings<String> get description => $composableBuilder(
|
||||
column: $table.description,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
@@ -3025,17 +3087,20 @@ class $$PlayerTableTableAnnotationComposer
|
||||
GeneratedColumn<String> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get name =>
|
||||
$composableBuilder(column: $table.name, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get nameCount =>
|
||||
$composableBuilder(column: $table.nameCount, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get description => $composableBuilder(
|
||||
column: $table.description,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
Expression<T> playerGroupTableRefs<T extends Object>(
|
||||
Expression<T> Function($$PlayerGroupTableTableAnnotationComposer a) f,
|
||||
) {
|
||||
@@ -3145,29 +3210,33 @@ class $$PlayerTableTableTableManager
|
||||
updateCompanionCallback:
|
||||
({
|
||||
Value<String> id = const Value.absent(),
|
||||
Value<String> name = const Value.absent(),
|
||||
Value<String> description = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<String> name = const Value.absent(),
|
||||
Value<int> nameCount = const Value.absent(),
|
||||
Value<String> description = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => PlayerTableCompanion(
|
||||
id: id,
|
||||
name: name,
|
||||
description: description,
|
||||
createdAt: createdAt,
|
||||
name: name,
|
||||
nameCount: nameCount,
|
||||
description: description,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String id,
|
||||
required String name,
|
||||
required String description,
|
||||
required DateTime createdAt,
|
||||
required String name,
|
||||
Value<int> nameCount = const Value.absent(),
|
||||
required String description,
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => PlayerTableCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
description: description,
|
||||
createdAt: createdAt,
|
||||
name: name,
|
||||
nameCount: nameCount,
|
||||
description: description,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
|
||||
@@ -2,9 +2,10 @@ import 'package:drift/drift.dart';
|
||||
|
||||
class PlayerTable extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get description => text()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
TextColumn get name => text()();
|
||||
IntColumn get nameCount => integer().withDefault(const Constant(0))();
|
||||
TextColumn get description => text()();
|
||||
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class Game {
|
||||
final String id;
|
||||
@@ -33,7 +33,10 @@ class Game {
|
||||
: id = json['id'],
|
||||
createdAt = DateTime.parse(json['createdAt']),
|
||||
name = json['name'],
|
||||
ruleset = Ruleset.values.firstWhere((e) => e.name == json['ruleset']),
|
||||
ruleset = Ruleset.values.firstWhere(
|
||||
(e) => e.name == json['ruleset'],
|
||||
orElse: () => Ruleset.singleWinner,
|
||||
),
|
||||
description = json['description'],
|
||||
color = GameColor.values.firstWhere((e) => e.name == json['color']),
|
||||
icon = json['icon'];
|
||||
@@ -49,4 +52,3 @@ class Game {
|
||||
'icon': icon,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,27 +15,25 @@ class Match {
|
||||
final Group? group;
|
||||
final List<Player> players;
|
||||
final String notes;
|
||||
Map<String, List<ScoreEntry>> scores;
|
||||
Player? winner;
|
||||
Map<String, ScoreEntry?> scores;
|
||||
|
||||
Match({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
this.endedAt,
|
||||
required this.name,
|
||||
required this.game,
|
||||
required this.players,
|
||||
this.endedAt,
|
||||
this.group,
|
||||
this.players = const [],
|
||||
this.notes = '',
|
||||
Map<String, List<ScoreEntry>>? scores,
|
||||
this.winner,
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
Map<String, ScoreEntry?>? scores,
|
||||
}) : id = id ?? const Uuid().v4(),
|
||||
createdAt = createdAt ?? clock.now(),
|
||||
scores = scores ?? {for (var player in players) player.id: []};
|
||||
scores = scores ?? {for (Player p in players) p.id: null};
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, winner: $winner}';
|
||||
return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, mvp: $mvp}';
|
||||
}
|
||||
|
||||
/// Creates a Match instance from a JSON object where related objects are
|
||||
@@ -57,7 +55,16 @@ class Match {
|
||||
),
|
||||
group = null,
|
||||
players = [],
|
||||
scores = json['scores'],
|
||||
scores = json['scores'] != null
|
||||
? (json['scores'] as Map<String, dynamic>).map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
value != null
|
||||
? ScoreEntry.fromJson(value as Map<String, dynamic>)
|
||||
: null,
|
||||
),
|
||||
)
|
||||
: {},
|
||||
notes = json['notes'] ?? '';
|
||||
|
||||
/// Converts the Match instance to a JSON object. Related objects are
|
||||
@@ -71,10 +78,62 @@ class Match {
|
||||
'gameId': game.id,
|
||||
'groupId': group?.id,
|
||||
'playerIds': players.map((player) => player.id).toList(),
|
||||
'scores': scores.map(
|
||||
(playerId, scoreList) =>
|
||||
MapEntry(playerId, scoreList.map((score) => score.toJson()).toList()),
|
||||
),
|
||||
'scores': scores.map((key, value) => MapEntry(key, value?.toJson())),
|
||||
'notes': notes,
|
||||
};
|
||||
|
||||
List<Player> get mvp {
|
||||
if (players.isEmpty || scores.isEmpty) return [];
|
||||
|
||||
switch (game.ruleset) {
|
||||
case Ruleset.highestScore:
|
||||
return _getPlayersWithHighestScore();
|
||||
|
||||
case Ruleset.lowestScore:
|
||||
return _getPlayersWithLowestScore();
|
||||
|
||||
case Ruleset.singleWinner:
|
||||
return _getPlayersWithHighestScore().take(1).toList();
|
||||
|
||||
case Ruleset.singleLoser:
|
||||
return _getPlayersWithLowestScore().take(1).toList();
|
||||
|
||||
case Ruleset.multipleWinners:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
List<Player> _getPlayersWithHighestScore() {
|
||||
if (players.isEmpty || scores.values.every((score) => score == null)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final int highestScore = players
|
||||
.map((player) => scores[player.id]?.score)
|
||||
.whereType<int>()
|
||||
.reduce((max, score) => score > max ? score : max);
|
||||
|
||||
return players.where((player) {
|
||||
final playerScores = scores[player.id];
|
||||
if (playerScores == null) return false;
|
||||
return playerScores.score == highestScore;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<Player> _getPlayersWithLowestScore() {
|
||||
if (players.isEmpty || scores.values.every((score) => score == null)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final int lowestScore = players
|
||||
.map((player) => scores[player.id]?.score)
|
||||
.whereType<int>()
|
||||
.reduce((min, score) => score < min ? score : min);
|
||||
|
||||
return players.where((player) {
|
||||
final playerScore = scores[player.id];
|
||||
if (playerScore == null) return false;
|
||||
return playerScore.score == lowestScore;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ class Player {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final String name;
|
||||
int nameCount;
|
||||
final String description;
|
||||
|
||||
Player({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
required this.name,
|
||||
this.nameCount = 0,
|
||||
String? description,
|
||||
}) : id = id ?? const Uuid().v4(),
|
||||
createdAt = createdAt ?? clock.now(),
|
||||
@@ -18,7 +20,7 @@ class Player {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Player{id: $id, name: $name, description: $description}';
|
||||
return 'Player{id: $id, createdAt: $createdAt, name: $name, nameCount: $nameCount, description: $description}';
|
||||
}
|
||||
|
||||
/// Creates a Player instance from a JSON object.
|
||||
@@ -26,6 +28,7 @@ class Player {
|
||||
: id = json['id'],
|
||||
createdAt = DateTime.parse(json['createdAt']),
|
||||
name = json['name'],
|
||||
nameCount = 0,
|
||||
description = json['description'];
|
||||
|
||||
/// Converts the Player instance to a JSON object.
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
class ScoreEntry {
|
||||
int roundNumber = 0;
|
||||
final int roundNumber;
|
||||
final int score;
|
||||
final int change;
|
||||
|
||||
ScoreEntry({
|
||||
required this.roundNumber,
|
||||
required this.score,
|
||||
required this.change,
|
||||
});
|
||||
ScoreEntry({required this.score, this.roundNumber = 0, this.change = 0});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ScoreEntry{roundNumber: $roundNumber, score: $score, change: $change}';
|
||||
}
|
||||
|
||||
ScoreEntry.fromJson(Map<String, dynamic> json)
|
||||
: roundNumber = json['roundNumber'],
|
||||
|
||||
@@ -22,10 +22,11 @@
|
||||
"days_ago": "vor {count} Tagen",
|
||||
"delete": "Löschen",
|
||||
"delete_all_data": "Alle Daten löschen",
|
||||
"delete_group": "Diese Gruppe löschen",
|
||||
"delete_group": "Gruppe löschen",
|
||||
"delete_match": "Spiel löschen",
|
||||
"edit_group": "Gruppe bearbeiten",
|
||||
"edit_match": "Gruppe bearbeiten",
|
||||
"enter_points": "Punkte eingeben",
|
||||
"enter_results": "Ergebnisse eintragen",
|
||||
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
|
||||
"error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen",
|
||||
@@ -74,6 +75,8 @@
|
||||
"player_name": "Spieler:innenname",
|
||||
"players": "Spieler:innen",
|
||||
"players_count": "{count} Spieler",
|
||||
"point": "Punkt",
|
||||
"points": "Punkte",
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"quick_create": "Schnellzugriff",
|
||||
"recent_matches": "Letzte Spiele",
|
||||
@@ -87,12 +90,14 @@
|
||||
"save_changes": "Änderungen speichern",
|
||||
"search_for_groups": "Nach Gruppen suchen",
|
||||
"search_for_players": "Nach Spieler:innen suchen",
|
||||
"select_winner": "Gewinner:in wählen:",
|
||||
"select_winner": "Gewinner:in wählen",
|
||||
"select_loser": "Verlierer:in wählen",
|
||||
"selected_players": "Ausgewählte Spieler:innen",
|
||||
"settings": "Einstellungen",
|
||||
"single_loser": "Ein:e Verlierer:in",
|
||||
"single_winner": "Ein:e Gewinner:in",
|
||||
"highest_score": "Höchste Punkte",
|
||||
"loser": "Verlierer:in",
|
||||
"lowest_score": "Niedrigste Punkte",
|
||||
"multiple_winners": "Mehrere Gewinner:innen",
|
||||
"statistics": "Statistiken",
|
||||
@@ -100,6 +105,7 @@
|
||||
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
|
||||
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht",
|
||||
"this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.",
|
||||
"tie": "Unentschieden",
|
||||
"today_at": "Heute um",
|
||||
"undo": "Rückgängig",
|
||||
"unknown_exception": "Unbekannter Fehler (siehe Konsole)",
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
"@edit_match": {
|
||||
"description": "Button & Appbar label for editing a match"
|
||||
},
|
||||
"@enter_points": {
|
||||
"description": "Label to enter players points"
|
||||
},
|
||||
"@enter_results": {
|
||||
"description": "Button text to enter match results"
|
||||
},
|
||||
@@ -232,6 +235,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@points": {
|
||||
"description": "Points label"
|
||||
},
|
||||
"@privacy_policy": {
|
||||
"description": "Privacy policy menu item"
|
||||
},
|
||||
@@ -271,6 +277,9 @@
|
||||
"@select_winner": {
|
||||
"description": "Label to select the winner"
|
||||
},
|
||||
"@select_loser": {
|
||||
"description": "Label to select the loser"
|
||||
},
|
||||
"@selected_players": {
|
||||
"description": "Shows the number of selected players"
|
||||
},
|
||||
@@ -351,6 +360,7 @@
|
||||
"delete_match": "Delete Match",
|
||||
"edit_group": "Edit Group",
|
||||
"edit_match": "Edit Match",
|
||||
"enter_points": "Enter points",
|
||||
"enter_results": "Enter Results",
|
||||
"error_creating_group": "Error while creating group, please try again",
|
||||
"error_deleting_group": "Error while deleting group, please try again",
|
||||
@@ -399,6 +409,8 @@
|
||||
"player_name": "Player name",
|
||||
"players": "Players",
|
||||
"players_count": "{count} Players",
|
||||
"point": "Point",
|
||||
"points": "Points",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"quick_create": "Quick Create",
|
||||
"recent_matches": "Recent Matches",
|
||||
@@ -411,12 +423,14 @@
|
||||
"save_changes": "Save Changes",
|
||||
"search_for_groups": "Search for groups",
|
||||
"search_for_players": "Search for players",
|
||||
"select_winner": "Select Winner:",
|
||||
"select_winner": "Select Winner",
|
||||
"select_loser": "Select Loser",
|
||||
"selected_players": "Selected players",
|
||||
"settings": "Settings",
|
||||
"single_loser": "Single Loser",
|
||||
"single_winner": "Single Winner",
|
||||
"highest_score": "Highest Score",
|
||||
"loser": "Loser",
|
||||
"lowest_score": "Lowest Score",
|
||||
"multiple_winners": "Multiple Winners",
|
||||
"statistics": "Statistics",
|
||||
@@ -424,6 +438,7 @@
|
||||
"successfully_added_player": "Successfully added player {playerName}",
|
||||
"there_is_no_group_matching_your_search": "There is no group matching your search",
|
||||
"this_cannot_be_undone": "This can't be undone.",
|
||||
"tie": "Tie",
|
||||
"today_at": "Today at",
|
||||
"undo": "Undo",
|
||||
"unknown_exception": "Unknown Exception (see console)",
|
||||
|
||||
@@ -254,6 +254,12 @@ abstract class AppLocalizations {
|
||||
/// **'Edit Match'**
|
||||
String get edit_match;
|
||||
|
||||
/// Label to enter players points
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter points'**
|
||||
String get enter_points;
|
||||
|
||||
/// Button text to enter match results
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -542,6 +548,18 @@ abstract class AppLocalizations {
|
||||
/// **'{count} Players'**
|
||||
String players_count(int count);
|
||||
|
||||
/// No description provided for @point.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Point'**
|
||||
String get point;
|
||||
|
||||
/// Points label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Points'**
|
||||
String get points;
|
||||
|
||||
/// Privacy policy menu item
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -617,9 +635,15 @@ abstract class AppLocalizations {
|
||||
/// Label to select the winner
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select Winner:'**
|
||||
/// **'Select Winner'**
|
||||
String get select_winner;
|
||||
|
||||
/// Label to select the loser
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select Loser'**
|
||||
String get select_loser;
|
||||
|
||||
/// Shows the number of selected players
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -650,6 +674,12 @@ abstract class AppLocalizations {
|
||||
/// **'Highest Score'**
|
||||
String get highest_score;
|
||||
|
||||
/// No description provided for @loser.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Loser'**
|
||||
String get loser;
|
||||
|
||||
/// No description provided for @lowest_score.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -692,6 +722,12 @@ abstract class AppLocalizations {
|
||||
/// **'This can\'t be undone.'**
|
||||
String get this_cannot_be_undone;
|
||||
|
||||
/// No description provided for @tie.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tie'**
|
||||
String get tie;
|
||||
|
||||
/// Date format for today
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -79,7 +79,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get delete_all_data => 'Alle Daten löschen';
|
||||
|
||||
@override
|
||||
String get delete_group => 'Diese Gruppe löschen';
|
||||
String get delete_group => 'Gruppe löschen';
|
||||
|
||||
@override
|
||||
String get delete_match => 'Spiel löschen';
|
||||
@@ -90,6 +90,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get edit_match => 'Gruppe bearbeiten';
|
||||
|
||||
@override
|
||||
String get enter_points => 'Punkte eingeben';
|
||||
|
||||
@override
|
||||
String get enter_results => 'Ergebnisse eintragen';
|
||||
|
||||
@@ -240,6 +243,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return '$count Spieler';
|
||||
}
|
||||
|
||||
@override
|
||||
String get point => 'Punkt';
|
||||
|
||||
@override
|
||||
String get points => 'Punkte';
|
||||
|
||||
@override
|
||||
String get privacy_policy => 'Datenschutzerklärung';
|
||||
|
||||
@@ -281,7 +290,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get search_for_players => 'Nach Spieler:innen suchen';
|
||||
|
||||
@override
|
||||
String get select_winner => 'Gewinner:in wählen:';
|
||||
String get select_winner => 'Gewinner:in wählen';
|
||||
|
||||
@override
|
||||
String get select_loser => 'Verlierer:in wählen';
|
||||
|
||||
@override
|
||||
String get selected_players => 'Ausgewählte Spieler:innen';
|
||||
@@ -298,6 +310,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get highest_score => 'Höchste Punkte';
|
||||
|
||||
@override
|
||||
String get loser => 'Verlierer:in';
|
||||
|
||||
@override
|
||||
String get lowest_score => 'Niedrigste Punkte';
|
||||
|
||||
@@ -323,6 +338,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get this_cannot_be_undone =>
|
||||
'Dies kann nicht rückgängig gemacht werden.';
|
||||
|
||||
@override
|
||||
String get tie => 'Unentschieden';
|
||||
|
||||
@override
|
||||
String get today_at => 'Heute um';
|
||||
|
||||
|
||||
@@ -90,6 +90,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get edit_match => 'Edit Match';
|
||||
|
||||
@override
|
||||
String get enter_points => 'Enter points';
|
||||
|
||||
@override
|
||||
String get enter_results => 'Enter Results';
|
||||
|
||||
@@ -240,6 +243,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return '$count Players';
|
||||
}
|
||||
|
||||
@override
|
||||
String get point => 'Point';
|
||||
|
||||
@override
|
||||
String get points => 'Points';
|
||||
|
||||
@override
|
||||
String get privacy_policy => 'Privacy Policy';
|
||||
|
||||
@@ -281,7 +290,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get search_for_players => 'Search for players';
|
||||
|
||||
@override
|
||||
String get select_winner => 'Select Winner:';
|
||||
String get select_winner => 'Select Winner';
|
||||
|
||||
@override
|
||||
String get select_loser => 'Select Loser';
|
||||
|
||||
@override
|
||||
String get selected_players => 'Selected players';
|
||||
@@ -298,6 +310,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get highest_score => 'Highest Score';
|
||||
|
||||
@override
|
||||
String get loser => 'Loser';
|
||||
|
||||
@override
|
||||
String get lowest_score => 'Lowest Score';
|
||||
|
||||
@@ -322,6 +337,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get this_cannot_be_undone => 'This can\'t be undone.';
|
||||
|
||||
@override
|
||||
String get tie => 'Tie';
|
||||
|
||||
@override
|
||||
String get today_at => 'Today at';
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
final groupName = _groupNameController.text.trim();
|
||||
|
||||
final success = await db.groupDao.addGroup(
|
||||
group: Group(name: groupName, description: '', members: selectedPlayers),
|
||||
group: Group(name: groupName, members: selectedPlayers),
|
||||
);
|
||||
|
||||
return success;
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/adaptive_page_route.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
@@ -10,10 +12,10 @@ import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart';
|
||||
import 'package:tallee/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/animated_dialog_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/custom_alert_dialog.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/tiles/info_tile.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
@@ -70,23 +72,16 @@ class _GroupDetailViewState extends State<GroupDetailView> {
|
||||
context: context,
|
||||
builder: (context) => CustomAlertDialog(
|
||||
title: '${loc.delete_group}?',
|
||||
content: loc.this_cannot_be_undone,
|
||||
content: Text(loc.this_cannot_be_undone),
|
||||
actions: [
|
||||
AnimatedDialogButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(
|
||||
loc.cancel,
|
||||
style: const TextStyle(color: CustomTheme.textColor),
|
||||
),
|
||||
),
|
||||
AnimatedDialogButton(
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(
|
||||
loc.delete,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.secondaryColor,
|
||||
),
|
||||
),
|
||||
text: loc.delete,
|
||||
),
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
buttonType: ButtonType.secondary,
|
||||
text: loc.cancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -153,6 +148,7 @@ class _GroupDetailViewState extends State<GroupDetailView> {
|
||||
children: _group.members.map((member) {
|
||||
return TextIconTile(
|
||||
text: member.name,
|
||||
suffixText: getNameCountText(member),
|
||||
iconEnabled: false,
|
||||
);
|
||||
}).toList(),
|
||||
@@ -259,28 +255,37 @@ class _GroupDetailViewState extends State<GroupDetailView> {
|
||||
|
||||
/// Determines the best player in the group based on match wins
|
||||
String _getBestPlayer(List<Match> matches) {
|
||||
final bestPlayerCounts = <Player, int>{};
|
||||
final mvpCounts = <Player, int>{};
|
||||
|
||||
// Count wins for each player
|
||||
for (var match in matches) {
|
||||
if (match.winner != null &&
|
||||
_group.members.any((m) => m.id == match.winner?.id)) {
|
||||
print(match.winner);
|
||||
bestPlayerCounts.update(
|
||||
match.winner!,
|
||||
(value) => value + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
final mvps = match.mvp;
|
||||
for (final mvpPlayer in mvps) {
|
||||
if (_group.members.any((m) => m.id == mvpPlayer.id)) {
|
||||
mvpCounts.update(mvpPlayer, (value) => value + 1, ifAbsent: () => 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort players by win count
|
||||
final sortedPlayers = bestPlayerCounts.entries.toList()
|
||||
final sortedMvps = mvpCounts.entries.toList()
|
||||
..sort((a, b) => b.value.compareTo(a.value));
|
||||
|
||||
// Get the best player
|
||||
bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-';
|
||||
if (sortedMvps.isEmpty) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return bestPlayer;
|
||||
// Check if there are multiple players with the same value
|
||||
final highestMvpCount = sortedMvps.first.value;
|
||||
final topPlayers = sortedMvps
|
||||
.where((entry) => entry.value == highestMvpCount)
|
||||
.toList();
|
||||
switch (topPlayers.length) {
|
||||
case 0:
|
||||
return '-';
|
||||
case 1:
|
||||
return topPlayers.first.key.name;
|
||||
default:
|
||||
final loc = AppLocalizations.of(context);
|
||||
return loc.tie;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class _GroupViewState extends State<GroupView> {
|
||||
Group(
|
||||
name: 'Skeleton Group',
|
||||
description: '',
|
||||
members: List.filled(6, Player(name: 'Skeleton Player', description: '')),
|
||||
members: List.filled(6, Player(name: 'Skeleton Player')),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -43,21 +43,41 @@ class _HomeViewState extends State<HomeView> {
|
||||
Match(
|
||||
name: 'Skeleton Match',
|
||||
game: Game(
|
||||
name: '',
|
||||
name: 'Skeleton Game',
|
||||
ruleset: Ruleset.singleWinner,
|
||||
description: '',
|
||||
description: 'This is a skeleton game description.',
|
||||
color: GameColor.blue,
|
||||
icon: '',
|
||||
),
|
||||
group: Group(
|
||||
name: 'Skeleton Group',
|
||||
description: '',
|
||||
description: 'This is a skeleton group description.',
|
||||
members: [
|
||||
Player(name: 'Skeleton Player 1', description: ''),
|
||||
Player(name: 'Skeleton Player 2', description: ''),
|
||||
Player(
|
||||
name:
|
||||
'Skeleton Player 1'
|
||||
'',
|
||||
),
|
||||
Player(
|
||||
name:
|
||||
'Skeleton Player 2'
|
||||
'',
|
||||
),
|
||||
],
|
||||
),
|
||||
notes: '',
|
||||
notes: 'These are skeleton notes.',
|
||||
players: [
|
||||
Player(
|
||||
name:
|
||||
'Skeleton Player 1'
|
||||
'',
|
||||
),
|
||||
Player(
|
||||
name:
|
||||
'Skeleton Player 2'
|
||||
'',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -125,7 +145,11 @@ class _HomeViewState extends State<HomeView> {
|
||||
MatchResultView(match: match),
|
||||
),
|
||||
);
|
||||
await updatedWinnerInRecentMatches(match.id);
|
||||
await loadRecentMatches();
|
||||
|
||||
setState(() {
|
||||
print('loaded');
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -224,15 +248,12 @@ class _HomeViewState extends State<HomeView> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Updates the winner information for a specific match in the recent matches list.
|
||||
Future<void> updatedWinnerInRecentMatches(String matchId) async {
|
||||
Future<void> loadRecentMatches() async {
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
final winner = await db.scoreEntryDao.getWinner(matchId: matchId);
|
||||
final matchIndex = recentMatches.indexWhere((match) => match.id == matchId);
|
||||
if (matchIndex != -1) {
|
||||
setState(() {
|
||||
recentMatches[matchIndex].winner = winner;
|
||||
});
|
||||
}
|
||||
final matches = await db.matchDao.getAllMatches();
|
||||
recentMatches =
|
||||
(matches..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
|
||||
.take(2)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/models/game.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart';
|
||||
@@ -13,14 +13,14 @@ class ChooseGameView extends StatefulWidget {
|
||||
const ChooseGameView({
|
||||
super.key,
|
||||
required this.games,
|
||||
required this.initialGameIndex,
|
||||
required this.initialGameId,
|
||||
});
|
||||
|
||||
/// A list of tuples containing the game name, description and ruleset
|
||||
final List<(String, String, Ruleset)> games;
|
||||
final List<Game> games;
|
||||
|
||||
/// The index of the initially selected game
|
||||
final int initialGameIndex;
|
||||
/// The id of the initially selected game
|
||||
final String initialGameId;
|
||||
|
||||
@override
|
||||
State<ChooseGameView> createState() => _ChooseGameViewState();
|
||||
@@ -31,11 +31,11 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
final TextEditingController searchBarController = TextEditingController();
|
||||
|
||||
/// Currently selected game index
|
||||
late int selectedGameIndex;
|
||||
late String selectedGameId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
selectedGameIndex = widget.initialGameIndex;
|
||||
selectedGameId = widget.initialGameId;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -49,7 +49,13 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(selectedGameIndex);
|
||||
Navigator.of(context).pop(
|
||||
selectedGameId == ''
|
||||
? null
|
||||
: widget.games.firstWhere(
|
||||
(game) => game.id == selectedGameId,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(loc.choose_game),
|
||||
@@ -62,7 +68,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
if (didPop) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(selectedGameIndex);
|
||||
Navigator.of(context).pop(widget.initialGameId);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -79,19 +85,19 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
itemCount: widget.games.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return TitleDescriptionListTile(
|
||||
title: widget.games[index].$1,
|
||||
description: widget.games[index].$2,
|
||||
title: widget.games[index].name,
|
||||
description: widget.games[index].description,
|
||||
badgeText: translateRulesetToString(
|
||||
widget.games[index].$3,
|
||||
widget.games[index].ruleset,
|
||||
context,
|
||||
),
|
||||
isHighlighted: selectedGameIndex == index,
|
||||
isHighlighted: selectedGameId == widget.games[index].id,
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
if (selectedGameIndex == index) {
|
||||
selectedGameIndex = -1;
|
||||
if (selectedGameId != widget.games[index].id) {
|
||||
selectedGameId = widget.games[index].id;
|
||||
} else {
|
||||
selectedGameIndex = index;
|
||||
selectedGameId = '';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -20,7 +20,9 @@ import 'package:tallee/presentation/widgets/tiles/choose_tile.dart';
|
||||
|
||||
class CreateMatchView extends StatefulWidget {
|
||||
/// A view that allows creating a new match
|
||||
/// [onWinnerChanged]: Optional callback invoked when the winner is changed
|
||||
/// - [onWinnerChanged]: Optional callback invoked when the winner is changed
|
||||
/// - [matchToEdit]: An optional match to prefill the fields for editing.
|
||||
/// - [onMatchUpdated]: Optional callback invoked when the match is updated (only in
|
||||
const CreateMatchView({
|
||||
super.key,
|
||||
this.onWinnerChanged,
|
||||
@@ -28,13 +30,11 @@ class CreateMatchView extends StatefulWidget {
|
||||
this.onMatchUpdated,
|
||||
});
|
||||
|
||||
/// Optional callback invoked when the winner is changed
|
||||
final VoidCallback? onWinnerChanged;
|
||||
|
||||
/// Optional callback invoked when the match is updated
|
||||
final void Function(Match)? onMatchUpdated;
|
||||
|
||||
/// An optional match to prefill the fields
|
||||
/// An optional match to prefill the fields for editing.
|
||||
final Match? matchToEdit;
|
||||
|
||||
@override
|
||||
@@ -50,20 +50,12 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
/// Hint text for the match name input field
|
||||
String? hintText;
|
||||
|
||||
/// List of all groups from the database
|
||||
List<Group> groupsList = [];
|
||||
|
||||
/// List of all players from the database
|
||||
List<Player> playerList = [];
|
||||
List<Game> gamesList = [];
|
||||
|
||||
/// The currently selected group
|
||||
Group? selectedGroup;
|
||||
|
||||
/// The index of the currently selected game in [games] to mark it in
|
||||
/// the [ChooseGameView]
|
||||
int selectedGameIndex = -1;
|
||||
|
||||
/// The currently selected players
|
||||
Game? selectedGame;
|
||||
List<Player> selectedPlayers = [];
|
||||
|
||||
/// GlobalKey for ScaffoldMessenger to show snackbars
|
||||
@@ -81,12 +73,14 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
Future.wait([
|
||||
db.groupDao.getAllGroups(),
|
||||
db.playerDao.getAllPlayers(),
|
||||
db.gameDao.getAllGames(),
|
||||
]).then((result) async {
|
||||
groupsList = result[0] as List<Group>;
|
||||
playerList = result[1] as List<Player>;
|
||||
gamesList = (result[2] as List<Game>);
|
||||
|
||||
// If a match is provided, prefill the fields
|
||||
if (widget.matchToEdit != null) {
|
||||
if (isEditMode()) {
|
||||
prefillMatchDetails();
|
||||
}
|
||||
});
|
||||
@@ -105,20 +99,11 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
hintText ??= loc.match_name;
|
||||
}
|
||||
|
||||
List<(String, String, Ruleset)> games = [
|
||||
('Example Game 1', 'This is a description', Ruleset.lowestScore),
|
||||
('Example Game 2', '', Ruleset.singleWinner),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
final buttonText = widget.matchToEdit != null
|
||||
? loc.save_changes
|
||||
: loc.create_match;
|
||||
final viewTitle = widget.matchToEdit != null
|
||||
? loc.edit_match
|
||||
: loc.create_new_match;
|
||||
final buttonText = isEditMode() ? loc.save_changes : loc.create_match;
|
||||
final viewTitle = isEditMode() ? loc.edit_match : loc.create_new_match;
|
||||
|
||||
return ScaffoldMessenger(
|
||||
key: _scaffoldMessengerKey,
|
||||
@@ -140,21 +125,21 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
),
|
||||
ChooseTile(
|
||||
title: loc.game,
|
||||
trailingText: selectedGameIndex == -1
|
||||
? loc.none
|
||||
: games[selectedGameIndex].$1,
|
||||
trailingText: selectedGame == null
|
||||
? loc.none_group
|
||||
: selectedGame!.name,
|
||||
onPressed: () async {
|
||||
selectedGameIndex = await Navigator.of(context).push(
|
||||
selectedGame = await Navigator.of(context).push(
|
||||
adaptivePageRoute(
|
||||
builder: (context) => ChooseGameView(
|
||||
games: games,
|
||||
initialGameIndex: selectedGameIndex,
|
||||
games: gamesList,
|
||||
initialGameId: selectedGame?.id ?? '',
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
if (selectedGameIndex != -1) {
|
||||
hintText = games[selectedGameIndex].$1;
|
||||
if (selectedGame != null) {
|
||||
hintText = selectedGame!.name;
|
||||
} else {
|
||||
hintText = loc.match_name;
|
||||
}
|
||||
@@ -225,6 +210,10 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
);
|
||||
}
|
||||
|
||||
bool isEditMode() {
|
||||
return widget.matchToEdit != null;
|
||||
}
|
||||
|
||||
/// Determines whether the "Create Match" button should be enabled.
|
||||
///
|
||||
/// Returns `true` if:
|
||||
@@ -232,7 +221,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
/// - Either a group is selected OR at least 2 players are selected
|
||||
bool _enableCreateGameButton() {
|
||||
return (selectedGroup != null ||
|
||||
(selectedPlayers.length > 1) && selectedGameIndex != -1);
|
||||
(selectedPlayers.length > 1) && selectedGame != null);
|
||||
}
|
||||
|
||||
// If a match was provided to the view, it updates the match in the database
|
||||
@@ -240,7 +229,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
// If no match was provided, it creates a new match in the database and
|
||||
// navigates to the MatchResultView for the newly created match.
|
||||
void buttonNavigation(BuildContext context) async {
|
||||
if (widget.matchToEdit != null) {
|
||||
if (isEditMode()) {
|
||||
await updateMatch();
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
@@ -266,9 +255,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
/// Updates attributes of the existing match in the database based on the
|
||||
/// changes made in the edit view.
|
||||
Future<void> updateMatch() async {
|
||||
//TODO: Remove when Games implemented
|
||||
final tempGame = await getTemporaryGame();
|
||||
|
||||
final updatedMatch = Match(
|
||||
id: widget.matchToEdit!.id,
|
||||
name: _matchNameController.text.isEmpty
|
||||
@@ -276,8 +262,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
: _matchNameController.text.trim(),
|
||||
group: selectedGroup,
|
||||
players: selectedPlayers,
|
||||
game: tempGame,
|
||||
winner: widget.matchToEdit!.winner,
|
||||
game: widget.matchToEdit!.game,
|
||||
createdAt: widget.matchToEdit!.createdAt,
|
||||
endedAt: widget.matchToEdit!.endedAt,
|
||||
notes: widget.matchToEdit!.notes,
|
||||
@@ -314,9 +299,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
matchId: widget.matchToEdit!.id,
|
||||
playerId: player.id,
|
||||
);
|
||||
if (widget.matchToEdit!.winner?.id == player.id) {
|
||||
updatedMatch.winner = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,8 +308,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
// Creates a new match and adds it to the database.
|
||||
// Returns the created match.
|
||||
Future<Match> createMatch() async {
|
||||
final tempGame = await getTemporaryGame();
|
||||
|
||||
Match match = Match(
|
||||
name: _matchNameController.text.isEmpty
|
||||
? (hintText ?? '')
|
||||
@@ -335,35 +315,18 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
createdAt: DateTime.now(),
|
||||
group: selectedGroup,
|
||||
players: selectedPlayers,
|
||||
game: tempGame,
|
||||
game: selectedGame!,
|
||||
);
|
||||
await db.matchDao.addMatch(match: match);
|
||||
return match;
|
||||
}
|
||||
|
||||
// TODO: Remove when games fully implemented
|
||||
Future<Game> getTemporaryGame() async {
|
||||
Game? game;
|
||||
|
||||
final selectedGame = games[selectedGameIndex];
|
||||
game = Game(
|
||||
name: selectedGame.$1,
|
||||
description: selectedGame.$2,
|
||||
ruleset: selectedGame.$3,
|
||||
color: GameColor.blue,
|
||||
icon: '',
|
||||
);
|
||||
|
||||
await db.gameDao.addGame(game: game);
|
||||
return game;
|
||||
}
|
||||
|
||||
// If a match was provided to the view, this method prefills the input fields
|
||||
void prefillMatchDetails() {
|
||||
final match = widget.matchToEdit!;
|
||||
_matchNameController.text = match.name;
|
||||
selectedPlayers = match.players;
|
||||
selectedGameIndex = 0;
|
||||
selectedGame = match.game;
|
||||
|
||||
if (match.group != null) {
|
||||
selectedGroup = match.group;
|
||||
|
||||
@@ -4,15 +4,16 @@ import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/adaptive_page_route.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/animated_dialog_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/custom_alert_dialog.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/tiles/info_tile.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
@@ -64,23 +65,16 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
context: context,
|
||||
builder: (context) => CustomAlertDialog(
|
||||
title: '${loc.delete_match}?',
|
||||
content: loc.this_cannot_be_undone,
|
||||
content: Text(loc.this_cannot_be_undone),
|
||||
actions: [
|
||||
AnimatedDialogButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(
|
||||
loc.cancel,
|
||||
style: const TextStyle(color: CustomTheme.textColor),
|
||||
),
|
||||
),
|
||||
AnimatedDialogButton(
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(
|
||||
loc.delete,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.secondaryColor,
|
||||
),
|
||||
),
|
||||
text: loc.delete,
|
||||
),
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
buttonType: ButtonType.secondary,
|
||||
text: loc.cancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -161,6 +155,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
children: match.players.map((player) {
|
||||
return TextIconTile(
|
||||
text: player.name,
|
||||
suffixText: getNameCountText(player),
|
||||
iconEnabled: false,
|
||||
);
|
||||
}).toList(),
|
||||
@@ -175,37 +170,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
vertical: 4,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
/// TODO: Implement different ruleset results display
|
||||
if (match.winner != null) ...[
|
||||
Text(
|
||||
loc.winner,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
match.winner!.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
Text(
|
||||
loc.no_results_entered_yet,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
child: getResultWidget(loc),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -232,7 +197,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
text: loc.enter_results,
|
||||
icon: Icons.emoji_events,
|
||||
onPressed: () async {
|
||||
match.winner = await Navigator.push(
|
||||
await Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: true,
|
||||
@@ -264,4 +229,108 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
});
|
||||
widget.onMatchUpdate.call();
|
||||
}
|
||||
|
||||
/// Returns the widget to be displayed in the result [InfoTile]
|
||||
Widget getResultWidget(AppLocalizations loc) {
|
||||
if (isSingleRowResult()) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: getSingleResultRow(loc),
|
||||
);
|
||||
} else {
|
||||
return getScoreResultWidget(loc);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the result row for single winner/loser rulesets or a placeholder
|
||||
/// if no result is entered yet
|
||||
List<Widget> getSingleResultRow(AppLocalizations loc) {
|
||||
// Single Winner
|
||||
if (match.mvp.isNotEmpty && match.game.ruleset == Ruleset.singleWinner) {
|
||||
return [
|
||||
Text(
|
||||
loc.winner,
|
||||
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
|
||||
),
|
||||
Text(
|
||||
match.mvp.first.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
];
|
||||
// Single Loser
|
||||
} else if (match.game.ruleset == Ruleset.singleLoser) {
|
||||
return [
|
||||
Text(
|
||||
loc.loser,
|
||||
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
|
||||
),
|
||||
Text(
|
||||
match.mvp.first.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
];
|
||||
// No result entered yet
|
||||
} else {
|
||||
return [
|
||||
Text(
|
||||
loc.no_results_entered_yet,
|
||||
style: const TextStyle(fontSize: 14, color: CustomTheme.textColor),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the result widget for scores
|
||||
Widget getScoreResultWidget(AppLocalizations loc) {
|
||||
List<(String, int)> playerScores = [];
|
||||
for (var player in match.players) {
|
||||
int score = match.scores[player.id]?.score ?? 0;
|
||||
playerScores.add((player.name, score));
|
||||
}
|
||||
if (widget.match.game.ruleset == Ruleset.highestScore) {
|
||||
playerScores.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
} else if (widget.match.game.ruleset == Ruleset.lowestScore) {
|
||||
playerScores.sort((a, b) => a.$2.compareTo(b.$2));
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
for (var score in playerScores)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
score.$1,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
getPointLabel(loc, score.$2),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Returns if the result can be displayed in a single row
|
||||
bool isSingleRowResult() {
|
||||
return match.game.ruleset == Ruleset.singleWinner ||
|
||||
match.game.ruleset == Ruleset.singleLoser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.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/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/score_entry.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart';
|
||||
|
||||
class MatchResultView extends StatefulWidget {
|
||||
/// A view that allows selecting and saving the winner of a match
|
||||
@@ -26,30 +30,61 @@ class MatchResultView extends StatefulWidget {
|
||||
class _MatchResultViewState extends State<MatchResultView> {
|
||||
late final AppDatabase db;
|
||||
|
||||
late final Ruleset ruleset;
|
||||
|
||||
/// List of all players who participated in the match
|
||||
late final List<Player> allPlayers;
|
||||
|
||||
/// List of text controllers for score entry, one for each player
|
||||
late final List<TextEditingController> controller;
|
||||
|
||||
late bool canSave;
|
||||
|
||||
/// Currently selected winner player
|
||||
Player? _selectedPlayer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
ruleset = widget.match.game.ruleset;
|
||||
canSave = !rulesetSupportsScoreEntry();
|
||||
|
||||
allPlayers = widget.match.players;
|
||||
allPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||
|
||||
if (widget.match.winner != null) {
|
||||
_selectedPlayer = allPlayers.firstWhere(
|
||||
(p) => p.id == widget.match.winner!.id,
|
||||
);
|
||||
controller = List.generate(
|
||||
allPlayers.length,
|
||||
(index) => TextEditingController()..addListener(() => onTextEnter()),
|
||||
);
|
||||
|
||||
if (widget.match.mvp.isNotEmpty) {
|
||||
if (rulesetSupportsWinnerSelection()) {
|
||||
_selectedPlayer = allPlayers.firstWhere(
|
||||
(p) => p.id == widget.match.mvp.first.id,
|
||||
);
|
||||
} else if (rulesetSupportsScoreEntry()) {
|
||||
for (int i = 0; i < allPlayers.length; i++) {
|
||||
final scoreList = widget.match.scores[allPlayers[i].id];
|
||||
final score = scoreList?.score ?? 0;
|
||||
controller[i].text = score.toString();
|
||||
}
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in controller) {
|
||||
c.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(
|
||||
@@ -85,67 +120,169 @@ class _MatchResultViewState extends State<MatchResultView> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
loc.select_winner,
|
||||
'${getTitleForRuleset(loc)}:',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: RadioGroup<Player>(
|
||||
groupValue: _selectedPlayer,
|
||||
onChanged: (Player? value) async {
|
||||
setState(() {
|
||||
_selectedPlayer = value;
|
||||
});
|
||||
await _handleWinnerSaving();
|
||||
},
|
||||
child: ListView.builder(
|
||||
if (rulesetSupportsWinnerSelection())
|
||||
Expanded(
|
||||
child: RadioGroup<Player>(
|
||||
groupValue: _selectedPlayer,
|
||||
onChanged: (Player? value) async {
|
||||
setState(() {
|
||||
_selectedPlayer = value;
|
||||
});
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: allPlayers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return CustomRadioListTile(
|
||||
text: allPlayers[index].name,
|
||||
value: allPlayers[index],
|
||||
onContainerTap: (value) async {
|
||||
setState(() {
|
||||
// Check if the already selected player is the same as the newly tapped player.
|
||||
if (_selectedPlayer == value) {
|
||||
// If yes deselected the player by setting it to null.
|
||||
_selectedPlayer = null;
|
||||
} else {
|
||||
// If no assign the newly tapped player to the selected player.
|
||||
(_selectedPlayer = value);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (rulesetSupportsScoreEntry())
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: allPlayers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return CustomRadioListTile(
|
||||
return ScoreListTile(
|
||||
text: allPlayers[index].name,
|
||||
value: allPlayers[index],
|
||||
onContainerTap: (value) async {
|
||||
setState(() {
|
||||
// Check if the already selected player is the same as the newly tapped player.
|
||||
if (_selectedPlayer == value) {
|
||||
// If yes deselected the player by setting it to null.
|
||||
_selectedPlayer = null;
|
||||
} else {
|
||||
// If no assign the newly tapped player to the selected player.
|
||||
(_selectedPlayer = value);
|
||||
}
|
||||
});
|
||||
await _handleWinnerSaving();
|
||||
},
|
||||
controller: controller[index],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Divider(indent: 20),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomWidthButton(
|
||||
text: loc.save_changes,
|
||||
sizeRelativeToWidth: 0.95,
|
||||
onPressed: canSave
|
||||
? () async {
|
||||
final ending = DateTime.now();
|
||||
await db.matchDao.updateMatchEndedAt(
|
||||
matchId: widget.match.id,
|
||||
endedAt: ending,
|
||||
);
|
||||
await _handleSaving();
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop(_selectedPlayer);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Updated [canSave] everytime a text is entered in one of the score entry fields.
|
||||
void onTextEnter() {
|
||||
if (rulesetSupportsScoreEntry()) {
|
||||
setState(() {
|
||||
canSave = controller.every((c) => c.text.isNotEmpty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles saving or removing the winner in the database
|
||||
/// based on the current selection.
|
||||
Future<void> _handleWinnerSaving() async {
|
||||
Future<void> _handleSaving() async {
|
||||
if (ruleset == Ruleset.singleWinner) {
|
||||
await _handleWinner();
|
||||
} else if (ruleset == Ruleset.singleLoser) {
|
||||
await _handleLoser();
|
||||
} else if (ruleset == Ruleset.lowestScore ||
|
||||
ruleset == Ruleset.highestScore) {
|
||||
await _handleScores();
|
||||
}
|
||||
|
||||
widget.onWinnerChanged?.call();
|
||||
}
|
||||
|
||||
/// Handles saving or removing the winner in the database.
|
||||
Future<bool> _handleWinner() async {
|
||||
if (_selectedPlayer == null) {
|
||||
await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
|
||||
return await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
|
||||
} else {
|
||||
await db.scoreEntryDao.setWinner(
|
||||
return await db.scoreEntryDao.setWinner(
|
||||
matchId: widget.match.id,
|
||||
playerId: _selectedPlayer!.id,
|
||||
);
|
||||
}
|
||||
widget.onWinnerChanged?.call();
|
||||
}
|
||||
|
||||
/// Handles saving or removing the loser in the database.
|
||||
Future<bool> _handleLoser() async {
|
||||
if (_selectedPlayer == null) {
|
||||
return await db.scoreEntryDao.removeLooser(matchId: widget.match.id);
|
||||
} else {
|
||||
return await db.scoreEntryDao.setLooser(
|
||||
matchId: widget.match.id,
|
||||
playerId: _selectedPlayer!.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles saving the scores for each player in the database.
|
||||
Future<void> _handleScores() async {
|
||||
for (int i = 0; i < allPlayers.length; i++) {
|
||||
var text = controller[i].text;
|
||||
if (text.isEmpty) {
|
||||
text = '0';
|
||||
}
|
||||
final score = int.parse(text);
|
||||
await db.scoreEntryDao.addScore(
|
||||
matchId: widget.match.id,
|
||||
playerId: allPlayers[i].id,
|
||||
entry: ScoreEntry(roundNumber: 0, score: score, change: 0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String getTitleForRuleset(AppLocalizations loc) {
|
||||
switch (ruleset) {
|
||||
case Ruleset.singleWinner:
|
||||
return loc.select_winner;
|
||||
case Ruleset.singleLoser:
|
||||
return loc.select_loser;
|
||||
default:
|
||||
return loc.enter_points;
|
||||
}
|
||||
}
|
||||
|
||||
bool rulesetSupportsWinnerSelection() {
|
||||
return ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser;
|
||||
}
|
||||
|
||||
bool rulesetSupportsScoreEntry() {
|
||||
return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,20 +37,16 @@ class _MatchViewState extends State<MatchView> {
|
||||
Match(
|
||||
name: 'Skeleton match name',
|
||||
game: Game(
|
||||
name: '',
|
||||
name: 'Game name',
|
||||
ruleset: Ruleset.singleWinner,
|
||||
description: '',
|
||||
color: GameColor.blue,
|
||||
icon: '',
|
||||
),
|
||||
group: Group(
|
||||
name: 'Group name',
|
||||
description: '',
|
||||
members: List.filled(5, Player(name: 'Player', description: '')),
|
||||
members: List.filled(5, Player(name: 'Player')),
|
||||
),
|
||||
winner: Player(name: 'Player', description: ''),
|
||||
players: [Player(name: 'Player', description: '')],
|
||||
notes: '',
|
||||
players: [Player(name: 'Player')],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -116,7 +112,7 @@ class _MatchViewState extends State<MatchView> {
|
||||
Positioned(
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 20,
|
||||
child: MainMenuButton(
|
||||
text: 'Spiel erstellen',
|
||||
text: loc.create_match,
|
||||
icon: RpgAwesome.clovers_card,
|
||||
onPressed: () async {
|
||||
Navigator.push(
|
||||
|
||||
@@ -30,7 +30,6 @@ const allDependencies = <Package>[
|
||||
_cli_util,
|
||||
_clock,
|
||||
_code_assets,
|
||||
_code_builder,
|
||||
_collection,
|
||||
_convert,
|
||||
_coverage,
|
||||
@@ -109,6 +108,7 @@ const allDependencies = <Package>[
|
||||
_pubspec_parse,
|
||||
_quiver,
|
||||
_recase,
|
||||
_record_use,
|
||||
_retry,
|
||||
_rfc_6901,
|
||||
_safe_url_check,
|
||||
@@ -159,14 +159,14 @@ const allDependencies = <Package>[
|
||||
|
||||
/// Direct `dependencies`.
|
||||
const dependencies = <Package>[
|
||||
_flutter,
|
||||
_flutter_localizations,
|
||||
_clock,
|
||||
_cupertino_icons,
|
||||
_drift,
|
||||
_drift_flutter,
|
||||
_file_picker,
|
||||
_file_saver,
|
||||
_flutter,
|
||||
_flutter_localizations,
|
||||
_fluttericon,
|
||||
_font_awesome_flutter,
|
||||
_intl,
|
||||
@@ -567,17 +567,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// build_runner 2.13.1
|
||||
/// build_runner 2.14.0
|
||||
const _build_runner = Package(
|
||||
name: 'build_runner',
|
||||
description: 'A build system for Dart code generation and modular compilation.',
|
||||
repository: 'https://github.com/dart-lang/build/tree/master/build_runner',
|
||||
authors: [],
|
||||
version: '2.13.1',
|
||||
version: '2.14.0',
|
||||
spdxIdentifiers: ['BSD-3-Clause'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [PackageRef('analyzer'), PackageRef('args'), PackageRef('async'), PackageRef('build'), PackageRef('build_config'), PackageRef('build_daemon'), PackageRef('built_collection'), PackageRef('built_value'), PackageRef('code_builder'), PackageRef('collection'), PackageRef('convert'), PackageRef('crypto'), PackageRef('dart_style'), PackageRef('glob'), PackageRef('graphs'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('json_annotation'), PackageRef('logging'), PackageRef('meta'), PackageRef('mime'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('pub_semver'), PackageRef('shelf'), PackageRef('shelf_web_socket'), PackageRef('stream_transform'), PackageRef('watcher'), PackageRef('web_socket_channel'), PackageRef('yaml')],
|
||||
dependencies: [PackageRef('analyzer'), PackageRef('args'), PackageRef('async'), PackageRef('build'), PackageRef('build_config'), PackageRef('build_daemon'), PackageRef('built_collection'), PackageRef('built_value'), PackageRef('collection'), PackageRef('convert'), PackageRef('crypto'), PackageRef('dart_style'), PackageRef('glob'), PackageRef('graphs'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('json_annotation'), PackageRef('logging'), PackageRef('meta'), PackageRef('mime'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('pub_semver'), PackageRef('shelf'), PackageRef('shelf_web_socket'), PackageRef('stream_transform'), PackageRef('watcher'), PackageRef('web_socket_channel'), PackageRef('yaml')],
|
||||
devDependencies: [PackageRef('stream_channel'), PackageRef('test')],
|
||||
license: '''Copyright 2016, the Dart project authors.
|
||||
|
||||
@@ -1153,47 +1153,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// code_builder 4.11.1
|
||||
const _code_builder = Package(
|
||||
name: 'code_builder',
|
||||
description: 'A fluent, builder-based library for generating valid Dart code.',
|
||||
repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/code_builder',
|
||||
authors: [],
|
||||
version: '4.11.1',
|
||||
spdxIdentifiers: ['BSD-3-Clause'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [PackageRef('built_collection'), PackageRef('built_value'), PackageRef('collection'), PackageRef('matcher'), PackageRef('meta')],
|
||||
devDependencies: [PackageRef('build'), PackageRef('build_runner'), PackageRef('dart_style'), PackageRef('source_gen'), PackageRef('test')],
|
||||
license: '''Copyright 2016, the Dart project authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// collection 1.19.1
|
||||
const _collection = Package(
|
||||
name: 'collection',
|
||||
@@ -2418,14 +2377,14 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// file_picker 10.3.10
|
||||
/// file_picker 11.0.2
|
||||
const _file_picker = Package(
|
||||
name: 'file_picker',
|
||||
description: 'A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.',
|
||||
homepage: 'https://github.com/miguelpruivo/plugins_flutter_file_picker',
|
||||
repository: 'https://github.com/miguelpruivo/flutter_file_picker',
|
||||
authors: [],
|
||||
version: '10.3.10',
|
||||
version: '11.0.2',
|
||||
spdxIdentifiers: ['MIT'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
@@ -2731,13 +2690,13 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// font_awesome_flutter 10.12.0
|
||||
/// font_awesome_flutter 11.0.0
|
||||
const _font_awesome_flutter = Package(
|
||||
name: 'font_awesome_flutter',
|
||||
description: 'The Font Awesome Icon pack available as Flutter Icons. Provides 2000 additional icons to use in your apps.',
|
||||
repository: 'https://github.com/fluttercommunity/font_awesome_flutter',
|
||||
authors: [],
|
||||
version: '10.12.0',
|
||||
version: '11.0.0',
|
||||
spdxIdentifiers: ['MIT'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
@@ -2892,17 +2851,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// hooks 1.0.2
|
||||
/// hooks 1.0.3
|
||||
const _hooks = Package(
|
||||
name: 'hooks',
|
||||
description: 'A library that contains a Dart API for the JSON-based protocol for `hook/build.dart` and `hook/link.dart`.',
|
||||
repository: 'https://github.com/dart-lang/native/tree/main/pkgs/hooks',
|
||||
authors: [],
|
||||
version: '1.0.2',
|
||||
version: '1.0.3',
|
||||
spdxIdentifiers: ['BSD-3-Clause'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [PackageRef('collection'), PackageRef('crypto'), PackageRef('logging'), PackageRef('meta'), PackageRef('pub_semver'), PackageRef('yaml')],
|
||||
dependencies: [PackageRef('collection'), PackageRef('crypto'), PackageRef('logging'), PackageRef('meta'), PackageRef('pub_semver'), PackageRef('record_use'), PackageRef('yaml')],
|
||||
devDependencies: [PackageRef('args'), PackageRef('code_assets'), PackageRef('glob'), PackageRef('json_schema'), PackageRef('path'), PackageRef('test')],
|
||||
license: '''Copyright 2025, the Dart project authors.
|
||||
|
||||
@@ -5106,6 +5065,48 @@ Redistribution and use in source and binary forms, with or without modification,
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// record_use 0.6.0
|
||||
const _record_use = Package(
|
||||
name: 'record_use',
|
||||
description: '''The serialization logic and API for the usage recording SDK feature.
|
||||
''',
|
||||
repository: 'https://github.com/dart-lang/native/tree/main/pkgs/record_use',
|
||||
authors: [],
|
||||
version: '0.6.0',
|
||||
spdxIdentifiers: ['BSD-3-Clause'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [PackageRef('collection'), PackageRef('meta'), PackageRef('pub_semver')],
|
||||
devDependencies: [PackageRef('json_schema'), PackageRef('test')],
|
||||
license: '''Copyright 2024, the Dart project authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// retry 3.1.2
|
||||
const _retry = Package(
|
||||
name: 'retry',
|
||||
@@ -37480,13 +37481,13 @@ freely, subject to the following restrictions:
|
||||
3. This notice may not be removed or altered from any source distribution.''',
|
||||
);
|
||||
|
||||
/// vm_service 15.0.2
|
||||
/// vm_service 15.1.0
|
||||
const _vm_service = Package(
|
||||
name: 'vm_service',
|
||||
description: 'A library to communicate with a service implementing the Dart VM service protocol.',
|
||||
repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/vm_service',
|
||||
authors: [],
|
||||
version: '15.0.2',
|
||||
version: '15.1.0',
|
||||
spdxIdentifiers: ['BSD-3-Clause'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
@@ -37880,16 +37881,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.''',
|
||||
);
|
||||
|
||||
/// tallee 0.0.20+254
|
||||
/// tallee 0.0.23+257
|
||||
const _tallee = Package(
|
||||
name: 'tallee',
|
||||
description: 'Tracking App for Card Games',
|
||||
authors: [],
|
||||
version: '0.0.20+254',
|
||||
version: '0.0.23+257',
|
||||
spdxIdentifiers: ['LGPL-3.0'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('clock'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')],
|
||||
dependencies: [PackageRef('clock'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')],
|
||||
devDependencies: [PackageRef('flutter_test'), PackageRef('build_runner'), PackageRef('dart_pubspec_licenses'), PackageRef('drift_dev'), PackageRef('flutter_lints')],
|
||||
license: '''GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
@@ -9,8 +9,8 @@ import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/settings_view/licenses_view.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
|
||||
import 'package:tallee/presentation/widgets/custom_alert_dialog.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/tiles/settings_list_tile.dart';
|
||||
import 'package:tallee/services/data_transfer_service.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -122,25 +122,16 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
context: context,
|
||||
builder: (context) => CustomAlertDialog(
|
||||
title: '${loc.delete_all_data}?',
|
||||
content: loc.this_cannot_be_undone,
|
||||
content: Text(loc.this_cannot_be_undone),
|
||||
actions: [
|
||||
AnimatedDialogButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(
|
||||
loc.cancel,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedDialogButton(
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(
|
||||
loc.delete,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.secondaryColor,
|
||||
),
|
||||
),
|
||||
text: loc.delete,
|
||||
),
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
buttonType: ButtonType.secondary,
|
||||
text: loc.cancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -18,9 +18,18 @@ class StatisticsView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _StatisticsViewState extends State<StatisticsView> {
|
||||
List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1));
|
||||
List<(String, int)> matchCounts = List.filled(6, ('Skeleton Player', 1));
|
||||
List<(String, double)> winRates = List.filled(6, ('Skeleton Player', 1));
|
||||
List<(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
|
||||
@@ -121,7 +130,10 @@ class _StatisticsViewState extends State<StatisticsView> {
|
||||
players: players,
|
||||
context: context,
|
||||
);
|
||||
winRates = computeWinRatePercent(wins: winCounts, matches: matchCounts);
|
||||
winRates = computeWinRatePercent(
|
||||
winCounts: winCounts,
|
||||
matchCounts: matchCounts,
|
||||
);
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
@@ -130,47 +142,46 @@ class _StatisticsViewState extends State<StatisticsView> {
|
||||
|
||||
/// Calculates the number of wins for each player
|
||||
/// and returns a sorted list of tuples (playerName, winCount)
|
||||
List<(String, int)> _calculateWinsForAllPlayers({
|
||||
List<(Player, int)> _calculateWinsForAllPlayers({
|
||||
required List<Match> matches,
|
||||
required List<Player> players,
|
||||
required BuildContext context,
|
||||
}) {
|
||||
List<(String, int)> winCounts = [];
|
||||
List<(Player, int)> winCounts = [];
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
// Getting the winners
|
||||
for (var match in matches) {
|
||||
final winner = match.winner;
|
||||
if (winner != null) {
|
||||
final index = winCounts.indexWhere((entry) => entry.$1 == winner.id);
|
||||
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.id, current + 1);
|
||||
winCounts[index] = (winner, current + 1);
|
||||
} else {
|
||||
winCounts.add((winner.id, 1));
|
||||
winCounts.add((winner, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adding all players with zero wins
|
||||
for (var player in players) {
|
||||
final index = winCounts.indexWhere((entry) => entry.$1 == player.id);
|
||||
final index = winCounts.indexWhere((entry) => entry.$1.id == player.id);
|
||||
// -1 means player not found in winCounts
|
||||
if (index == -1) {
|
||||
winCounts.add((player.id, 0));
|
||||
winCounts.add((player, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Replace player IDs with names
|
||||
for (int i = 0; i < winCounts.length; i++) {
|
||||
final playerId = winCounts[i].$1;
|
||||
final playerId = winCounts[i].$1.id;
|
||||
final player = players.firstWhere(
|
||||
(p) => p.id == playerId,
|
||||
orElse: () =>
|
||||
Player(id: playerId, name: loc.not_available, description: ''),
|
||||
orElse: () => Player(id: playerId, name: loc.not_available),
|
||||
);
|
||||
winCounts[i] = (player.name, winCounts[i].$2);
|
||||
winCounts[i] = (player, winCounts[i].$2);
|
||||
}
|
||||
|
||||
winCounts.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
@@ -180,60 +191,51 @@ class _StatisticsViewState extends State<StatisticsView> {
|
||||
|
||||
/// Calculates the number of matches played for each player
|
||||
/// and returns a sorted list of tuples (playerName, matchCount)
|
||||
List<(String, int)> _calculateMatchAmountsForAllPlayers({
|
||||
List<(Player, int)> _calculateMatchAmountsForAllPlayers({
|
||||
required List<Match> matches,
|
||||
required List<Player> players,
|
||||
required BuildContext context,
|
||||
}) {
|
||||
List<(String, int)> matchCounts = [];
|
||||
List<(Player, int)> matchCounts = [];
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
// Counting matches for each player
|
||||
for (var match in matches) {
|
||||
if (match.group != null) {
|
||||
final members = match.group!.members.map((p) => p.id).toList();
|
||||
for (var playerId in members) {
|
||||
final index = matchCounts.indexWhere((entry) => entry.$1 == playerId);
|
||||
// -1 means player not found in matchCounts
|
||||
if (index != -1) {
|
||||
final current = matchCounts[index].$2;
|
||||
matchCounts[index] = (playerId, current + 1);
|
||||
} else {
|
||||
matchCounts.add((playerId, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
final members = match.players.map((p) => p.id).toList();
|
||||
for (var playerId in members) {
|
||||
final index = matchCounts.indexWhere((entry) => entry.$1 == playerId);
|
||||
// -1 means player not found in matchCounts
|
||||
if (index != -1) {
|
||||
final current = matchCounts[index].$2;
|
||||
matchCounts[index] = (playerId, current + 1);
|
||||
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 {
|
||||
matchCounts.add((playerId, 1));
|
||||
// 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 == player.id);
|
||||
final index = matchCounts.indexWhere((entry) => entry.$1.id == player.id);
|
||||
// -1 means player not found in matchCounts
|
||||
if (index == -1) {
|
||||
matchCounts.add((player.id, 0));
|
||||
matchCounts.add((player, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Replace player IDs with names
|
||||
for (int i = 0; i < matchCounts.length; i++) {
|
||||
final playerId = matchCounts[i].$1;
|
||||
final playerId = matchCounts[i].$1.id;
|
||||
final player = players.firstWhere(
|
||||
(p) => p.id == playerId,
|
||||
orElse: () =>
|
||||
Player(id: playerId, name: loc.not_available, description: ''),
|
||||
orElse: () => Player(id: playerId, name: loc.not_available),
|
||||
);
|
||||
matchCounts[i] = (player.name, matchCounts[i].$2);
|
||||
matchCounts[i] = (player, matchCounts[i].$2);
|
||||
}
|
||||
|
||||
matchCounts.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
@@ -241,25 +243,24 @@ class _StatisticsViewState extends State<StatisticsView> {
|
||||
return matchCounts;
|
||||
}
|
||||
|
||||
// dart
|
||||
List<(String, double)> computeWinRatePercent({
|
||||
required List<(String, int)> wins,
|
||||
required List<(String, int)> matches,
|
||||
List<(Player, double)> computeWinRatePercent({
|
||||
required List<(Player, int)> winCounts,
|
||||
required List<(Player, int)> matchCounts,
|
||||
}) {
|
||||
final Map<String, int> winsMap = {for (var e in wins) e.$1: e.$2};
|
||||
final Map<String, int> matchesMap = {for (var e in matches) e.$1: e.$2};
|
||||
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 names = {...winsMap.keys, ...matchesMap.keys};
|
||||
final player = {...matchesMap.keys};
|
||||
|
||||
// Calculate win rates
|
||||
final result = names.map((name) {
|
||||
final result = player.map((name) {
|
||||
final int w = winsMap[name] ?? 0;
|
||||
final int g = matchesMap[name] ?? 0;
|
||||
final int m = matchesMap[name] ?? 0;
|
||||
// Calculate percentage and round to 2 decimal places
|
||||
// Avoid division by zero
|
||||
final double percent = (g > 0)
|
||||
? double.parse(((w / g)).toStringAsFixed(2))
|
||||
final double percent = (m > 0)
|
||||
? double.parse(((w / m)).toStringAsFixed(2))
|
||||
: 0;
|
||||
return (name, percent);
|
||||
}).toList();
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
|
||||
class AnimatedDialogButton extends StatefulWidget {
|
||||
/// A custom animated button widget that provides a scaling and opacity effect
|
||||
/// when pressed.
|
||||
/// - [onPressed]: Callback function that is triggered when the button is pressed.
|
||||
/// - [child]: The child widget to be displayed inside the button, typically a text or icon.
|
||||
/// - [buttonText]: The text to be displayed on the button.
|
||||
/// - [buttonType]: The type of the button, which determines its styling.
|
||||
/// - [buttonConstraints]: Optional constraints to control the button's size.
|
||||
const AnimatedDialogButton({
|
||||
super.key,
|
||||
required this.buttonText,
|
||||
required this.onPressed,
|
||||
required this.child,
|
||||
this.buttonConstraints,
|
||||
this.buttonType = ButtonType.primary,
|
||||
});
|
||||
|
||||
/// Callback function that is triggered when the button is pressed.
|
||||
final String buttonText;
|
||||
|
||||
final VoidCallback onPressed;
|
||||
|
||||
/// The child widget to be displayed inside the button, typically a text or icon.
|
||||
final Widget child;
|
||||
final BoxConstraints? buttonConstraints;
|
||||
|
||||
final ButtonType buttonType;
|
||||
|
||||
@override
|
||||
State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState();
|
||||
@@ -27,6 +33,29 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textStyling = TextStyle(
|
||||
color: widget.buttonType == ButtonType.primary
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
final buttonDecoration = widget.buttonType == ButtonType.primary
|
||||
// Primary
|
||||
? BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
)
|
||||
: widget.buttonType == ButtonType.secondary
|
||||
// Secondary
|
||||
? BoxDecoration(
|
||||
border: BoxBorder.all(color: Colors.white, width: 2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
)
|
||||
// Tertiary
|
||||
: const BoxDecoration();
|
||||
|
||||
return GestureDetector(
|
||||
onTapDown: (_) => setState(() => _isPressed = true),
|
||||
onTapUp: (_) => setState(() => _isPressed = false),
|
||||
@@ -38,10 +67,18 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
|
||||
child: AnimatedOpacity(
|
||||
opacity: _isPressed ? 0.6 : 1.0,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: Container(
|
||||
decoration: CustomTheme.standardBoxDecoration,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 6),
|
||||
child: widget.child,
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: widget.buttonConstraints,
|
||||
decoration: buttonDecoration,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
widget.buttonText,
|
||||
style: textStyling,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
|
||||
|
||||
class CustomAlertDialog extends StatelessWidget {
|
||||
/// A custom alert dialog widget that provides a os unspecific AlertDialog,
|
||||
@@ -16,20 +17,23 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String content;
|
||||
final List<Widget> actions;
|
||||
final Widget content;
|
||||
final List<CustomDialogAction> actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(title, style: const TextStyle(color: CustomTheme.textColor)),
|
||||
content: Text(
|
||||
content,
|
||||
style: const TextStyle(color: CustomTheme.textColor),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
content: content,
|
||||
actions: actions,
|
||||
backgroundColor: CustomTheme.boxColor,
|
||||
actionsAlignment: MainAxisAlignment.spaceAround,
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: CustomTheme.standardBorderRadiusAll,
|
||||
side: const BorderSide(color: CustomTheme.boxBorderColor),
|
||||
32
lib/presentation/widgets/dialog/custom_dialog_action.dart
Normal file
32
lib/presentation/widgets/dialog/custom_dialog_action.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
|
||||
|
||||
class CustomDialogAction extends StatelessWidget {
|
||||
/// A custom dialog action widget that represents a button in a dialog.
|
||||
/// - [text]: The text to be displayed on the button.
|
||||
/// - [buttonType]: The type of the button, which determines its styling.
|
||||
/// - [onPressed]: Callback function that is triggered when the button is pressed.
|
||||
const CustomDialogAction({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.text,
|
||||
this.buttonType = ButtonType.primary,
|
||||
});
|
||||
|
||||
final String text;
|
||||
|
||||
final ButtonType buttonType;
|
||||
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogButton(
|
||||
onPressed: onPressed,
|
||||
buttonText: text,
|
||||
buttonType: buttonType,
|
||||
buttonConstraints: const BoxConstraints(minWidth: 300),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
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/data/db/database.dart';
|
||||
@@ -62,7 +63,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
/// Skeleton data used while loading players.
|
||||
late final List<Player> skeletonData = List.filled(
|
||||
7,
|
||||
Player(name: 'Player 0', description: ''),
|
||||
Player(name: 'Player 0'),
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -140,6 +141,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: TextIconTile(
|
||||
text: player.name,
|
||||
suffixText: getNameCountText(player),
|
||||
onIconTap: () {
|
||||
setState(() {
|
||||
// Removes the player from the selection and notifies the parent.
|
||||
@@ -193,6 +195,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return TextIconListTile(
|
||||
text: suggestedPlayers[index].name,
|
||||
suffixText: getNameCountText(suggestedPlayers[index]),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
// If the player is not already selected
|
||||
@@ -282,7 +285,8 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
final loc = AppLocalizations.of(context);
|
||||
final playerName = _searchBarController.text.trim();
|
||||
|
||||
final createdPlayer = Player(name: playerName, description: '');
|
||||
int nameCount = _calculateNameCount(playerName);
|
||||
final createdPlayer = Player(name: playerName, nameCount: nameCount);
|
||||
final success = await db.playerDao.addPlayer(player: createdPlayer);
|
||||
|
||||
if (!context.mounted) return;
|
||||
@@ -295,6 +299,22 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
}
|
||||
}
|
||||
|
||||
int _calculateNameCount(String playerName) {
|
||||
final playersWithSameName =
|
||||
allPlayers.where((player) => player.name == playerName).toList()
|
||||
..sort((a, b) => a.nameCount.compareTo(b.nameCount));
|
||||
|
||||
if (playersWithSameName.isEmpty) {
|
||||
return 0;
|
||||
} else if (playersWithSameName.length == 1) {
|
||||
// Initialize nameCount
|
||||
playersWithSameName[0].nameCount = 1;
|
||||
}
|
||||
|
||||
// Return following count
|
||||
return playersWithSameName.length + 1;
|
||||
}
|
||||
|
||||
/// Updates the state after successfully adding a new player.
|
||||
void _handleSuccessfulPlayerCreation(Player player) {
|
||||
selectedPlayers.insert(0, player);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
@@ -81,7 +82,11 @@ class _GroupTileState extends State<GroupTile> {
|
||||
for (var member in [
|
||||
...widget.group.members,
|
||||
]..sort((a, b) => a.name.compareTo(b.name)))
|
||||
TextIconTile(text: member.name, iconEnabled: false),
|
||||
TextIconTile(
|
||||
text: member.name,
|
||||
suffixText: getNameCountText(member),
|
||||
iconEnabled: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2.5),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
@@ -44,7 +45,6 @@ class _MatchTileState extends State<MatchTile> {
|
||||
Widget build(BuildContext context) {
|
||||
final match = widget.match;
|
||||
final group = match.group;
|
||||
final winner = match.winner;
|
||||
final players = [...match.players]
|
||||
..sort((a, b) => a.name.compareTo(b.name));
|
||||
final loc = AppLocalizations.of(context);
|
||||
@@ -79,8 +79,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Group Info
|
||||
if (group != null) ...[
|
||||
Row(
|
||||
children: [
|
||||
@@ -95,7 +94,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 4),
|
||||
] else if (widget.compact) ...[
|
||||
Row(
|
||||
children: [
|
||||
@@ -110,10 +109,69 @@ class _MatchTileState extends State<MatchTile> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 6),
|
||||
] else ...[
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
|
||||
if (winner != null) ...[
|
||||
// Game + Ruleset Badge
|
||||
if (!widget.compact)
|
||||
IntrinsicHeight(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Game
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.primaryColor.withAlpha(230),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
bottomLeft: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Text(
|
||||
match.game.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Ruleset
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.primaryColor.withAlpha(140),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(8),
|
||||
bottomRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Text(
|
||||
translateRulesetToString(match.game.ruleset, context),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Winner / In Progress Info
|
||||
if (match.mvp.isNotEmpty) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
@@ -129,15 +187,11 @@ class _MatchTileState extends State<MatchTile> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.emoji_events,
|
||||
size: 20,
|
||||
color: Colors.amber,
|
||||
),
|
||||
getMvpIcon(),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${loc.winner}: ${winner.name}',
|
||||
getMvpText(loc),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -189,6 +243,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// Players List
|
||||
if (players.isNotEmpty && widget.compact == false) ...[
|
||||
Text(
|
||||
loc.players,
|
||||
@@ -203,7 +258,11 @@ class _MatchTileState extends State<MatchTile> {
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: players.map((player) {
|
||||
return TextIconTile(text: player.name, iconEnabled: false);
|
||||
return TextIconTile(
|
||||
text: player.name,
|
||||
suffixText: getNameCountText(player),
|
||||
iconEnabled: false,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
@@ -230,4 +289,44 @@ class _MatchTileState extends State<MatchTile> {
|
||||
return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}';
|
||||
}
|
||||
}
|
||||
|
||||
String getMvpText(AppLocalizations loc) {
|
||||
if (widget.match.mvp.isEmpty) return '';
|
||||
final ruleset = widget.match.game.ruleset;
|
||||
|
||||
if (ruleset == Ruleset.singleWinner) {
|
||||
return '${loc.winner}: ${widget.match.mvp.first.name}';
|
||||
} else if (ruleset == Ruleset.singleLoser) {
|
||||
return '${loc.loser}: ${widget.match.mvp.first.name}';
|
||||
} else if (ruleset == Ruleset.highestScore ||
|
||||
ruleset == Ruleset.lowestScore) {
|
||||
final mvp = widget.match.mvp;
|
||||
final mvpScore = widget.match.scores[mvp.first.id]?.score ?? 0;
|
||||
final mvpNames = mvp.map((player) => player.name).join(', ');
|
||||
|
||||
return '${loc.winner}: $mvpNames (${getPointLabel(loc, mvpScore)})';
|
||||
}
|
||||
return '${loc.winner}: n.A.';
|
||||
}
|
||||
|
||||
Icon getMvpIcon() {
|
||||
const Icon(Icons.emoji_events, size: 20, color: Colors.amber);
|
||||
|
||||
switch (widget.match.game.ruleset) {
|
||||
case Ruleset.singleWinner:
|
||||
return const Icon(Icons.emoji_events, size: 20, color: Colors.amber);
|
||||
case Ruleset.singleLoser:
|
||||
return const Icon(
|
||||
Icons.sentiment_dissatisfied_outlined,
|
||||
size: 20,
|
||||
color: Colors.blue,
|
||||
);
|
||||
case Ruleset.lowestScore:
|
||||
return const Icon(Icons.arrow_downward, size: 20, color: Colors.orange);
|
||||
case Ruleset.highestScore:
|
||||
return const Icon(Icons.arrow_upward, size: 20, color: Colors.green);
|
||||
default:
|
||||
return const Icon(Icons.emoji_events, size: 20, color: Colors.amber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
83
lib/presentation/widgets/tiles/score_list_tile.dart
Normal file
83
lib/presentation/widgets/tiles/score_list_tile.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
|
||||
class ScoreListTile extends StatelessWidget {
|
||||
/// A custom list tile widget that has a text field for inputting a score.
|
||||
/// - [text]: The leading text to be displayed.
|
||||
/// - [controller]: The controller for the text field to input the score.
|
||||
const ScoreListTile({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
/// The text to display next to the radio button.
|
||||
final String text;
|
||||
|
||||
final TextEditingController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: const BoxDecoration(color: CustomTheme.boxColor),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
|
||||
),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 4,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
cursorColor: CustomTheme.textColor,
|
||||
decoration: InputDecoration(
|
||||
hintText: loc.points,
|
||||
counterText: '',
|
||||
filled: true,
|
||||
fillColor: CustomTheme.onBoxColor,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 0,
|
||||
vertical: 0,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: CustomTheme.textColor.withAlpha(100),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(
|
||||
color: CustomTheme.primaryColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
|
||||
|
||||
@@ -32,7 +35,7 @@ class StatisticsTile extends StatelessWidget {
|
||||
final double width;
|
||||
|
||||
/// A list of tuples containing labels and their corresponding numeric values.
|
||||
final List<(String, num)> values;
|
||||
final List<(Player, num)> values;
|
||||
|
||||
/// The maximum number of items to display.
|
||||
final int itemCount;
|
||||
@@ -89,11 +92,29 @@ class StatisticsTile extends StatelessWidget {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: Text(
|
||||
values[index].$1,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
child: RichText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: values[index].$1.name,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -9,6 +9,7 @@ class TextIconListTile extends StatelessWidget {
|
||||
const TextIconListTile({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.suffixText = '',
|
||||
this.iconEnabled = true,
|
||||
this.onPressed,
|
||||
});
|
||||
@@ -16,6 +17,9 @@ class TextIconListTile extends StatelessWidget {
|
||||
/// The text to display in the tile.
|
||||
final String text;
|
||||
|
||||
/// An optional suffix text to display after the main text.
|
||||
final String suffixText;
|
||||
|
||||
/// A boolean to determine if the icon should be displayed.
|
||||
final bool iconEnabled;
|
||||
|
||||
@@ -35,12 +39,27 @@ class TextIconListTile extends StatelessWidget {
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.5),
|
||||
child: Text(
|
||||
text,
|
||||
child: RichText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
text: TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: suffixText,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: CustomTheme.textColor.withAlpha(100),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -9,6 +9,7 @@ class TextIconTile extends StatelessWidget {
|
||||
const TextIconTile({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.suffixText = '',
|
||||
this.iconEnabled = true,
|
||||
this.onIconTap,
|
||||
});
|
||||
@@ -16,6 +17,8 @@ class TextIconTile extends StatelessWidget {
|
||||
/// The text to display in the tile.
|
||||
final String text;
|
||||
|
||||
final String suffixText;
|
||||
|
||||
/// A boolean to determine if the icon should be displayed.
|
||||
final bool iconEnabled;
|
||||
|
||||
@@ -36,10 +39,28 @@ class TextIconTile extends StatelessWidget {
|
||||
children: [
|
||||
if (iconEnabled) const SizedBox(width: 3),
|
||||
Flexible(
|
||||
child: Text(
|
||||
text,
|
||||
child: RichText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
text: TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: suffixText,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: CustomTheme.textColor.withAlpha(120),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (iconEnabled) ...<Widget>[
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:tallee/data/models/game.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/score_entry.dart';
|
||||
import 'package:tallee/data/models/team.dart';
|
||||
|
||||
class DataTransferService {
|
||||
@@ -36,59 +37,12 @@ class DataTransferService {
|
||||
final games = await db.gameDao.getAllGames();
|
||||
final teams = await db.teamDao.getAllTeams();
|
||||
|
||||
// Construct a JSON representation of the data in normalized format
|
||||
final Map<String, dynamic> jsonMap = {
|
||||
'players': players.map((p) => p.toJson()).toList(),
|
||||
'games': games.map((g) => g.toJson()).toList(),
|
||||
'groups': groups
|
||||
.map(
|
||||
(g) => {
|
||||
'id': g.id,
|
||||
'name': g.name,
|
||||
'description': g.description,
|
||||
'createdAt': g.createdAt.toIso8601String(),
|
||||
'memberIds': (g.members).map((m) => m.id).toList(),
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
'teams': teams
|
||||
.map(
|
||||
(t) => {
|
||||
'id': t.id,
|
||||
'name': t.name,
|
||||
'createdAt': t.createdAt.toIso8601String(),
|
||||
'memberIds': (t.members).map((m) => m.id).toList(),
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
'matches': matches
|
||||
.map(
|
||||
(m) => {
|
||||
'id': m.id,
|
||||
'name': m.name,
|
||||
'createdAt': m.createdAt.toIso8601String(),
|
||||
'endedAt': m.endedAt?.toIso8601String(),
|
||||
'gameId': m.game.id,
|
||||
'groupId': m.group?.id,
|
||||
'playerIds': m.players.map((p) => p.id).toList(),
|
||||
'scores': m.scores.map(
|
||||
(playerId, scores) => MapEntry(
|
||||
playerId,
|
||||
scores
|
||||
.map(
|
||||
(s) => {
|
||||
'roundNumber': s.roundNumber,
|
||||
'score': s.score,
|
||||
'change': s.change,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
'notes': m.notes,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
'players': players.map((player) => player.toJson()).toList(),
|
||||
'games': games.map((game) => game.toJson()).toList(),
|
||||
'groups': groups.map((group) => group.toJson()).toList(),
|
||||
'teams': teams.map((team) => team.toJson()).toList(),
|
||||
'matches': matches.map((match) => match.toJson()).toList(),
|
||||
};
|
||||
|
||||
return json.encode(jsonMap);
|
||||
@@ -105,7 +59,7 @@ class DataTransferService {
|
||||
) async {
|
||||
try {
|
||||
final bytes = Uint8List.fromList(utf8.encode(jsonString));
|
||||
final path = await FilePicker.platform.saveFile(
|
||||
final path = await FilePicker.saveFile(
|
||||
fileName: '$fileName.json',
|
||||
bytes: bytes,
|
||||
);
|
||||
@@ -126,7 +80,7 @@ class DataTransferService {
|
||||
static Future<ImportResult> importData(BuildContext context) async {
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
|
||||
final path = await FilePicker.platform.pickFiles(
|
||||
final path = await FilePicker.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
@@ -284,6 +238,15 @@ class DataTransferService {
|
||||
? DateTime.parse(map['endedAt'] as String)
|
||||
: null;
|
||||
final notes = map['notes'] as String? ?? '';
|
||||
final scoresJson = map['scores'] as Map<String, dynamic>? ?? {};
|
||||
final scores = scoresJson.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
value != null
|
||||
? ScoreEntry.fromJson(value as Map<String, dynamic>)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
|
||||
// Link attributes to objects
|
||||
final game = gamesMap[gameId] ?? getFallbackGame();
|
||||
@@ -305,6 +268,7 @@ class DataTransferService {
|
||||
createdAt: createdAt,
|
||||
endedAt: endedAt,
|
||||
notes: notes,
|
||||
scores: scores,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user