Merge remote-tracking branch 'origin/development' into bug/195-datenbank-onDelete-ueberpruefen

# Conflicts:
#	lib/data/db/tables/player_table.dart
This commit is contained in:
gelbeinhalb
2026-04-30 11:20:47 +02:00
51 changed files with 1842 additions and 795 deletions

View File

@@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
/// Translates a [Ruleset] enum value to its corresponding localized string.
@@ -43,3 +44,18 @@ String getExtraPlayerCount(Match match) {
}
return ' + ${count.toString()}';
}
String getNameCountText(Player player) {
if (player.nameCount >= 1) {
return ' #${player.nameCount}';
}
return '';
}
String getPointLabel(AppLocalizations loc, int points) {
if (points == 1) {
return '$points ${loc.point}';
} else {
return '$points ${loc.points}';
}
}

View File

@@ -85,21 +85,21 @@ class CustomTheme {
);
static const SearchBarThemeData searchBarTheme = SearchBarThemeData(
textStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.textColor)),
hintStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.hintColor)),
textStyle: WidgetStatePropertyAll(TextStyle(color: textColor)),
hintStyle: WidgetStatePropertyAll(TextStyle(color: hintColor)),
);
static final RadioThemeData radioTheme = RadioThemeData(
fillColor: WidgetStateProperty.resolveWith<Color>((states) {
if (states.contains(WidgetState.selected)) {
return CustomTheme.primaryColor;
return primaryColor;
}
return CustomTheme.textColor;
return textColor;
}),
);
static const InputDecorationTheme inputDecorationTheme = InputDecorationTheme(
labelStyle: TextStyle(color: CustomTheme.textColor),
hintStyle: TextStyle(color: CustomTheme.hintColor),
labelStyle: TextStyle(color: textColor),
hintStyle: TextStyle(color: hintColor),
);
}

View File

@@ -34,7 +34,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
matchId: row.id,
);
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
return Match(
id: row.id,
name: row.name,
@@ -45,7 +44,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
createdAt: row.createdAt,
endedAt: row.endedAt,
scores: scores,
winner: winner,
);
}),
);
@@ -68,8 +66,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId);
final winner = await db.scoreEntryDao.getWinner(matchId: matchId);
return Match(
id: result.id,
name: result.name,
@@ -80,7 +76,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
createdAt: result.createdAt,
endedAt: result.endedAt,
scores: scores,
winner: winner,
);
}
@@ -110,19 +105,14 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
}
for (final pid in match.scores.keys) {
final playerScores = match.scores[pid]!;
await db.scoreEntryDao.addScoresAsList(
entrys: playerScores,
playerId: pid,
matchId: match.id,
);
}
if (match.winner != null) {
await db.scoreEntryDao.setWinner(
matchId: match.id,
playerId: match.winner!.id,
);
final playerScores = match.scores[pid];
if (playerScores != null) {
await db.scoreEntryDao.addScore(
entry: playerScores,
playerId: pid,
matchId: match.id,
);
}
}
});
}
@@ -140,6 +130,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
uniqueGames[match.game.id] = match.game;
}
// Add games
if (uniqueGames.isNotEmpty) {
await db.batch(
(b) => b.insertAll(
@@ -162,7 +153,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
);
}
// Add all groups of the matches in batch
// Add groups
await db.batch(
(b) => b.insertAll(
db.groupTable,
@@ -181,7 +172,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
),
);
// Add all matches in batch
// Add matches
await db.batch(
(b) => b.insertAll(
matchTable,
@@ -202,7 +193,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
),
);
// Add all players of the matches in batch (unique)
// Add players
final uniquePlayers = <String, Player>{};
for (final match in matches) {
for (final p in match.players) {
@@ -235,7 +226,27 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
);
}
// Add all player-match associations in batch
await db.batch((b) {
for (final match in matches) {
for (final entry in match.scores.entries) {
if (entry.value != null) {
b.insert(
db.scoreEntryTable,
ScoreEntryTableCompanion.insert(
matchId: match.id,
playerId: entry.key,
score: entry.value!.score,
roundNumber: entry.value!.roundNumber,
change: entry.value!.change,
),
mode: InsertMode.insertOrReplace,
);
}
}
}
});
// Add player-match associations
await db.batch((b) {
for (final match in matches) {
for (final p in match.players) {
@@ -251,7 +262,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
}
});
// Add all player-group associations in batch
// Add player-group associations
await db.batch((b) {
for (final match in matches) {
if (match.group != null) {
@@ -300,7 +311,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
final group = await db.groupDao.getGroupById(groupId: groupId);
final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? [];
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
return Match(
id: row.id,
name: row.name,
@@ -310,7 +320,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
notes: row.notes ?? '',
createdAt: row.createdAt,
endedAt: row.endedAt,
winner: winner,
);
}),
);

View File

@@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:flutter/cupertino.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/player_table.dart';
import 'package:tallee/data/models/player.dart';
@@ -20,6 +21,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
name: row.name,
description: row.description,
createdAt: row.createdAt,
nameCount: row.nameCount,
),
)
.toList();
@@ -34,6 +36,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
name: result.name,
description: result.description,
createdAt: result.createdAt,
nameCount: result.nameCount,
);
}
@@ -42,12 +45,15 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// the new one.
Future<bool> addPlayer({required Player player}) async {
if (!await playerExists(playerId: player.id)) {
final int nameCount = await calculateNameCount(name: player.name);
await into(playerTable).insert(
PlayerTableCompanion.insert(
id: player.id,
name: player.name,
description: player.description,
createdAt: player.createdAt,
nameCount: Value(nameCount),
),
mode: InsertMode.insertOrReplace,
);
@@ -62,20 +68,67 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
Future<bool> addPlayersAsList({required List<Player> players}) async {
if (players.isEmpty) return false;
// Filter out players that already exist
final newPlayers = <Player>[];
for (final player in players) {
if (!await playerExists(playerId: player.id)) {
newPlayers.add(player);
}
}
if (newPlayers.isEmpty) return false;
// Group players by name
final nameGroups = <String, List<Player>>{};
for (final player in newPlayers) {
nameGroups.putIfAbsent(player.name, () => []).add(player);
}
final playersToInsert = <PlayerTableCompanion>[];
// Process each group of players with the same name
for (final entry in nameGroups.entries) {
final name = entry.key;
final playersWithName = entry.value;
// Get the current nameCount
var nameCount = await calculateNameCount(name: name);
// One player with the same name
if (playersWithName.length == 1) {
final player = playersWithName[0];
playersToInsert.add(
PlayerTableCompanion.insert(
id: player.id,
name: player.name,
description: player.description,
createdAt: player.createdAt,
nameCount: Value(nameCount),
),
);
} else {
if (nameCount == 0) nameCount++;
// Multiple players with the same name
for (var i = 0; i < playersWithName.length; i++) {
final player = playersWithName[i];
playersToInsert.add(
PlayerTableCompanion.insert(
id: player.id,
name: player.name,
description: player.description,
createdAt: player.createdAt,
nameCount: Value(nameCount + i),
),
);
}
}
}
await db.batch(
(b) => b.insertAll(
playerTable,
players
.map(
(player) => PlayerTableCompanion.insert(
id: player.id,
name: player.name,
description: player.description,
createdAt: player.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrIgnore,
playersToInsert,
mode: InsertMode.insertOrReplace,
),
);
@@ -90,7 +143,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
return rowsAffected > 0;
}
/// Checks if a player with the given [id] exists in the database.
/// Checks if a player with the given [playerId] exists in the database.
/// Returns `true` if the player exists, `false` otherwise.
Future<bool> playerExists({required String playerId}) async {
final query = select(playerTable)..where((p) => p.id.equals(playerId));
@@ -103,9 +156,38 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
required String playerId,
required String newName,
}) async {
// Get previous name and name count for the player before updating
final previousPlayerName =
await (select(playerTable)..where((p) => p.id.equals(playerId)))
.map((row) => row.name)
.getSingleOrNull() ??
'';
final previousNameCount = await getNameCount(name: previousPlayerName);
await (update(playerTable)..where((p) => p.id.equals(playerId))).write(
PlayerTableCompanion(name: Value(newName)),
);
// Update name count for the new name
final count = await calculateNameCount(name: newName);
if (count > 0) {
await (update(playerTable)..where((p) => p.name.equals(newName))).write(
PlayerTableCompanion(nameCount: Value(count)),
);
}
if (previousNameCount > 0) {
// Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount
final player = await getPlayerWithHighestNameCount(
name: previousPlayerName,
);
if (player != null) {
await updateNameCount(
playerId: player.id,
nameCount: previousNameCount,
);
}
}
}
/// Retrieves the total count of players in the database.
@@ -117,6 +199,76 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
return count ?? 0;
}
/// Retrieves the count of players with the given [name].
Future<int> getNameCount({required String name}) async {
final query = select(playerTable)..where((p) => p.name.equals(name));
final result = await query.get();
return result.length;
}
/// Updates the nameCount for the player with the given [playerId] to [nameCount].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateNameCount({
required String playerId,
required int nameCount,
}) async {
final query = update(playerTable)..where((p) => p.id.equals(playerId));
final rowsAffected = await query.write(
PlayerTableCompanion(nameCount: Value(nameCount)),
);
return rowsAffected > 0;
}
@visibleForTesting
Future<Player?> getPlayerWithHighestNameCount({required String name}) async {
final query = select(playerTable)
..where((p) => p.name.equals(name))
..orderBy([(p) => OrderingTerm.desc(p.nameCount)])
..limit(1);
final result = await query.getSingleOrNull();
if (result != null) {
return Player(
id: result.id,
name: result.name,
description: result.description,
createdAt: result.createdAt,
nameCount: result.nameCount,
);
}
return null;
}
@visibleForTesting
Future<int> calculateNameCount({required String name}) async {
final count = await getNameCount(name: name);
final int nameCount;
if (count == 1) {
// If one other player exists with the same name, initialize the nameCount
await initializeNameCount(name: name);
// And for the new player, set nameCount to 2
nameCount = 2;
} else if (count > 1) {
// If more than one player exists with the same name, just increment
// the nameCount for the new player
nameCount = count + 1;
} else {
// If no other players exist with the same name, set nameCount to 0
nameCount = 0;
}
return nameCount;
}
@visibleForTesting
Future<bool> initializeNameCount({required String name}) async {
final rowsAffected =
await (update(playerTable)..where((p) => p.name.equals(name))).write(
const PlayerTableCompanion(nameCount: Value(1)),
);
return rowsAffected > 0;
}
/// Deletes all players from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteAllPlayers() async {

View File

@@ -24,7 +24,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
matchId: matchId,
teamId: Value(teamId),
),
mode: InsertMode.insertOrIgnore,
mode: InsertMode.insertOrReplace,
);
}

View File

