Verschiedene Regelsätze implementieren #194
@@ -1,5 +1,9 @@
|
|||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
avoid_print: false
|
avoid_print: false
|
||||||
@@ -11,8 +15,4 @@ linter:
|
|||||||
prefer_const_literals_to_create_immutables: true
|
prefer_const_literals_to_create_immutables: true
|
||||||
unnecessary_const: true
|
unnecessary_const: true
|
||||||
lines_longer_than_80_chars: false
|
lines_longer_than_80_chars: false
|
||||||
constant_identifier_names: false
|
constant_identifier_names: false
|
||||||
|
|
||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
- lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart
|
|
||||||
@@ -147,13 +147,19 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"endedAt": {
|
"endedAt": {
|
||||||
"type": ["string", "null"]
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"gameId": {
|
"gameId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"groupId": {
|
"groupId": {
|
||||||
"type": ["string", "null"]
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"playerIds": {
|
"playerIds": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -163,22 +169,28 @@
|
|||||||
},
|
},
|
||||||
"scores": {
|
"scores": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"items": {
|
"additionalProperties": {
|
||||||
"type": "array",
|
"oneOf": [
|
||||||
"items": {
|
{
|
||||||
"type": "string",
|
"type": "null"
|
||||||
"properties": {
|
},
|
||||||
"roundNumber": {
|
{
|
||||||
"type": "number"
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"roundNumber": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"score": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"score": {
|
"required": ["roundNumber", "score", "change"],
|
||||||
"type": "number"
|
"additionalProperties": false
|
||||||
},
|
|
||||||
"change": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
|
|||||||
@@ -51,3 +51,11 @@ String getNameCountText(Player player) {
|
|||||||
}
|
}
|
||||||
return '';
|
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(
|
static const SearchBarThemeData searchBarTheme = SearchBarThemeData(
|
||||||
textStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.textColor)),
|
textStyle: WidgetStatePropertyAll(TextStyle(color: textColor)),
|
||||||
hintStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.hintColor)),
|
hintStyle: WidgetStatePropertyAll(TextStyle(color: hintColor)),
|
||||||
);
|
);
|
||||||
|
|
||||||
static final RadioThemeData radioTheme = RadioThemeData(
|
static final RadioThemeData radioTheme = RadioThemeData(
|
||||||
fillColor: WidgetStateProperty.resolveWith<Color>((states) {
|
fillColor: WidgetStateProperty.resolveWith<Color>((states) {
|
||||||
if (states.contains(WidgetState.selected)) {
|
if (states.contains(WidgetState.selected)) {
|
||||||
return CustomTheme.primaryColor;
|
return primaryColor;
|
||||||
}
|
}
|
||||||
return CustomTheme.textColor;
|
return textColor;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
static const InputDecorationTheme inputDecorationTheme = InputDecorationTheme(
|
static const InputDecorationTheme inputDecorationTheme = InputDecorationTheme(
|
||||||
labelStyle: TextStyle(color: CustomTheme.textColor),
|
labelStyle: TextStyle(color: textColor),
|
||||||
hintStyle: TextStyle(color: CustomTheme.hintColor),
|
hintStyle: TextStyle(color: hintColor),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
|||||||
matchId: row.id,
|
matchId: row.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
|
|
||||||
return Match(
|
return Match(
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
@@ -45,7 +44,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
|||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
endedAt: row.endedAt,
|
endedAt: row.endedAt,
|
||||||
scores: scores,
|
scores: scores,
|
||||||
winner: winner,
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -68,8 +66,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
|||||||
|
|
||||||
final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId);
|
final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId);
|
||||||
|
|
||||||
final winner = await db.scoreEntryDao.getWinner(matchId: matchId);
|
|
||||||
|
|
||||||
return Match(
|
return Match(
|
||||||
id: result.id,
|
id: result.id,
|
||||||
name: result.name,
|
name: result.name,
|
||||||
@@ -80,7 +76,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
|||||||
createdAt: result.createdAt,
|
createdAt: result.createdAt,
|
||||||
endedAt: result.endedAt,
|
endedAt: result.endedAt,
|
||||||
scores: scores,
|
scores: scores,
|
||||||
winner: winner,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,19 +105,14 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (final pid in match.scores.keys) {
|
for (final pid in match.scores.keys) {
|
||||||
final playerScores = match.scores[pid]!;
|
final playerScores = match.scores[pid];
|
||||||
await db.scoreEntryDao.addScoresAsList(
|
if (playerScores != null) {
|
||||||
entrys: playerScores,
|
await db.scoreEntryDao.addScore(
|
||||||
playerId: pid,
|
entry: playerScores,
|
||||||
matchId: match.id,
|
playerId: pid,
|
||||||
);
|
matchId: match.id,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
if (match.winner != null) {
|
|
||||||
await db.scoreEntryDao.setWinner(
|
|
||||||
matchId: match.id,
|
|
||||||
playerId: match.winner!.id,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -140,6 +130,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
|||||||
uniqueGames[match.game.id] = match.game;
|
uniqueGames[match.game.id] = match.game;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add games
|
||||||
if (uniqueGames.isNotEmpty) {
|
if (uniqueGames.isNotEmpty) {
|
||||||
await db.batch(
|
await db.batch(
|
||||||
(b) => b.insertAll(
|
(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(
|
await db.batch(
|
||||||
(b) => b.insertAll(
|
(b) => b.insertAll(
|
||||||
db.groupTable,
|
db.groupTable,
|
||||||
@@ -181,7 +172,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add all matches in batch
|
// Add matches
|
||||||
await db.batch(
|
await db.batch(
|
||||||
(b) => b.insertAll(
|
(b) => b.insertAll(
|
||||||
matchTable,
|
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>{};
|
final uniquePlayers = <String, Player>{};
|
||||||
for (final match in matches) {
|
for (final match in matches) {
|
||||||
for (final p in match.players) {
|
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) {
|
await db.batch((b) {
|
||||||
for (final match in matches) {
|
for (final match in matches) {
|
||||||
for (final p in match.players) {
|
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) {
|
await db.batch((b) {
|
||||||
for (final match in matches) {
|
for (final match in matches) {
|
||||||
if (match.group != null) {
|
if (match.group != null) {
|
||||||
@@ -300,7 +311,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
|||||||
final group = await db.groupDao.getGroupById(groupId: groupId);
|
final group = await db.groupDao.getGroupById(groupId: groupId);
|
||||||
final players =
|
final players =
|
||||||
await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? [];
|
await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? [];
|
||||||
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
|
|
||||||
return Match(
|
return Match(
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
@@ -310,7 +320,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
|||||||
notes: row.notes ?? '',
|
notes: row.notes ?? '',
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
endedAt: row.endedAt,
|
endedAt: row.endedAt,
|
||||||
winner: winner,
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
|||||||
matchId: matchId,
|
matchId: matchId,
|
||||||
teamId: Value(teamId),
|
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.
|
/// Retrieves all scores for a specific match.
|
||||||
Future<Map<String, List<ScoreEntry>>> getAllMatchScores({
|
Future<Map<String, ScoreEntry?>> getAllMatchScores({
|
||||||
required String matchId,
|
required String matchId,
|
||||||
}) async {
|
}) async {
|
||||||
final query = select(scoreEntryTable)
|
final query = select(scoreEntryTable)
|
||||||
..where((s) => s.matchId.equals(matchId));
|
..where((s) => s.matchId.equals(matchId));
|
||||||
final result = await query.get();
|
final result = await query.get();
|
||||||
|
|
||||||
final Map<String, List<ScoreEntry>> scoresByPlayer = {};
|
final Map<String, ScoreEntry?> scoresByPlayer = {};
|
||||||
for (final row in result) {
|
for (final row in result) {
|
||||||
final score = ScoreEntry(
|
final score = ScoreEntry(
|
||||||
roundNumber: row.roundNumber,
|
roundNumber: row.roundNumber,
|
||||||
score: row.score,
|
score: row.score,
|
||||||
change: row.change,
|
change: row.change,
|
||||||
);
|
);
|
||||||
scoresByPlayer.putIfAbsent(row.playerId, () => []).add(score);
|
scoresByPlayer[row.playerId] = score;
|
||||||
}
|
}
|
||||||
|
|
||||||
return scoresByPlayer;
|
return scoresByPlayer;
|
||||||
@@ -235,22 +235,29 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
|||||||
return rowsAffected > 0;
|
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 {
|
Future<Player?> getWinner({required String matchId}) async {
|
||||||
final query = select(scoreEntryTable)
|
final query =
|
||||||
..where((s) => s.matchId.equals(matchId))
|
select(scoreEntryTable).join([
|
||||||
..orderBy([(s) => OrderingTerm.desc(s.score)])
|
innerJoin(
|
||||||
..limit(1);
|
db.playerTable,
|
||||||
final result = await query.getSingleOrNull();
|
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(
|
return Player(
|
||||||
id: player.id,
|
id: playerData.id,
|
||||||
name: player.name,
|
name: playerData.name,
|
||||||
createdAt: player.createdAt,
|
createdAt: playerData.createdAt,
|
||||||
description: player.description,
|
description: playerData.description,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,20 +302,29 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
|||||||
return rowsAffected > 0;
|
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 {
|
Future<Player?> getLooser({required String matchId}) async {
|
||||||
final query = select(scoreEntryTable)
|
final query =
|
||||||
..where((s) => s.matchId.equals(matchId) & s.score.equals(0));
|
select(scoreEntryTable).join([
|
||||||
final result = await query.getSingleOrNull();
|
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(
|
return Player(
|
||||||
id: player.id,
|
id: playerData.id,
|
||||||
name: player.name,
|
name: playerData.name,
|
||||||
createdAt: player.createdAt,
|
createdAt: playerData.createdAt,
|
||||||
description: player.description,
|
description: playerData.description,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
import 'package:tallee/core/enums.dart';
|
import 'package:tallee/core/enums.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class Game {
|
class Game {
|
||||||
final String id;
|
final String id;
|
||||||
@@ -33,7 +33,10 @@ class Game {
|
|||||||
: id = json['id'],
|
: id = json['id'],
|
||||||
createdAt = DateTime.parse(json['createdAt']),
|
createdAt = DateTime.parse(json['createdAt']),
|
||||||
name = json['name'],
|
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'],
|
description = json['description'],
|
||||||
color = GameColor.values.firstWhere((e) => e.name == json['color']),
|
color = GameColor.values.firstWhere((e) => e.name == json['color']),
|
||||||
icon = json['icon'];
|
icon = json['icon'];
|
||||||
@@ -49,4 +52,3 @@ class Game {
|
|||||||
'icon': icon,
|
'icon': icon,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,27 +15,25 @@ class Match {
|
|||||||
final Group? group;
|
final Group? group;
|
||||||
final List<Player> players;
|
final List<Player> players;
|
||||||
final String notes;
|
final String notes;
|
||||||
Map<String, List<ScoreEntry>> scores;
|
Map<String, ScoreEntry?> scores;
|
||||||
Player? winner;
|
|
||||||
|
|
||||||
Match({
|
Match({
|
||||||
String? id,
|
|
||||||
DateTime? createdAt,
|
|
||||||
this.endedAt,
|
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.game,
|
required this.game,
|
||||||
|
required this.players,
|
||||||
|
this.endedAt,
|
||||||
this.group,
|
this.group,
|
||||||
this.players = const [],
|
|
||||||
this.notes = '',
|
this.notes = '',
|
||||||
Map<String, List<ScoreEntry>>? scores,
|
String? id,
|
||||||
this.winner,
|
DateTime? createdAt,
|
||||||
|
Map<String, ScoreEntry?>? scores,
|
||||||
}) : id = id ?? const Uuid().v4(),
|
}) : id = id ?? const Uuid().v4(),
|
||||||
createdAt = createdAt ?? clock.now(),
|
createdAt = createdAt ?? clock.now(),
|
||||||
scores = scores ?? {for (var player in players) player.id: []};
|
scores = scores ?? {for (Player p in players) p.id: null};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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
|
/// Creates a Match instance from a JSON object where related objects are
|
||||||
@@ -57,7 +55,16 @@ class Match {
|
|||||||
),
|
),
|
||||||
group = null,
|
group = null,
|
||||||
players = [],
|
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'] ?? '';
|
notes = json['notes'] ?? '';
|
||||||
|
|
||||||
/// Converts the Match instance to a JSON object. Related objects are
|
/// Converts the Match instance to a JSON object. Related objects are
|
||||||
@@ -71,10 +78,62 @@ class Match {
|
|||||||
'gameId': game.id,
|
'gameId': game.id,
|
||||||
'groupId': group?.id,
|
'groupId': group?.id,
|
||||||
'playerIds': players.map((player) => player.id).toList(),
|
'playerIds': players.map((player) => player.id).toList(),
|
||||||
'scores': scores.map(
|
'scores': scores.map((key, value) => MapEntry(key, value?.toJson())),
|
||||||
(playerId, scoreList) =>
|
|
||||||
MapEntry(playerId, scoreList.map((score) => score.toJson()).toList()),
|
|
||||||
),
|
|
||||||
'notes': notes,
|
'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 {
|
class ScoreEntry {
|
||||||
int roundNumber = 0;
|
final int roundNumber;
|
||||||
final int score;
|
final int score;
|
||||||
final int change;
|
final int change;
|
||||||
|
|
||||||
ScoreEntry({
|
ScoreEntry({required this.score, this.roundNumber = 0, this.change = 0});
|
||||||
required this.roundNumber,
|
|
||||||
required this.score,
|
@override
|
||||||
required this.change,
|
String toString() {
|
||||||
});
|
return 'ScoreEntry{roundNumber: $roundNumber, score: $score, change: $change}';
|
||||||
|
}
|
||||||
|
|
||||||
ScoreEntry.fromJson(Map<String, dynamic> json)
|
ScoreEntry.fromJson(Map<String, dynamic> json)
|
||||||
: roundNumber = json['roundNumber'],
|
: roundNumber = json['roundNumber'],
|
||||||
|
|||||||
@@ -22,10 +22,11 @@
|
|||||||
"days_ago": "vor {count} Tagen",
|
"days_ago": "vor {count} Tagen",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"delete_all_data": "Alle Daten 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",
|
"delete_match": "Spiel löschen",
|
||||||
"edit_group": "Gruppe bearbeiten",
|
"edit_group": "Gruppe bearbeiten",
|
||||||
"edit_match": "Gruppe bearbeiten",
|
"edit_match": "Gruppe bearbeiten",
|
||||||
|
"enter_points": "Punkte eingeben",
|
||||||
"enter_results": "Ergebnisse eintragen",
|
"enter_results": "Ergebnisse eintragen",
|
||||||
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
|
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
|
||||||
"error_deleting_group": "Fehler beim Löschen 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",
|
"player_name": "Spieler:innenname",
|
||||||
"players": "Spieler:innen",
|
"players": "Spieler:innen",
|
||||||
"players_count": "{count} Spieler",
|
"players_count": "{count} Spieler",
|
||||||
|
"point": "Punkt",
|
||||||
|
"points": "Punkte",
|
||||||
"privacy_policy": "Datenschutzerklärung",
|
"privacy_policy": "Datenschutzerklärung",
|
||||||
"quick_create": "Schnellzugriff",
|
"quick_create": "Schnellzugriff",
|
||||||
"recent_matches": "Letzte Spiele",
|
"recent_matches": "Letzte Spiele",
|
||||||
@@ -87,12 +90,14 @@
|
|||||||
"save_changes": "Änderungen speichern",
|
"save_changes": "Änderungen speichern",
|
||||||
"search_for_groups": "Nach Gruppen suchen",
|
"search_for_groups": "Nach Gruppen suchen",
|
||||||
"search_for_players": "Nach Spieler:innen suchen",
|
"search_for_players": "Nach Spieler:innen suchen",
|
||||||
"select_winner": "Gewinner:in wählen:",
|
"select_winner": "Gewinner:in wählen",
|
||||||
|
"select_loser": "Verlierer:in wählen",
|
||||||
"selected_players": "Ausgewählte Spieler:innen",
|
"selected_players": "Ausgewählte Spieler:innen",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"single_loser": "Ein:e Verlierer:in",
|
"single_loser": "Ein:e Verlierer:in",
|
||||||
"single_winner": "Ein:e Gewinner:in",
|
"single_winner": "Ein:e Gewinner:in",
|
||||||
"highest_score": "Höchste Punkte",
|
"highest_score": "Höchste Punkte",
|
||||||
|
"loser": "Verlierer:in",
|
||||||
"lowest_score": "Niedrigste Punkte",
|
"lowest_score": "Niedrigste Punkte",
|
||||||
"multiple_winners": "Mehrere Gewinner:innen",
|
"multiple_winners": "Mehrere Gewinner:innen",
|
||||||
"statistics": "Statistiken",
|
"statistics": "Statistiken",
|
||||||
@@ -100,6 +105,7 @@
|
|||||||
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
|
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
|
||||||
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht",
|
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht",
|
||||||
"this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.",
|
"this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.",
|
||||||
|
"tie": "Unentschieden",
|
||||||
"today_at": "Heute um",
|
"today_at": "Heute um",
|
||||||
"undo": "Rückgängig",
|
"undo": "Rückgängig",
|
||||||
"unknown_exception": "Unbekannter Fehler (siehe Konsole)",
|
"unknown_exception": "Unbekannter Fehler (siehe Konsole)",
|
||||||
|
|||||||
@@ -83,6 +83,9 @@
|
|||||||
"@edit_match": {
|
"@edit_match": {
|
||||||
"description": "Button & Appbar label for editing a match"
|
"description": "Button & Appbar label for editing a match"
|
||||||
},
|
},
|
||||||
|
"@enter_points": {
|
||||||
|
"description": "Label to enter players points"
|
||||||
|
},
|
||||||
"@enter_results": {
|
"@enter_results": {
|
||||||
"description": "Button text to enter match results"
|
"description": "Button text to enter match results"
|
||||||
},
|
},
|
||||||
@@ -232,6 +235,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@points": {
|
||||||
|
"description": "Points label"
|
||||||
|
},
|
||||||
"@privacy_policy": {
|
"@privacy_policy": {
|
||||||
"description": "Privacy policy menu item"
|
"description": "Privacy policy menu item"
|
||||||
},
|
},
|
||||||
@@ -271,6 +277,9 @@
|
|||||||
"@select_winner": {
|
"@select_winner": {
|
||||||
"description": "Label to select the winner"
|
"description": "Label to select the winner"
|
||||||
},
|
},
|
||||||
|
"@select_loser": {
|
||||||
|
"description": "Label to select the loser"
|
||||||
|
},
|
||||||
"@selected_players": {
|
"@selected_players": {
|
||||||
"description": "Shows the number of selected players"
|
"description": "Shows the number of selected players"
|
||||||
},
|
},
|
||||||
@@ -351,6 +360,7 @@
|
|||||||
"delete_match": "Delete Match",
|
"delete_match": "Delete Match",
|
||||||
"edit_group": "Edit Group",
|
"edit_group": "Edit Group",
|
||||||
"edit_match": "Edit Match",
|
"edit_match": "Edit Match",
|
||||||
|
"enter_points": "Enter points",
|
||||||
"enter_results": "Enter Results",
|
"enter_results": "Enter Results",
|
||||||
"error_creating_group": "Error while creating group, please try again",
|
"error_creating_group": "Error while creating group, please try again",
|
||||||
"error_deleting_group": "Error while deleting group, please try again",
|
"error_deleting_group": "Error while deleting group, please try again",
|
||||||
@@ -399,6 +409,8 @@
|
|||||||
"player_name": "Player name",
|
"player_name": "Player name",
|
||||||
"players": "Players",
|
"players": "Players",
|
||||||
"players_count": "{count} Players",
|
"players_count": "{count} Players",
|
||||||
|
"point": "Point",
|
||||||
|
"points": "Points",
|
||||||
"privacy_policy": "Privacy Policy",
|
"privacy_policy": "Privacy Policy",
|
||||||
"quick_create": "Quick Create",
|
"quick_create": "Quick Create",
|
||||||
"recent_matches": "Recent Matches",
|
"recent_matches": "Recent Matches",
|
||||||
@@ -411,12 +423,14 @@
|
|||||||
"save_changes": "Save Changes",
|
"save_changes": "Save Changes",
|
||||||
"search_for_groups": "Search for groups",
|
"search_for_groups": "Search for groups",
|
||||||
"search_for_players": "Search for players",
|
"search_for_players": "Search for players",
|
||||||
"select_winner": "Select Winner:",
|
"select_winner": "Select Winner",
|
||||||
|
"select_loser": "Select Loser",
|
||||||
"selected_players": "Selected players",
|
"selected_players": "Selected players",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"single_loser": "Single Loser",
|
"single_loser": "Single Loser",
|
||||||
"single_winner": "Single Winner",
|
"single_winner": "Single Winner",
|
||||||
"highest_score": "Highest Score",
|
"highest_score": "Highest Score",
|
||||||
|
"loser": "Loser",
|
||||||
"lowest_score": "Lowest Score",
|
"lowest_score": "Lowest Score",
|
||||||
"multiple_winners": "Multiple Winners",
|
"multiple_winners": "Multiple Winners",
|
||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
@@ -424,6 +438,7 @@
|
|||||||
"successfully_added_player": "Successfully added player {playerName}",
|
"successfully_added_player": "Successfully added player {playerName}",
|
||||||
"there_is_no_group_matching_your_search": "There is no group matching your search",
|
"there_is_no_group_matching_your_search": "There is no group matching your search",
|
||||||
"this_cannot_be_undone": "This can't be undone.",
|
"this_cannot_be_undone": "This can't be undone.",
|
||||||
|
"tie": "Tie",
|
||||||
"today_at": "Today at",
|
"today_at": "Today at",
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"unknown_exception": "Unknown Exception (see console)",
|
"unknown_exception": "Unknown Exception (see console)",
|
||||||
|
|||||||
@@ -254,6 +254,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Edit Match'**
|
/// **'Edit Match'**
|
||||||
String get 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
|
/// Button text to enter match results
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -542,6 +548,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'{count} Players'**
|
/// **'{count} Players'**
|
||||||
String players_count(int count);
|
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
|
/// Privacy policy menu item
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -617,9 +635,15 @@ abstract class AppLocalizations {
|
|||||||
/// Label to select the winner
|
/// Label to select the winner
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Select Winner:'**
|
/// **'Select Winner'**
|
||||||
String get 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
|
/// Shows the number of selected players
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -650,6 +674,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Highest Score'**
|
/// **'Highest Score'**
|
||||||
String get 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.
|
/// No description provided for @lowest_score.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -692,6 +722,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'This can\'t be undone.'**
|
/// **'This can\'t be undone.'**
|
||||||
String get this_cannot_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
|
/// Date format for today
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get delete_all_data => 'Alle Daten löschen';
|
String get delete_all_data => 'Alle Daten löschen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get delete_group => 'Diese Gruppe löschen';
|
String get delete_group => 'Gruppe löschen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get delete_match => 'Spiel löschen';
|
String get delete_match => 'Spiel löschen';
|
||||||
@@ -90,6 +90,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get edit_match => 'Gruppe bearbeiten';
|
String get edit_match => 'Gruppe bearbeiten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enter_points => 'Punkte eingeben';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get enter_results => 'Ergebnisse eintragen';
|
String get enter_results => 'Ergebnisse eintragen';
|
||||||
|
|
||||||
@@ -240,6 +243,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
return '$count Spieler';
|
return '$count Spieler';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get point => 'Punkt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get points => 'Punkte';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get privacy_policy => 'Datenschutzerklärung';
|
String get privacy_policy => 'Datenschutzerklärung';
|
||||||
|
|
||||||
@@ -281,7 +290,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get search_for_players => 'Nach Spieler:innen suchen';
|
String get search_for_players => 'Nach Spieler:innen suchen';
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
String get selected_players => 'Ausgewählte Spieler:innen';
|
String get selected_players => 'Ausgewählte Spieler:innen';
|
||||||
@@ -298,6 +310,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get highest_score => 'Höchste Punkte';
|
String get highest_score => 'Höchste Punkte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loser => 'Verlierer:in';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get lowest_score => 'Niedrigste Punkte';
|
String get lowest_score => 'Niedrigste Punkte';
|
||||||
|
|
||||||
@@ -323,6 +338,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get this_cannot_be_undone =>
|
String get this_cannot_be_undone =>
|
||||||
'Dies kann nicht rückgängig gemacht werden.';
|
'Dies kann nicht rückgängig gemacht werden.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tie => 'Unentschieden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get today_at => 'Heute um';
|
String get today_at => 'Heute um';
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get edit_match => 'Edit Match';
|
String get edit_match => 'Edit Match';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enter_points => 'Enter points';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get enter_results => 'Enter Results';
|
String get enter_results => 'Enter Results';
|
||||||
|
|
||||||
@@ -240,6 +243,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
return '$count Players';
|
return '$count Players';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get point => 'Point';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get points => 'Points';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get privacy_policy => 'Privacy Policy';
|
String get privacy_policy => 'Privacy Policy';
|
||||||
|
|
||||||
@@ -281,7 +290,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get search_for_players => 'Search for players';
|
String get search_for_players => 'Search for players';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get select_winner => 'Select Winner:';
|
String get select_winner => 'Select Winner';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get select_loser => 'Select Loser';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get selected_players => 'Selected players';
|
String get selected_players => 'Selected players';
|
||||||
@@ -298,6 +310,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get highest_score => 'Highest Score';
|
String get highest_score => 'Highest Score';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loser => 'Loser';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get lowest_score => 'Lowest Score';
|
String get lowest_score => 'Lowest Score';
|
||||||
|
|
||||||
@@ -322,6 +337,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get this_cannot_be_undone => 'This can\'t be undone.';
|
String get this_cannot_be_undone => 'This can\'t be undone.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tie => 'Tie';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get today_at => 'Today at';
|
String get today_at => 'Today at';
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
|||||||
final groupName = _groupNameController.text.trim();
|
final groupName = _groupNameController.text.trim();
|
||||||
|
|
||||||
final success = await db.groupDao.addGroup(
|
final success = await db.groupDao.addGroup(
|
||||||
group: Group(name: groupName, description: '', members: selectedPlayers),
|
group: Group(name: groupName, members: selectedPlayers),
|
||||||
);
|
);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
|||||||
@@ -255,28 +255,37 @@ class _GroupDetailViewState extends State<GroupDetailView> {
|
|||||||
|
|
||||||
/// Determines the best player in the group based on match wins
|
/// Determines the best player in the group based on match wins
|
||||||
String _getBestPlayer(List<Match> matches) {
|
String _getBestPlayer(List<Match> matches) {
|
||||||
final bestPlayerCounts = <Player, int>{};
|
final mvpCounts = <Player, int>{};
|
||||||
|
|
||||||
// Count wins for each player
|
|
||||||
for (var match in matches) {
|
for (var match in matches) {
|
||||||
if (match.winner != null &&
|
final mvps = match.mvp;
|
||||||
_group.members.any((m) => m.id == match.winner?.id)) {
|
for (final mvpPlayer in mvps) {
|
||||||
print(match.winner);
|
if (_group.members.any((m) => m.id == mvpPlayer.id)) {
|
||||||
bestPlayerCounts.update(
|
mvpCounts.update(mvpPlayer, (value) => value + 1, ifAbsent: () => 1);
|
||||||
match.winner!,
|
}
|
||||||
(value) => value + 1,
|
|
||||||
ifAbsent: () => 1,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort players by win count
|
final sortedMvps = mvpCounts.entries.toList()
|
||||||
final sortedPlayers = bestPlayerCounts.entries.toList()
|
|
||||||
..sort((a, b) => b.value.compareTo(a.value));
|
..sort((a, b) => b.value.compareTo(a.value));
|
||||||
|
|
||||||
// Get the best player
|
if (sortedMvps.isEmpty) {
|
||||||
bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-';
|
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(
|
Group(
|
||||||
name: 'Skeleton Group',
|
name: 'Skeleton Group',
|
||||||
description: '',
|
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(
|
Match(
|
||||||
name: 'Skeleton Match',
|
name: 'Skeleton Match',
|
||||||
game: Game(
|
game: Game(
|
||||||
name: '',
|
name: 'Skeleton Game',
|
||||||
ruleset: Ruleset.singleWinner,
|
ruleset: Ruleset.singleWinner,
|
||||||
description: '',
|
description: 'This is a skeleton game description.',
|
||||||
color: GameColor.blue,
|
color: GameColor.blue,
|
||||||
icon: '',
|
icon: '',
|
||||||
),
|
),
|
||||||
group: Group(
|
group: Group(
|
||||||
name: 'Skeleton Group',
|
name: 'Skeleton Group',
|
||||||
description: '',
|
description: 'This is a skeleton group description.',
|
||||||
members: [
|
members: [
|
||||||
Player(name: 'Skeleton Player 1', description: ''),
|
Player(
|
||||||
Player(name: 'Skeleton Player 2', description: ''),
|
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),
|
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> loadRecentMatches() async {
|
||||||
Future<void> updatedWinnerInRecentMatches(String matchId) async {
|
|
||||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
final winner = await db.scoreEntryDao.getWinner(matchId: matchId);
|
final matches = await db.matchDao.getAllMatches();
|
||||||
final matchIndex = recentMatches.indexWhere((match) => match.id == matchId);
|
recentMatches =
|
||||||
if (matchIndex != -1) {
|
(matches..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
|
||||||
setState(() {
|
.take(2)
|
||||||
recentMatches[matchIndex].winner = winner;
|
.toList();
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:tallee/core/common.dart';
|
import 'package:tallee/core/common.dart';
|
||||||
import 'package:tallee/core/custom_theme.dart';
|
import 'package:tallee/core/custom_theme.dart';
|
||||||
import 'package:tallee/core/enums.dart';
|
import 'package:tallee/data/models/game.dart';
|
||||||
import 'package:tallee/l10n/generated/app_localizations.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/text_input/custom_search_bar.dart';
|
||||||
import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart';
|
import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart';
|
||||||
@@ -13,14 +13,14 @@ class ChooseGameView extends StatefulWidget {
|
|||||||
const ChooseGameView({
|
const ChooseGameView({
|
||||||
super.key,
|
super.key,
|
||||||
required this.games,
|
required this.games,
|
||||||
required this.initialGameIndex,
|
required this.initialGameId,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// A list of tuples containing the game name, description and ruleset
|
/// 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
|
/// The id of the initially selected game
|
||||||
final int initialGameIndex;
|
final String initialGameId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChooseGameView> createState() => _ChooseGameViewState();
|
State<ChooseGameView> createState() => _ChooseGameViewState();
|
||||||
@@ -31,11 +31,11 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
|||||||
final TextEditingController searchBarController = TextEditingController();
|
final TextEditingController searchBarController = TextEditingController();
|
||||||
|
|
||||||
/// Currently selected game index
|
/// Currently selected game index
|
||||||
late int selectedGameIndex;
|
late String selectedGameId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
selectedGameIndex = widget.initialGameIndex;
|
selectedGameId = widget.initialGameId;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,13 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
|||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios),
|
icon: const Icon(Icons.arrow_back_ios),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(selectedGameIndex);
|
Navigator.of(context).pop(
|
||||||
|
selectedGameId == ''
|
||||||
|
? null
|
||||||
|
: widget.games.firstWhere(
|
||||||
|
(game) => game.id == selectedGameId,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: Text(loc.choose_game),
|
title: Text(loc.choose_game),
|
||||||
@@ -62,7 +68,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
|||||||
if (didPop) {
|
if (didPop) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop(selectedGameIndex);
|
Navigator.of(context).pop(widget.initialGameId);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -79,19 +85,19 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
|||||||
itemCount: widget.games.length,
|
itemCount: widget.games.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
return TitleDescriptionListTile(
|
return TitleDescriptionListTile(
|
||||||
title: widget.games[index].$1,
|
title: widget.games[index].name,
|
||||||
description: widget.games[index].$2,
|
description: widget.games[index].description,
|
||||||
badgeText: translateRulesetToString(
|
badgeText: translateRulesetToString(
|
||||||
widget.games[index].$3,
|
widget.games[index].ruleset,
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
isHighlighted: selectedGameIndex == index,
|
isHighlighted: selectedGameId == widget.games[index].id,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedGameIndex == index) {
|
if (selectedGameId != widget.games[index].id) {
|
||||||
selectedGameIndex = -1;
|
selectedGameId = widget.games[index].id;
|
||||||
} else {
|
} else {
|
||||||
selectedGameIndex = index;
|
selectedGameId = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import 'package:tallee/presentation/widgets/tiles/choose_tile.dart';
|
|||||||
|
|
||||||
class CreateMatchView extends StatefulWidget {
|
class CreateMatchView extends StatefulWidget {
|
||||||
/// A view that allows creating a new match
|
/// 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({
|
const CreateMatchView({
|
||||||
super.key,
|
super.key,
|
||||||
this.onWinnerChanged,
|
this.onWinnerChanged,
|
||||||
@@ -28,13 +30,11 @@ class CreateMatchView extends StatefulWidget {
|
|||||||
this.onMatchUpdated,
|
this.onMatchUpdated,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Optional callback invoked when the winner is changed
|
|
||||||
final VoidCallback? onWinnerChanged;
|
final VoidCallback? onWinnerChanged;
|
||||||
|
|
||||||
/// Optional callback invoked when the match is updated
|
|
||||||
final void Function(Match)? onMatchUpdated;
|
final void Function(Match)? onMatchUpdated;
|
||||||
|
|
||||||
/// An optional match to prefill the fields
|
/// An optional match to prefill the fields for editing.
|
||||||
final Match? matchToEdit;
|
final Match? matchToEdit;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -50,20 +50,12 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
|||||||
/// Hint text for the match name input field
|
/// Hint text for the match name input field
|
||||||
String? hintText;
|
String? hintText;
|
||||||
|
|
||||||
/// List of all groups from the database
|
|
||||||
List<Group> groupsList = [];
|
List<Group> groupsList = [];
|
||||||
|
|
||||||
/// List of all players from the database
|
|
||||||
List<Player> playerList = [];
|
List<Player> playerList = [];
|
||||||
|
List<Game> gamesList = [];
|
||||||
|
|
||||||
/// The currently selected group
|
|
||||||
Group? selectedGroup;
|
Group? selectedGroup;
|
||||||
|
Game? selectedGame;
|
||||||
/// The index of the currently selected game in [games] to mark it in
|
|
||||||
/// the [ChooseGameView]
|
|
||||||
int selectedGameIndex = -1;
|
|
||||||
|
|
||||||
/// The currently selected players
|
|
||||||
List<Player> selectedPlayers = [];
|
List<Player> selectedPlayers = [];
|
||||||
|
|
||||||
/// GlobalKey for ScaffoldMessenger to show snackbars
|
/// GlobalKey for ScaffoldMessenger to show snackbars
|
||||||
@@ -81,12 +73,14 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
|||||||
Future.wait([
|
Future.wait([
|
||||||
db.groupDao.getAllGroups(),
|
db.groupDao.getAllGroups(),
|
||||||
db.playerDao.getAllPlayers(),
|
db.playerDao.getAllPlayers(),
|
||||||
|
db.gameDao.getAllGames(),
|
||||||
]).then((result) async {
|
]).then((result) async {
|
||||||
groupsList = result[0] as List<Group>;
|
groupsList = result[0] as List<Group>;
|
||||||
playerList = result[1] as List<Player>;
|
playerList = result[1] as List<Player>;
|
||||||
|
gamesList = (result[2] as List<Game>);
|
||||||
|
|
||||||
// If a match is provided, prefill the fields
|
// If a match is provided, prefill the fields
|
||||||
if (widget.matchToEdit != null) {
|
if (isEditMode()) {
|
||||||
prefillMatchDetails();
|
prefillMatchDetails();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -105,20 +99,11 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
|||||||
hintText ??= loc.match_name;
|
hintText ??= loc.match_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<(String, String, Ruleset)> games = [
|
|
||||||
('Example Game 1', 'This is a description', Ruleset.lowestScore),
|
|
||||||
('Example Game 2', '', Ruleset.singleWinner),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final loc = AppLocalizations.of(context);
|
final loc = AppLocalizations.of(context);
|
||||||
final buttonText = widget.matchToEdit != null
|
final buttonText = isEditMode() ? loc.save_changes : loc.create_match;
|
||||||
? loc.save_changes
|
final viewTitle = isEditMode() ? loc.edit_match : loc.create_new_match;
|
||||||
: loc.create_match;
|
|
||||||
final viewTitle = widget.matchToEdit != null
|
|
||||||
? loc.edit_match
|
|
||||||
: loc.create_new_match;
|
|
||||||
|
|
||||||
return ScaffoldMessenger(
|
return ScaffoldMessenger(
|
||||||
key: _scaffoldMessengerKey,
|
key: _scaffoldMessengerKey,
|
||||||
@@ -140,21 +125,21 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
|||||||
),
|
),
|
||||||
ChooseTile(
|
ChooseTile(
|
||||||
title: loc.game,
|
title: loc.game,
|
||||||
trailingText: selectedGameIndex == -1
|
trailingText: selectedGame == null
|
||||||
? loc.none
|
? loc.none_group
|
||||||
: games[selectedGameIndex].$1,
|
: selectedGame!.name,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
selectedGameIndex = await Navigator.of(context).push(
|
selectedGame = await Navigator.of(context).push(
|
||||||
adaptivePageRoute(
|
adaptivePageRoute(
|
||||||
builder: (context) => ChooseGameView(
|
builder: (context) => ChooseGameView(
|
||||||
games: games,
|
games: gamesList,
|
||||||
initialGameIndex: selectedGameIndex,
|
initialGameId: selectedGame?.id ?? '',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedGameIndex != -1) {
|
if (selectedGame != null) {
|
||||||
hintText = games[selectedGameIndex].$1;
|
hintText = selectedGame!.name;
|
||||||
} else {
|
} else {
|
||||||
hintText = loc.match_name;
|
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.
|
/// Determines whether the "Create Match" button should be enabled.
|
||||||
///
|
///
|
||||||
/// Returns `true` if:
|
/// Returns `true` if:
|
||||||
@@ -232,7 +221,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
|||||||
/// - Either a group is selected OR at least 2 players are selected
|
/// - Either a group is selected OR at least 2 players are selected
|
||||||
bool _enableCreateGameButton() {
|
bool _enableCreateGameButton() {
|
||||||
return (selectedGroup != null ||
|
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
|
// 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
|
// If no match was provided, it creates a new match in the database and
|
||||||
// navigates to the MatchResultView for the newly created match.
|
// navigates to the MatchResultView for the newly created match.
|
||||||
void buttonNavigation(BuildContext context) async {
|
void buttonNavigation(BuildContext context) async {
|
||||||
if (widget.matchToEdit != null) {
|
if (isEditMode()) {
|
||||||
await updateMatch();
|
await updateMatch();
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -266,9 +255,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
|||||||
/// Updates attributes of the existing match in the database based on the
|
/// Updates attributes of the existing match in the database based on the
|
||||||
/// changes made in the edit view.
|
/// changes made in the edit view.
|
||||||
Future<void> updateMatch() async {
|
Future<void> updateMatch() async {
|
||||||
//TODO: Remove when Games implemented
|
|
||||||
final tempGame = await getTemporaryGame();
|
|
||||||
|
|
||||||
final updatedMatch = Match(
|
final updatedMatch = Match(
|
||||||
id: widget.matchToEdit!.id,
|
id: widget.matchToEdit!.id,
|
||||||
name: _matchNameController.text.isEmpty
|
name: _matchNameController.text.isEmpty
|
||||||
@@ -276,8 +262,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
|||||||
: _matchNameController.text.trim(),
|
: _matchNameController.text.trim(),
|
||||||
group: selectedGroup,
|
group: selectedGroup,
|
||||||
players: selectedPlayers,
|
players: selectedPlayers,
|
||||||
game: tempGame,
|
game: widget.matchToEdit!.game,
|
||||||
winner: widget.matchToEdit!.winner,
|
|
||||||
createdAt: widget.matchToEdit!.createdAt,
|
createdAt: widget.matchToEdit!.createdAt,
|
||||||
endedAt: widget.matchToEdit!.endedAt,
|
endedAt: widget.matchToEdit!.endedAt,
|
||||||
notes: widget.matchToEdit!.notes,
|
notes: widget.matchToEdit!.notes,
|
||||||
@@ -314,9 +299,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
|||||||
matchId: widget.matchToEdit!.id,
|
matchId: widget.matchToEdit!.id,
|
||||||
playerId: player.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.
|
// Creates a new match and adds it to the database.
|
||||||
// Returns the created match.
|
// Returns the created match.
|
||||||
Future<Match> createMatch() async {
|
Future<Match> createMatch() async {
|
||||||
final tempGame = await getTemporaryGame();
|
|
||||||
|
|
||||||
Match match = Match(
|
Match match = Match(
|
||||||
name: _matchNameController.text.isEmpty
|
name: _matchNameController.text.isEmpty
|
||||||
? (hintText ?? '')
|
? (hintText ?? '')
|
||||||
@@ -335,35 +315,18 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
|||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
group: selectedGroup,
|
group: selectedGroup,
|
||||||
players: selectedPlayers,
|
players: selectedPlayers,
|
||||||
game: tempGame,
|
game: selectedGame!,
|
||||||
);
|
);
|
||||||
await db.matchDao.addMatch(match: match);
|
await db.matchDao.addMatch(match: match);
|
||||||
return 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
|
// If a match was provided to the view, this method prefills the input fields
|
||||||
void prefillMatchDetails() {
|
void prefillMatchDetails() {
|
||||||
final match = widget.matchToEdit!;
|
final match = widget.matchToEdit!;
|
||||||
_matchNameController.text = match.name;
|
_matchNameController.text = match.name;
|
||||||
selectedPlayers = match.players;
|
selectedPlayers = match.players;
|
||||||
selectedGameIndex = 0;
|
selectedGame = match.game;
|
||||||
|
|
||||||
if (match.group != null) {
|
if (match.group != null) {
|
||||||
selectedGroup = match.group;
|
selectedGroup = match.group;
|
||||||
|
|||||||
@@ -170,37 +170,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
|||||||
vertical: 4,
|
vertical: 4,
|
||||||
horizontal: 8,
|
horizontal: 8,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: getResultWidget(loc),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -227,7 +197,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
|||||||
text: loc.enter_results,
|
text: loc.enter_results,
|
||||||
icon: Icons.emoji_events,
|
icon: Icons.emoji_events,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
match.winner = await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
adaptivePageRoute(
|
adaptivePageRoute(
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
@@ -259,4 +229,108 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
|||||||
});
|
});
|
||||||
widget.onMatchUpdate.call();
|
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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tallee/core/custom_theme.dart';
|
import 'package:tallee/core/custom_theme.dart';
|
||||||
|
import 'package:tallee/core/enums.dart';
|
||||||
import 'package:tallee/data/db/database.dart';
|
import 'package:tallee/data/db/database.dart';
|
||||||
import 'package:tallee/data/models/match.dart';
|
import 'package:tallee/data/models/match.dart';
|
||||||
import 'package:tallee/data/models/player.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/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/custom_radio_list_tile.dart';
|
||||||
|
import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart';
|
||||||
|
|
||||||
class MatchResultView extends StatefulWidget {
|
class MatchResultView extends StatefulWidget {
|
||||||
/// A view that allows selecting and saving the winner of a match
|
/// 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> {
|
class _MatchResultViewState extends State<MatchResultView> {
|
||||||
late final AppDatabase db;
|
late final AppDatabase db;
|
||||||
|
|
||||||
|
late final Ruleset ruleset;
|
||||||
|
|
||||||
/// List of all players who participated in the match
|
/// List of all players who participated in the match
|
||||||
late final List<Player> allPlayers;
|
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
|
/// Currently selected winner player
|
||||||
Player? _selectedPlayer;
|
Player? _selectedPlayer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
db = Provider.of<AppDatabase>(context, listen: false);
|
db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
|
ruleset = widget.match.game.ruleset;
|
||||||
|
canSave = !rulesetSupportsScoreEntry();
|
||||||
|
|
||||||
allPlayers = widget.match.players;
|
allPlayers = widget.match.players;
|
||||||
allPlayers.sort((a, b) => a.name.compareTo(b.name));
|
allPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||||
|
|
||||||
if (widget.match.winner != null) {
|
controller = List.generate(
|
||||||
_selectedPlayer = allPlayers.firstWhere(
|
allPlayers.length,
|
||||||
(p) => p.id == widget.match.winner!.id,
|
(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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final loc = AppLocalizations.of(context);
|
final loc = AppLocalizations.of(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: CustomTheme.backgroundColor,
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -85,67 +120,169 @@ class _MatchResultViewState extends State<MatchResultView> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
loc.select_winner,
|
'${getTitleForRuleset(loc)}:',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Expanded(
|
if (rulesetSupportsWinnerSelection())
|
||||||
child: RadioGroup<Player>(
|
Expanded(
|
||||||
groupValue: _selectedPlayer,
|
child: RadioGroup<Player>(
|
||||||
onChanged: (Player? value) async {
|
groupValue: _selectedPlayer,
|
||||||
setState(() {
|
onChanged: (Player? value) async {
|
||||||
_selectedPlayer = value;
|
setState(() {
|
||||||
});
|
_selectedPlayer = value;
|
||||||
await _handleWinnerSaving();
|
});
|
||||||
},
|
},
|
||||||
child: ListView.builder(
|
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,
|
itemCount: allPlayers.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return CustomRadioListTile(
|
return ScoreListTile(
|
||||||
text: allPlayers[index].name,
|
text: allPlayers[index].name,
|
||||||
value: allPlayers[index],
|
controller: controller[index],
|
||||||
onContainerTap: (value) async {
|
);
|
||||||
setState(() {
|
},
|
||||||
// Check if the already selected player is the same as the newly tapped player.
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
if (_selectedPlayer == value) {
|
return const Padding(
|
||||||
// If yes deselected the player by setting it to null.
|
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||||
_selectedPlayer = null;
|
child: Divider(indent: 20),
|
||||||
} else {
|
|
||||||
// If no assign the newly tapped player to the selected player.
|
|
||||||
(_selectedPlayer = value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await _handleWinnerSaving();
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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
|
/// Handles saving or removing the winner in the database
|
||||||
/// based on the current selection.
|
/// 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) {
|
if (_selectedPlayer == null) {
|
||||||
await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
|
return await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
|
||||||
} else {
|
} else {
|
||||||
await db.scoreEntryDao.setWinner(
|
return await db.scoreEntryDao.setWinner(
|
||||||
matchId: widget.match.id,
|
matchId: widget.match.id,
|
||||||
playerId: _selectedPlayer!.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(
|
Match(
|
||||||
name: 'Skeleton match name',
|
name: 'Skeleton match name',
|
||||||
game: Game(
|
game: Game(
|
||||||
name: '',
|
name: 'Game name',
|
||||||
ruleset: Ruleset.singleWinner,
|
ruleset: Ruleset.singleWinner,
|
||||||
description: '',
|
|
||||||
color: GameColor.blue,
|
color: GameColor.blue,
|
||||||
icon: '',
|
icon: '',
|
||||||
),
|
),
|
||||||
group: Group(
|
group: Group(
|
||||||
name: 'Group name',
|
name: 'Group name',
|
||||||
description: '',
|
members: List.filled(5, Player(name: 'Player')),
|
||||||
members: List.filled(5, Player(name: 'Player', description: '')),
|
|
||||||
),
|
),
|
||||||
winner: Player(name: 'Player', description: ''),
|
players: [Player(name: 'Player')],
|
||||||
players: [Player(name: 'Player', description: '')],
|
|
||||||
notes: '',
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -116,7 +112,7 @@ class _MatchViewState extends State<MatchView> {
|
|||||||
Positioned(
|
Positioned(
|
||||||
bottom: MediaQuery.paddingOf(context).bottom + 20,
|
bottom: MediaQuery.paddingOf(context).bottom + 20,
|
||||||
child: MainMenuButton(
|
child: MainMenuButton(
|
||||||
text: 'Spiel erstellen',
|
text: loc.create_match,
|
||||||
icon: RpgAwesome.clovers_card,
|
icon: RpgAwesome.clovers_card,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
|
|||||||
@@ -152,8 +152,8 @@ class _StatisticsViewState extends State<StatisticsView> {
|
|||||||
|
|
||||||
// Getting the winners
|
// Getting the winners
|
||||||
for (var match in matches) {
|
for (var match in matches) {
|
||||||
final winner = match.winner;
|
final mvps = match.mvp;
|
||||||
if (winner != null) {
|
for (var winner in mvps) {
|
||||||
final index = winCounts.indexWhere((entry) => entry.$1.id == winner.id);
|
final index = winCounts.indexWhere((entry) => entry.$1.id == winner.id);
|
||||||
// -1 means winner not found in winCounts
|
// -1 means winner not found in winCounts
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
@@ -179,8 +179,7 @@ class _StatisticsViewState extends State<StatisticsView> {
|
|||||||
final playerId = winCounts[i].$1.id;
|
final playerId = winCounts[i].$1.id;
|
||||||
final player = players.firstWhere(
|
final player = players.firstWhere(
|
||||||
(p) => p.id == playerId,
|
(p) => p.id == playerId,
|
||||||
orElse: () =>
|
orElse: () => Player(id: playerId, name: loc.not_available),
|
||||||
Player(id: playerId, name: loc.not_available, description: ''),
|
|
||||||
);
|
);
|
||||||
winCounts[i] = (player, winCounts[i].$2);
|
winCounts[i] = (player, winCounts[i].$2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
|||||||
/// Skeleton data used while loading players.
|
/// Skeleton data used while loading players.
|
||||||
late final List<Player> skeletonData = List.filled(
|
late final List<Player> skeletonData = List.filled(
|
||||||
7,
|
7,
|
||||||
Player(name: 'Player 0', description: ''),
|
Player(name: 'Player 0'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:tallee/core/common.dart';
|
import 'package:tallee/core/common.dart';
|
||||||
import 'package:tallee/core/custom_theme.dart';
|
import 'package:tallee/core/custom_theme.dart';
|
||||||
|
import 'package:tallee/core/enums.dart';
|
||||||
import 'package:tallee/data/models/match.dart';
|
import 'package:tallee/data/models/match.dart';
|
||||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||||
@@ -44,7 +45,6 @@ class _MatchTileState extends State<MatchTile> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final match = widget.match;
|
final match = widget.match;
|
||||||
final group = match.group;
|
final group = match.group;
|
||||||
final winner = match.winner;
|
|
||||||
final players = [...match.players]
|
final players = [...match.players]
|
||||||
..sort((a, b) => a.name.compareTo(b.name));
|
..sort((a, b) => a.name.compareTo(b.name));
|
||||||
final loc = AppLocalizations.of(context);
|
final loc = AppLocalizations.of(context);
|
||||||
@@ -79,8 +79,7 @@ class _MatchTileState extends State<MatchTile> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
// Group Info
|
||||||
|
|
||||||
if (group != null) ...[
|
if (group != null) ...[
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -95,7 +94,7 @@ class _MatchTileState extends State<MatchTile> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 4),
|
||||||
] else if (widget.compact) ...[
|
] else if (widget.compact) ...[
|
||||||
Row(
|
Row(
|
||||||
children: [
|
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(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
@@ -129,15 +187,11 @@ class _MatchTileState extends State<MatchTile> {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
getMvpIcon(),
|
||||||
Icons.emoji_events,
|
|
||||||
size: 20,
|
|
||||||
color: Colors.amber,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${loc.winner}: ${winner.name}',
|
getMvpText(loc),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -189,6 +243,7 @@ class _MatchTileState extends State<MatchTile> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Players List
|
||||||
if (players.isNotEmpty && widget.compact == false) ...[
|
if (players.isNotEmpty && widget.compact == false) ...[
|
||||||
Text(
|
Text(
|
||||||
loc.players,
|
loc.players,
|
||||||
@@ -234,4 +289,44 @@ class _MatchTileState extends State<MatchTile> {
|
|||||||
return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}';
|
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/group.dart';
|
||||||
import 'package:tallee/data/models/match.dart';
|
import 'package:tallee/data/models/match.dart';
|
||||||
import 'package:tallee/data/models/player.dart';
|
import 'package:tallee/data/models/player.dart';
|
||||||
|
import 'package:tallee/data/models/score_entry.dart';
|
||||||
import 'package:tallee/data/models/team.dart';
|
import 'package:tallee/data/models/team.dart';
|
||||||
|
|
||||||
class DataTransferService {
|
class DataTransferService {
|
||||||
@@ -36,59 +37,12 @@ class DataTransferService {
|
|||||||
final games = await db.gameDao.getAllGames();
|
final games = await db.gameDao.getAllGames();
|
||||||
final teams = await db.teamDao.getAllTeams();
|
final teams = await db.teamDao.getAllTeams();
|
||||||
|
|
||||||
// Construct a JSON representation of the data in normalized format
|
|
||||||
final Map<String, dynamic> jsonMap = {
|
final Map<String, dynamic> jsonMap = {
|
||||||
'players': players.map((p) => p.toJson()).toList(),
|
'players': players.map((player) => player.toJson()).toList(),
|
||||||
'games': games.map((g) => g.toJson()).toList(),
|
'games': games.map((game) => game.toJson()).toList(),
|
||||||
'groups': groups
|
'groups': groups.map((group) => group.toJson()).toList(),
|
||||||
.map(
|
'teams': teams.map((team) => team.toJson()).toList(),
|
||||||
(g) => {
|
'matches': matches.map((match) => match.toJson()).toList(),
|
||||||
'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(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return json.encode(jsonMap);
|
return json.encode(jsonMap);
|
||||||
@@ -105,7 +59,7 @@ class DataTransferService {
|
|||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final bytes = Uint8List.fromList(utf8.encode(jsonString));
|
final bytes = Uint8List.fromList(utf8.encode(jsonString));
|
||||||
final path = await FilePicker.platform.saveFile(
|
final path = await FilePicker.saveFile(
|
||||||
fileName: '$fileName.json',
|
fileName: '$fileName.json',
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
);
|
);
|
||||||
@@ -126,7 +80,7 @@ class DataTransferService {
|
|||||||
static Future<ImportResult> importData(BuildContext context) async {
|
static Future<ImportResult> importData(BuildContext context) async {
|
||||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
|
|
||||||
final path = await FilePicker.platform.pickFiles(
|
final path = await FilePicker.pickFiles(
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowedExtensions: ['json'],
|
allowedExtensions: ['json'],
|
||||||
);
|
);
|
||||||
@@ -284,6 +238,15 @@ class DataTransferService {
|
|||||||
? DateTime.parse(map['endedAt'] as String)
|
? DateTime.parse(map['endedAt'] as String)
|
||||||
: null;
|
: null;
|
||||||
final notes = map['notes'] as String? ?? '';
|
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
|
// Link attributes to objects
|
||||||
final game = gamesMap[gameId] ?? getFallbackGame();
|
final game = gamesMap[gameId] ?? getFallbackGame();
|
||||||
@@ -305,6 +268,7 @@ class DataTransferService {
|
|||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
endedAt: endedAt,
|
endedAt: endedAt,
|
||||||
notes: notes,
|
notes: notes,
|
||||||
|
scores: scores,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|||||||
14
pubspec.yaml
14
pubspec.yaml
@@ -7,18 +7,18 @@ environment:
|
|||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
flutter_localizations:
|
|
||||||
sdk: flutter
|
|
||||||
clock: ^1.1.2
|
clock: ^1.1.2
|
||||||
cupertino_icons: ^1.0.6
|
cupertino_icons: ^1.0.6
|
||||||
drift: ^2.27.0
|
drift: ^2.27.0
|
||||||
drift_flutter: ^0.2.4
|
drift_flutter: ^0.2.4
|
||||||
file_picker: ^10.3.6
|
file_picker: ^11.0.2
|
||||||
file_saver: ^0.3.1
|
file_saver: ^0.3.1
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
fluttericon: ^2.0.0
|
fluttericon: ^2.0.0
|
||||||
font_awesome_flutter: ^10.12.0
|
font_awesome_flutter: ^11.0.0
|
||||||
intl: any
|
intl: any
|
||||||
json_schema: ^5.2.2
|
json_schema: ^5.2.2
|
||||||
package_info_plus: ^9.0.0
|
package_info_plus: ^9.0.0
|
||||||
@@ -33,7 +33,7 @@ dev_dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
build_runner: ^2.7.0
|
build_runner: ^2.7.0
|
||||||
dart_pubspec_licenses: ^3.0.14
|
dart_pubspec_licenses: ^3.0.14
|
||||||
drift_dev: ^2.29.0
|
drift_dev: ^2.27.0
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
withClock(fakeClock, () {
|
withClock(fakeClock, () {
|
||||||
testPlayer1 = Player(name: 'Alice', description: '');
|
testPlayer1 = Player(name: 'Alice');
|
||||||
testPlayer2 = Player(name: 'Bob', description: '');
|
testPlayer2 = Player(name: 'Bob');
|
||||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
testPlayer3 = Player(name: 'Charlie');
|
||||||
testPlayer4 = Player(name: 'Diana', description: '');
|
testPlayer4 = Player(name: 'Diana');
|
||||||
testGroup1 = Group(
|
testGroup1 = Group(
|
||||||
name: 'Test Group',
|
name: 'Test Group',
|
||||||
description: '',
|
description: '',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:tallee/data/models/game.dart';
|
|||||||
import 'package:tallee/data/models/group.dart';
|
import 'package:tallee/data/models/group.dart';
|
||||||
import 'package:tallee/data/models/match.dart';
|
import 'package:tallee/data/models/match.dart';
|
||||||
import 'package:tallee/data/models/player.dart';
|
import 'package:tallee/data/models/player.dart';
|
||||||
|
import 'package:tallee/data/models/score_entry.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late AppDatabase database;
|
late AppDatabase database;
|
||||||
@@ -36,11 +37,11 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
withClock(fakeClock, () {
|
withClock(fakeClock, () {
|
||||||
testPlayer1 = Player(name: 'Alice', description: '');
|
testPlayer1 = Player(name: 'Alice');
|
||||||
testPlayer2 = Player(name: 'Bob', description: '');
|
testPlayer2 = Player(name: 'Bob');
|
||||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
testPlayer3 = Player(name: 'Charlie');
|
||||||
testPlayer4 = Player(name: 'Diana', description: '');
|
testPlayer4 = Player(name: 'Diana');
|
||||||
testPlayer5 = Player(name: 'Eve', description: '');
|
testPlayer5 = Player(name: 'Eve');
|
||||||
testGroup1 = Group(
|
testGroup1 = Group(
|
||||||
name: 'Test Group 1',
|
name: 'Test Group 1',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -63,29 +64,24 @@ void main() {
|
|||||||
game: testGame,
|
game: testGame,
|
||||||
group: testGroup1,
|
group: testGroup1,
|
||||||
players: [testPlayer4, testPlayer5],
|
players: [testPlayer4, testPlayer5],
|
||||||
winner: testPlayer4,
|
scores: {testPlayer4.id: ScoreEntry(score: 1)},
|
||||||
notes: '',
|
|
||||||
);
|
);
|
||||||
testMatch2 = Match(
|
testMatch2 = Match(
|
||||||
name: 'Second Test Match',
|
name: 'Second Test Match',
|
||||||
game: testGame,
|
game: testGame,
|
||||||
group: testGroup2,
|
group: testGroup2,
|
||||||
players: [testPlayer1, testPlayer2, testPlayer3],
|
players: [testPlayer1, testPlayer2, testPlayer3],
|
||||||
winner: testPlayer2,
|
|
||||||
notes: '',
|
|
||||||
);
|
);
|
||||||
testMatchOnlyPlayers = Match(
|
testMatchOnlyPlayers = Match(
|
||||||
name: 'Test Match with Players',
|
name: 'Test Match with Players',
|
||||||
game: testGame,
|
game: testGame,
|
||||||
players: [testPlayer1, testPlayer2, testPlayer3],
|
players: [testPlayer1, testPlayer2, testPlayer3],
|
||||||
winner: testPlayer3,
|
|
||||||
notes: '',
|
|
||||||
);
|
);
|
||||||
testMatchOnlyGroup = Match(
|
testMatchOnlyGroup = Match(
|
||||||
name: 'Test Match with Group',
|
name: 'Test Match with Group',
|
||||||
game: testGame,
|
game: testGame,
|
||||||
group: testGroup2,
|
group: testGroup2,
|
||||||
notes: '',
|
players: testGroup2.members,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await database.playerDao.addPlayersAsList(
|
await database.playerDao.addPlayersAsList(
|
||||||
@@ -289,8 +285,8 @@ void main() {
|
|||||||
matchId: testMatch1.id,
|
matchId: testMatch1.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(fetchedMatch.winner, isNotNull);
|
expect(fetchedMatch.mvp, isNotNull);
|
||||||
expect(fetchedMatch.winner!.id, testPlayer4.id);
|
expect(fetchedMatch.mvp.first.id, testPlayer4.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Setting a winner works correctly', () async {
|
test('Setting a winner works correctly', () async {
|
||||||
@@ -304,8 +300,8 @@ void main() {
|
|||||||
final fetchedMatch = await database.matchDao.getMatchById(
|
final fetchedMatch = await database.matchDao.getMatchById(
|
||||||
matchId: testMatch1.id,
|
matchId: testMatch1.id,
|
||||||
);
|
);
|
||||||
expect(fetchedMatch.winner, isNotNull);
|
expect(fetchedMatch.mvp, isNotNull);
|
||||||
expect(fetchedMatch.winner!.id, testPlayer5.id);
|
expect(fetchedMatch.mvp.first.id, testPlayer5.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
withClock(fakeClock, () {
|
withClock(fakeClock, () {
|
||||||
testPlayer1 = Player(name: 'Alice', description: '');
|
testPlayer1 = Player(name: 'Alice');
|
||||||
testPlayer2 = Player(name: 'Bob', description: '');
|
testPlayer2 = Player(name: 'Bob');
|
||||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
testPlayer3 = Player(name: 'Charlie');
|
||||||
testPlayer4 = Player(name: 'Diana', description: '');
|
testPlayer4 = Player(name: 'Diana');
|
||||||
testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]);
|
testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]);
|
||||||
testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]);
|
testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]);
|
||||||
testTeam3 = Team(name: 'Team Gamma', members: [testPlayer1, testPlayer3]);
|
testTeam3 = Team(name: 'Team Gamma', members: [testPlayer1, testPlayer3]);
|
||||||
@@ -343,8 +343,16 @@ void main() {
|
|||||||
// Verifies that teams with overlapping members are independent.
|
// Verifies that teams with overlapping members are independent.
|
||||||
test('Teams with overlapping members are independent', () async {
|
test('Teams with overlapping members are independent', () async {
|
||||||
// Create two matches since player_match has primary key {playerId, matchId}
|
// Create two matches since player_match has primary key {playerId, matchId}
|
||||||
final match1 = Match(name: 'Match 1', game: testGame1, notes: '');
|
final match1 = Match(
|
||||||
final match2 = Match(name: 'Match 2', game: testGame2, notes: '');
|
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: match1);
|
||||||
await database.matchDao.addMatch(match: match2);
|
await database.matchDao.addMatch(match: match2);
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
withClock(fakeClock, () {
|
withClock(fakeClock, () {
|
||||||
testPlayer1 = Player(name: 'Test Player', description: '');
|
testPlayer1 = Player(name: 'Test Player');
|
||||||
testPlayer2 = Player(name: 'Second Player', description: '');
|
testPlayer2 = Player(name: 'Second Player');
|
||||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
testPlayer3 = Player(name: 'Charlie');
|
||||||
testPlayer4 = Player(name: 'Diana', description: '');
|
testPlayer4 = Player(name: 'Diana');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
@@ -348,7 +348,7 @@ void main() {
|
|||||||
|
|
||||||
// Verifies that a player with empty string name is stored correctly.
|
// Verifies that a player with empty string name is stored correctly.
|
||||||
test('Player with empty string name is stored correctly', () async {
|
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);
|
await database.playerDao.addPlayer(player: emptyNamePlayer);
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ void main() {
|
|||||||
// Verifies that a player with very long name is stored correctly.
|
// Verifies that a player with very long name is stored correctly.
|
||||||
test('Player with very long name is stored correctly', () async {
|
test('Player with very long name is stored correctly', () async {
|
||||||
final longName = 'A' * 1000;
|
final longName = 'A' * 1000;
|
||||||
final longNamePlayer = Player(name: longName, description: '');
|
final longNamePlayer = Player(name: longName);
|
||||||
|
|
||||||
await database.playerDao.addPlayer(player: longNamePlayer);
|
await database.playerDao.addPlayer(player: longNamePlayer);
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
withClock(fakeClock, () {
|
withClock(fakeClock, () {
|
||||||
testPlayer1 = Player(name: 'Alice', description: '');
|
testPlayer1 = Player(name: 'Alice');
|
||||||
testPlayer2 = Player(name: 'Bob', description: '');
|
testPlayer2 = Player(name: 'Bob');
|
||||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
testPlayer3 = Player(name: 'Charlie');
|
||||||
testPlayer4 = Player(name: 'Diana', description: '');
|
testPlayer4 = Player(name: 'Diana');
|
||||||
testGroup = Group(
|
testGroup = Group(
|
||||||
name: 'Test Group',
|
name: 'Test Group',
|
||||||
description: '',
|
description: '',
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
withClock(fakeClock, () {
|
withClock(fakeClock, () {
|
||||||
testPlayer1 = Player(name: 'Alice', description: '');
|
testPlayer1 = Player(name: 'Alice');
|
||||||
testPlayer2 = Player(name: 'Bob', description: '');
|
testPlayer2 = Player(name: 'Bob');
|
||||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
testPlayer3 = Player(name: 'Charlie');
|
||||||
testPlayer4 = Player(name: 'Diana', description: '');
|
testPlayer4 = Player(name: 'Diana');
|
||||||
testPlayer5 = Player(name: 'Eve', description: '');
|
testPlayer5 = Player(name: 'Eve');
|
||||||
testPlayer6 = Player(name: 'Frank', description: '');
|
testPlayer6 = Player(name: 'Frank');
|
||||||
testGroup = Group(
|
testGroup = Group(
|
||||||
name: 'Test Group',
|
name: 'Test Group',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -58,14 +58,13 @@ void main() {
|
|||||||
testMatchOnlyGroup = Match(
|
testMatchOnlyGroup = Match(
|
||||||
name: 'Test Match with Group',
|
name: 'Test Match with Group',
|
||||||
game: testGame,
|
game: testGame,
|
||||||
|
players: testGroup.members,
|
||||||
group: testGroup,
|
group: testGroup,
|
||||||
notes: '',
|
|
||||||
);
|
);
|
||||||
testMatchOnlyPlayers = Match(
|
testMatchOnlyPlayers = Match(
|
||||||
name: 'Test Match with Players',
|
name: 'Test Match with Players',
|
||||||
game: testGame,
|
game: testGame,
|
||||||
players: [testPlayer4, testPlayer5, testPlayer6],
|
players: [testPlayer4, testPlayer5, testPlayer6],
|
||||||
notes: '',
|
|
||||||
);
|
);
|
||||||
testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]);
|
testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]);
|
||||||
testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]);
|
testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]);
|
||||||
@@ -96,7 +95,7 @@ void main() {
|
|||||||
matchId: testMatchOnlyGroup.id,
|
matchId: testMatchOnlyGroup.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(matchHasPlayers, false);
|
expect(matchHasPlayers, true);
|
||||||
|
|
||||||
await database.playerMatchDao.addPlayerToMatch(
|
await database.playerMatchDao.addPlayerToMatch(
|
||||||
matchId: testMatchOnlyGroup.id,
|
matchId: testMatchOnlyGroup.id,
|
||||||
@@ -397,7 +396,6 @@ void main() {
|
|||||||
matchId: testMatchOnlyGroup.id,
|
matchId: testMatchOnlyGroup.id,
|
||||||
teamId: testTeam1.id,
|
teamId: testTeam1.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(playersInTeam.length, 2);
|
expect(playersInTeam.length, 2);
|
||||||
final playerIds = playersInTeam.map((p) => p.id).toSet();
|
final playerIds = playersInTeam.map((p) => p.id).toSet();
|
||||||
expect(playerIds.contains(testPlayer1.id), true);
|
expect(playerIds.contains(testPlayer1.id), true);
|
||||||
@@ -426,18 +424,16 @@ void main() {
|
|||||||
playerId: testPlayer1.id,
|
playerId: testPlayer1.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Try to add the same player again with different score
|
|
||||||
await database.playerMatchDao.addPlayerToMatch(
|
await database.playerMatchDao.addPlayerToMatch(
|
||||||
matchId: testMatchOnlyGroup.id,
|
matchId: testMatchOnlyGroup.id,
|
||||||
playerId: testPlayer1.id,
|
playerId: testPlayer1.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify player count is still 1
|
|
||||||
final players = await database.playerMatchDao.getPlayersOfMatch(
|
final players = await database.playerMatchDao.getPlayersOfMatch(
|
||||||
matchId: testMatchOnlyGroup.id,
|
matchId: testMatchOnlyGroup.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(players?.length, 1);
|
expect(players?.length, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
@@ -546,6 +542,7 @@ void main() {
|
|||||||
matchId: testMatchOnlyGroup.id,
|
matchId: testMatchOnlyGroup.id,
|
||||||
teamId: testTeam1.id,
|
teamId: testTeam1.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(playersInTeam1.length, 2);
|
expect(playersInTeam1.length, 2);
|
||||||
final team1Ids = playersInTeam1.map((p) => p.id).toSet();
|
final team1Ids = playersInTeam1.map((p) => p.id).toSet();
|
||||||
expect(team1Ids.contains(testPlayer1.id), true);
|
expect(team1Ids.contains(testPlayer1.id), true);
|
||||||
@@ -568,13 +565,11 @@ void main() {
|
|||||||
name: 'Match 1',
|
name: 'Match 1',
|
||||||
game: testGame,
|
game: testGame,
|
||||||
players: playersList,
|
players: playersList,
|
||||||
notes: '',
|
|
||||||
);
|
);
|
||||||
final match2 = Match(
|
final match2 = Match(
|
||||||
name: 'Match 2',
|
name: 'Match 2',
|
||||||
game: testGame,
|
game: testGame,
|
||||||
players: playersList,
|
players: playersList,
|
||||||
notes: '',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
withClock(fakeClock, () {
|
withClock(fakeClock, () {
|
||||||
testPlayer1 = Player(name: 'Alice', description: '');
|
testPlayer1 = Player(name: 'Alice');
|
||||||
testPlayer2 = Player(name: 'Bob', description: '');
|
testPlayer2 = Player(name: 'Bob');
|
||||||
testPlayer3 = Player(name: 'Charlie', description: '');
|
testPlayer3 = Player(name: 'Charlie');
|
||||||
testGame = Game(
|
testGame = Game(
|
||||||
name: 'Test Game',
|
name: 'Test Game',
|
||||||
ruleset: Ruleset.singleWinner,
|
ruleset: Ruleset.singleWinner,
|
||||||
@@ -44,13 +44,11 @@ void main() {
|
|||||||
name: 'Test Match 1',
|
name: 'Test Match 1',
|
||||||
game: testGame,
|
game: testGame,
|
||||||
players: [testPlayer1, testPlayer2],
|
players: [testPlayer1, testPlayer2],
|
||||||
notes: '',
|
|
||||||
);
|
);
|
||||||
testMatch2 = Match(
|
testMatch2 = Match(
|
||||||
name: 'Test Match 2',
|
name: 'Test Match 2',
|
||||||
game: testGame,
|
game: testGame,
|
||||||
players: [testPlayer2, testPlayer3],
|
players: [testPlayer2, testPlayer3],
|
||||||
notes: '',
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,8 +229,8 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(scores.length, 2);
|
expect(scores.length, 2);
|
||||||
expect(scores[testPlayer1.id]!.length, 2);
|
expect(scores[testPlayer1.id]!, isNotNull);
|
||||||
expect(scores[testPlayer2.id]!.length, 1);
|
expect(scores[testPlayer2.id]!, isNotNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getAllMatchScores() with no scores saved', () async {
|
test('getAllMatchScores() with no scores saved', () async {
|
||||||
|
|||||||
@@ -64,14 +64,8 @@ void main() {
|
|||||||
players: [testPlayer1, testPlayer2],
|
players: [testPlayer1, testPlayer2],
|
||||||
notes: 'Test notes',
|
notes: 'Test notes',
|
||||||
scores: {
|
scores: {
|
||||||
testPlayer1.id: [
|
testPlayer1.id: ScoreEntry(roundNumber: 1, score: 10, change: 10),
|
||||||
ScoreEntry(roundNumber: 1, score: 10, change: 10),
|
testPlayer2.id: ScoreEntry(roundNumber: 1, score: 15, change: 15),
|
||||||
ScoreEntry(roundNumber: 2, score: 20, change: 10),
|
|
||||||
],
|
|
||||||
testPlayer2.id: [
|
|
||||||
ScoreEntry(roundNumber: 1, score: 15, change: 15),
|
|
||||||
ScoreEntry(roundNumber: 2, score: 25, change: 10),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -302,46 +296,25 @@ void main() {
|
|||||||
final scoresJson = matchData['scores'] as Map<String, dynamic>;
|
final scoresJson = matchData['scores'] as Map<String, dynamic>;
|
||||||
expect(scoresJson, isA<Map<String, dynamic>>());
|
expect(scoresJson, isA<Map<String, dynamic>>());
|
||||||
|
|
||||||
final scores = scoresJson.map(
|
// Verify scores are properly structured (single score per player, not list)
|
||||||
(playerId, scoreList) => MapEntry(
|
expect(scoresJson[testPlayer1.id], isNotNull);
|
||||||
playerId,
|
expect(scoresJson[testPlayer2.id], isNotNull);
|
||||||
(scoreList as List)
|
|
||||||
.map((s) => ScoreEntry.fromJson(s as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
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 */
|
// Parse player 2 score
|
||||||
// General structure
|
final player2ScoreJson =
|
||||||
expect(scores[testPlayer1.id], isNotNull);
|
scoresJson[testPlayer2.id] as Map<String, dynamic>;
|
||||||
expect(scores[testPlayer1.id]!.length, 2);
|
final player2Score = ScoreEntry.fromJson(player2ScoreJson);
|
||||||
|
expect(player2Score.roundNumber, 1);
|
||||||
// Round 1
|
expect(player2Score.score, 15);
|
||||||
expect(scores[testPlayer1.id]![0].roundNumber, 1);
|
expect(player2Score.change, 15);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Match without group is handled correctly', (tester) async {
|
testWidgets('Match without group is handled correctly', (tester) async {
|
||||||
@@ -904,14 +877,8 @@ void main() {
|
|||||||
'playerIds': [testPlayer1.id, testPlayer2.id],
|
'playerIds': [testPlayer1.id, testPlayer2.id],
|
||||||
'notes': testMatch.notes,
|
'notes': testMatch.notes,
|
||||||
'scores': {
|
'scores': {
|
||||||
testPlayer1.id: [
|
testPlayer1.id: {'roundNumber': 1, 'score': 10, 'change': 10},
|
||||||
{'roundNumber': 1, 'score': 10, 'change': 10},
|
testPlayer2.id: {'roundNumber': 1, 'score': 15, 'change': 15},
|
||||||
{'roundNumber': 2, 'score': 20, 'change': 10},
|
|
||||||
],
|
|
||||||
testPlayer2.id: [
|
|
||||||
{'roundNumber': 1, 'score': 15, 'change': 15},
|
|
||||||
{'roundNumber': 2, 'score': 25, 'change': 10},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
'createdAt': testMatch.createdAt.toIso8601String(),
|
'createdAt': testMatch.createdAt.toIso8601String(),
|
||||||
'endedAt': null,
|
'endedAt': null,
|
||||||
|
|||||||
Reference in New Issue
Block a user