Verschiedene Regelsätze implementieren #194
@@ -1,5 +1,9 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart
|
||||
|
||||
linter:
|
||||
rules:
|
||||
avoid_print: false
|
||||
@@ -11,8 +15,4 @@ linter:
|
||||
prefer_const_literals_to_create_immutables: true
|
||||
unnecessary_const: true
|
||||
lines_longer_than_80_chars: false
|
||||
constant_identifier_names: false
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart
|
||||
constant_identifier_names: false
|
||||
@@ -147,13 +147,19 @@
|
||||
"type": "string"
|
||||
},
|
||||
"endedAt": {
|
||||
"type": ["string", "null"]
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gameId": {
|
||||
"type": "string"
|
||||
},
|
||||
"groupId": {
|
||||
"type": ["string", "null"]
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"playerIds": {
|
||||
"type": "array",
|
||||
@@ -163,22 +169,28 @@
|
||||
},
|
||||
"scores": {
|
||||
"type": "object",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"properties": {
|
||||
"roundNumber": {
|
||||
"type": "number"
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"score": {
|
||||
"type": "number"
|
||||
},
|
||||
"change": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"score": {
|
||||
"type": "number"
|
||||
},
|
||||
"change": {
|
||||
"type": "number"
|
||||
}
|
||||
"required": ["roundNumber", "score", "change"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"notes": {
|
||||
|
||||
@@ -51,3 +51,11 @@ String getNameCountText(Player player) {
|
||||
}
|
||||
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,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -255,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;
|
||||
|
||||
@@ -170,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),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -227,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,
|
||||
@@ -259,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(
|
||||
|
||||
@@ -152,8 +152,8 @@ class _StatisticsViewState extends State<StatisticsView> {
|
||||
|
||||
// Getting the winners
|
||||
for (var match in matches) {
|
||||
final winner = match.winner;
|
||||
if (winner != null) {
|
||||
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) {
|
||||
@@ -179,8 +179,7 @@ class _StatisticsViewState extends State<StatisticsView> {
|
||||
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, winCounts[i].$2);
|
||||
}
|
||||
|
||||
@@ -63,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
|
||||
|
||||
@@ -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,
|
||||
@@ -234,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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
14
pubspec.yaml
14
pubspec.yaml
@@ -7,18 +7,18 @@ environment:
|
||||
sdk: ^3.8.1
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
clock: ^1.1.2
|
||||
cupertino_icons: ^1.0.6
|
||||
drift: ^2.27.0
|
||||
drift_flutter: ^0.2.4
|
||||
file_picker: ^10.3.6
|
||||
file_picker: ^11.0.2
|
||||
file_saver: ^0.3.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
fluttericon: ^2.0.0
|
||||
font_awesome_flutter: ^10.12.0
|
||||
font_awesome_flutter: ^11.0.0
|
||||
intl: any
|
||||
json_schema: ^5.2.2
|
||||
package_info_plus: ^9.0.0
|
||||
@@ -33,7 +33,7 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
build_runner: ^2.7.0
|
||||
dart_pubspec_licenses: ^3.0.14
|
||||
drift_dev: ^2.29.0
|
||||
drift_dev: ^2.27.0
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
|
||||
@@ -29,10 +29,10 @@ void main() {
|
||||
);
|
||||
|
||||
withClock(fakeClock, () {
|
||||
testPlayer1 = Player(name: 'Alice', description: '');
|
||||
testPlayer2 = Player(name: 'Bob', description: '');
|
||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
||||
testPlayer4 = Player(name: 'Diana', description: '');
|
||||
testPlayer1 = Player(name: 'Alice');
|
||||
testPlayer2 = Player(name: 'Bob');
|
||||
testPlayer3 = Player(name: 'Charlie');
|
||||
testPlayer4 = Player(name: 'Diana');
|
||||
testGroup1 = Group(
|
||||
name: 'Test Group',
|
||||
description: '',
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:tallee/data/models/game.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/score_entry.dart';
|
||||
|
||||
void main() {
|
||||
late AppDatabase database;
|
||||
@@ -36,11 +37,11 @@ void main() {
|
||||
);
|
||||
|
||||
withClock(fakeClock, () {
|
||||
testPlayer1 = Player(name: 'Alice', description: '');
|
||||
testPlayer2 = Player(name: 'Bob', description: '');
|
||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
||||
testPlayer4 = Player(name: 'Diana', description: '');
|
||||
testPlayer5 = Player(name: 'Eve', description: '');
|
||||
testPlayer1 = Player(name: 'Alice');
|
||||
testPlayer2 = Player(name: 'Bob');
|
||||
testPlayer3 = Player(name: 'Charlie');
|
||||
testPlayer4 = Player(name: 'Diana');
|
||||
testPlayer5 = Player(name: 'Eve');
|
||||
testGroup1 = Group(
|
||||
name: 'Test Group 1',
|
||||
description: '',
|
||||
@@ -63,29 +64,24 @@ void main() {
|
||||
game: testGame,
|
||||
group: testGroup1,
|
||||
players: [testPlayer4, testPlayer5],
|
||||
winner: testPlayer4,
|
||||
notes: '',
|
||||
scores: {testPlayer4.id: ScoreEntry(score: 1)},
|
||||
);
|
||||
testMatch2 = Match(
|
||||
name: 'Second Test Match',
|
||||
game: testGame,
|
||||
group: testGroup2,
|
||||
players: [testPlayer1, testPlayer2, testPlayer3],
|
||||
winner: testPlayer2,
|
||||
notes: '',
|
||||
);
|
||||
testMatchOnlyPlayers = Match(
|
||||
name: 'Test Match with Players',
|
||||
game: testGame,
|
||||
players: [testPlayer1, testPlayer2, testPlayer3],
|
||||
winner: testPlayer3,
|
||||
notes: '',
|
||||
);
|
||||
testMatchOnlyGroup = Match(
|
||||
name: 'Test Match with Group',
|
||||
game: testGame,
|
||||
group: testGroup2,
|
||||
notes: '',
|
||||
players: testGroup2.members,
|
||||
);
|
||||
});
|
||||
await database.playerDao.addPlayersAsList(
|
||||
@@ -289,8 +285,8 @@ void main() {
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
|
||||
expect(fetchedMatch.winner, isNotNull);
|
||||
expect(fetchedMatch.winner!.id, testPlayer4.id);
|
||||
expect(fetchedMatch.mvp, isNotNull);
|
||||
expect(fetchedMatch.mvp.first.id, testPlayer4.id);
|
||||
});
|
||||
|
||||
test('Setting a winner works correctly', () async {
|
||||
@@ -304,8 +300,8 @@ void main() {
|
||||
final fetchedMatch = await database.matchDao.getMatchById(
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(fetchedMatch.winner, isNotNull);
|
||||
expect(fetchedMatch.winner!.id, testPlayer5.id);
|
||||
expect(fetchedMatch.mvp, isNotNull);
|
||||
expect(fetchedMatch.mvp.first.id, testPlayer5.id);
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@@ -33,10 +33,10 @@ void main() {
|
||||
);
|
||||
|
||||
withClock(fakeClock, () {
|
||||
testPlayer1 = Player(name: 'Alice', description: '');
|
||||
testPlayer2 = Player(name: 'Bob', description: '');
|
||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
||||
testPlayer4 = Player(name: 'Diana', description: '');
|
||||
testPlayer1 = Player(name: 'Alice');
|
||||
testPlayer2 = Player(name: 'Bob');
|
||||
testPlayer3 = Player(name: 'Charlie');
|
||||
testPlayer4 = Player(name: 'Diana');
|
||||
testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]);
|
||||
testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]);
|
||||
testTeam3 = Team(name: 'Team Gamma', members: [testPlayer1, testPlayer3]);
|
||||
@@ -343,8 +343,16 @@ void main() {
|
||||
// Verifies that teams with overlapping members are independent.
|
||||
test('Teams with overlapping members are independent', () async {
|
||||
// Create two matches since player_match has primary key {playerId, matchId}
|
||||
final match1 = Match(name: 'Match 1', game: testGame1, notes: '');
|
||||
final match2 = Match(name: 'Match 2', game: testGame2, notes: '');
|
||||
final match1 = Match(
|
||||
name: 'Match 1',
|
||||
game: testGame1,
|
||||
players: [testPlayer1, testPlayer2],
|
||||
);
|
||||
final match2 = Match(
|
||||
name: 'Match 2',
|
||||
game: testGame2,
|
||||
players: [testPlayer1, testPlayer2],
|
||||
);
|
||||
await database.matchDao.addMatch(match: match1);
|
||||
await database.matchDao.addMatch(match: match2);
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ void main() {
|
||||
);
|
||||
|
||||
withClock(fakeClock, () {
|
||||
testPlayer1 = Player(name: 'Test Player', description: '');
|
||||
testPlayer2 = Player(name: 'Second Player', description: '');
|
||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
||||
testPlayer4 = Player(name: 'Diana', description: '');
|
||||
testPlayer1 = Player(name: 'Test Player');
|
||||
testPlayer2 = Player(name: 'Second Player');
|
||||
testPlayer3 = Player(name: 'Charlie');
|
||||
testPlayer4 = Player(name: 'Diana');
|
||||
});
|
||||
});
|
||||
tearDown(() async {
|
||||
@@ -348,7 +348,7 @@ void main() {
|
||||
|
||||
// Verifies that a player with empty string name is stored correctly.
|
||||
test('Player with empty string name is stored correctly', () async {
|
||||
final emptyNamePlayer = Player(name: '', description: '');
|
||||
final emptyNamePlayer = Player(name: '');
|
||||
|
||||
await database.playerDao.addPlayer(player: emptyNamePlayer);
|
||||
|
||||
@@ -361,7 +361,7 @@ void main() {
|
||||
// Verifies that a player with very long name is stored correctly.
|
||||
test('Player with very long name is stored correctly', () async {
|
||||
final longName = 'A' * 1000;
|
||||
final longNamePlayer = Player(name: longName, description: '');
|
||||
final longNamePlayer = Player(name: longName);
|
||||
|
||||
await database.playerDao.addPlayer(player: longNamePlayer);
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ void main() {
|
||||
);
|
||||
|
||||
withClock(fakeClock, () {
|
||||
testPlayer1 = Player(name: 'Alice', description: '');
|
||||
testPlayer2 = Player(name: 'Bob', description: '');
|
||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
||||
testPlayer4 = Player(name: 'Diana', description: '');
|
||||
testPlayer1 = Player(name: 'Alice');
|
||||
testPlayer2 = Player(name: 'Bob');
|
||||
testPlayer3 = Player(name: 'Charlie');
|
||||
testPlayer4 = Player(name: 'Diana');
|
||||
testGroup = Group(
|
||||
name: 'Test Group',
|
||||
description: '',
|
||||
|
||||
@@ -37,12 +37,12 @@ void main() {
|
||||
);
|
||||
|
||||
withClock(fakeClock, () {
|
||||
testPlayer1 = Player(name: 'Alice', description: '');
|
||||
testPlayer2 = Player(name: 'Bob', description: '');
|
||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
||||
testPlayer4 = Player(name: 'Diana', description: '');
|
||||
testPlayer5 = Player(name: 'Eve', description: '');
|
||||
testPlayer6 = Player(name: 'Frank', description: '');
|
||||
testPlayer1 = Player(name: 'Alice');
|
||||
testPlayer2 = Player(name: 'Bob');
|
||||
testPlayer3 = Player(name: 'Charlie');
|
||||
testPlayer4 = Player(name: 'Diana');
|
||||
testPlayer5 = Player(name: 'Eve');
|
||||
testPlayer6 = Player(name: 'Frank');
|
||||
testGroup = Group(
|
||||
name: 'Test Group',
|
||||
description: '',
|
||||
@@ -58,14 +58,13 @@ void main() {
|
||||
testMatchOnlyGroup = Match(
|
||||
name: 'Test Match with Group',
|
||||
game: testGame,
|
||||
players: testGroup.members,
|
||||
group: testGroup,
|
||||
notes: '',
|
||||
);
|
||||
testMatchOnlyPlayers = Match(
|
||||
name: 'Test Match with Players',
|
||||
game: testGame,
|
||||
players: [testPlayer4, testPlayer5, testPlayer6],
|
||||
notes: '',
|
||||
);
|
||||
testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]);
|
||||
testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]);
|
||||
@@ -96,7 +95,7 @@ void main() {
|
||||
matchId: testMatchOnlyGroup.id,
|
||||
);
|
||||
|
||||
expect(matchHasPlayers, false);
|
||||
expect(matchHasPlayers, true);
|
||||
|
||||
await database.playerMatchDao.addPlayerToMatch(
|
||||
matchId: testMatchOnlyGroup.id,
|
||||
@@ -397,7 +396,6 @@ void main() {
|
||||
matchId: testMatchOnlyGroup.id,
|
||||
teamId: testTeam1.id,
|
||||
);
|
||||
|
||||
expect(playersInTeam.length, 2);
|
||||
final playerIds = playersInTeam.map((p) => p.id).toSet();
|
||||
expect(playerIds.contains(testPlayer1.id), true);
|
||||
@@ -426,18 +424,16 @@ void main() {
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
|
||||
// Try to add the same player again with different score
|
||||
await database.playerMatchDao.addPlayerToMatch(
|
||||
matchId: testMatchOnlyGroup.id,
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
|
||||
// Verify player count is still 1
|
||||
final players = await database.playerMatchDao.getPlayersOfMatch(
|
||||
matchId: testMatchOnlyGroup.id,
|
||||
);
|
||||
|
||||
expect(players?.length, 1);
|
||||
expect(players?.length, 3);
|
||||
});
|
||||
|
||||
test(
|
||||
@@ -546,6 +542,7 @@ void main() {
|
||||
matchId: testMatchOnlyGroup.id,
|
||||
teamId: testTeam1.id,
|
||||
);
|
||||
|
||||
expect(playersInTeam1.length, 2);
|
||||
final team1Ids = playersInTeam1.map((p) => p.id).toSet();
|
||||
expect(team1Ids.contains(testPlayer1.id), true);
|
||||
@@ -568,13 +565,11 @@ void main() {
|
||||
name: 'Match 1',
|
||||
game: testGame,
|
||||
players: playersList,
|
||||
notes: '',
|
||||
);
|
||||
final match2 = Match(
|
||||
name: 'Match 2',
|
||||
game: testGame,
|
||||
players: playersList,
|
||||
notes: '',
|
||||
);
|
||||
|
||||
await Future.wait([
|
||||
|
||||
@@ -30,9 +30,9 @@ void main() {
|
||||
);
|
||||
|
||||
withClock(fakeClock, () {
|
||||
testPlayer1 = Player(name: 'Alice', description: '');
|
||||
testPlayer2 = Player(name: 'Bob', description: '');
|
||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
||||
testPlayer1 = Player(name: 'Alice');
|
||||
testPlayer2 = Player(name: 'Bob');
|
||||
testPlayer3 = Player(name: 'Charlie');
|
||||
testGame = Game(
|
||||
name: 'Test Game',
|
||||
ruleset: Ruleset.singleWinner,
|
||||
@@ -44,13 +44,11 @@ void main() {
|
||||
name: 'Test Match 1',
|
||||
game: testGame,
|
||||
players: [testPlayer1, testPlayer2],
|
||||
notes: '',
|
||||
);
|
||||
testMatch2 = Match(
|
||||
name: 'Test Match 2',
|
||||
game: testGame,
|
||||
players: [testPlayer2, testPlayer3],
|
||||
notes: '',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -231,8 +229,8 @@ void main() {
|
||||
);
|
||||
|
||||
expect(scores.length, 2);
|
||||
expect(scores[testPlayer1.id]!.length, 2);
|
||||
expect(scores[testPlayer2.id]!.length, 1);
|
||||
expect(scores[testPlayer1.id]!, isNotNull);
|
||||
expect(scores[testPlayer2.id]!, isNotNull);
|
||||
});
|
||||
|
||||
test('getAllMatchScores() with no scores saved', () async {
|
||||
|
||||
@@ -64,14 +64,8 @@ void main() {
|
||||
players: [testPlayer1, testPlayer2],
|
||||
notes: 'Test notes',
|
||||
scores: {
|
||||
testPlayer1.id: [
|
||||
ScoreEntry(roundNumber: 1, score: 10, change: 10),
|
||||
ScoreEntry(roundNumber: 2, score: 20, change: 10),
|
||||
],
|
||||
testPlayer2.id: [
|
||||
ScoreEntry(roundNumber: 1, score: 15, change: 15),
|
||||
ScoreEntry(roundNumber: 2, score: 25, change: 10),
|
||||
],
|
||||
testPlayer1.id: ScoreEntry(roundNumber: 1, score: 10, change: 10),
|
||||
testPlayer2.id: ScoreEntry(roundNumber: 1, score: 15, change: 15),
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -302,46 +296,25 @@ void main() {
|
||||
final scoresJson = matchData['scores'] as Map<String, dynamic>;
|
||||
expect(scoresJson, isA<Map<String, dynamic>>());
|
||||
|
||||
final scores = scoresJson.map(
|
||||
(playerId, scoreList) => MapEntry(
|
||||
playerId,
|
||||
(scoreList as List)
|
||||
.map((s) => ScoreEntry.fromJson(s as Map<String, dynamic>))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
// Verify scores are properly structured (single score per player, not list)
|
||||
expect(scoresJson[testPlayer1.id], isNotNull);
|
||||
expect(scoresJson[testPlayer2.id], isNotNull);
|
||||
|
||||
expect(scores, isA<Map<String, List<ScoreEntry>>>());
|
||||
// Parse player 1 score
|
||||
final player1ScoreJson =
|
||||
scoresJson[testPlayer1.id] as Map<String, dynamic>;
|
||||
final player1Score = ScoreEntry.fromJson(player1ScoreJson);
|
||||
expect(player1Score.roundNumber, 1);
|
||||
expect(player1Score.score, 10);
|
||||
expect(player1Score.change, 10);
|
||||
|
||||
/* Player 1 scores */
|
||||
// General structure
|
||||
expect(scores[testPlayer1.id], isNotNull);
|
||||
expect(scores[testPlayer1.id]!.length, 2);
|
||||
|
||||
// Round 1
|
||||
expect(scores[testPlayer1.id]![0].roundNumber, 1);
|
||||
expect(scores[testPlayer1.id]![0].score, 10);
|
||||
expect(scores[testPlayer1.id]![0].change, 10);
|
||||
|
||||
// Round 2
|
||||
expect(scores[testPlayer1.id]![1].roundNumber, 2);
|
||||
expect(scores[testPlayer1.id]![1].score, 20);
|
||||
expect(scores[testPlayer1.id]![1].change, 10);
|
||||
|
||||
/* Player 2 scores */
|
||||
// General structure
|
||||
expect(scores[testPlayer2.id], isNotNull);
|
||||
expect(scores[testPlayer2.id]!.length, 2);
|
||||
|
||||
// Round 1
|
||||
expect(scores[testPlayer2.id]![0].roundNumber, 1);
|
||||
expect(scores[testPlayer2.id]![0].score, 15);
|
||||
expect(scores[testPlayer2.id]![0].change, 15);
|
||||
|
||||
// Round 2
|
||||
expect(scores[testPlayer2.id]![1].roundNumber, 2);
|
||||
expect(scores[testPlayer2.id]![1].score, 25);
|
||||
expect(scores[testPlayer2.id]![1].change, 10);
|
||||
// Parse player 2 score
|
||||
final player2ScoreJson =
|
||||
scoresJson[testPlayer2.id] as Map<String, dynamic>;
|
||||
final player2Score = ScoreEntry.fromJson(player2ScoreJson);
|
||||
expect(player2Score.roundNumber, 1);
|
||||
expect(player2Score.score, 15);
|
||||
expect(player2Score.change, 15);
|
||||
});
|
||||
|
||||
testWidgets('Match without group is handled correctly', (tester) async {
|
||||
@@ -904,14 +877,8 @@ void main() {
|
||||
'playerIds': [testPlayer1.id, testPlayer2.id],
|
||||
'notes': testMatch.notes,
|
||||
'scores': {
|
||||
testPlayer1.id: [
|
||||
{'roundNumber': 1, 'score': 10, 'change': 10},
|
||||
{'roundNumber': 2, 'score': 20, 'change': 10},
|
||||
],
|
||||
testPlayer2.id: [
|
||||
{'roundNumber': 1, 'score': 15, 'change': 15},
|
||||
{'roundNumber': 2, 'score': 25, 'change': 10},
|
||||
],
|
||||
testPlayer1.id: {'roundNumber': 1, 'score': 10, 'change': 10},
|
||||
testPlayer2.id: {'roundNumber': 1, 'score': 15, 'change': 15},
|
||||
},
|
||||
'createdAt': testMatch.createdAt.toIso8601String(),
|
||||
'endedAt': null,
|
||||
|
||||
Reference in New Issue
Block a user