@@ -83,21 +83,21 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
}
/// Retrieves all scores for a specific match.
Future<Map<String, List<ScoreEntry>>> getAllMatchScores({
Future<Map<String, ScoreEntry?>> getAllMatchScores({
required String matchId,
}) async {
final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId));
final result = await query.get();
final Map<String, List<ScoreEntry>> scoresByPlayer = {};
final Map<String, ScoreEntry?> scoresByPlayer = {};
for (final row in result) {
final score = ScoreEntry(
roundNumber: row.roundNumber,
score: row.score,
change: row.change,
);
scoresByPlayer.putIfAbsent(row.playerId, () => []).add(score);
scoresByPlayer[row.playerId] = score;
}
return scoresByPlayer;
@@ -235,22 +235,29 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
return rowsAffected > 0;
}
// Retrieves the winner of a match based on the highest score.
// Retrieves the winner of a match by looking for a score entry where score
/// is 1. Returns `null` if no player found, else the first with the score.
Future<Player?> getWinner({required String matchId}) async {
final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId))
..orderBy([(s) => OrderingTerm.desc(s.score)])
..limit(1);
final result = await query.getSingleOrNull();
final query =
select(scoreEntryTable).join([
innerJoin(
db.playerTable,
db.playerTable.id.equalsExp(scoreEntryTable.playerId),
),
])..where(
scoreEntryTable.matchId.equals(matchId) &
scoreEntryTable.score.equals(1),
);
if (result == null) return null;
final result = await query.get();
if (result.isEmpty) return null;
final player = await db.playerDao.getPlayerById(playerId: result.playerId);
final playerData = result.first.readTable(db.playerTable);
return Player(
id: player.id,
name: player.name,
createdAt: player.createdAt,
description: player.description,
id: playerData.id,
name: playerData.name,
createdAt: playerData.createdAt,
description: playerData.description,
);
}
@@ -295,20 +302,29 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
return rowsAffected > 0;
}
/// Retrieves the looser of a match based on the score 0.
/// Retrieves the looser of a match by looking for a score entry where score
/// is 0. Returns `null` if no player found, else the first with the score.
Future<Player?> getLooser({required String matchId}) async {
final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId) & s.score.equals(0));
final result = await query.getSingleOrNull();
final query =
select(scoreEntryTable).join([
innerJoin(
db.playerTable,
db.playerTable.id.equalsExp(scoreEntryTable.playerId),
),
])..where(
scoreEntryTable.matchId.equals(matchId) &
scoreEntryTable.score.equals(0),
);
if (result == null) return null;
final result = await query.get();
if (result.isEmpty) return null;
final player = await db.playerDao.getPlayerById(playerId: result.playerId);
final playerData = result.first.readTable(db.playerTable);
return Player(
id: player.id,
name: player.name,
createdAt: player.createdAt,
description: player.description,
id: playerData.id,
name: playerData.name,
createdAt: playerData.createdAt,
description: playerData.description,
);
}

View File

@@ -18,6 +18,17 @@ class $PlayerTableTable extends PlayerTable
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _createdAtMeta = const VerificationMeta(
'createdAt',
);
@override
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at',
aliasedName,
false,
type: DriftSqlType.dateTime,
requiredDuringInsert: true,
);
static const VerificationMeta _nameMeta = const VerificationMeta('name');
@override
late final GeneratedColumn<String> name = GeneratedColumn<String>(
@@ -27,6 +38,18 @@ class $PlayerTableTable extends PlayerTable
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _nameCountMeta = const VerificationMeta(
'nameCount',
);
@override
late final GeneratedColumn<int> nameCount = GeneratedColumn<int>(
'name_count',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const Constant(0),
);
static const VerificationMeta _descriptionMeta = const VerificationMeta(
'description',
);
@@ -38,19 +61,14 @@ class $PlayerTableTable extends PlayerTable
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _createdAtMeta = const VerificationMeta(
'createdAt',
);
@override
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at',
aliasedName,
false,
type: DriftSqlType.dateTime,
requiredDuringInsert: true,
);
@override
List<GeneratedColumn> get $columns => [id, name, description, createdAt];
List<GeneratedColumn> get $columns => [
id,
createdAt,
name,
nameCount,
description,
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
@@ -68,6 +86,14 @@ class $PlayerTableTable extends PlayerTable
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('created_at')) {
context.handle(
_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
);
} else if (isInserting) {
context.missing(_createdAtMeta);
}
if (data.containsKey('name')) {
context.handle(
_nameMeta,
@@ -76,6 +102,12 @@ class $PlayerTableTable extends PlayerTable
} else if (isInserting) {
context.missing(_nameMeta);
}
if (data.containsKey('name_count')) {
context.handle(
_nameCountMeta,
nameCount.isAcceptableOrUnknown(data['name_count']!, _nameCountMeta),
);
}
if (data.containsKey('description')) {
context.handle(
_descriptionMeta,
@@ -87,14 +119,6 @@ class $PlayerTableTable extends PlayerTable
} else if (isInserting) {
context.missing(_descriptionMeta);
}
if (data.containsKey('created_at')) {
context.handle(
_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
);
} else if (isInserting) {
context.missing(_createdAtMeta);
}
return context;
}
@@ -108,18 +132,22 @@ class $PlayerTableTable extends PlayerTable
DriftSqlType.string,
data['${effectivePrefix}id'],
)!,
createdAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}created_at'],
)!,
name: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}name'],
)!,
nameCount: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}name_count'],
)!,
description: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}description'],
)!,
createdAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}created_at'],
)!,
);
}
@@ -131,31 +159,35 @@ class $PlayerTableTable extends PlayerTable
class PlayerTableData extends DataClass implements Insertable<PlayerTableData> {
final String id;
final String name;
final String description;
final DateTime createdAt;
final String name;
final int nameCount;
final String description;
const PlayerTableData({
required this.id,
required this.name,
required this.description,
required this.createdAt,
required this.name,
required this.nameCount,
required this.description,
});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
map['name'] = Variable<String>(name);
map['description'] = Variable<String>(description);
map['created_at'] = Variable<DateTime>(createdAt);
map['name'] = Variable<String>(name);
map['name_count'] = Variable<int>(nameCount);
map['description'] = Variable<String>(description);
return map;
}
PlayerTableCompanion toCompanion(bool nullToAbsent) {
return PlayerTableCompanion(
id: Value(id),
name: Value(name),
description: Value(description),
createdAt: Value(createdAt),
name: Value(name),
nameCount: Value(nameCount),
description: Value(description),
);
}
@@ -166,9 +198,10 @@ class PlayerTableData extends DataClass implements Insertable<PlayerTableData> {
serializer ??= driftRuntimeOptions.defaultSerializer;
return PlayerTableData(
id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']),
description: serializer.fromJson<String>(json['description']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
name: serializer.fromJson<String>(json['name']),
nameCount: serializer.fromJson<int>(json['nameCount']),
description: serializer.fromJson<String>(json['description']),
);
}
@override
@@ -176,31 +209,35 @@ class PlayerTableData extends DataClass implements Insertable<PlayerTableData> {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name),
'description': serializer.toJson<String>(description),
'createdAt': serializer.toJson<DateTime>(createdAt),
'name': serializer.toJson<String>(name),
'nameCount': serializer.toJson<int>(nameCount),
'description': serializer.toJson<String>(description),
};
}
PlayerTableData copyWith({
String? id,
String? name,
String? description,
DateTime? createdAt,
String? name,
int? nameCount,
String? description,
}) => PlayerTableData(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
createdAt: createdAt ?? this.createdAt,
name: name ?? this.name,
nameCount: nameCount ?? this.nameCount,
description: description ?? this.description,
);
PlayerTableData copyWithCompanion(PlayerTableCompanion data) {
return PlayerTableData(
id: data.id.present ? data.id.value : this.id,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
name: data.name.present ? data.name.value : this.name,
nameCount: data.nameCount.present ? data.nameCount.value : this.nameCount,
description: data.description.present
? data.description.value
: this.description,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@@ -208,76 +245,85 @@ class PlayerTableData extends DataClass implements Insertable<PlayerTableData> {
String toString() {
return (StringBuffer('PlayerTableData(')
..write('id: $id, ')
..write('createdAt: $createdAt, ')
..write('name: $name, ')
..write('description: $description, ')
..write('createdAt: $createdAt')
..write('nameCount: $nameCount, ')
..write('description: $description')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, name, description, createdAt);
int get hashCode => Object.hash(id, createdAt, name, nameCount, description);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is PlayerTableData &&
other.id == this.id &&
other.createdAt == this.createdAt &&
other.name == this.name &&
other.description == this.description &&
other.createdAt == this.createdAt);
other.nameCount == this.nameCount &&
other.description == this.description);
}
class PlayerTableCompanion extends UpdateCompanion<PlayerTableData> {
final Value<String> id;
final Value<String> name;
final Value<String> description;
final Value<DateTime> createdAt;
final Value<String> name;
final Value<int> nameCount;
final Value<String> description;
final Value<int> rowid;
const PlayerTableCompanion({
this.id = const Value.absent(),
this.name = const Value.absent(),
this.description = const Value.absent(),
this.createdAt = const Value.absent(),
this.name = const Value.absent(),
this.nameCount = const Value.absent(),
this.description = const Value.absent(),
this.rowid = const Value.absent(),
});
PlayerTableCompanion.insert({
required String id,
required String name,
required String description,
required DateTime createdAt,
required String name,
this.nameCount = const Value.absent(),
required String description,
this.rowid = const Value.absent(),
}) : id = Value(id),
createdAt = Value(createdAt),
name = Value(name),
description = Value(description),
createdAt = Value(createdAt);
description = Value(description);
static Insertable<PlayerTableData> custom({
Expression<String>? id,
Expression<String>? name,
Expression<String>? description,
Expression<DateTime>? createdAt,
Expression<String>? name,
Expression<int>? nameCount,
Expression<String>? description,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
if (description != null) 'description': description,
if (createdAt != null) 'created_at': createdAt,
if (name != null) 'name': name,
if (nameCount != null) 'name_count': nameCount,
if (description != null) 'description': description,
if (rowid != null) 'rowid': rowid,
});
}
PlayerTableCompanion copyWith({
Value<String>? id,
Value<String>? name,
Value<String>? description,
Value<DateTime>? createdAt,
Value<String>? name,
Value<int>? nameCount,
Value<String>? description,
Value<int>? rowid,
}) {
return PlayerTableCompanion(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
createdAt: createdAt ?? this.createdAt,
name: name ?? this.name,
nameCount: nameCount ?? this.nameCount,
description: description ?? this.description,
rowid: rowid ?? this.rowid,
);
}
@@ -288,15 +334,18 @@ class PlayerTableCompanion extends UpdateCompanion<PlayerTableData> {
if (id.present) {
map['id'] = Variable<String>(id.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
if (name.present) {
map['name'] = Variable<String>(name.value);
}
if (nameCount.present) {
map['name_count'] = Variable<int>(nameCount.value);
}
if (description.present) {
map['description'] = Variable<String>(description.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
@@ -307,9 +356,10 @@ class PlayerTableCompanion extends UpdateCompanion<PlayerTableData> {
String toString() {
return (StringBuffer('PlayerTableCompanion(')
..write('id: $id, ')
..write('name: $name, ')
..write('description: $description, ')
..write('createdAt: $createdAt, ')
..write('name: $name, ')
..write('nameCount: $nameCount, ')
..write('description: $description, ')
..write('rowid: $rowid')
..write(')'))
.toString();
@@ -2790,17 +2840,19 @@ abstract class _$AppDatabase extends GeneratedDatabase {
typedef $$PlayerTableTableCreateCompanionBuilder =
PlayerTableCompanion Function({
required String id,
required String name,
required String description,
required DateTime createdAt,
required String name,
Value<int> nameCount,
required String description,
Value<int> rowid,
});
typedef $$PlayerTableTableUpdateCompanionBuilder =
PlayerTableCompanion Function({
Value<String> id,
Value<String> name,
Value<String> description,
Value<DateTime> createdAt,
Value<String> name,
Value<int> nameCount,
Value<String> description,
Value<int> rowid,
});
@@ -2892,18 +2944,23 @@ class $$PlayerTableTableFilterComposer
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get name => $composableBuilder(
column: $table.name,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get description => $composableBuilder(
column: $table.description,
ColumnFilters<int> get nameCount => $composableBuilder(
column: $table.nameCount,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
ColumnFilters<String> get description => $composableBuilder(
column: $table.description,
builder: (column) => ColumnFilters(column),
);
@@ -2997,18 +3054,23 @@ class $$PlayerTableTableOrderingComposer
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get name => $composableBuilder(
column: $table.name,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get description => $composableBuilder(
column: $table.description,
ColumnOrderings<int> get nameCount => $composableBuilder(
column: $table.nameCount,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
ColumnOrderings<String> get description => $composableBuilder(
column: $table.description,
builder: (column) => ColumnOrderings(column),
);
}
@@ -3025,17 +3087,20 @@ class $$PlayerTableTableAnnotationComposer
GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
GeneratedColumn<int> get nameCount =>
$composableBuilder(column: $table.nameCount, builder: (column) => column);
GeneratedColumn<String> get description => $composableBuilder(
column: $table.description,
builder: (column) => column,
);
GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
Expression<T> playerGroupTableRefs<T extends Object>(
Expression<T> Function($$PlayerGroupTableTableAnnotationComposer a) f,
) {
@@ -3145,29 +3210,33 @@ class $$PlayerTableTableTableManager
updateCompanionCallback:
({
Value<String> id = const Value.absent(),
Value<String> name = const Value.absent(),
Value<String> description = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
Value<String> name = const Value.absent(),
Value<int> nameCount = const Value.absent(),
Value<String> description = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => PlayerTableCompanion(
id: id,
name: name,
description: description,
createdAt: createdAt,
name: name,
nameCount: nameCount,
description: description,
rowid: rowid,
),
createCompanionCallback:
({
required String id,
required String name,
required String description,
required DateTime createdAt,
required String name,
Value<int> nameCount = const Value.absent(),
required String description,
Value<int> rowid = const Value.absent(),
}) => PlayerTableCompanion.insert(
id: id,
name: name,
description: description,
createdAt: createdAt,
name: name,
nameCount: nameCount,
description: description,
rowid: rowid,
),
withReferenceMapper: (p0) => p0

View File

@@ -2,9 +2,10 @@ import 'package:drift/drift.dart';
class PlayerTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get description => text()();
DateTimeColumn get createdAt => dateTime()();
TextColumn get name => text()();
IntColumn get nameCount => integer().withDefault(const Constant(0))();
TextColumn get description => text()();
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
@override

View File

@@ -1,6 +1,6 @@
import 'package:clock/clock.dart';
import 'package:uuid/uuid.dart';
import 'package:tallee/core/enums.dart';
import 'package:uuid/uuid.dart';
class Game {
final String id;
@@ -33,7 +33,10 @@ class Game {
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'],
ruleset = Ruleset.values.firstWhere((e) => e.name == json['ruleset']),
ruleset = Ruleset.values.firstWhere(
(e) => e.name == json['ruleset'],
orElse: () => Ruleset.singleWinner,
),
description = json['description'],
color = GameColor.values.firstWhere((e) => e.name == json['color']),
icon = json['icon'];
@@ -49,4 +52,3 @@ class Game {
'icon': icon,
};
}

View File

@@ -15,27 +15,25 @@ class Match {
final Group? group;
final List<Player> players;
final String notes;
Map<String, List<ScoreEntry>> scores;
Player? winner;
Map<String, ScoreEntry?> scores;
Match({
String? id,
DateTime? createdAt,
this.endedAt,
required this.name,
required this.game,
required this.players,
this.endedAt,
this.group,
this.players = const [],
this.notes = '',
Map<String, List<ScoreEntry>>? scores,
this.winner,
String? id,
DateTime? createdAt,
Map<String, ScoreEntry?>? scores,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(),
scores = scores ?? {for (var player in players) player.id: []};
scores = scores ?? {for (Player p in players) p.id: null};
@override
String toString() {
return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, winner: $winner}';
return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, mvp: $mvp}';
}
/// Creates a Match instance from a JSON object where related objects are
@@ -57,7 +55,16 @@ class Match {
),
group = null,
players = [],
scores = json['scores'],
scores = json['scores'] != null
? (json['scores'] as Map<String, dynamic>).map(
(key, value) => MapEntry(
key,
value != null
? ScoreEntry.fromJson(value as Map<String, dynamic>)
: null,
),
)
: {},
notes = json['notes'] ?? '';
/// Converts the Match instance to a JSON object. Related objects are
@@ -71,10 +78,62 @@ class Match {
'gameId': game.id,
'groupId': group?.id,
'playerIds': players.map((player) => player.id).toList(),
'scores': scores.map(
(playerId, scoreList) =>
MapEntry(playerId, scoreList.map((score) => score.toJson()).toList()),
),
'scores': scores.map((key, value) => MapEntry(key, value?.toJson())),
'notes': notes,
};
List<Player> get mvp {
if (players.isEmpty || scores.isEmpty) return [];
switch (game.ruleset) {
case Ruleset.highestScore:
return _getPlayersWithHighestScore();
case Ruleset.lowestScore:
return _getPlayersWithLowestScore();
case Ruleset.singleWinner:
return _getPlayersWithHighestScore().take(1).toList();
case Ruleset.singleLoser:
return _getPlayersWithLowestScore().take(1).toList();
case Ruleset.multipleWinners:
return [];
}
}
List<Player> _getPlayersWithHighestScore() {
if (players.isEmpty || scores.values.every((score) => score == null)) {
return [];
}
final int highestScore = players
.map((player) => scores[player.id]?.score)
.whereType<int>()
.reduce((max, score) => score > max ? score : max);
return players.where((player) {
final playerScores = scores[player.id];
if (playerScores == null) return false;
return playerScores.score == highestScore;
}).toList();
}
List<Player> _getPlayersWithLowestScore() {
if (players.isEmpty || scores.values.every((score) => score == null)) {
return [];
}
final int lowestScore = players
.map((player) => scores[player.id]?.score)
.whereType<int>()
.reduce((min, score) => score < min ? score : min);
return players.where((player) {
final playerScore = scores[player.id];
if (playerScore == null) return false;
return playerScore.score == lowestScore;
}).toList();
}
}

View File

@@ -5,12 +5,14 @@ class Player {
final String id;
final DateTime createdAt;
final String name;
int nameCount;
final String description;
Player({
String? id,
DateTime? createdAt,
required this.name,
this.nameCount = 0,
String? description,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(),
@@ -18,7 +20,7 @@ class Player {
@override
String toString() {
return 'Player{id: $id, name: $name, description: $description}';
return 'Player{id: $id, createdAt: $createdAt, name: $name, nameCount: $nameCount, description: $description}';
}
/// Creates a Player instance from a JSON object.
@@ -26,6 +28,7 @@ class Player {
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'],
nameCount = 0,
description = json['description'];
/// Converts the Player instance to a JSON object.

View File

@@ -1,13 +1,14 @@
class ScoreEntry {
int roundNumber = 0;
final int roundNumber;
final int score;
final int change;
ScoreEntry({
required this.roundNumber,
required this.score,
required this.change,
});
ScoreEntry({required this.score, this.roundNumber = 0, this.change = 0});
@override
String toString() {
return 'ScoreEntry{roundNumber: $roundNumber, score: $score, change: $change}';
}
ScoreEntry.fromJson(Map<String, dynamic> json)
: roundNumber = json['roundNumber'],

View File

@@ -22,10 +22,11 @@
"days_ago": "vor {count} Tagen",
"delete": "Löschen",
"delete_all_data": "Alle Daten löschen",
"delete_group": "Diese Gruppe löschen",
"delete_group": "Gruppe löschen",
"delete_match": "Spiel löschen",
"edit_group": "Gruppe bearbeiten",
"edit_match": "Gruppe bearbeiten",
"enter_points": "Punkte eingeben",
"enter_results": "Ergebnisse eintragen",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
"error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen",
@@ -74,6 +75,8 @@
"player_name": "Spieler:innenname",
"players": "Spieler:innen",
"players_count": "{count} Spieler",
"point": "Punkt",
"points": "Punkte",
"privacy_policy": "Datenschutzerklärung",
"quick_create": "Schnellzugriff",
"recent_matches": "Letzte Spiele",
@@ -87,12 +90,14 @@
"save_changes": "Änderungen speichern",
"search_for_groups": "Nach Gruppen suchen",
"search_for_players": "Nach Spieler:innen suchen",
"select_winner": "Gewinner:in wählen:",
"select_winner": "Gewinner:in wählen",
"select_loser": "Verlierer:in wählen",
"selected_players": "Ausgewählte Spieler:innen",
"settings": "Einstellungen",
"single_loser": "Ein:e Verlierer:in",
"single_winner": "Ein:e Gewinner:in",
"highest_score": "Höchste Punkte",
"loser": "Verlierer:in",
"lowest_score": "Niedrigste Punkte",
"multiple_winners": "Mehrere Gewinner:innen",
"statistics": "Statistiken",
@@ -100,6 +105,7 @@
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht",
"this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.",
"tie": "Unentschieden",
"today_at": "Heute um",
"undo": "Rückgängig",
"unknown_exception": "Unbekannter Fehler (siehe Konsole)",

View File

@@ -83,6 +83,9 @@
"@edit_match": {
"description": "Button & Appbar label for editing a match"
},
"@enter_points": {
"description": "Label to enter players points"
},
"@enter_results": {
"description": "Button text to enter match results"
},
@@ -232,6 +235,9 @@
}
}
},
"@points": {
"description": "Points label"
},
"@privacy_policy": {
"description": "Privacy policy menu item"
},
@@ -271,6 +277,9 @@
"@select_winner": {
"description": "Label to select the winner"
},
"@select_loser": {
"description": "Label to select the loser"
},
"@selected_players": {
"description": "Shows the number of selected players"
},
@@ -351,6 +360,7 @@
"delete_match": "Delete Match",
"edit_group": "Edit Group",
"edit_match": "Edit Match",
"enter_points": "Enter points",
"enter_results": "Enter Results",
"error_creating_group": "Error while creating group, please try again",
"error_deleting_group": "Error while deleting group, please try again",
@@ -399,6 +409,8 @@
"player_name": "Player name",
"players": "Players",
"players_count": "{count} Players",
"point": "Point",
"points": "Points",
"privacy_policy": "Privacy Policy",
"quick_create": "Quick Create",
"recent_matches": "Recent Matches",
@@ -411,12 +423,14 @@
"save_changes": "Save Changes",
"search_for_groups": "Search for groups",
"search_for_players": "Search for players",
"select_winner": "Select Winner:",
"select_winner": "Select Winner",
"select_loser": "Select Loser",
"selected_players": "Selected players",
"settings": "Settings",
"single_loser": "Single Loser",
"single_winner": "Single Winner",
"highest_score": "Highest Score",
"loser": "Loser",
"lowest_score": "Lowest Score",
"multiple_winners": "Multiple Winners",
"statistics": "Statistics",
@@ -424,6 +438,7 @@
"successfully_added_player": "Successfully added player {playerName}",
"there_is_no_group_matching_your_search": "There is no group matching your search",
"this_cannot_be_undone": "This can't be undone.",
"tie": "Tie",
"today_at": "Today at",
"undo": "Undo",
"unknown_exception": "Unknown Exception (see console)",

View File

@@ -254,6 +254,12 @@ abstract class AppLocalizations {
/// **'Edit Match'**
String get edit_match;
/// Label to enter players points
///
/// In en, this message translates to:
/// **'Enter points'**
String get enter_points;
/// Button text to enter match results
///
/// In en, this message translates to:
@@ -542,6 +548,18 @@ abstract class AppLocalizations {
/// **'{count} Players'**
String players_count(int count);
/// No description provided for @point.
///
/// In en, this message translates to:
/// **'Point'**
String get point;
/// Points label
///
/// In en, this message translates to:
/// **'Points'**
String get points;
/// Privacy policy menu item
///
/// In en, this message translates to:
@@ -617,9 +635,15 @@ abstract class AppLocalizations {
/// Label to select the winner
///
/// In en, this message translates to:
/// **'Select Winner:'**
/// **'Select Winner'**
String get select_winner;
/// Label to select the loser
///
/// In en, this message translates to:
/// **'Select Loser'**
String get select_loser;
/// Shows the number of selected players
///
/// In en, this message translates to:
@@ -650,6 +674,12 @@ abstract class AppLocalizations {
/// **'Highest Score'**
String get highest_score;
/// No description provided for @loser.
///
/// In en, this message translates to:
/// **'Loser'**
String get loser;
/// No description provided for @lowest_score.
///
/// In en, this message translates to:
@@ -692,6 +722,12 @@ abstract class AppLocalizations {
/// **'This can\'t be undone.'**
String get this_cannot_be_undone;
/// No description provided for @tie.
///
/// In en, this message translates to:
/// **'Tie'**
String get tie;
/// Date format for today
///
/// In en, this message translates to:

View File

@@ -79,7 +79,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get delete_all_data => 'Alle Daten löschen';
@override
String get delete_group => 'Diese Gruppe löschen';
String get delete_group => 'Gruppe löschen';
@override
String get delete_match => 'Spiel löschen';
@@ -90,6 +90,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get edit_match => 'Gruppe bearbeiten';
@override
String get enter_points => 'Punkte eingeben';
@override
String get enter_results => 'Ergebnisse eintragen';
@@ -240,6 +243,12 @@ class AppLocalizationsDe extends AppLocalizations {
return '$count Spieler';
}
@override
String get point => 'Punkt';
@override
String get points => 'Punkte';
@override
String get privacy_policy => 'Datenschutzerklärung';
@@ -281,7 +290,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get search_for_players => 'Nach Spieler:innen suchen';
@override
String get select_winner => 'Gewinner:in wählen:';
String get select_winner => 'Gewinner:in wählen';
@override
String get select_loser => 'Verlierer:in wählen';
@override
String get selected_players => 'Ausgewählte Spieler:innen';
@@ -298,6 +310,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get highest_score => 'Höchste Punkte';
@override
String get loser => 'Verlierer:in';
@override
String get lowest_score => 'Niedrigste Punkte';
@@ -323,6 +338,9 @@ class AppLocalizationsDe extends AppLocalizations {
String get this_cannot_be_undone =>
'Dies kann nicht rückgängig gemacht werden.';
@override
String get tie => 'Unentschieden';
@override
String get today_at => 'Heute um';

View File

@@ -90,6 +90,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get edit_match => 'Edit Match';
@override
String get enter_points => 'Enter points';
@override
String get enter_results => 'Enter Results';
@@ -240,6 +243,12 @@ class AppLocalizationsEn extends AppLocalizations {
return '$count Players';
}
@override
String get point => 'Point';
@override
String get points => 'Points';
@override
String get privacy_policy => 'Privacy Policy';
@@ -281,7 +290,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get search_for_players => 'Search for players';
@override
String get select_winner => 'Select Winner:';
String get select_winner => 'Select Winner';
@override
String get select_loser => 'Select Loser';
@override
String get selected_players => 'Selected players';
@@ -298,6 +310,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get highest_score => 'Highest Score';
@override
String get loser => 'Loser';
@override
String get lowest_score => 'Lowest Score';
@@ -322,6 +337,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get this_cannot_be_undone => 'This can\'t be undone.';
@override
String get tie => 'Tie';
@override
String get today_at => 'Today at';

View File

@@ -148,7 +148,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
final groupName = _groupNameController.text.trim();
final success = await db.groupDao.addGroup(
group: Group(name: groupName, description: '', members: selectedPlayers),
group: Group(name: groupName, members: selectedPlayers),
);
return success;

View File

@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/match.dart';
@@ -10,10 +12,10 @@ import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -70,23 +72,16 @@ class _GroupDetailViewState extends State<GroupDetailView> {
context: context,
builder: (context) => CustomAlertDialog(
title: '${loc.delete_group}?',
content: loc.this_cannot_be_undone,
content: Text(loc.this_cannot_be_undone),
actions: [
AnimatedDialogButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(
loc.cancel,
style: const TextStyle(color: CustomTheme.textColor),
),
),
AnimatedDialogButton(
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(true),
child: Text(
loc.delete,
style: const TextStyle(
color: CustomTheme.secondaryColor,
),
),
text: loc.delete,
),
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
buttonType: ButtonType.secondary,
text: loc.cancel,
),
],
),
@@ -153,6 +148,7 @@ class _GroupDetailViewState extends State<GroupDetailView> {
children: _group.members.map((member) {
return TextIconTile(
text: member.name,
suffixText: getNameCountText(member),
iconEnabled: false,
);
}).toList(),
@@ -259,28 +255,37 @@ class _GroupDetailViewState extends State<GroupDetailView> {
/// Determines the best player in the group based on match wins
String _getBestPlayer(List<Match> matches) {
final bestPlayerCounts = <Player, int>{};
final mvpCounts = <Player, int>{};
// Count wins for each player
for (var match in matches) {
if (match.winner != null &&
_group.members.any((m) => m.id == match.winner?.id)) {
print(match.winner);
bestPlayerCounts.update(
match.winner!,
(value) => value + 1,
ifAbsent: () => 1,
);
final mvps = match.mvp;
for (final mvpPlayer in mvps) {
if (_group.members.any((m) => m.id == mvpPlayer.id)) {
mvpCounts.update(mvpPlayer, (value) => value + 1, ifAbsent: () => 1);
}
}
}
// Sort players by win count
final sortedPlayers = bestPlayerCounts.entries.toList()
final sortedMvps = mvpCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
// Get the best player
bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-';
if (sortedMvps.isEmpty) {
return '-';
}
return bestPlayer;
// Check if there are multiple players with the same value
final highestMvpCount = sortedMvps.first.value;
final topPlayers = sortedMvps
.where((entry) => entry.value == highestMvpCount)
.toList();
switch (topPlayers.length) {
case 0:
return '-';
case 1:
return topPlayers.first.key.name;
default:
final loc = AppLocalizations.of(context);
return loc.tie;
}
}
}

View File

@@ -36,7 +36,7 @@ class _GroupViewState extends State<GroupView> {
Group(
name: 'Skeleton Group',
description: '',
members: List.filled(6, Player(name: 'Skeleton Player', description: '')),
members: List.filled(6, Player(name: 'Skeleton Player')),
),
);

View File

@@ -43,21 +43,41 @@ class _HomeViewState extends State<HomeView> {
Match(
name: 'Skeleton Match',
game: Game(
name: '',
name: 'Skeleton Game',
ruleset: Ruleset.singleWinner,
description: '',
description: 'This is a skeleton game description.',
color: GameColor.blue,
icon: '',
),
group: Group(
name: 'Skeleton Group',
description: '',
description: 'This is a skeleton group description.',
members: [
Player(name: 'Skeleton Player 1', description: ''),
Player(name: 'Skeleton Player 2', description: ''),
Player(
name:
'Skeleton Player 1'
'',
),
Player(
name:
'Skeleton Player 2'
'',
),
],
),
notes: '',
notes: 'These are skeleton notes.',
players: [
Player(
name:
'Skeleton Player 1'
'',
),
Player(
name:
'Skeleton Player 2'
'',
),
],
),
);
@@ -125,7 +145,11 @@ class _HomeViewState extends State<HomeView> {
MatchResultView(match: match),
),
);
await updatedWinnerInRecentMatches(match.id);
await loadRecentMatches();
setState(() {
print('loaded');
});
},
),
)
@@ -224,15 +248,12 @@ class _HomeViewState extends State<HomeView> {
});
}
/// Updates the winner information for a specific match in the recent matches list.
Future<void> updatedWinnerInRecentMatches(String matchId) async {
Future<void> loadRecentMatches() async {
final db = Provider.of<AppDatabase>(context, listen: false);
final winner = await db.scoreEntryDao.getWinner(matchId: matchId);
final matchIndex = recentMatches.indexWhere((match) => match.id == matchId);
if (matchIndex != -1) {
setState(() {
recentMatches[matchIndex].winner = winner;
});
}
final matches = await db.matchDao.getAllMatches();
recentMatches =
(matches..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2)
.toList();
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart';
@@ -13,14 +13,14 @@ class ChooseGameView extends StatefulWidget {
const ChooseGameView({
super.key,
required this.games,
required this.initialGameIndex,
required this.initialGameId,
});
/// A list of tuples containing the game name, description and ruleset
final List<(String, String, Ruleset)> games;
final List<Game> games;
/// The index of the initially selected game
final int initialGameIndex;
/// The id of the initially selected game
final String initialGameId;
@override
State<ChooseGameView> createState() => _ChooseGameViewState();
@@ -31,11 +31,11 @@ class _ChooseGameViewState extends State<ChooseGameView> {
final TextEditingController searchBarController = TextEditingController();
/// Currently selected game index
late int selectedGameIndex;
late String selectedGameId;
@override
void initState() {
selectedGameIndex = widget.initialGameIndex;
selectedGameId = widget.initialGameId;
super.initState();
}
@@ -49,7 +49,13 @@ class _ChooseGameViewState extends State<ChooseGameView> {
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(selectedGameIndex);
Navigator.of(context).pop(
selectedGameId == ''
? null
: widget.games.firstWhere(
(game) => game.id == selectedGameId,
),
);
},
),
title: Text(loc.choose_game),
@@ -62,7 +68,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
if (didPop) {
return;
}
Navigator.of(context).pop(selectedGameIndex);
Navigator.of(context).pop(widget.initialGameId);
},
child: Column(
children: [
@@ -79,19 +85,19 @@ class _ChooseGameViewState extends State<ChooseGameView> {
itemCount: widget.games.length,
itemBuilder: (BuildContext context, int index) {
return TitleDescriptionListTile(
title: widget.games[index].$1,
description: widget.games[index].$2,
title: widget.games[index].name,
description: widget.games[index].description,
badgeText: translateRulesetToString(
widget.games[index].$3,
widget.games[index].ruleset,
context,
),
isHighlighted: selectedGameIndex == index,
isHighlighted: selectedGameId == widget.games[index].id,
onPressed: () async {
setState(() {
if (selectedGameIndex == index) {
selectedGameIndex = -1;
if (selectedGameId != widget.games[index].id) {
selectedGameId = widget.games[index].id;
} else {
selectedGameIndex = index;
selectedGameId = '';
}
});
},

View File

@@ -20,7 +20,9 @@ import 'package:tallee/presentation/widgets/tiles/choose_tile.dart';
class CreateMatchView extends StatefulWidget {
/// A view that allows creating a new match
/// [onWinnerChanged]: Optional callback invoked when the winner is changed
/// - [onWinnerChanged]: Optional callback invoked when the winner is changed
/// - [matchToEdit]: An optional match to prefill the fields for editing.
/// - [onMatchUpdated]: Optional callback invoked when the match is updated (only in
const CreateMatchView({
super.key,
this.onWinnerChanged,
@@ -28,13 +30,11 @@ class CreateMatchView extends StatefulWidget {
this.onMatchUpdated,
});
/// Optional callback invoked when the winner is changed
final VoidCallback? onWinnerChanged;
/// Optional callback invoked when the match is updated
final void Function(Match)? onMatchUpdated;
/// An optional match to prefill the fields
/// An optional match to prefill the fields for editing.
final Match? matchToEdit;
@override
@@ -50,20 +50,12 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// Hint text for the match name input field
String? hintText;
/// List of all groups from the database
List<Group> groupsList = [];
/// List of all players from the database
List<Player> playerList = [];
List<Game> gamesList = [];
/// The currently selected group
Group? selectedGroup;
/// The index of the currently selected game in [games] to mark it in
/// the [ChooseGameView]
int selectedGameIndex = -1;
/// The currently selected players
Game? selectedGame;
List<Player> selectedPlayers = [];
/// GlobalKey for ScaffoldMessenger to show snackbars
@@ -81,12 +73,14 @@ class _CreateMatchViewState extends State<CreateMatchView> {
Future.wait([
db.groupDao.getAllGroups(),
db.playerDao.getAllPlayers(),
db.gameDao.getAllGames(),
]).then((result) async {
groupsList = result[0] as List<Group>;
playerList = result[1] as List<Player>;
gamesList = (result[2] as List<Game>);
// If a match is provided, prefill the fields
if (widget.matchToEdit != null) {
if (isEditMode()) {
prefillMatchDetails();
}
});
@@ -105,20 +99,11 @@ class _CreateMatchViewState extends State<CreateMatchView> {
hintText ??= loc.match_name;
}
List<(String, String, Ruleset)> games = [
('Example Game 1', 'This is a description', Ruleset.lowestScore),
('Example Game 2', '', Ruleset.singleWinner),
];
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
final buttonText = widget.matchToEdit != null
? loc.save_changes
: loc.create_match;
final viewTitle = widget.matchToEdit != null
? loc.edit_match
: loc.create_new_match;
final buttonText = isEditMode() ? loc.save_changes : loc.create_match;
final viewTitle = isEditMode() ? loc.edit_match : loc.create_new_match;
return ScaffoldMessenger(
key: _scaffoldMessengerKey,
@@ -140,21 +125,21 @@ class _CreateMatchViewState extends State<CreateMatchView> {
),
ChooseTile(
title: loc.game,
trailingText: selectedGameIndex == -1
? loc.none
: games[selectedGameIndex].$1,
trailingText: selectedGame == null
? loc.none_group
: selectedGame!.name,
onPressed: () async {
selectedGameIndex = await Navigator.of(context).push(
selectedGame = await Navigator.of(context).push(
adaptivePageRoute(
builder: (context) => ChooseGameView(
games: games,
initialGameIndex: selectedGameIndex,
games: gamesList,
initialGameId: selectedGame?.id ?? '',
),
),
);
setState(() {
if (selectedGameIndex != -1) {
hintText = games[selectedGameIndex].$1;
if (selectedGame != null) {
hintText = selectedGame!.name;
} else {
hintText = loc.match_name;
}
@@ -225,6 +210,10 @@ class _CreateMatchViewState extends State<CreateMatchView> {
);
}
bool isEditMode() {
return widget.matchToEdit != null;
}
/// Determines whether the "Create Match" button should be enabled.
///
/// Returns `true` if:
@@ -232,7 +221,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// - Either a group is selected OR at least 2 players are selected
bool _enableCreateGameButton() {
return (selectedGroup != null ||
(selectedPlayers.length > 1) && selectedGameIndex != -1);
(selectedPlayers.length > 1) && selectedGame != null);
}
// If a match was provided to the view, it updates the match in the database
@@ -240,7 +229,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
// If no match was provided, it creates a new match in the database and
// navigates to the MatchResultView for the newly created match.
void buttonNavigation(BuildContext context) async {
if (widget.matchToEdit != null) {
if (isEditMode()) {
await updateMatch();
if (context.mounted) {
Navigator.pop(context);
@@ -266,9 +255,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// Updates attributes of the existing match in the database based on the
/// changes made in the edit view.
Future<void> updateMatch() async {
//TODO: Remove when Games implemented
final tempGame = await getTemporaryGame();
final updatedMatch = Match(
id: widget.matchToEdit!.id,
name: _matchNameController.text.isEmpty
@@ -276,8 +262,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
: _matchNameController.text.trim(),
group: selectedGroup,
players: selectedPlayers,
game: tempGame,
winner: widget.matchToEdit!.winner,
game: widget.matchToEdit!.game,
createdAt: widget.matchToEdit!.createdAt,
endedAt: widget.matchToEdit!.endedAt,
notes: widget.matchToEdit!.notes,
@@ -314,9 +299,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
matchId: widget.matchToEdit!.id,
playerId: player.id,
);
if (widget.matchToEdit!.winner?.id == player.id) {
updatedMatch.winner = null;
}
}
}
@@ -326,8 +308,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
// Creates a new match and adds it to the database.
// Returns the created match.
Future<Match> createMatch() async {
final tempGame = await getTemporaryGame();
Match match = Match(
name: _matchNameController.text.isEmpty
? (hintText ?? '')
@@ -335,35 +315,18 @@ class _CreateMatchViewState extends State<CreateMatchView> {
createdAt: DateTime.now(),
group: selectedGroup,
players: selectedPlayers,
game: tempGame,
game: selectedGame!,
);
await db.matchDao.addMatch(match: match);
return match;
}
// TODO: Remove when games fully implemented
Future<Game> getTemporaryGame() async {
Game? game;
final selectedGame = games[selectedGameIndex];
game = Game(
name: selectedGame.$1,
description: selectedGame.$2,
ruleset: selectedGame.$3,
color: GameColor.blue,
icon: '',
);
await db.gameDao.addGame(game: game);
return game;
}
// If a match was provided to the view, this method prefills the input fields
void prefillMatchDetails() {
final match = widget.matchToEdit!;
_matchNameController.text = match.name;
selectedPlayers = match.players;
selectedGameIndex = 0;
selectedGame = match.game;
if (match.group != null) {
selectedGroup = match.group;

View File

@@ -4,15 +4,16 @@ import 'package:provider/provider.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -64,23 +65,16 @@ class _MatchDetailViewState extends State<MatchDetailView> {
context: context,
builder: (context) => CustomAlertDialog(
title: '${loc.delete_match}?',
content: loc.this_cannot_be_undone,
content: Text(loc.this_cannot_be_undone),
actions: [
AnimatedDialogButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(
loc.cancel,
style: const TextStyle(color: CustomTheme.textColor),
),
),
AnimatedDialogButton(
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(true),
child: Text(
loc.delete,
style: const TextStyle(
color: CustomTheme.secondaryColor,
),
),
text: loc.delete,
),
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
buttonType: ButtonType.secondary,
text: loc.cancel,
),
],
),
@@ -161,6 +155,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
children: match.players.map((player) {
return TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
iconEnabled: false,
);
}).toList(),
@@ -175,37 +170,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
vertical: 4,
horizontal: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
/// TODO: Implement different ruleset results display
if (match.winner != null) ...[
Text(
loc.winner,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
Text(
match.winner!.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
] else ...[
Text(
loc.no_results_entered_yet,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
],
],
),
child: getResultWidget(loc),
),
),
],
@@ -232,7 +197,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
text: loc.enter_results,
icon: Icons.emoji_events,
onPressed: () async {
match.winner = await Navigator.push(
await Navigator.push(
context,
adaptivePageRoute(
fullscreenDialog: true,
@@ -264,4 +229,108 @@ class _MatchDetailViewState extends State<MatchDetailView> {
});
widget.onMatchUpdate.call();
}
/// Returns the widget to be displayed in the result [InfoTile]
Widget getResultWidget(AppLocalizations loc) {
if (isSingleRowResult()) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: getSingleResultRow(loc),
);
} else {
return getScoreResultWidget(loc);
}
}
/// Returns the result row for single winner/loser rulesets or a placeholder
/// if no result is entered yet
List<Widget> getSingleResultRow(AppLocalizations loc) {
// Single Winner
if (match.mvp.isNotEmpty && match.game.ruleset == Ruleset.singleWinner) {
return [
Text(
loc.winner,
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
),
Text(
match.mvp.first.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
];
// Single Loser
} else if (match.game.ruleset == Ruleset.singleLoser) {
return [
Text(
loc.loser,
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
),
Text(
match.mvp.first.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
];
// No result entered yet
} else {
return [
Text(
loc.no_results_entered_yet,
style: const TextStyle(fontSize: 14, color: CustomTheme.textColor),
),
];
}
}
/// Returns the result widget for scores
Widget getScoreResultWidget(AppLocalizations loc) {
List<(String, int)> playerScores = [];
for (var player in match.players) {
int score = match.scores[player.id]?.score ?? 0;
playerScores.add((player.name, score));
}
if (widget.match.game.ruleset == Ruleset.highestScore) {
playerScores.sort((a, b) => b.$2.compareTo(a.$2));
} else if (widget.match.game.ruleset == Ruleset.lowestScore) {
playerScores.sort((a, b) => a.$2.compareTo(b.$2));
}
return Column(
children: [
for (var score in playerScores)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
score.$1,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
Text(
getPointLabel(loc, score.$2),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
],
),
],
);
}
// Returns if the result can be displayed in a single row
bool isSingleRowResult() {
return match.game.ruleset == Ruleset.singleWinner ||
match.game.ruleset == Ruleset.singleLoser;
}
}

View File

@@ -1,11 +1,15 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart';
class MatchResultView extends StatefulWidget {
/// A view that allows selecting and saving the winner of a match
@@ -26,30 +30,61 @@ class MatchResultView extends StatefulWidget {
class _MatchResultViewState extends State<MatchResultView> {
late final AppDatabase db;
late final Ruleset ruleset;
/// List of all players who participated in the match
late final List<Player> allPlayers;
/// List of text controllers for score entry, one for each player
late final List<TextEditingController> controller;
late bool canSave;
/// Currently selected winner player
Player? _selectedPlayer;
@override
void initState() {
db = Provider.of<AppDatabase>(context, listen: false);
ruleset = widget.match.game.ruleset;
canSave = !rulesetSupportsScoreEntry();
allPlayers = widget.match.players;
allPlayers.sort((a, b) => a.name.compareTo(b.name));
if (widget.match.winner != null) {
_selectedPlayer = allPlayers.firstWhere(
(p) => p.id == widget.match.winner!.id,
);
controller = List.generate(
allPlayers.length,
(index) => TextEditingController()..addListener(() => onTextEnter()),
);
if (widget.match.mvp.isNotEmpty) {
if (rulesetSupportsWinnerSelection()) {
_selectedPlayer = allPlayers.firstWhere(
(p) => p.id == widget.match.mvp.first.id,
);
} else if (rulesetSupportsScoreEntry()) {
for (int i = 0; i < allPlayers.length; i++) {
final scoreList = widget.match.scores[allPlayers[i].id];
final score = scoreList?.score ?? 0;
controller[i].text = score.toString();
}
}
super.initState();
}
super.initState();
}
@override
void dispose() {
for (final c in controller) {
c.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
@@ -85,67 +120,169 @@ class _MatchResultViewState extends State<MatchResultView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.select_winner,
'${getTitleForRuleset(loc)}:',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Expanded(
child: RadioGroup<Player>(
groupValue: _selectedPlayer,
onChanged: (Player? value) async {
setState(() {
_selectedPlayer = value;
});
await _handleWinnerSaving();
},
child: ListView.builder(
if (rulesetSupportsWinnerSelection())
Expanded(
child: RadioGroup<Player>(
groupValue: _selectedPlayer,
onChanged: (Player? value) async {
setState(() {
_selectedPlayer = value;
});
},
child: ListView.builder(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return CustomRadioListTile(
text: allPlayers[index].name,
value: allPlayers[index],
onContainerTap: (value) async {
setState(() {
// Check if the already selected player is the same as the newly tapped player.
if (_selectedPlayer == value) {
// If yes deselected the player by setting it to null.
_selectedPlayer = null;
} else {
// If no assign the newly tapped player to the selected player.
(_selectedPlayer = value);
}
});
},
);
},
),
),
),
if (rulesetSupportsScoreEntry())
Expanded(
child: ListView.separated(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return CustomRadioListTile(
return ScoreListTile(
text: allPlayers[index].name,
value: allPlayers[index],
onContainerTap: (value) async {
setState(() {
// Check if the already selected player is the same as the newly tapped player.
if (_selectedPlayer == value) {
// If yes deselected the player by setting it to null.
_selectedPlayer = null;
} else {
// If no assign the newly tapped player to the selected player.
(_selectedPlayer = value);
}
});
await _handleWinnerSaving();
},
controller: controller[index],
);
},
separatorBuilder: (BuildContext context, int index) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Divider(indent: 20),
);
},
),
),
),
],
),
),
),
CustomWidthButton(
text: loc.save_changes,
sizeRelativeToWidth: 0.95,
onPressed: canSave
? () async {
final ending = DateTime.now();
await db.matchDao.updateMatchEndedAt(
matchId: widget.match.id,
endedAt: ending,
);
await _handleSaving();
if (!context.mounted) return;
Navigator.of(context).pop(_selectedPlayer);
}
: null,
),
],
),
),
);
}
/// Updated [canSave] everytime a text is entered in one of the score entry fields.
void onTextEnter() {
if (rulesetSupportsScoreEntry()) {
setState(() {
canSave = controller.every((c) => c.text.isNotEmpty);
});
}
}
/// Handles saving or removing the winner in the database
/// based on the current selection.
Future<void> _handleWinnerSaving() async {
Future<void> _handleSaving() async {
if (ruleset == Ruleset.singleWinner) {
await _handleWinner();
} else if (ruleset == Ruleset.singleLoser) {
await _handleLoser();
} else if (ruleset == Ruleset.lowestScore ||
ruleset == Ruleset.highestScore) {
await _handleScores();
}
widget.onWinnerChanged?.call();
}
/// Handles saving or removing the winner in the database.
Future<bool> _handleWinner() async {
if (_selectedPlayer == null) {
await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
return await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
} else {
await db.scoreEntryDao.setWinner(
return await db.scoreEntryDao.setWinner(
matchId: widget.match.id,
playerId: _selectedPlayer!.id,
);
}
widget.onWinnerChanged?.call();
}
/// Handles saving or removing the loser in the database.
Future<bool> _handleLoser() async {
if (_selectedPlayer == null) {
return await db.scoreEntryDao.removeLooser(matchId: widget.match.id);
} else {
return await db.scoreEntryDao.setLooser(
matchId: widget.match.id,
playerId: _selectedPlayer!.id,
);
}
}
/// Handles saving the scores for each player in the database.
Future<void> _handleScores() async {
for (int i = 0; i < allPlayers.length; i++) {
var text = controller[i].text;
if (text.isEmpty) {
text = '0';
}
final score = int.parse(text);
await db.scoreEntryDao.addScore(
matchId: widget.match.id,
playerId: allPlayers[i].id,
entry: ScoreEntry(roundNumber: 0, score: score, change: 0),
);
}
}
String getTitleForRuleset(AppLocalizations loc) {
switch (ruleset) {
case Ruleset.singleWinner:
return loc.select_winner;
case Ruleset.singleLoser:
return loc.select_loser;
default:
return loc.enter_points;
}
}
bool rulesetSupportsWinnerSelection() {
return ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser;
}
bool rulesetSupportsScoreEntry() {
return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore;
}
}

View File

@@ -37,20 +37,16 @@ class _MatchViewState extends State<MatchView> {
Match(
name: 'Skeleton match name',
game: Game(
name: '',
name: 'Game name',
ruleset: Ruleset.singleWinner,
description: '',
color: GameColor.blue,
icon: '',
),
group: Group(
name: 'Group name',
description: '',
members: List.filled(5, Player(name: 'Player', description: '')),
members: List.filled(5, Player(name: 'Player')),
),
winner: Player(name: 'Player', description: ''),
players: [Player(name: 'Player', description: '')],
notes: '',
players: [Player(name: 'Player')],
),
);
@@ -116,7 +112,7 @@ class _MatchViewState extends State<MatchView> {
Positioned(
bottom: MediaQuery.paddingOf(context).bottom + 20,
child: MainMenuButton(
text: 'Spiel erstellen',
text: loc.create_match,
icon: RpgAwesome.clovers_card,
onPressed: () async {
Navigator.push(

View File

@@ -30,7 +30,6 @@ const allDependencies = <Package>[
_cli_util,
_clock,
_code_assets,
_code_builder,
_collection,
_convert,
_coverage,
@@ -109,6 +108,7 @@ const allDependencies = <Package>[
_pubspec_parse,
_quiver,
_recase,
_record_use,
_retry,
_rfc_6901,
_safe_url_check,
@@ -159,14 +159,14 @@ const allDependencies = <Package>[
/// Direct `dependencies`.
const dependencies = <Package>[
_flutter,
_flutter_localizations,
_clock,
_cupertino_icons,
_drift,
_drift_flutter,
_file_picker,
_file_saver,
_flutter,
_flutter_localizations,
_fluttericon,
_font_awesome_flutter,
_intl,
@@ -567,17 +567,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// build_runner 2.13.1
/// build_runner 2.14.0
const _build_runner = Package(
name: 'build_runner',
description: 'A build system for Dart code generation and modular compilation.',
repository: 'https://github.com/dart-lang/build/tree/master/build_runner',
authors: [],
version: '2.13.1',
version: '2.14.0',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('analyzer'), PackageRef('args'), PackageRef('async'), PackageRef('build'), PackageRef('build_config'), PackageRef('build_daemon'), PackageRef('built_collection'), PackageRef('built_value'), PackageRef('code_builder'), PackageRef('collection'), PackageRef('convert'), PackageRef('crypto'), PackageRef('dart_style'), PackageRef('glob'), PackageRef('graphs'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('json_annotation'), PackageRef('logging'), PackageRef('meta'), PackageRef('mime'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('pub_semver'), PackageRef('shelf'), PackageRef('shelf_web_socket'), PackageRef('stream_transform'), PackageRef('watcher'), PackageRef('web_socket_channel'), PackageRef('yaml')],
dependencies: [PackageRef('analyzer'), PackageRef('args'), PackageRef('async'), PackageRef('build'), PackageRef('build_config'), PackageRef('build_daemon'), PackageRef('built_collection'), PackageRef('built_value'), PackageRef('collection'), PackageRef('convert'), PackageRef('crypto'), PackageRef('dart_style'), PackageRef('glob'), PackageRef('graphs'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('json_annotation'), PackageRef('logging'), PackageRef('meta'), PackageRef('mime'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('pub_semver'), PackageRef('shelf'), PackageRef('shelf_web_socket'), PackageRef('stream_transform'), PackageRef('watcher'), PackageRef('web_socket_channel'), PackageRef('yaml')],
devDependencies: [PackageRef('stream_channel'), PackageRef('test')],
license: '''Copyright 2016, the Dart project authors.
@@ -1153,47 +1153,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// code_builder 4.11.1
const _code_builder = Package(
name: 'code_builder',
description: 'A fluent, builder-based library for generating valid Dart code.',
repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/code_builder',
authors: [],
version: '4.11.1',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('built_collection'), PackageRef('built_value'), PackageRef('collection'), PackageRef('matcher'), PackageRef('meta')],
devDependencies: [PackageRef('build'), PackageRef('build_runner'), PackageRef('dart_style'), PackageRef('source_gen'), PackageRef('test')],
license: '''Copyright 2016, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// collection 1.19.1
const _collection = Package(
name: 'collection',
@@ -2418,14 +2377,14 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// file_picker 10.3.10
/// file_picker 11.0.2
const _file_picker = Package(
name: 'file_picker',
description: 'A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.',
homepage: 'https://github.com/miguelpruivo/plugins_flutter_file_picker',
repository: 'https://github.com/miguelpruivo/flutter_file_picker',
authors: [],
version: '10.3.10',
version: '11.0.2',
spdxIdentifiers: ['MIT'],
isMarkdown: false,
isSdk: false,
@@ -2731,13 +2690,13 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// font_awesome_flutter 10.12.0
/// font_awesome_flutter 11.0.0
const _font_awesome_flutter = Package(
name: 'font_awesome_flutter',
description: 'The Font Awesome Icon pack available as Flutter Icons. Provides 2000 additional icons to use in your apps.',
repository: 'https://github.com/fluttercommunity/font_awesome_flutter',
authors: [],
version: '10.12.0',
version: '11.0.0',
spdxIdentifiers: ['MIT'],
isMarkdown: false,
isSdk: false,
@@ -2892,17 +2851,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// hooks 1.0.2
/// hooks 1.0.3
const _hooks = Package(
name: 'hooks',
description: 'A library that contains a Dart API for the JSON-based protocol for `hook/build.dart` and `hook/link.dart`.',
repository: 'https://github.com/dart-lang/native/tree/main/pkgs/hooks',
authors: [],
version: '1.0.2',
version: '1.0.3',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('collection'), PackageRef('crypto'), PackageRef('logging'), PackageRef('meta'), PackageRef('pub_semver'), PackageRef('yaml')],
dependencies: [PackageRef('collection'), PackageRef('crypto'), PackageRef('logging'), PackageRef('meta'), PackageRef('pub_semver'), PackageRef('record_use'), PackageRef('yaml')],
devDependencies: [PackageRef('args'), PackageRef('code_assets'), PackageRef('glob'), PackageRef('json_schema'), PackageRef('path'), PackageRef('test')],
license: '''Copyright 2025, the Dart project authors.
@@ -5106,6 +5065,48 @@ Redistribution and use in source and binary forms, with or without modification,
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// record_use 0.6.0
const _record_use = Package(
name: 'record_use',
description: '''The serialization logic and API for the usage recording SDK feature.
''',
repository: 'https://github.com/dart-lang/native/tree/main/pkgs/record_use',
authors: [],
version: '0.6.0',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('collection'), PackageRef('meta'), PackageRef('pub_semver')],
devDependencies: [PackageRef('json_schema'), PackageRef('test')],
license: '''Copyright 2024, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// retry 3.1.2
const _retry = Package(
name: 'retry',
@@ -37480,13 +37481,13 @@ freely, subject to the following restrictions:
3. This notice may not be removed or altered from any source distribution.''',
);
/// vm_service 15.0.2
/// vm_service 15.1.0
const _vm_service = Package(
name: 'vm_service',
description: 'A library to communicate with a service implementing the Dart VM service protocol.',
repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/vm_service',
authors: [],
version: '15.0.2',
version: '15.1.0',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
@@ -37880,16 +37881,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''',
);
/// tallee 0.0.20+254
/// tallee 0.0.23+257
const _tallee = Package(
name: 'tallee',
description: 'Tracking App for Card Games',
authors: [],
version: '0.0.20+254',
version: '0.0.23+257',
spdxIdentifiers: ['LGPL-3.0'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('clock'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')],
dependencies: [PackageRef('clock'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')],
devDependencies: [PackageRef('flutter_test'), PackageRef('build_runner'), PackageRef('dart_pubspec_licenses'), PackageRef('drift_dev'), PackageRef('flutter_lints')],
license: '''GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007

View File

@@ -9,8 +9,8 @@ import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/settings_view/licenses_view.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:tallee/presentation/widgets/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
import 'package:tallee/presentation/widgets/tiles/settings_list_tile.dart';
import 'package:tallee/services/data_transfer_service.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -122,25 +122,16 @@ class _SettingsViewState extends State<SettingsView> {
context: context,
builder: (context) => CustomAlertDialog(
title: '${loc.delete_all_data}?',
content: loc.this_cannot_be_undone,
content: Text(loc.this_cannot_be_undone),
actions: [
AnimatedDialogButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(
loc.cancel,
style: const TextStyle(
color: CustomTheme.textColor,
),
),
),
AnimatedDialogButton(
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(true),
child: Text(
loc.delete,
style: const TextStyle(
color: CustomTheme.secondaryColor,
),
),
text: loc.delete,
),
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
buttonType: ButtonType.secondary,
text: loc.cancel,
),
],
),

View File

@@ -18,9 +18,18 @@ class StatisticsView extends StatefulWidget {
}
class _StatisticsViewState extends State<StatisticsView> {
List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1));
List<(String, int)> matchCounts = List.filled(6, ('Skeleton Player', 1));
List<(String, double)> winRates = List.filled(6, ('Skeleton Player', 1));
List<(Player, int)> winCounts = List.filled(6, (
Player(name: 'Skeleton Player'),
1,
));
List<(Player, int)> matchCounts = List.filled(6, (
Player(name: 'Skeleton Player'),
1,
));
List<(Player, double)> winRates = List.filled(6, (
Player(name: 'Skeleton Player'),
1,
));
bool isLoading = true;
@override
@@ -121,7 +130,10 @@ class _StatisticsViewState extends State<StatisticsView> {
players: players,
context: context,
);
winRates = computeWinRatePercent(wins: winCounts, matches: matchCounts);
winRates = computeWinRatePercent(
winCounts: winCounts,
matchCounts: matchCounts,
);
setState(() {
isLoading = false;
});
@@ -130,47 +142,46 @@ class _StatisticsViewState extends State<StatisticsView> {
/// Calculates the number of wins for each player
/// and returns a sorted list of tuples (playerName, winCount)
List<(String, int)> _calculateWinsForAllPlayers({
List<(Player, int)> _calculateWinsForAllPlayers({
required List<Match> matches,
required List<Player> players,
required BuildContext context,
}) {
List<(String, int)> winCounts = [];
List<(Player, int)> winCounts = [];
final loc = AppLocalizations.of(context);
// Getting the winners
for (var match in matches) {
final winner = match.winner;
if (winner != null) {
final index = winCounts.indexWhere((entry) => entry.$1 == winner.id);
final mvps = match.mvp;
for (var winner in mvps) {
final index = winCounts.indexWhere((entry) => entry.$1.id == winner.id);
// -1 means winner not found in winCounts
if (index != -1) {
final current = winCounts[index].$2;
winCounts[index] = (winner.id, current + 1);
winCounts[index] = (winner, current + 1);
} else {
winCounts.add((winner.id, 1));
winCounts.add((winner, 1));
}
}
}
// Adding all players with zero wins
for (var player in players) {
final index = winCounts.indexWhere((entry) => entry.$1 == player.id);
final index = winCounts.indexWhere((entry) => entry.$1.id == player.id);
// -1 means player not found in winCounts
if (index == -1) {
winCounts.add((player.id, 0));
winCounts.add((player, 0));
}
}
// Replace player IDs with names
for (int i = 0; i < winCounts.length; i++) {
final playerId = winCounts[i].$1;
final playerId = winCounts[i].$1.id;
final player = players.firstWhere(
(p) => p.id == playerId,
orElse: () =>
Player(id: playerId, name: loc.not_available, description: ''),
orElse: () => Player(id: playerId, name: loc.not_available),
);
winCounts[i] = (player.name, winCounts[i].$2);
winCounts[i] = (player, winCounts[i].$2);
}
winCounts.sort((a, b) => b.$2.compareTo(a.$2));
@@ -180,60 +191,51 @@ class _StatisticsViewState extends State<StatisticsView> {
/// Calculates the number of matches played for each player
/// and returns a sorted list of tuples (playerName, matchCount)
List<(String, int)> _calculateMatchAmountsForAllPlayers({
List<(Player, int)> _calculateMatchAmountsForAllPlayers({
required List<Match> matches,
required List<Player> players,
required BuildContext context,
}) {
List<(String, int)> matchCounts = [];
List<(Player, int)> matchCounts = [];
final loc = AppLocalizations.of(context);
// Counting matches for each player
for (var match in matches) {
if (match.group != null) {
final members = match.group!.members.map((p) => p.id).toList();
for (var playerId in members) {
final index = matchCounts.indexWhere((entry) => entry.$1 == playerId);
// -1 means player not found in matchCounts
if (index != -1) {
final current = matchCounts[index].$2;
matchCounts[index] = (playerId, current + 1);
} else {
matchCounts.add((playerId, 1));
}
}
}
final members = match.players.map((p) => p.id).toList();
for (var playerId in members) {
final index = matchCounts.indexWhere((entry) => entry.$1 == playerId);
// -1 means player not found in matchCounts
if (index != -1) {
final current = matchCounts[index].$2;
matchCounts[index] = (playerId, current + 1);
for (Player player in match.players) {
// Check if the player is already in matchCounts
final index = matchCounts.indexWhere(
(entry) => entry.$1.id == player.id,
);
// -1 -> not found
if (index == -1) {
// Add new entry
matchCounts.add((player, 1));
} else {
matchCounts.add((playerId, 1));
// Update existing entry
final currentMatchAmount = matchCounts[index].$2;
matchCounts[index] = (player, currentMatchAmount + 1);
}
}
}
// Adding all players with zero matches
for (var player in players) {
final index = matchCounts.indexWhere((entry) => entry.$1 == player.id);
final index = matchCounts.indexWhere((entry) => entry.$1.id == player.id);
// -1 means player not found in matchCounts
if (index == -1) {
matchCounts.add((player.id, 0));
matchCounts.add((player, 0));
}
}
// Replace player IDs with names
for (int i = 0; i < matchCounts.length; i++) {
final playerId = matchCounts[i].$1;
final playerId = matchCounts[i].$1.id;
final player = players.firstWhere(
(p) => p.id == playerId,
orElse: () =>
Player(id: playerId, name: loc.not_available, description: ''),
orElse: () => Player(id: playerId, name: loc.not_available),
);
matchCounts[i] = (player.name, matchCounts[i].$2);
matchCounts[i] = (player, matchCounts[i].$2);
}
matchCounts.sort((a, b) => b.$2.compareTo(a.$2));
@@ -241,25 +243,24 @@ class _StatisticsViewState extends State<StatisticsView> {
return matchCounts;
}
// dart
List<(String, double)> computeWinRatePercent({
required List<(String, int)> wins,
required List<(String, int)> matches,
List<(Player, double)> computeWinRatePercent({
required List<(Player, int)> winCounts,
required List<(Player, int)> matchCounts,
}) {
final Map<String, int> winsMap = {for (var e in wins) e.$1: e.$2};
final Map<String, int> matchesMap = {for (var e in matches) e.$1: e.$2};
final Map<Player, int> winsMap = {for (var e in winCounts) e.$1: e.$2};
final Map<Player, int> matchesMap = {for (var e in matchCounts) e.$1: e.$2};
// Get all unique player names
final names = {...winsMap.keys, ...matchesMap.keys};
final player = {...matchesMap.keys};
// Calculate win rates
final result = names.map((name) {
final result = player.map((name) {
final int w = winsMap[name] ?? 0;
final int g = matchesMap[name] ?? 0;
final int m = matchesMap[name] ?? 0;
// Calculate percentage and round to 2 decimal places
// Avoid division by zero
final double percent = (g > 0)
? double.parse(((w / g)).toStringAsFixed(2))
final double percent = (m > 0)
? double.parse(((w / m)).toStringAsFixed(2))
: 0;
return (name, percent);
}).toList();

View File

@@ -1,22 +1,28 @@
import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
class AnimatedDialogButton extends StatefulWidget {
/// A custom animated button widget that provides a scaling and opacity effect
/// when pressed.
/// - [onPressed]: Callback function that is triggered when the button is pressed.
/// - [child]: The child widget to be displayed inside the button, typically a text or icon.
/// - [buttonText]: The text to be displayed on the button.
/// - [buttonType]: The type of the button, which determines its styling.
/// - [buttonConstraints]: Optional constraints to control the button's size.
const AnimatedDialogButton({
super.key,
required this.buttonText,
required this.onPressed,
required this.child,
this.buttonConstraints,
this.buttonType = ButtonType.primary,
});
/// Callback function that is triggered when the button is pressed.
final String buttonText;
final VoidCallback onPressed;
/// The child widget to be displayed inside the button, typically a text or icon.
final Widget child;
final BoxConstraints? buttonConstraints;
final ButtonType buttonType;
@override
State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState();
@@ -27,6 +33,29 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
@override
Widget build(BuildContext context) {
final textStyling = TextStyle(
color: widget.buttonType == ButtonType.primary
? Colors.black
: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
);
final buttonDecoration = widget.buttonType == ButtonType.primary
// Primary
? BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
)
: widget.buttonType == ButtonType.secondary
// Secondary
? BoxDecoration(
border: BoxBorder.all(color: Colors.white, width: 2),
borderRadius: BorderRadius.circular(12),
)
// Tertiary
: const BoxDecoration();
return GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false),
@@ -38,10 +67,18 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
child: AnimatedOpacity(
opacity: _isPressed ? 0.6 : 1.0,
duration: const Duration(milliseconds: 100),
child: Container(
decoration: CustomTheme.standardBoxDecoration,
padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 6),
child: widget.child,
child: Center(
child: Container(
constraints: widget.buttonConstraints,
decoration: buttonDecoration,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
margin: const EdgeInsets.symmetric(vertical: 8),
child: Text(
widget.buttonText,
style: textStyling,
textAlign: TextAlign.center,
),
),
),
),
),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
class CustomAlertDialog extends StatelessWidget {
/// A custom alert dialog widget that provides a os unspecific AlertDialog,
@@ -16,20 +17,23 @@ class CustomAlertDialog extends StatelessWidget {
});
final String title;
final String content;
final List<Widget> actions;
final Widget content;
final List<CustomDialogAction> actions;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(title, style: const TextStyle(color: CustomTheme.textColor)),
content: Text(
content,
style: const TextStyle(color: CustomTheme.textColor),
title: Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
),
content: content,
actions: actions,
backgroundColor: CustomTheme.boxColor,
actionsAlignment: MainAxisAlignment.spaceAround,
actionsAlignment: MainAxisAlignment.center,
shape: RoundedRectangleBorder(
borderRadius: CustomTheme.standardBorderRadiusAll,
side: const BorderSide(color: CustomTheme.boxBorderColor),

View File

@@ -0,0 +1,32 @@
import 'package:flutter/cupertino.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
class CustomDialogAction extends StatelessWidget {
/// A custom dialog action widget that represents a button in a dialog.
/// - [text]: The text to be displayed on the button.
/// - [buttonType]: The type of the button, which determines its styling.
/// - [onPressed]: Callback function that is triggered when the button is pressed.
const CustomDialogAction({
super.key,
required this.onPressed,
required this.text,
this.buttonType = ButtonType.primary,
});
final String text;
final ButtonType buttonType;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return AnimatedDialogButton(
onPressed: onPressed,
buttonText: text,
buttonType: buttonType,
buttonConstraints: const BoxConstraints(minWidth: 300),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart';
@@ -62,7 +63,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
/// Skeleton data used while loading players.
late final List<Player> skeletonData = List.filled(
7,
Player(name: 'Player 0', description: ''),
Player(name: 'Player 0'),
);
@override
@@ -140,6 +141,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
padding: const EdgeInsets.only(right: 8.0),
child: TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
onIconTap: () {
setState(() {
// Removes the player from the selection and notifies the parent.
@@ -193,6 +195,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
itemBuilder: (BuildContext context, int index) {
return TextIconListTile(
text: suggestedPlayers[index].name,
suffixText: getNameCountText(suggestedPlayers[index]),
onPressed: () {
setState(() {
// If the player is not already selected
@@ -282,7 +285,8 @@ class _PlayerSelectionState extends State<PlayerSelection> {
final loc = AppLocalizations.of(context);
final playerName = _searchBarController.text.trim();
final createdPlayer = Player(name: playerName, description: '');
int nameCount = _calculateNameCount(playerName);
final createdPlayer = Player(name: playerName, nameCount: nameCount);
final success = await db.playerDao.addPlayer(player: createdPlayer);
if (!context.mounted) return;
@@ -295,6 +299,22 @@ class _PlayerSelectionState extends State<PlayerSelection> {
}
}
int _calculateNameCount(String playerName) {
final playersWithSameName =
allPlayers.where((player) => player.name == playerName).toList()
..sort((a, b) => a.nameCount.compareTo(b.nameCount));
if (playersWithSameName.isEmpty) {
return 0;
} else if (playersWithSameName.length == 1) {
// Initialize nameCount
playersWithSameName[0].nameCount = 1;
}
// Return following count
return playersWithSameName.length + 1;
}
/// Updates the state after successfully adding a new player.
void _handleSuccessfulPlayerCreation(Player player) {
selectedPlayers.insert(0, player);

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -81,7 +82,11 @@ class _GroupTileState extends State<GroupTile> {
for (var member in [
...widget.group.members,
]..sort((a, b) => a.name.compareTo(b.name)))
TextIconTile(text: member.name, iconEnabled: false),
TextIconTile(
text: member.name,
suffixText: getNameCountText(member),
iconEnabled: false,
),
],
),
const SizedBox(height: 2.5),

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -44,7 +45,6 @@ class _MatchTileState extends State<MatchTile> {
Widget build(BuildContext context) {
final match = widget.match;
final group = match.group;
final winner = match.winner;
final players = [...match.players]
..sort((a, b) => a.name.compareTo(b.name));
final loc = AppLocalizations.of(context);
@@ -79,8 +79,7 @@ class _MatchTileState extends State<MatchTile> {
],
),
const SizedBox(height: 8),
// Group Info
if (group != null) ...[
Row(
children: [
@@ -95,7 +94,7 @@ class _MatchTileState extends State<MatchTile> {
),
],
),
const SizedBox(height: 12),
const SizedBox(height: 4),
] else if (widget.compact) ...[
Row(
children: [
@@ -110,10 +109,69 @@ class _MatchTileState extends State<MatchTile> {
),
],
),
const SizedBox(height: 12),
const SizedBox(height: 6),
] else ...[
const SizedBox(height: 8),
],
if (winner != null) ...[
// Game + Ruleset Badge
if (!widget.compact)
IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Game
Container(
decoration: BoxDecoration(
color: CustomTheme.primaryColor.withAlpha(230),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
bottomLeft: Radius.circular(8),
),
),
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 8,
),
child: Text(
match.game.name,
style: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
// Ruleset
Container(
decoration: BoxDecoration(
color: CustomTheme.primaryColor.withAlpha(140),
borderRadius: const BorderRadius.only(
topRight: Radius.circular(8),
bottomRight: Radius.circular(8),
),
),
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 8,
),
child: Text(
translateRulesetToString(match.game.ruleset, context),
style: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const SizedBox(height: 12),
// Winner / In Progress Info
if (match.mvp.isNotEmpty) ...[
Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
@@ -129,15 +187,11 @@ class _MatchTileState extends State<MatchTile> {
),
child: Row(
children: [
const Icon(
Icons.emoji_events,
size: 20,
color: Colors.amber,
),
getMvpIcon(),
const SizedBox(width: 8),
Expanded(
child: Text(
'${loc.winner}: ${winner.name}',
getMvpText(loc),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
@@ -189,6 +243,7 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(height: 12),
],
// Players List
if (players.isNotEmpty && widget.compact == false) ...[
Text(
loc.players,
@@ -203,7 +258,11 @@ class _MatchTileState extends State<MatchTile> {
spacing: 6,
runSpacing: 6,
children: players.map((player) {
return TextIconTile(text: player.name, iconEnabled: false);
return TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
iconEnabled: false,
);
}).toList(),
),
],
@@ -230,4 +289,44 @@ class _MatchTileState extends State<MatchTile> {
return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}';
}
}
String getMvpText(AppLocalizations loc) {
if (widget.match.mvp.isEmpty) return '';
final ruleset = widget.match.game.ruleset;
if (ruleset == Ruleset.singleWinner) {
return '${loc.winner}: ${widget.match.mvp.first.name}';
} else if (ruleset == Ruleset.singleLoser) {
return '${loc.loser}: ${widget.match.mvp.first.name}';
} else if (ruleset == Ruleset.highestScore ||
ruleset == Ruleset.lowestScore) {
final mvp = widget.match.mvp;
final mvpScore = widget.match.scores[mvp.first.id]?.score ?? 0;
final mvpNames = mvp.map((player) => player.name).join(', ');
return '${loc.winner}: $mvpNames (${getPointLabel(loc, mvpScore)})';
}
return '${loc.winner}: n.A.';
}
Icon getMvpIcon() {
const Icon(Icons.emoji_events, size: 20, color: Colors.amber);
switch (widget.match.game.ruleset) {
case Ruleset.singleWinner:
return const Icon(Icons.emoji_events, size: 20, color: Colors.amber);
case Ruleset.singleLoser:
return const Icon(
Icons.sentiment_dissatisfied_outlined,
size: 20,
color: Colors.blue,
);
case Ruleset.lowestScore:
return const Icon(Icons.arrow_downward, size: 20, color: Colors.orange);
case Ruleset.highestScore:
return const Icon(Icons.arrow_upward, size: 20, color: Colors.green);
default:
return const Icon(Icons.emoji_events, size: 20, color: Colors.amber);
}
}
}

View 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,
),
),
),
),
),
],
),
);
}
}

View File

@@ -1,6 +1,9 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
@@ -32,7 +35,7 @@ class StatisticsTile extends StatelessWidget {
final double width;
/// A list of tuples containing labels and their corresponding numeric values.
final List<(String, num)> values;
final List<(Player, num)> values;
/// The maximum number of items to display.
final int itemCount;
@@ -89,11 +92,29 @@ class StatisticsTile extends StatelessWidget {
),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
values[index].$1,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: values[index].$1.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: getNameCountText(values[index].$1),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor.withAlpha(
150,
),
),
),
],
),
),
),

View File

@@ -9,6 +9,7 @@ class TextIconListTile extends StatelessWidget {
const TextIconListTile({
super.key,
required this.text,
this.suffixText = '',
this.iconEnabled = true,
this.onPressed,
});
@@ -16,6 +17,9 @@ class TextIconListTile extends StatelessWidget {
/// The text to display in the tile.
final String text;
/// An optional suffix text to display after the main text.
final String suffixText;
/// A boolean to determine if the icon should be displayed.
final bool iconEnabled;
@@ -35,12 +39,27 @@ class TextIconListTile extends StatelessWidget {
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12.5),
child: Text(
text,
child: RichText(
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: text,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
TextSpan(
text: suffixText,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: CustomTheme.textColor.withAlpha(100),
),
),
],
),
),
),

View File

@@ -9,6 +9,7 @@ class TextIconTile extends StatelessWidget {
const TextIconTile({
super.key,
required this.text,
this.suffixText = '',
this.iconEnabled = true,
this.onIconTap,
});
@@ -16,6 +17,8 @@ class TextIconTile extends StatelessWidget {
/// The text to display in the tile.
final String text;
final String suffixText;
/// A boolean to determine if the icon should be displayed.
final bool iconEnabled;
@@ -36,10 +39,28 @@ class TextIconTile extends StatelessWidget {
children: [
if (iconEnabled) const SizedBox(width: 3),
Flexible(
child: Text(
text,
child: RichText(
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: text,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
TextSpan(
text: suffixText,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: CustomTheme.textColor.withAlpha(120),
),
),
],
),
),
),
if (iconEnabled) ...<Widget>[

View File

@@ -12,6 +12,7 @@ import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
import 'package:tallee/data/models/team.dart';
class DataTransferService {
@@ -36,59 +37,12 @@ class DataTransferService {
final games = await db.gameDao.getAllGames();
final teams = await db.teamDao.getAllTeams();
// Construct a JSON representation of the data in normalized format
final Map<String, dynamic> jsonMap = {
'players': players.map((p) => p.toJson()).toList(),
'games': games.map((g) => g.toJson()).toList(),
'groups': groups
.map(
(g) => {
'id': g.id,
'name': g.name,
'description': g.description,
'createdAt': g.createdAt.toIso8601String(),
'memberIds': (g.members).map((m) => m.id).toList(),
},
)
.toList(),
'teams': teams
.map(
(t) => {
'id': t.id,
'name': t.name,
'createdAt': t.createdAt.toIso8601String(),
'memberIds': (t.members).map((m) => m.id).toList(),
},
)
.toList(),
'matches': matches
.map(
(m) => {
'id': m.id,
'name': m.name,
'createdAt': m.createdAt.toIso8601String(),
'endedAt': m.endedAt?.toIso8601String(),
'gameId': m.game.id,
'groupId': m.group?.id,
'playerIds': m.players.map((p) => p.id).toList(),
'scores': m.scores.map(
(playerId, scores) => MapEntry(
playerId,
scores
.map(
(s) => {
'roundNumber': s.roundNumber,
'score': s.score,
'change': s.change,
},
)
.toList(),
),
),
'notes': m.notes,
},
)
.toList(),
'players': players.map((player) => player.toJson()).toList(),
'games': games.map((game) => game.toJson()).toList(),
'groups': groups.map((group) => group.toJson()).toList(),
'teams': teams.map((team) => team.toJson()).toList(),
'matches': matches.map((match) => match.toJson()).toList(),
};
return json.encode(jsonMap);
@@ -105,7 +59,7 @@ class DataTransferService {
) async {
try {
final bytes = Uint8List.fromList(utf8.encode(jsonString));
final path = await FilePicker.platform.saveFile(
final path = await FilePicker.saveFile(
fileName: '$fileName.json',
bytes: bytes,
);
@@ -126,7 +80,7 @@ class DataTransferService {
static Future<ImportResult> importData(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false);
final path = await FilePicker.platform.pickFiles(
final path = await FilePicker.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
);
@@ -284,6 +238,15 @@ class DataTransferService {
? DateTime.parse(map['endedAt'] as String)
: null;
final notes = map['notes'] as String? ?? '';
final scoresJson = map['scores'] as Map<String, dynamic>? ?? {};
final scores = scoresJson.map(
(key, value) => MapEntry(
key,
value != null
? ScoreEntry.fromJson(value as Map<String, dynamic>)
: null,
),
);
// Link attributes to objects
final game = gamesMap[gameId] ?? getFallbackGame();
@@ -305,6 +268,7 @@ class DataTransferService {
createdAt: createdAt,
endedAt: endedAt,
notes: notes,
scores: scores,
);
}).toList();
}