16 Commits

Author SHA1 Message Date
1f17b80f64 Merge branch 'development' into feature/180-Spielerprofile-implementieren
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 51s
Pull Request Pipeline / localizations (pull_request) Successful in 28s
Pull Request Pipeline / test (pull_request) Successful in 49s
2026-05-23 00:42:59 +02:00
fad5a392cd Merge branch 'development' into feature/180-Spielerprofile-implementieren
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 53s
Pull Request Pipeline / localizations (pull_request) Successful in 25s
# Conflicts:
#	lib/l10n/arb/app_de.arb
#	lib/l10n/arb/app_en.arb
#	lib/l10n/generated/app_localizations.dart
#	lib/l10n/generated/app_localizations_de.dart
#	lib/l10n/generated/app_localizations_en.dart
2026-05-23 00:22:03 +02:00
5a652a5f2c feat: updatePlayerName keeps created order in nameCount
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 54s
2026-05-22 20:06:27 +02:00
4dcd4f0f71 fix: name count 3 player issue 2026-05-22 20:00:28 +02:00
25bc213769 Merge remote-tracking branch 'origin/feature/180-Spielerprofile-implementieren' into feature/180-Spielerprofile-implementieren
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 47s
Pull Request Pipeline / lint (pull_request) Successful in 54s
2026-05-22 08:45:43 +02:00
9adcc29cda fix: updatePlayerName corrects the name count after renaming to different name
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 47s
Pull Request Pipeline / lint (pull_request) Successful in 55s
2026-05-21 23:57:59 +02:00
78c59a9b52 feat: add localization for no matches played and not part of any group 2026-05-21 16:20:48 +02:00
bf2cd2bf58 feat: add player creation callbacks to update member and match lists when group/match creation is canceled but player created
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 55s
2026-05-21 16:08:59 +02:00
ccb0d32c54 fix: tests for name count
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 54s
2026-05-21 15:45:29 +02:00
82095ab41a fix: name count 2026-05-21 15:33:26 +02:00
2a38462c57 fix linter issues
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 44s
Pull Request Pipeline / lint (pull_request) Successful in 56s
2026-05-21 10:34:01 +02:00
9909d959b0 implement missing localization
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 47s
Pull Request Pipeline / lint (pull_request) Failing after 56s
2026-05-21 10:33:16 +02:00
b61a93328f made alertDialog Confirm Button deactivate based on input, fix app skeleton alignment issue, implement correct nameCount Display
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 42s
Pull Request Pipeline / lint (pull_request) Failing after 51s
2026-05-21 09:47:49 +02:00
679e869229 fix: player count calc error
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 44s
Pull Request Pipeline / lint (pull_request) Failing after 52s
2026-05-20 19:58:59 +02:00
869c70ff63 add player change callbacks and improve player detail view
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 47s
Pull Request Pipeline / lint (pull_request) Failing after 53s
2026-05-20 19:50:34 +02:00
b305145d34 implement basic player_detail_view.dart
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 45s
Pull Request Pipeline / lint (pull_request) Failing after 51s
2026-05-20 15:15:47 +02:00
64 changed files with 1841 additions and 3218 deletions

View File

@@ -166,9 +166,6 @@
"notes": { "notes": {
"type": "string" "type": "string"
}, },
"isTeamMatch": {
"type": "boolean"
},
"teams": { "teams": {
"type": ["array", "null"] "type": ["array", "null"]
} }
@@ -180,8 +177,7 @@
"createdAt", "createdAt",
"gameId", "gameId",
"playerIds", "playerIds",
"notes", "notes"
"isTeamMatch"
] ]
} }
} }

View File

@@ -24,62 +24,47 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) {
} }
} }
// Returns a AppColor enum value based on the provided team [index]. /// Translates a [GameColor] enum value to its corresponding localized string.
AppColor getTeamColor(int index) { String translateGameColorToString(GameColor color, BuildContext context) {
final colors = [
AppColor.red,
AppColor.blue,
AppColor.green,
AppColor.yellow,
AppColor.purple,
AppColor.orange,
AppColor.pink,
AppColor.teal,
];
return colors[index % colors.length];
}
/// Translates a [AppColor] enum value to its corresponding localized string.
String translateGameColorToString(AppColor color, BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
switch (color) { switch (color) {
case AppColor.red: case GameColor.red:
return loc.color_red; return loc.color_red;
case AppColor.blue: case GameColor.blue:
return loc.color_blue; return loc.color_blue;
case AppColor.green: case GameColor.green:
return loc.color_green; return loc.color_green;
case AppColor.yellow: case GameColor.yellow:
return loc.color_yellow; return loc.color_yellow;
case AppColor.purple: case GameColor.purple:
return loc.color_purple; return loc.color_purple;
case AppColor.orange: case GameColor.orange:
return loc.color_orange; return loc.color_orange;
case AppColor.pink: case GameColor.pink:
return loc.color_pink; return loc.color_pink;
case AppColor.teal: case GameColor.teal:
return loc.color_teal; return loc.color_teal;
} }
} }
/// Returns the [Color] object corresponding to a [AppColor] enum value. /// Returns the [Color] object corresponding to a [GameColor] enum value.
Color getColorFromGameColor(AppColor color) { Color getColorFromGameColor(GameColor color) {
switch (color) { switch (color) {
case AppColor.red: case GameColor.red:
return Colors.red; return Colors.red;
case AppColor.blue: case GameColor.blue:
return Colors.blue; return Colors.blue;
case AppColor.green: case GameColor.green:
return Colors.green; return Colors.green;
case AppColor.yellow: case GameColor.yellow:
return const Color(0xFFF7CA28); return const Color(0xFFF7CA28);
case AppColor.purple: case GameColor.purple:
return Colors.purple; return Colors.purple;
case AppColor.orange: case GameColor.orange:
return const Color(0xFFef681f); return const Color(0xFFef681f);
case AppColor.pink: case GameColor.pink:
return Colors.pink; return Colors.pink;
case AppColor.teal: case GameColor.teal:
return Colors.teal; return Colors.teal;
} }
} }
@@ -92,10 +77,11 @@ IconData getRulesetIcon(Ruleset ruleset) {
case Ruleset.lowestScore: case Ruleset.lowestScore:
return Icons.arrow_downward; return Icons.arrow_downward;
case Ruleset.singleWinner: case Ruleset.singleWinner:
case Ruleset.multipleWinners:
return Icons.emoji_events; return Icons.emoji_events;
case Ruleset.singleLoser: case Ruleset.singleLoser:
return Icons.sentiment_dissatisfied; return Icons.sentiment_dissatisfied;
case Ruleset.multipleWinners:
return Icons.group;
case Ruleset.placement: case Ruleset.placement:
return RpgAwesome.podium; return RpgAwesome.podium;
} }
@@ -127,7 +113,6 @@ String getExtraPlayerCount(Match match) {
return ' + ${count.toString()}'; return ' + ${count.toString()}';
} }
/// Returns the player name count if greater 0 in the format " #2", otherwise an empty string
String getNameCountText(Player player) { String getNameCountText(Player player) {
if (player.nameCount >= 1) { if (player.nameCount >= 1) {
return ' #${player.nameCount}'; return ' #${player.nameCount}';
@@ -135,7 +120,6 @@ String getNameCountText(Player player) {
return ''; return '';
} }
/// Returns the correct singular or plural form of "point(s)" based on the [points] value.
String getPointLabel(AppLocalizations loc, int points) { String getPointLabel(AppLocalizations loc, int points) {
if (points == 1) { if (points == 1) {
return '$points ${loc.point}'; return '$points ${loc.point}';

View File

@@ -65,11 +65,7 @@ class CustomTheme {
static BoxDecoration highlightedBoxDecoration = BoxDecoration( static BoxDecoration highlightedBoxDecoration = BoxDecoration(
color: boxColor, color: boxColor,
border: Border.all( border: Border.all(color: textColor, width: 2),
color: textColor,
width: 2,
strokeAlign: BorderSide.strokeAlignCenter,
),
borderRadius: standardBorderRadiusAll, borderRadius: standardBorderRadiusAll,
); );

View File

@@ -42,5 +42,5 @@ enum Ruleset {
singleLoser, singleLoser,
} }
/// Different colors for highlighting content /// Different colors for highlighting games
enum AppColor { red, orange, yellow, green, teal, blue, purple, pink } enum GameColor { red, orange, yellow, green, teal, blue, purple, pink }

View File

@@ -92,7 +92,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
name: row.name, name: row.name,
ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset),
description: row.description, description: row.description,
color: AppColor.values.firstWhere((e) => e.name == row.color), color: GameColor.values.firstWhere((e) => e.name == row.color),
icon: row.icon, icon: row.icon,
createdAt: row.createdAt, createdAt: row.createdAt,
), ),
@@ -109,7 +109,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
name: result.name, name: result.name,
ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset), ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset),
description: result.description, description: result.description,
color: AppColor.values.firstWhere((e) => e.name == result.color), color: GameColor.values.firstWhere((e) => e.name == result.color),
icon: result.icon, icon: result.icon,
createdAt: result.createdAt, createdAt: result.createdAt,
); );
@@ -156,7 +156,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Updates the color of the game with the given [gameId]. /// Updates the color of the game with the given [gameId].
Future<bool> updateGameColor({ Future<bool> updateGameColor({
required String gameId, required String gameId,
required AppColor color, required GameColor color,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(gameTable)..where((g) => g.id.equals(gameId))).write( await (update(gameTable)..where((g) => g.id.equals(gameId))).write(

View File

@@ -185,6 +185,38 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
return count ?? 0; return count ?? 0;
} }
/// Retrieves all groups a specific player belongs to.
/// Returns an empty list if the player is not part of any group.
Future<List<Group>> getGroupsByPlayer({required String playerId}) async {
final playerGroups = await (select(
playerGroupTable,
)..where((pg) => pg.playerId.equals(playerId))).get();
if (playerGroups.isEmpty) return [];
final groupIds = playerGroups.map((pg) => pg.groupId).toSet().toList();
final rows =
await (select(groupTable)
..where((g) => g.id.isIn(groupIds))
..orderBy([(g) => OrderingTerm.desc(g.createdAt)]))
.get();
return Future.wait(
rows.map((groupData) async {
final members = await db.playerGroupDao.getPlayersOfGroup(
groupId: groupData.id,
);
return Group(
id: groupData.id,
name: groupData.name,
description: groupData.description,
members: members,
createdAt: groupData.createdAt,
);
}),
);
}
/// Checks if a group with the given [groupId] exists in the database. /// Checks if a group with the given [groupId] exists in the database.
/// Returns `true` if the group exists, `false` otherwise. /// Returns `true` if the group exists, `false` otherwise.
Future<bool> groupExists({required String groupId}) async { Future<bool> groupExists({required String groupId}) async {

View File

@@ -30,7 +30,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
gameId: match.game.id, gameId: match.game.id,
groupId: Value(match.group?.id), groupId: Value(match.group?.id),
name: match.name, name: match.name,
isTeamMatch: Value(match.isTeamMatch),
notes: match.notes, notes: match.notes,
createdAt: match.createdAt, createdAt: match.createdAt,
endedAt: Value(match.endedAt), endedAt: Value(match.endedAt),
@@ -143,7 +142,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
gameId: match.game.id, gameId: match.game.id,
groupId: Value(match.group?.id), groupId: Value(match.group?.id),
name: match.name, name: match.name,
isTeamMatch: Value(match.isTeamMatch),
notes: match.notes, notes: match.notes,
createdAt: match.createdAt, createdAt: match.createdAt,
endedAt: Value(match.endedAt), endedAt: Value(match.endedAt),
@@ -302,7 +300,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
group: group, group: group,
players: players, players: players,
teams: teams.isEmpty ? null : teams, teams: teams.isEmpty ? null : teams,
isTeamMatch: row.isTeamMatch,
notes: row.notes, notes: row.notes,
createdAt: row.createdAt, createdAt: row.createdAt,
endedAt: row.endedAt, endedAt: row.endedAt,
@@ -337,7 +334,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
group: group, group: group,
players: players, players: players,
teams: teams.isEmpty ? null : teams, teams: teams.isEmpty ? null : teams,
isTeamMatch: result.isTeamMatch,
notes: result.notes, notes: result.notes,
createdAt: result.createdAt, createdAt: result.createdAt,
endedAt: result.endedAt, endedAt: result.endedAt,
@@ -356,6 +352,53 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return count ?? 0; return count ?? 0;
} }
Future<List<Match>> getMatchesByPlayer({required String playerId}) async {
final playerMatches = await (select(
playerMatchTable,
)..where((pm) => pm.playerId.equals(playerId))).get();
if (playerMatches.isEmpty) return [];
final matchIds = playerMatches.map((pm) => pm.matchId).toSet().toList();
final rows =
await (select(matchTable)
..where((m) => m.id.isIn(matchIds))
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
.get();
return Future.wait(
rows.map((row) async {
final game = await db.gameDao.getGameById(gameId: row.gameId);
Group? group;
if (row.groupId != null) {
group = await db.groupDao.getGroupById(groupId: row.groupId!);
}
final players = await db.playerMatchDao.getPlayersOfMatch(
matchId: row.id,
);
final scores = await db.scoreEntryDao.getAllMatchScores(
matchId: row.id,
);
final teams = await _getMatchTeams(matchId: row.id);
return Match(
id: row.id,
name: row.name,
game: game,
group: group,
players: players,
teams: teams.isEmpty ? null : teams,
notes: row.notes,
createdAt: row.createdAt,
endedAt: row.endedAt,
scores: scores,
);
}),
);
}
/// Retrieves all matches associated with the given [groupId]. /// Retrieves all matches associated with the given [groupId].
/// Queries the database directly, filtering by [groupId]. /// Queries the database directly, filtering by [groupId].
Future<List<Match>> getMatchesByGroup({required String groupId}) async { Future<List<Match>> getMatchesByGroup({required String groupId}) async {
@@ -377,7 +420,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
group: group, group: group,
players: players, players: players,
teams: teams.isEmpty ? null : teams, teams: teams.isEmpty ? null : teams,
isTeamMatch: row.isTeamMatch,
notes: row.notes, notes: row.notes,
createdAt: row.createdAt, createdAt: row.createdAt,
endedAt: row.endedAt, endedAt: row.endedAt,
@@ -406,8 +448,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
teamIds.map((teamId) => db.teamDao.getTeamById(teamId: teamId)), teamIds.map((teamId) => db.teamDao.getTeamById(teamId: teamId)),
); );
return teams return teams;
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
} }
/* Update */ /* Update */

View File

@@ -17,7 +17,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// the new one. /// the new one.
Future<bool> addPlayer({required Player player}) async { Future<bool> addPlayer({required Player player}) async {
if (!await playerExists(playerId: player.id)) { if (!await playerExists(playerId: player.id)) {
final int nameCount = await calculateNameCount(name: player.name); final int nameCount = await _processNameCount(name: player.name);
await into(playerTable).insert( await into(playerTable).insert(
PlayerTableCompanion.insert( PlayerTableCompanion.insert(
@@ -64,7 +64,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
final playersWithName = entry.value; final playersWithName = entry.value;
// Get the current nameCount // Get the current nameCount
var nameCount = await calculateNameCount(name: name); var nameCount = await _processNameCount(name: name);
// One player with the same name // One player with the same name
if (playersWithName.length == 1) { if (playersWithName.length == 1) {
@@ -159,44 +159,63 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/* Update */ /* Update */
/// Updates the name of the player with the given [playerId] to [name]. /// Updates the name of the player with the given [playerId] to [name].
///
/// Keeps the `nameCount` values of the affected name groups consistent:
/// - The renamed player gets a fresh `nameCount` for the new name group.
/// - All players in the previous name group whose `nameCount` was greater
/// than the removed one get decremented by 1, so the numbering stays
/// contiguous (1..N) in `createdAt` order.
/// - If only one player remains in the previous name group, their
/// `nameCount` is reset to 0.
Future<bool> updatePlayerName({ Future<bool> updatePlayerName({
required String playerId, required String playerId,
required String name, required String name,
}) async { }) async {
// Get previous name and name count for the player before updating return transaction(() async {
final previousPlayerName = final previousPlayer = await (select(
await (select(playerTable)..where((p) => p.id.equals(playerId))) playerTable,
.map((row) => row.name) )..where((p) => p.id.equals(playerId))).getSingleOrNull();
.getSingleOrNull() ?? if (previousPlayer == null) return false;
'';
final previousNameCount = await getNameCount(name: previousPlayerName);
final rowsAffected = final previousName = previousPlayer.name;
await (update(playerTable)..where((p) => p.id.equals(playerId))).write( final previousCount = previousPlayer.nameCount;
PlayerTableCompanion(name: Value(name)),
);
// Update name count for the new name // Determine the nameCount for the renamed player in the new group.
final count = await calculateNameCount(name: name); final newNameCount = await _processNameCount(name: name);
if (count > 0) {
await (update(playerTable)..where((p) => p.name.equals(name))).write(
PlayerTableCompanion(nameCount: Value(count)),
);
}
if (previousNameCount > 0) { final rowsAffected =
// Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount await (update(
final player = await getPlayerWithHighestNameCount( playerTable,
name: previousPlayerName, )..where((p) => p.id.equals(playerId))).write(
); PlayerTableCompanion(
if (player != null) { name: Value(name),
await updateNameCount( nameCount: Value(newNameCount),
playerId: player.id, ),
nameCount: previousNameCount, );
);
// Consolidate the previous name group.
final remainingCount = await getNameCount(name: previousName);
if (remainingCount == 1) {
// Only one player left
await (update(playerTable)..where((p) => p.name.equals(previousName)))
.write(const PlayerTableCompanion(nameCount: Value(0)));
} else if (remainingCount > 1 && previousCount > 0) {
// Shift every player above the gap down by one to keep numbering in order.
await (update(playerTable)..where(
(p) =>
p.name.equals(previousName) &
p.nameCount.isBiggerThanValue(previousCount),
))
.write(
PlayerTableCompanion.custom(
nameCount: playerTable.nameCount - const Constant(1),
),
);
} }
}
return rowsAffected > 0; return rowsAffected > 0;
});
} }
/// Updates the description of the player with the given [playerId] to /// Updates the description of the player with the given [playerId] to
@@ -226,6 +245,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/* Name count management */ /* Name count management */
/// Retrieves the count of players with the given [name]. /// Retrieves the count of players with the given [name].
/// Returns the highest name count if players with the same name exist,
/// otherwise `null`.
Future<int> getNameCount({required String name}) async { Future<int> getNameCount({required String name}) async {
final query = select(playerTable)..where((p) => p.name.equals(name)); final query = select(playerTable)..where((p) => p.name.equals(name));
final result = await query.get(); final result = await query.get();
@@ -264,25 +285,39 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
return null; return null;
} }
/// Processes the name count for a new player with the given [name].
///- 0 Player: returning 0
///- 1 Player: returning 2, and initializes the nameCount for the existing player to 1
///- Other: returning the existing count + 1
Future<int> _processNameCount({required String name}) async {
final nameCount = await calculateNameCount(name: name);
if (nameCount == 2) {
// If one other player exists with the same name, initialize the nameCount
await initializeNameCount(name: name);
}
return nameCount;
}
@visibleForTesting @visibleForTesting
/// Calculates the name count for a new player with the given [name].
/// - 0 Players: Name count is 0
/// - 1 Player: Name count is 2 (since the existing player will be 1)
/// - Other: Name count is the existing count + 1
Future<int> calculateNameCount({required String name}) async { Future<int> calculateNameCount({required String name}) async {
final count = await getNameCount(name: name); final count = await getNameCount(name: name);
final int nameCount; final int nameCount;
if (count == 1) { if (count == 0) {
// If one other player exists with the same name, initialize the nameCount // If no other players exist with the same name, the returned nameCount is 0
await initializeNameCount(name: name); nameCount = 0;
// And for the new player, set nameCount to 2 } else if (count == 1) {
// If one other player with the name count exists, the returned name count is 2
nameCount = 2; nameCount = 2;
} else if (count > 1) { } else {
// If more than one player exists with the same name, just increment // If more than one player exists with the same name, just increment
// the nameCount for the new player // the nameCount for the new player
nameCount = count + 1; nameCount = count + 1;
} else {
// If no other players exist with the same name, set nameCount to 0
nameCount = 0;
} }
return nameCount; return nameCount;
} }

View File

@@ -74,8 +74,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
(row) => db.playerDao.getPlayerById(playerId: row.playerId), (row) => db.playerDao.getPlayerById(playerId: row.playerId),
); );
final players = await Future.wait(futures); final players = await Future.wait(futures);
return players return players;
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
} }
/// Retrieves a list of [Player]s associated with a specific team in a match. /// Retrieves a list of [Player]s associated with a specific team in a match.

View File

@@ -16,12 +16,12 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
/* Create */ /* Create */
/// Adds a score entry to the database. /// Adds a score entry to the database.
Future<bool> addScore({ Future<void> addScore({
required String playerId, required String playerId,
required String matchId, required String matchId,
required ScoreEntry entry, required ScoreEntry entry,
}) async { }) async {
final rowsAffected = await into(scoreEntryTable).insert( await into(scoreEntryTable).insert(
ScoreEntryTableCompanion.insert( ScoreEntryTableCompanion.insert(
playerId: playerId, playerId: playerId,
matchId: matchId, matchId: matchId,
@@ -31,8 +31,6 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
), ),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
); );
return rowsAffected > 0;
} }
Future<void> addScoresAsList({ Future<void> addScoresAsList({

View File

@@ -1,10 +1,8 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:tallee/data/db/tables/team_table.dart'; import 'package:tallee/data/db/tables/team_table.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
import 'package:tallee/data/models/team.dart'; import 'package:tallee/data/models/team.dart';
part 'team_dao.g.dart'; part 'team_dao.g.dart';
@@ -24,8 +22,6 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: team.id, id: team.id,
name: team.name, name: team.name,
createdAt: team.createdAt, createdAt: team.createdAt,
color: Value(team.color.name),
score: Value(team.score),
), ),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
); );
@@ -60,8 +56,6 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: team.id, id: team.id,
name: team.name, name: team.name,
createdAt: team.createdAt, createdAt: team.createdAt,
color: Value(team.color.name),
score: Value(team.score),
), ),
) )
.toList(), .toList(),
@@ -116,32 +110,12 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: row.id, id: row.id,
name: row.name, name: row.name,
createdAt: row.createdAt, createdAt: row.createdAt,
color: AppColor.values.byName(row.color),
score: row.score,
members: members, members: members,
); );
}), }),
); );
} }
Future<List<Team>> getTeamsByMatchId({required String matchId}) async {
final playerMatchQuery = select(db.playerMatchTable)
..where((pm) => pm.matchId.equals(matchId));
final playerMatches = await playerMatchQuery.get();
if (playerMatches.isEmpty) return [];
final teamIds = playerMatches
.map((pm) => pm.teamId)
.whereType<String>()
.toSet();
final teams = await Future.wait(
teamIds.map((id) => getTeamById(teamId: id)),
);
return teams;
}
/// Retrieves a [Team] by its [teamId], including its members. /// Retrieves a [Team] by its [teamId], including its members.
Future<Team> getTeamById({required String teamId}) async { Future<Team> getTeamById({required String teamId}) async {
final query = select(teamTable)..where((t) => t.id.equals(teamId)); final query = select(teamTable)..where((t) => t.id.equals(teamId));
@@ -151,8 +125,6 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: result.id, id: result.id,
name: result.name, name: result.name,
createdAt: result.createdAt, createdAt: result.createdAt,
color: AppColor.values.byName(result.color),
score: result.score,
members: members, members: members,
); );
} }
@@ -173,8 +145,7 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
final players = await Future.wait( final players = await Future.wait(
playerIds.map((id) => db.playerDao.getPlayerById(playerId: id)), playerIds.map((id) => db.playerDao.getPlayerById(playerId: id)),
); );
return players return players;
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
} }
/* Update */ /* Update */
@@ -191,81 +162,6 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
return rowsAffected > 0; return rowsAffected > 0;
} }
/// Updates the color of the team with the given [teamId].
Future<bool> updateTeamColor({
required String teamId,
required AppColor color,
}) async {
final rowsAffected =
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
TeamTableCompanion(color: Value(color.name)),
);
return rowsAffected > 0;
}
/// Updates the score of the team with the given [teamId].
/// Updates the member scores correspondingly
Future<bool> updateTeamScore({
required String teamId,
required String matchId,
required int score,
}) async {
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
const TeamTableCompanion(score: Value(null)),
);
await _deleteAllScoresForMembersOfTeam(teamId: teamId, matchId: matchId);
final rowsAffected =
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
TeamTableCompanion(score: Value(score)),
);
final members = await _getTeamMembers(teamId: teamId);
for (final member in members) {
await db.scoreEntryDao.addScore(
playerId: member.id,
matchId: matchId,
entry: ScoreEntry(score: score),
);
}
return rowsAffected > 0;
}
Future<bool> removeScoreForTeam({
required String teamId,
required String matchId,
}) async {
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
const TeamTableCompanion(score: Value(null)),
);
await _deleteAllScoresForMembersOfTeam(teamId: teamId, matchId: matchId);
return true;
}
/// Removes the scores for all teams in the match with the given [matchId] by setting their scores to null.
Future<bool> removeAllTeamScores({required String matchId}) async {
// collect all teamIds for the given matchId from playerMatchTable
final teamIds =
await (selectOnly(playerMatchTable)
..addColumns([playerMatchTable.teamId])
..where(playerMatchTable.matchId.equals(matchId)))
.map((row) => row.read(playerMatchTable.teamId))
.get();
// filter null or duplicates
final filteredTeamIds = teamIds.whereType<String>().toSet().toList();
var rowsAffected = 0;
if (filteredTeamIds.isNotEmpty) {
rowsAffected =
await (update(teamTable)..where((t) => t.id.isIn(filteredTeamIds)))
.write(const TeamTableCompanion(score: Value(null)));
}
await db.scoreEntryDao.deleteAllScoresForMatch(matchId: matchId);
return rowsAffected > 0;
}
/* Delete */ /* Delete */
/// Deletes all teams from the database. /// Deletes all teams from the database.
@@ -283,92 +179,4 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
/* Score handling */
/// Sets the team with the given [teamId] as the winner of the match with the given [matchId] by assigning a score of 1.
/// Returns `true` if the score was updated successfully, `false` otherwise.
Future<bool> setWinnerTeam({
required String teamId,
required String matchId,
}) async {
return await updateTeamScore(teamId: teamId, matchId: matchId, score: 1);
}
/// Sets multiple teams as winners of the match with the given [matchId] by assigning a score of 1 to each team.
/// Returns `true` if all scores were updated successfully, `false` otherwise.
Future<bool> setWinnerTeams({
required List<Team> winners,
required String matchId,
}) async {
// Reset all team scores .
await removeAllTeamScores(matchId: matchId);
// Reset all score entries
for (final team in winners) {
await _deleteAllScoresForMembersOfTeam(teamId: team.id, matchId: matchId);
}
for (final team in winners) {
await updateTeamScore(teamId: team.id, matchId: matchId, score: 1);
}
return true;
}
/// Removes the winner status from all Teams with the given [matchId] by setting its score to null.
/// Returns `true` if the score was updated successfully, `false` otherwise.
Future<bool> removeWinnerTeam({required String matchId}) async {
return await removeAllTeamScores(matchId: matchId);
}
/// Sets the team with the given [teamId] as the loser of the match with the given [matchId] by assigning a score of 0.
/// Returns `true` if the score was updated successfully, `false` otherwise.
Future<bool> setLoserTeam({
required String teamId,
required String matchId,
}) async {
return await updateTeamScore(teamId: teamId, matchId: matchId, score: 0);
}
/// Removes the loser from the match with the given [matchId] by setting its score to null.
/// Returns `true` if the score was updated successfully, `false` otherwise.
Future<bool> removeLoserTeam({required String matchId}) async {
return await removeAllTeamScores(matchId: matchId);
}
/// Sets the placements for the teams in the match with the given [matchId] by assigning scores based on their order in the [teams] list.
/// Returns `true` if all scores were updated successfully, `false` otherwise.
Future<bool> setTeamPlacements({
required String matchId,
required List<Team> teams,
}) async {
List<bool?> success = List.generate(teams.length, (index) => null);
for (int i = 0; i < teams.length; i++) {
success[i] = await updateTeamScore(
matchId: matchId,
teamId: teams[i].id,
score: teams.length - i,
);
}
return success.every((result) => result == true);
}
/// Helper method to delete all scores for members of a team in a specific match.
Future<bool> _deleteAllScoresForMembersOfTeam({
required String teamId,
required String matchId,
}) async {
final playerMatchQuery = select(db.playerMatchTable)
..where((pm) => pm.teamId.equals(teamId) & pm.matchId.equals(matchId));
final playerMatches = await playerMatchQuery.get();
if (playerMatches.isEmpty) return false;
for (final pm in playerMatches) {
await db.scoreEntryDao.deleteAllScoresForPlayerInMatch(
playerId: pm.playerId,
matchId: matchId,
);
}
return true;
}
} }

View File

@@ -1185,21 +1185,6 @@ class $MatchTableTable extends MatchTable
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: true, requiredDuringInsert: true,
); );
static const VerificationMeta _isTeamMatchMeta = const VerificationMeta(
'isTeamMatch',
);
@override
late final GeneratedColumn<bool> isTeamMatch = GeneratedColumn<bool>(
'is_team_match',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("is_team_match" IN (0, 1))',
),
defaultValue: const Constant(false),
);
static const VerificationMeta _notesMeta = const VerificationMeta('notes'); static const VerificationMeta _notesMeta = const VerificationMeta('notes');
@override @override
late final GeneratedColumn<String> notes = GeneratedColumn<String>( late final GeneratedColumn<String> notes = GeneratedColumn<String>(
@@ -1237,7 +1222,6 @@ class $MatchTableTable extends MatchTable
gameId, gameId,
groupId, groupId,
name, name,
isTeamMatch,
notes, notes,
createdAt, createdAt,
endedAt, endedAt,
@@ -1281,15 +1265,6 @@ class $MatchTableTable extends MatchTable
} else if (isInserting) { } else if (isInserting) {
context.missing(_nameMeta); context.missing(_nameMeta);
} }
if (data.containsKey('is_team_match')) {
context.handle(
_isTeamMatchMeta,
isTeamMatch.isAcceptableOrUnknown(
data['is_team_match']!,
_isTeamMatchMeta,
),
);
}
if (data.containsKey('notes')) { if (data.containsKey('notes')) {
context.handle( context.handle(
_notesMeta, _notesMeta,
@@ -1337,10 +1312,6 @@ class $MatchTableTable extends MatchTable
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}name'], data['${effectivePrefix}name'],
)!, )!,
isTeamMatch: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}is_team_match'],
)!,
notes: attachedDatabase.typeMapping.read( notes: attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}notes'], data['${effectivePrefix}notes'],
@@ -1367,7 +1338,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
final String gameId; final String gameId;
final String? groupId; final String? groupId;
final String name; final String name;
final bool isTeamMatch;
final String notes; final String notes;
final DateTime createdAt; final DateTime createdAt;
final DateTime? endedAt; final DateTime? endedAt;
@@ -1376,7 +1346,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
required this.gameId, required this.gameId,
this.groupId, this.groupId,
required this.name, required this.name,
required this.isTeamMatch,
required this.notes, required this.notes,
required this.createdAt, required this.createdAt,
this.endedAt, this.endedAt,
@@ -1390,7 +1359,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
map['group_id'] = Variable<String>(groupId); map['group_id'] = Variable<String>(groupId);
} }
map['name'] = Variable<String>(name); map['name'] = Variable<String>(name);
map['is_team_match'] = Variable<bool>(isTeamMatch);
map['notes'] = Variable<String>(notes); map['notes'] = Variable<String>(notes);
map['created_at'] = Variable<DateTime>(createdAt); map['created_at'] = Variable<DateTime>(createdAt);
if (!nullToAbsent || endedAt != null) { if (!nullToAbsent || endedAt != null) {
@@ -1407,7 +1375,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
? const Value.absent() ? const Value.absent()
: Value(groupId), : Value(groupId),
name: Value(name), name: Value(name),
isTeamMatch: Value(isTeamMatch),
notes: Value(notes), notes: Value(notes),
createdAt: Value(createdAt), createdAt: Value(createdAt),
endedAt: endedAt == null && nullToAbsent endedAt: endedAt == null && nullToAbsent
@@ -1426,7 +1393,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
gameId: serializer.fromJson<String>(json['gameId']), gameId: serializer.fromJson<String>(json['gameId']),
groupId: serializer.fromJson<String?>(json['groupId']), groupId: serializer.fromJson<String?>(json['groupId']),
name: serializer.fromJson<String>(json['name']), name: serializer.fromJson<String>(json['name']),
isTeamMatch: serializer.fromJson<bool>(json['isTeamMatch']),
notes: serializer.fromJson<String>(json['notes']), notes: serializer.fromJson<String>(json['notes']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']), createdAt: serializer.fromJson<DateTime>(json['createdAt']),
endedAt: serializer.fromJson<DateTime?>(json['endedAt']), endedAt: serializer.fromJson<DateTime?>(json['endedAt']),
@@ -1440,7 +1406,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
'gameId': serializer.toJson<String>(gameId), 'gameId': serializer.toJson<String>(gameId),
'groupId': serializer.toJson<String?>(groupId), 'groupId': serializer.toJson<String?>(groupId),
'name': serializer.toJson<String>(name), 'name': serializer.toJson<String>(name),
'isTeamMatch': serializer.toJson<bool>(isTeamMatch),
'notes': serializer.toJson<String>(notes), 'notes': serializer.toJson<String>(notes),
'createdAt': serializer.toJson<DateTime>(createdAt), 'createdAt': serializer.toJson<DateTime>(createdAt),
'endedAt': serializer.toJson<DateTime?>(endedAt), 'endedAt': serializer.toJson<DateTime?>(endedAt),
@@ -1452,7 +1417,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
String? gameId, String? gameId,
Value<String?> groupId = const Value.absent(), Value<String?> groupId = const Value.absent(),
String? name, String? name,
bool? isTeamMatch,
String? notes, String? notes,
DateTime? createdAt, DateTime? createdAt,
Value<DateTime?> endedAt = const Value.absent(), Value<DateTime?> endedAt = const Value.absent(),
@@ -1461,7 +1425,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
gameId: gameId ?? this.gameId, gameId: gameId ?? this.gameId,
groupId: groupId.present ? groupId.value : this.groupId, groupId: groupId.present ? groupId.value : this.groupId,
name: name ?? this.name, name: name ?? this.name,
isTeamMatch: isTeamMatch ?? this.isTeamMatch,
notes: notes ?? this.notes, notes: notes ?? this.notes,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
endedAt: endedAt.present ? endedAt.value : this.endedAt, endedAt: endedAt.present ? endedAt.value : this.endedAt,
@@ -1472,9 +1435,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
gameId: data.gameId.present ? data.gameId.value : this.gameId, gameId: data.gameId.present ? data.gameId.value : this.gameId,
groupId: data.groupId.present ? data.groupId.value : this.groupId, groupId: data.groupId.present ? data.groupId.value : this.groupId,
name: data.name.present ? data.name.value : this.name, name: data.name.present ? data.name.value : this.name,
isTeamMatch: data.isTeamMatch.present
? data.isTeamMatch.value
: this.isTeamMatch,
notes: data.notes.present ? data.notes.value : this.notes, notes: data.notes.present ? data.notes.value : this.notes,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
endedAt: data.endedAt.present ? data.endedAt.value : this.endedAt, endedAt: data.endedAt.present ? data.endedAt.value : this.endedAt,
@@ -1488,7 +1448,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
..write('gameId: $gameId, ') ..write('gameId: $gameId, ')
..write('groupId: $groupId, ') ..write('groupId: $groupId, ')
..write('name: $name, ') ..write('name: $name, ')
..write('isTeamMatch: $isTeamMatch, ')
..write('notes: $notes, ') ..write('notes: $notes, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('endedAt: $endedAt') ..write('endedAt: $endedAt')
@@ -1497,16 +1456,8 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
} }
@override @override
int get hashCode => Object.hash( int get hashCode =>
id, Object.hash(id, gameId, groupId, name, notes, createdAt, endedAt);
gameId,
groupId,
name,
isTeamMatch,
notes,
createdAt,
endedAt,
);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@@ -1515,7 +1466,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
other.gameId == this.gameId && other.gameId == this.gameId &&
other.groupId == this.groupId && other.groupId == this.groupId &&
other.name == this.name && other.name == this.name &&
other.isTeamMatch == this.isTeamMatch &&
other.notes == this.notes && other.notes == this.notes &&
other.createdAt == this.createdAt && other.createdAt == this.createdAt &&
other.endedAt == this.endedAt); other.endedAt == this.endedAt);
@@ -1526,7 +1476,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
final Value<String> gameId; final Value<String> gameId;
final Value<String?> groupId; final Value<String?> groupId;
final Value<String> name; final Value<String> name;
final Value<bool> isTeamMatch;
final Value<String> notes; final Value<String> notes;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
final Value<DateTime?> endedAt; final Value<DateTime?> endedAt;
@@ -1536,7 +1485,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
this.gameId = const Value.absent(), this.gameId = const Value.absent(),
this.groupId = const Value.absent(), this.groupId = const Value.absent(),
this.name = const Value.absent(), this.name = const Value.absent(),
this.isTeamMatch = const Value.absent(),
this.notes = const Value.absent(), this.notes = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.endedAt = const Value.absent(), this.endedAt = const Value.absent(),
@@ -1547,7 +1495,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
required String gameId, required String gameId,
this.groupId = const Value.absent(), this.groupId = const Value.absent(),
required String name, required String name,
this.isTeamMatch = const Value.absent(),
required String notes, required String notes,
required DateTime createdAt, required DateTime createdAt,
this.endedAt = const Value.absent(), this.endedAt = const Value.absent(),
@@ -1562,7 +1509,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
Expression<String>? gameId, Expression<String>? gameId,
Expression<String>? groupId, Expression<String>? groupId,
Expression<String>? name, Expression<String>? name,
Expression<bool>? isTeamMatch,
Expression<String>? notes, Expression<String>? notes,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
Expression<DateTime>? endedAt, Expression<DateTime>? endedAt,
@@ -1573,7 +1519,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
if (gameId != null) 'game_id': gameId, if (gameId != null) 'game_id': gameId,
if (groupId != null) 'group_id': groupId, if (groupId != null) 'group_id': groupId,
if (name != null) 'name': name, if (name != null) 'name': name,
if (isTeamMatch != null) 'is_team_match': isTeamMatch,
if (notes != null) 'notes': notes, if (notes != null) 'notes': notes,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
if (endedAt != null) 'ended_at': endedAt, if (endedAt != null) 'ended_at': endedAt,
@@ -1586,7 +1531,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
Value<String>? gameId, Value<String>? gameId,
Value<String?>? groupId, Value<String?>? groupId,
Value<String>? name, Value<String>? name,
Value<bool>? isTeamMatch,
Value<String>? notes, Value<String>? notes,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
Value<DateTime?>? endedAt, Value<DateTime?>? endedAt,
@@ -1597,7 +1541,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
gameId: gameId ?? this.gameId, gameId: gameId ?? this.gameId,
groupId: groupId ?? this.groupId, groupId: groupId ?? this.groupId,
name: name ?? this.name, name: name ?? this.name,
isTeamMatch: isTeamMatch ?? this.isTeamMatch,
notes: notes ?? this.notes, notes: notes ?? this.notes,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
endedAt: endedAt ?? this.endedAt, endedAt: endedAt ?? this.endedAt,
@@ -1620,9 +1563,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
if (name.present) { if (name.present) {
map['name'] = Variable<String>(name.value); map['name'] = Variable<String>(name.value);
} }
if (isTeamMatch.present) {
map['is_team_match'] = Variable<bool>(isTeamMatch.value);
}
if (notes.present) { if (notes.present) {
map['notes'] = Variable<String>(notes.value); map['notes'] = Variable<String>(notes.value);
} }
@@ -1645,7 +1585,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
..write('gameId: $gameId, ') ..write('gameId: $gameId, ')
..write('groupId: $groupId, ') ..write('groupId: $groupId, ')
..write('name: $name, ') ..write('name: $name, ')
..write('isTeamMatch: $isTeamMatch, ')
..write('notes: $notes, ') ..write('notes: $notes, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('endedAt: $endedAt, ') ..write('endedAt: $endedAt, ')
@@ -1915,27 +1854,8 @@ class $TeamTableTable extends TeamTable
type: DriftSqlType.dateTime, type: DriftSqlType.dateTime,
requiredDuringInsert: true, requiredDuringInsert: true,
); );
static const VerificationMeta _colorMeta = const VerificationMeta('color');
@override @override
late final GeneratedColumn<String> color = GeneratedColumn<String>( List<GeneratedColumn> get $columns => [id, name, createdAt];
'color',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: false,
defaultValue: const Constant('blue'),
);
static const VerificationMeta _scoreMeta = const VerificationMeta('score');
@override
late final GeneratedColumn<int> score = GeneratedColumn<int>(
'score',
aliasedName,
true,
type: DriftSqlType.int,
requiredDuringInsert: false,
);
@override
List<GeneratedColumn> get $columns => [id, name, createdAt, color, score];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
@@ -1969,18 +1889,6 @@ class $TeamTableTable extends TeamTable
} else if (isInserting) { } else if (isInserting) {
context.missing(_createdAtMeta); context.missing(_createdAtMeta);
} }
if (data.containsKey('color')) {
context.handle(
_colorMeta,
color.isAcceptableOrUnknown(data['color']!, _colorMeta),
);
}
if (data.containsKey('score')) {
context.handle(
_scoreMeta,
score.isAcceptableOrUnknown(data['score']!, _scoreMeta),
);
}
return context; return context;
} }
@@ -2002,14 +1910,6 @@ class $TeamTableTable extends TeamTable
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}created_at'], data['${effectivePrefix}created_at'],
)!, )!,
color: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}color'],
)!,
score: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}score'],
),
); );
} }
@@ -2023,14 +1923,10 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
final String id; final String id;
final String name; final String name;
final DateTime createdAt; final DateTime createdAt;
final String color;
final int? score;
const TeamTableData({ const TeamTableData({
required this.id, required this.id,
required this.name, required this.name,
required this.createdAt, required this.createdAt,
required this.color,
this.score,
}); });
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
@@ -2038,10 +1934,6 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
map['id'] = Variable<String>(id); map['id'] = Variable<String>(id);
map['name'] = Variable<String>(name); map['name'] = Variable<String>(name);
map['created_at'] = Variable<DateTime>(createdAt); map['created_at'] = Variable<DateTime>(createdAt);
map['color'] = Variable<String>(color);
if (!nullToAbsent || score != null) {
map['score'] = Variable<int>(score);
}
return map; return map;
} }
@@ -2050,10 +1942,6 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
id: Value(id), id: Value(id),
name: Value(name), name: Value(name),
createdAt: Value(createdAt), createdAt: Value(createdAt),
color: Value(color),
score: score == null && nullToAbsent
? const Value.absent()
: Value(score),
); );
} }
@@ -2066,8 +1954,6 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
id: serializer.fromJson<String>(json['id']), id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']), name: serializer.fromJson<String>(json['name']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']), createdAt: serializer.fromJson<DateTime>(json['createdAt']),
color: serializer.fromJson<String>(json['color']),
score: serializer.fromJson<int?>(json['score']),
); );
} }
@override @override
@@ -2077,31 +1963,20 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
'id': serializer.toJson<String>(id), 'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name), 'name': serializer.toJson<String>(name),
'createdAt': serializer.toJson<DateTime>(createdAt), 'createdAt': serializer.toJson<DateTime>(createdAt),
'color': serializer.toJson<String>(color),
'score': serializer.toJson<int?>(score),
}; };
} }
TeamTableData copyWith({ TeamTableData copyWith({String? id, String? name, DateTime? createdAt}) =>
String? id, TeamTableData(
String? name, id: id ?? this.id,
DateTime? createdAt, name: name ?? this.name,
String? color, createdAt: createdAt ?? this.createdAt,
Value<int?> score = const Value.absent(), );
}) => TeamTableData(
id: id ?? this.id,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
color: color ?? this.color,
score: score.present ? score.value : this.score,
);
TeamTableData copyWithCompanion(TeamTableCompanion data) { TeamTableData copyWithCompanion(TeamTableCompanion data) {
return TeamTableData( return TeamTableData(
id: data.id.present ? data.id.value : this.id, id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name, name: data.name.present ? data.name.value : this.name,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
color: data.color.present ? data.color.value : this.color,
score: data.score.present ? data.score.value : this.score,
); );
} }
@@ -2110,47 +1985,37 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
return (StringBuffer('TeamTableData(') return (StringBuffer('TeamTableData(')
..write('id: $id, ') ..write('id: $id, ')
..write('name: $name, ') ..write('name: $name, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt')
..write('color: $color, ')
..write('score: $score')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => Object.hash(id, name, createdAt, color, score); int get hashCode => Object.hash(id, name, createdAt);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
(other is TeamTableData && (other is TeamTableData &&
other.id == this.id && other.id == this.id &&
other.name == this.name && other.name == this.name &&
other.createdAt == this.createdAt && other.createdAt == this.createdAt);
other.color == this.color &&
other.score == this.score);
} }
class TeamTableCompanion extends UpdateCompanion<TeamTableData> { class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
final Value<String> id; final Value<String> id;
final Value<String> name; final Value<String> name;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
final Value<String> color;
final Value<int?> score;
final Value<int> rowid; final Value<int> rowid;
const TeamTableCompanion({ const TeamTableCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
this.name = const Value.absent(), this.name = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.color = const Value.absent(),
this.score = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}); });
TeamTableCompanion.insert({ TeamTableCompanion.insert({
required String id, required String id,
required String name, required String name,
required DateTime createdAt, required DateTime createdAt,
this.color = const Value.absent(),
this.score = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : id = Value(id), }) : id = Value(id),
name = Value(name), name = Value(name),
@@ -2159,16 +2024,12 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
Expression<String>? id, Expression<String>? id,
Expression<String>? name, Expression<String>? name,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
Expression<String>? color,
Expression<int>? score,
Expression<int>? rowid, Expression<int>? rowid,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
if (id != null) 'id': id, if (id != null) 'id': id,
if (name != null) 'name': name, if (name != null) 'name': name,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
if (color != null) 'color': color,
if (score != null) 'score': score,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
}); });
} }
@@ -2177,16 +2038,12 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
Value<String>? id, Value<String>? id,
Value<String>? name, Value<String>? name,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
Value<String>? color,
Value<int?>? score,
Value<int>? rowid, Value<int>? rowid,
}) { }) {
return TeamTableCompanion( return TeamTableCompanion(
id: id ?? this.id, id: id ?? this.id,
name: name ?? this.name, name: name ?? this.name,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
color: color ?? this.color,
score: score ?? this.score,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
); );
} }
@@ -2203,12 +2060,6 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
if (createdAt.present) { if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value); map['created_at'] = Variable<DateTime>(createdAt.value);
} }
if (color.present) {
map['color'] = Variable<String>(color.value);
}
if (score.present) {
map['score'] = Variable<int>(score.value);
}
if (rowid.present) { if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value); map['rowid'] = Variable<int>(rowid.value);
} }
@@ -2221,8 +2072,6 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
..write('id: $id, ') ..write('id: $id, ')
..write('name: $name, ') ..write('name: $name, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('color: $color, ')
..write('score: $score, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
..write(')')) ..write(')'))
.toString(); .toString();
@@ -4243,7 +4092,6 @@ typedef $$MatchTableTableCreateCompanionBuilder =
required String gameId, required String gameId,
Value<String?> groupId, Value<String?> groupId,
required String name, required String name,
Value<bool> isTeamMatch,
required String notes, required String notes,
required DateTime createdAt, required DateTime createdAt,
Value<DateTime?> endedAt, Value<DateTime?> endedAt,
@@ -4255,7 +4103,6 @@ typedef $$MatchTableTableUpdateCompanionBuilder =
Value<String> gameId, Value<String> gameId,
Value<String?> groupId, Value<String?> groupId,
Value<String> name, Value<String> name,
Value<bool> isTeamMatch,
Value<String> notes, Value<String> notes,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<DateTime?> endedAt, Value<DateTime?> endedAt,
@@ -4368,11 +4215,6 @@ class $$MatchTableTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<bool> get isTeamMatch => $composableBuilder(
column: $table.isTeamMatch,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get notes => $composableBuilder( ColumnFilters<String> get notes => $composableBuilder(
column: $table.notes, column: $table.notes,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@@ -4504,11 +4346,6 @@ class $$MatchTableTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<bool> get isTeamMatch => $composableBuilder(
column: $table.isTeamMatch,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get notes => $composableBuilder( ColumnOrderings<String> get notes => $composableBuilder(
column: $table.notes, column: $table.notes,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@@ -4586,11 +4423,6 @@ class $$MatchTableTableAnnotationComposer
GeneratedColumn<String> get name => GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column); $composableBuilder(column: $table.name, builder: (column) => column);
GeneratedColumn<bool> get isTeamMatch => $composableBuilder(
column: $table.isTeamMatch,
builder: (column) => column,
);
GeneratedColumn<String> get notes => GeneratedColumn<String> get notes =>
$composableBuilder(column: $table.notes, builder: (column) => column); $composableBuilder(column: $table.notes, builder: (column) => column);
@@ -4734,7 +4566,6 @@ class $$MatchTableTableTableManager
Value<String> gameId = const Value.absent(), Value<String> gameId = const Value.absent(),
Value<String?> groupId = const Value.absent(), Value<String?> groupId = const Value.absent(),
Value<String> name = const Value.absent(), Value<String> name = const Value.absent(),
Value<bool> isTeamMatch = const Value.absent(),
Value<String> notes = const Value.absent(), Value<String> notes = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<DateTime?> endedAt = const Value.absent(), Value<DateTime?> endedAt = const Value.absent(),
@@ -4744,7 +4575,6 @@ class $$MatchTableTableTableManager
gameId: gameId, gameId: gameId,
groupId: groupId, groupId: groupId,
name: name, name: name,
isTeamMatch: isTeamMatch,
notes: notes, notes: notes,
createdAt: createdAt, createdAt: createdAt,
endedAt: endedAt, endedAt: endedAt,
@@ -4756,7 +4586,6 @@ class $$MatchTableTableTableManager
required String gameId, required String gameId,
Value<String?> groupId = const Value.absent(), Value<String?> groupId = const Value.absent(),
required String name, required String name,
Value<bool> isTeamMatch = const Value.absent(),
required String notes, required String notes,
required DateTime createdAt, required DateTime createdAt,
Value<DateTime?> endedAt = const Value.absent(), Value<DateTime?> endedAt = const Value.absent(),
@@ -4766,7 +4595,6 @@ class $$MatchTableTableTableManager
gameId: gameId, gameId: gameId,
groupId: groupId, groupId: groupId,
name: name, name: name,
isTeamMatch: isTeamMatch,
notes: notes, notes: notes,
createdAt: createdAt, createdAt: createdAt,
endedAt: endedAt, endedAt: endedAt,
@@ -5281,8 +5109,6 @@ typedef $$TeamTableTableCreateCompanionBuilder =
required String id, required String id,
required String name, required String name,
required DateTime createdAt, required DateTime createdAt,
Value<String> color,
Value<int?> score,
Value<int> rowid, Value<int> rowid,
}); });
typedef $$TeamTableTableUpdateCompanionBuilder = typedef $$TeamTableTableUpdateCompanionBuilder =
@@ -5290,8 +5116,6 @@ typedef $$TeamTableTableUpdateCompanionBuilder =
Value<String> id, Value<String> id,
Value<String> name, Value<String> name,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<String> color,
Value<int?> score,
Value<int> rowid, Value<int> rowid,
}); });
@@ -5347,16 +5171,6 @@ class $$TeamTableTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<String> get color => $composableBuilder(
column: $table.color,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get score => $composableBuilder(
column: $table.score,
builder: (column) => ColumnFilters(column),
);
Expression<bool> playerMatchTableRefs( Expression<bool> playerMatchTableRefs(
Expression<bool> Function($$PlayerMatchTableTableFilterComposer f) f, Expression<bool> Function($$PlayerMatchTableTableFilterComposer f) f,
) { ) {
@@ -5406,16 +5220,6 @@ class $$TeamTableTableOrderingComposer
column: $table.createdAt, column: $table.createdAt,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<String> get color => $composableBuilder(
column: $table.color,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get score => $composableBuilder(
column: $table.score,
builder: (column) => ColumnOrderings(column),
);
} }
class $$TeamTableTableAnnotationComposer class $$TeamTableTableAnnotationComposer
@@ -5436,12 +5240,6 @@ class $$TeamTableTableAnnotationComposer
GeneratedColumn<DateTime> get createdAt => GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column); $composableBuilder(column: $table.createdAt, builder: (column) => column);
GeneratedColumn<String> get color =>
$composableBuilder(column: $table.color, builder: (column) => column);
GeneratedColumn<int> get score =>
$composableBuilder(column: $table.score, builder: (column) => column);
Expression<T> playerMatchTableRefs<T extends Object>( Expression<T> playerMatchTableRefs<T extends Object>(
Expression<T> Function($$PlayerMatchTableTableAnnotationComposer a) f, Expression<T> Function($$PlayerMatchTableTableAnnotationComposer a) f,
) { ) {
@@ -5499,15 +5297,11 @@ class $$TeamTableTableTableManager
Value<String> id = const Value.absent(), Value<String> id = const Value.absent(),
Value<String> name = const Value.absent(), Value<String> name = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<String> color = const Value.absent(),
Value<int?> score = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => TeamTableCompanion( }) => TeamTableCompanion(
id: id, id: id,
name: name, name: name,
createdAt: createdAt, createdAt: createdAt,
color: color,
score: score,
rowid: rowid, rowid: rowid,
), ),
createCompanionCallback: createCompanionCallback:
@@ -5515,15 +5309,11 @@ class $$TeamTableTableTableManager
required String id, required String id,
required String name, required String name,
required DateTime createdAt, required DateTime createdAt,
Value<String> color = const Value.absent(),
Value<int?> score = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => TeamTableCompanion.insert( }) => TeamTableCompanion.insert(
id: id, id: id,
name: name, name: name,
createdAt: createdAt, createdAt: createdAt,
color: color,
score: score,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0

View File

@@ -12,7 +12,6 @@ class MatchTable extends Table {
.references(GroupTable, #id, onDelete: KeyAction.setNull) .references(GroupTable, #id, onDelete: KeyAction.setNull)
.nullable()(); .nullable()();
TextColumn get name => text()(); TextColumn get name => text()();
BoolColumn get isTeamMatch => boolean().withDefault(const Constant(false))();
TextColumn get notes => text()(); TextColumn get notes => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get endedAt => dateTime().nullable()(); DateTimeColumn get endedAt => dateTime().nullable()();

View File

@@ -4,8 +4,6 @@ class TeamTable extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get name => text()(); TextColumn get name => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
TextColumn get color => text().withDefault(const Constant('blue'))();
IntColumn get score => integer().nullable()();
@override @override
Set<Column<Object>> get primaryKey => {id}; Set<Column<Object>> get primaryKey => {id};

View File

@@ -8,13 +8,13 @@ class Game {
final String name; final String name;
final Ruleset ruleset; final Ruleset ruleset;
final String description; final String description;
final AppColor color; final GameColor color;
final String icon; final String icon;
Game({ Game({
required this.name, required this.name,
required this.ruleset, required this.ruleset,
this.color = AppColor.orange, this.color = GameColor.orange,
this.description = '', this.description = '',
this.icon = '', this.icon = '',
String? id, String? id,
@@ -33,7 +33,7 @@ class Game {
String? name, String? name,
Ruleset? ruleset, Ruleset? ruleset,
String? description, String? description,
AppColor? color, GameColor? color,
String? icon, String? icon,
}) { }) {
return Game( return Game(
@@ -73,10 +73,7 @@ class Game {
orElse: () => Ruleset.singleWinner, orElse: () => Ruleset.singleWinner,
), ),
description = json['description'], description = json['description'],
color = AppColor.values.firstWhere( color = GameColor.values.firstWhere((e) => e.name == json['color']),
(e) => e.name == json['color'],
orElse: () => AppColor.orange,
),
icon = json['icon']; icon = json['icon'];
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {

View File

@@ -16,10 +16,9 @@ class Match {
final Game game; final Game game;
final Group? group; final Group? group;
final List<Player> players; final List<Player> players;
final bool isTeamMatch;
final List<Team>? teams; final List<Team>? teams;
final String notes; final String notes;
final Map<String, ScoreEntry?> scores; Map<String, ScoreEntry?> scores;
Match({ Match({
required this.name, required this.name,
@@ -27,7 +26,6 @@ class Match {
required this.players, required this.players,
this.endedAt, this.endedAt,
this.group, this.group,
this.isTeamMatch = false,
this.teams, this.teams,
this.notes = '', this.notes = '',
String? id, String? id,
@@ -39,7 +37,7 @@ class Match {
@override @override
String toString() { String toString() {
return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, isTeamMatch: $isTeamMatch, teams: $teams, notes: $notes, scores: $scores, mvp: $mvp}'; return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, mvp: $mvp}';
} }
Match copyWith({ Match copyWith({
@@ -50,7 +48,6 @@ class Match {
Game? game, Game? game,
Group? group, Group? group,
List<Player>? players, List<Player>? players,
bool? isTeamMatch,
List<Team>? teams, List<Team>? teams,
String? notes, String? notes,
Map<String, ScoreEntry?>? scores, Map<String, ScoreEntry?>? scores,
@@ -63,7 +60,6 @@ class Match {
game: game ?? this.game, game: game ?? this.game,
group: group ?? this.group, group: group ?? this.group,
players: players ?? this.players, players: players ?? this.players,
isTeamMatch: isTeamMatch ?? this.isTeamMatch,
teams: teams ?? this.teams, teams: teams ?? this.teams,
notes: notes ?? this.notes, notes: notes ?? this.notes,
scores: scores ?? this.scores, scores: scores ?? this.scores,
@@ -82,7 +78,6 @@ class Match {
game == other.game && game == other.game &&
group == other.group && group == other.group &&
const DeepCollectionEquality().equals(players, other.players) && const DeepCollectionEquality().equals(players, other.players) &&
isTeamMatch == other.isTeamMatch &&
const DeepCollectionEquality().equals(teams, other.teams) && const DeepCollectionEquality().equals(teams, other.teams) &&
notes == other.notes && notes == other.notes &&
const DeepCollectionEquality().equals(scores, other.scores); const DeepCollectionEquality().equals(scores, other.scores);
@@ -96,7 +91,6 @@ class Match {
game, game,
group, group,
const DeepCollectionEquality().hash(players), const DeepCollectionEquality().hash(players),
isTeamMatch,
const DeepCollectionEquality().hash(teams), const DeepCollectionEquality().hash(teams),
notes, notes,
const DeepCollectionEquality().hash(scores), const DeepCollectionEquality().hash(scores),
@@ -113,12 +107,11 @@ class Match {
name: '', name: '',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: '', description: '',
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
), ),
group = null, group = null,
players = [], players = [],
isTeamMatch = json['isTeamMatch'],
teams = [], teams = [],
scores = json['scores'] != null scores = json['scores'] != null
? (json['scores'] as Map<String, dynamic>).map( ? (json['scores'] as Map<String, dynamic>).map(
@@ -140,13 +133,11 @@ class Match {
'gameId': game.id, 'gameId': game.id,
'groupId': group?.id, 'groupId': group?.id,
'playerIds': players.map((player) => player.id).toList(), 'playerIds': players.map((player) => player.id).toList(),
'isTeamMatch': isTeamMatch,
'teams': teams?.map((team) => team.toJson()).toList(), 'teams': teams?.map((team) => team.toJson()).toList(),
'scores': scores.map((key, value) => MapEntry(key, value?.toJson())), 'scores': scores.map((key, value) => MapEntry(key, value?.toJson())),
'notes': notes, 'notes': notes,
}; };
// Most Valuable Player(s) based on the match's ruleset
List<Player> get mvp { List<Player> get mvp {
if (players.isEmpty || scores.isEmpty) return []; if (players.isEmpty || scores.isEmpty) return [];
@@ -204,59 +195,4 @@ class Match {
return playerScore.score == lowestScore; return playerScore.score == lowestScore;
}).toList(); }).toList();
} }
// MVP for team-based matches (Most Valuable Team)
List<Team> get mvt {
if (teams == null || teams!.isEmpty) return [];
switch (game.ruleset) {
case Ruleset.highestScore:
return _getHighestScoreTeam();
case Ruleset.lowestScore:
return _getLowestScoreTeam();
case Ruleset.singleWinner:
return _getHighestScoreTeam().take(1).toList();
case Ruleset.singleLoser:
return _getLowestScoreTeam().take(1).toList();
case Ruleset.multipleWinners:
return _getHighestScoreTeam();
case Ruleset.placement:
return _getHighestScoreTeam().take(1).toList();
}
}
List<Team> _getHighestScoreTeam() {
if (teams!.every((team) => team.score == null)) {
return [];
}
final int highestScore = teams!
.map((team) => team.score)
.whereType<int>()
.reduce((max, score) => score > max ? score : max);
return teams!.where((team) {
return team.score == highestScore;
}).toList();
}
List<Team> _getLowestScoreTeam() {
if (teams!.every((team) => team.score == null)) {
return [];
}
final int lowestScore = teams!
.map((team) => team.score)
.whereType<int>()
.reduce((min, score) => score < min ? score : min);
return teams!.where((team) {
return team.score == lowestScore;
}).toList();
}
} }

View File

@@ -1,6 +1,5 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@@ -8,39 +7,31 @@ class Team {
final String id; final String id;
final String name; final String name;
final DateTime createdAt; final DateTime createdAt;
final AppColor color;
final int? score;
final List<Player> members; final List<Player> members;
Team({ Team({
String? id, String? id,
required this.name, required this.name,
DateTime? createdAt, DateTime? createdAt,
this.color = AppColor.blue,
this.score,
required this.members, required this.members,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(); createdAt = createdAt ?? clock.now();
@override @override
String toString() { String toString() {
return 'Team{id: $id, name: $name, color: $color, score: $score, members: $members}'; return 'Team{id: $id, name: $name, members: $members}';
} }
Team copyWith({ Team copyWith({
String? id, String? id,
String? name, String? name,
DateTime? createdAt, DateTime? createdAt,
AppColor? color,
int? score,
List<Player>? members, List<Player>? members,
}) { }) {
return Team( return Team(
id: id ?? this.id, id: id ?? this.id,
name: name ?? this.name, name: name ?? this.name,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
color: color ?? this.color,
score: score ?? this.score,
members: members ?? this.members, members: members ?? this.members,
); );
} }
@@ -53,8 +44,6 @@ class Team {
id == other.id && id == other.id &&
name == other.name && name == other.name &&
createdAt == other.createdAt && createdAt == other.createdAt &&
color == other.color &&
score == other.score &&
const DeepCollectionEquality().equals(members, other.members); const DeepCollectionEquality().equals(members, other.members);
@override @override
@@ -62,8 +51,6 @@ class Team {
id, id,
name, name,
createdAt, createdAt,
color,
score,
const DeepCollectionEquality().hash(members), const DeepCollectionEquality().hash(members),
); );
@@ -71,19 +58,12 @@ class Team {
: id = json['id'], : id = json['id'],
name = json['name'], name = json['name'],
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
color = AppColor.values.firstWhere(
(e) => e.name == json['color'],
orElse: () => AppColor.orange,
),
score = json['score'] ?? 0,
members = []; // Populated during import via DataTransferService members = []; // Populated during import via DataTransferService
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'name': name, 'name': name,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'color': color.name,
'score': score,
'memberIds': members.map((member) => member.id).toList(), 'memberIds': members.map((member) => member.id).toList(),
}; };
} }

View File

@@ -1,6 +1,5 @@
{ {
"@@locale": "de", "@@locale": "de",
"add_team": "Team hinzufügen",
"all_players": "Alle Spieler:innen", "all_players": "Alle Spieler:innen",
"all_players_selected": "Alle Spieler:innen ausgewählt", "all_players_selected": "Alle Spieler:innen ausgewählt",
"amount_of_matches": "Anzahl der Spiele", "amount_of_matches": "Anzahl der Spiele",
@@ -20,13 +19,13 @@
"color_red": "Rot", "color_red": "Rot",
"color_teal": "Türkis", "color_teal": "Türkis",
"color_yellow": "Gelb", "color_yellow": "Gelb",
"confirm": "Bestätigen",
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
"create_game": "Spielvorlage erstellen", "create_game": "Spielvorlage erstellen",
"create_group": "Gruppe erstellen", "create_group": "Gruppe erstellen",
"create_match": "Spiel erstellen", "create_match": "Spiel erstellen",
"create_new_group": "Neue Gruppe erstellen", "create_new_group": "Neue Gruppe erstellen",
"create_new_match": "Neues Spiel erstellen", "create_new_match": "Neues Spiel erstellen",
"create_teams": "Teams erstellen",
"created_on": "Erstellt am", "created_on": "Erstellt am",
"data": "Daten", "data": "Daten",
"data_successfully_deleted": "Daten erfolgreich gelöscht", "data_successfully_deleted": "Daten erfolgreich gelöscht",
@@ -46,11 +45,14 @@
}, },
"delete_group": "Gruppe löschen", "delete_group": "Gruppe löschen",
"delete_match": "Spiel löschen", "delete_match": "Spiel löschen",
"delete_player": "Spieler:in löschen",
"description": "Beschreibung", "description": "Beschreibung",
"drag_to_set_placement": "Ziehen um Platzierung zu setzen", "drag_to_set_placement": "Ziehen um Platzierung zu setzen",
"edit_game": "Spielvorlage bearbeiten", "edit_game": "Spielvorlage bearbeiten",
"edit_group": "Gruppe bearbeiten", "edit_group": "Gruppe bearbeiten",
"edit_match": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten",
"edit_name": "Name ändern",
"edit_player": "Spieler bearbeiten",
"enter_points": "Punkte eingeben", "enter_points": "Punkte eingeben",
"enter_results": "Ergebnisse eintragen", "enter_results": "Ergebnisse eintragen",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
@@ -68,6 +70,7 @@
"group_name": "Gruppenname", "group_name": "Gruppenname",
"group_profile": "Gruppenprofil", "group_profile": "Gruppenprofil",
"groups": "Gruppen", "groups": "Gruppen",
"groups_part_of": "Gruppen Teil von",
"highest_score": "Höchste Punkte", "highest_score": "Höchste Punkte",
"home": "Startseite", "home": "Startseite",
"import_canceled": "Import abgebrochen", "import_canceled": "Import abgebrochen",
@@ -81,12 +84,13 @@
"live_edit_mode": "Live-Bearbeitungsmodus", "live_edit_mode": "Live-Bearbeitungsmodus",
"loser": "Verlierer:in", "loser": "Verlierer:in",
"lowest_score": "Niedrigste Punkte", "lowest_score": "Niedrigste Punkte",
"manage_members": "Mitglieder bearbeiten",
"match_in_progress": "Spiel läuft...", "match_in_progress": "Spiel läuft...",
"match_name": "Spieltitel", "match_name": "Spieltitel",
"match_profile": "Spielprofil", "match_profile": "Spielprofil",
"matches": "Spiele", "matches": "Spiele",
"member": "Mitglied", "matches_part_of": "Spiele Teil von",
"matches_played": "Spiele gespielt",
"matches_won": "Spiele gewonnen",
"members": "Mitglieder", "members": "Mitglieder",
"most_points": "Höchste Punkte", "most_points": "Höchste Punkte",
"multiple_winners": "Mehrere Gewinner:innen", "multiple_winners": "Mehrere Gewinner:innen",
@@ -96,7 +100,7 @@
"no_license_text_available": "Kein Lizenztext verfügbar", "no_license_text_available": "Kein Lizenztext verfügbar",
"no_licenses_found": "Keine Lizenzen gefunden", "no_licenses_found": "Keine Lizenzen gefunden",
"no_matches_created_yet": "Noch keine Spiele erstellt", "no_matches_created_yet": "Noch keine Spiele erstellt",
"no_players_available": "Keine Spieler:innen verfügbar", "no_matches_played_yet": "Noch kein Spiel gespielt",
"no_players_created_yet": "Noch keine Spieler:in erstellt", "no_players_created_yet": "Noch keine Spieler:in erstellt",
"no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden", "no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
"no_players_selected": "Keine Spieler:innen ausgewählt", "no_players_selected": "Keine Spieler:innen ausgewählt",
@@ -104,21 +108,21 @@
"no_results_entered_yet": "Noch keine Ergebnisse eingetragen", "no_results_entered_yet": "Noch keine Ergebnisse eingetragen",
"no_second_match_available": "Kein zweites Spiel verfügbar", "no_second_match_available": "Kein zweites Spiel verfügbar",
"no_statistics_available": "Keine Statistiken verfügbar", "no_statistics_available": "Keine Statistiken verfügbar",
"no_teams_available": "Keine Teams verfügbar",
"none": "Kein", "none": "Kein",
"none_group": "Keine", "none_group": "Keine",
"not_available": "Nicht verfügbar", "not_available": "Nicht verfügbar",
"not_part_of_any_group": "Noch keiner Gruppe hinzugefügt",
"place": "Platz", "place": "Platz",
"placement": "Platzierung", "placement": "Platzierung",
"played_matches": "Gespielte Spiele", "played_matches": "Gespielte Spiele",
"player_name": "Spieler:innenname", "player_name": "Spieler:innenname",
"player_profile": "Spieler:in-Profil",
"players": "Spieler:innen", "players": "Spieler:innen",
"point": "Punkt", "point": "Punkt",
"points": "Punkte", "points": "Punkte",
"privacy_policy": "Datenschutzerklärung", "privacy_policy": "Datenschutzerklärung",
"quick_create": "Schnellzugriff", "quick_create": "Schnellzugriff",
"recent_matches": "Letzte Spiele", "recent_matches": "Letzte Spiele",
"redistribute": "Neu verteilen",
"result": "Ergebnis", "result": "Ergebnis",
"results": "Ergebnisse", "results": "Ergebnisse",
"ruleset": "Regelwerk", "ruleset": "Regelwerk",
@@ -134,15 +138,13 @@
"select_winner": "Gewinner:in wählen", "select_winner": "Gewinner:in wählen",
"select_winners": "Gewinner:innen wählen", "select_winners": "Gewinner:innen wählen",
"selected_players": "Ausgewählte Spieler:innen", "selected_players": "Ausgewählte Spieler:innen",
"set_name": "Name setzen",
"settings": "Einstellungen", "settings": "Einstellungen",
"single_loser": "Ein:e Verlierer:in", "single_loser": "Ein:e Verlierer:in",
"single_winner": "Ein:e Gewinner:in", "single_winner": "Ein:e Gewinner:in",
"statistics": "Statistiken", "statistics": "Statistiken",
"stats": "Statistiken", "stats": "Statistiken",
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
"team": "Team",
"team_match": "Teamspiel",
"teams": "Teams",
"there_are_no_games_matching_your_search": "Es gibt keine Spielvorlagen, die deiner Suche entspricht", "there_are_no_games_matching_your_search": "Es gibt keine Spielvorlagen, die deiner Suche entspricht",
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht",
"this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.",

View File

@@ -1,6 +1,5 @@
{ {
"@@locale": "en", "@@locale": "en",
"add_team": "Add Team",
"all_players": "All players", "all_players": "All players",
"all_players_selected": "All players selected", "all_players_selected": "All players selected",
"amount_of_matches": "Amount of Matches", "amount_of_matches": "Amount of Matches",
@@ -20,13 +19,13 @@
"color_red": "Red", "color_red": "Red",
"color_teal": "Teal", "color_teal": "Teal",
"color_yellow": "Yellow", "color_yellow": "Yellow",
"confirm": "Confirm",
"could_not_add_player": "Could not add player", "could_not_add_player": "Could not add player",
"create_game": "Create Game", "create_game": "Create Game",
"create_group": "Create Group", "create_group": "Create Group",
"create_match": "Create match", "create_match": "Create match",
"create_new_group": "Create new group", "create_new_group": "Create new group",
"create_new_match": "Create new match", "create_new_match": "Create new match",
"create_teams": "Create teams",
"created_on": "Created on", "created_on": "Created on",
"data": "Data", "data": "Data",
"data_successfully_deleted": "Data successfully deleted", "data_successfully_deleted": "Data successfully deleted",
@@ -46,11 +45,14 @@
}, },
"delete_group": "Delete Group", "delete_group": "Delete Group",
"delete_match": "Delete Match", "delete_match": "Delete Match",
"delete_player": "Delete player?",
"description": "Description", "description": "Description",
"drag_to_set_placement": "Drag to set placement", "drag_to_set_placement": "Drag to set placement",
"edit_game": "Edit Game", "edit_game": "Edit Game",
"edit_group": "Edit Group", "edit_group": "Edit Group",
"edit_match": "Edit Match", "edit_match": "Edit Match",
"edit_name": "Edit name",
"edit_player": "Edit player",
"enter_points": "Enter points", "enter_points": "Enter points",
"enter_results": "Enter Results", "enter_results": "Enter Results",
"error_creating_group": "Error while creating group, please try again", "error_creating_group": "Error while creating group, please try again",
@@ -68,6 +70,7 @@
"group_name": "Group name", "group_name": "Group name",
"group_profile": "Group Profile", "group_profile": "Group Profile",
"groups": "Groups", "groups": "Groups",
"groups_part_of": "Groups part of",
"highest_score": "Highest Score", "highest_score": "Highest Score",
"home": "Home", "home": "Home",
"import_canceled": "Import canceled", "import_canceled": "Import canceled",
@@ -81,12 +84,13 @@
"live_edit_mode": "Live Edit Mode", "live_edit_mode": "Live Edit Mode",
"loser": "Loser", "loser": "Loser",
"lowest_score": "Lowest Score", "lowest_score": "Lowest Score",
"manage_members": "Manage Members",
"match_in_progress": "Match in progress...", "match_in_progress": "Match in progress...",
"match_name": "Match name", "match_name": "Match name",
"match_profile": "Match Profile", "match_profile": "Match Profile",
"matches": "Matches", "matches": "Matches",
"member": "Member", "matches_part_of": "Matches part of",
"matches_played": "Matches played",
"matches_won": "Matches won",
"members": "Members", "members": "Members",
"most_points": "Most Points", "most_points": "Most Points",
"multiple_winners": "Multiple Winners", "multiple_winners": "Multiple Winners",
@@ -96,7 +100,7 @@
"no_license_text_available": "No license text available", "no_license_text_available": "No license text available",
"no_licenses_found": "No licenses found", "no_licenses_found": "No licenses found",
"no_matches_created_yet": "No matches created yet", "no_matches_created_yet": "No matches created yet",
"no_players_available": "No players available", "no_matches_played_yet": "No games played yet",
"no_players_created_yet": "No players created yet", "no_players_created_yet": "No players created yet",
"no_players_found_with_that_name": "No players found with that name", "no_players_found_with_that_name": "No players found with that name",
"no_players_selected": "No players selected", "no_players_selected": "No players selected",
@@ -104,21 +108,21 @@
"no_results_entered_yet": "No results entered yet", "no_results_entered_yet": "No results entered yet",
"no_second_match_available": "No second match available", "no_second_match_available": "No second match available",
"no_statistics_available": "No statistics available", "no_statistics_available": "No statistics available",
"no_teams_available": "No teams available",
"none": "None", "none": "None",
"none_group": "None", "none_group": "None",
"not_available": "Not available", "not_available": "Not available",
"not_part_of_any_group": "Not part of any group yet",
"place": "place", "place": "place",
"placement": "Placement", "placement": "Placement",
"played_matches": "Played Matches", "played_matches": "Played Matches",
"player_name": "Player name", "player_name": "Player name",
"player_profile": "Player Profile",
"players": "Players", "players": "Players",
"point": "Point", "point": "Point",
"points": "Points", "points": "Points",
"privacy_policy": "Privacy Policy", "privacy_policy": "Privacy Policy",
"quick_create": "Quick Create", "quick_create": "Quick Create",
"recent_matches": "Recent Matches", "recent_matches": "Recent Matches",
"redistribute": "Redistribute",
"results": "Results", "results": "Results",
"ruleset": "Ruleset", "ruleset": "Ruleset",
"ruleset_least_points": "Inverse scoring: the player with the fewest points wins.", "ruleset_least_points": "Inverse scoring: the player with the fewest points wins.",
@@ -133,6 +137,7 @@
"select_winner": "Select Winner", "select_winner": "Select Winner",
"select_winners": "Select Winners", "select_winners": "Select Winners",
"selected_players": "Selected players", "selected_players": "Selected players",
"set_name": "Set name",
"settings": "Settings", "settings": "Settings",
"single_loser": "Single Loser", "single_loser": "Single Loser",
"single_winner": "Single Winner", "single_winner": "Single Winner",
@@ -148,9 +153,6 @@
} }
} }
}, },
"team": "Team",
"team_match": "Team Match",
"teams": "Teams",
"there_are_no_games_matching_your_search": "There are no games matching your search", "there_are_no_games_matching_your_search": "There are no games matching your search",
"there_is_no_group_matching_your_search": "There is no group matching your search", "there_is_no_group_matching_your_search": "There is no group matching your search",
"this_cannot_be_undone": "This can't be undone.", "this_cannot_be_undone": "This can't be undone.",

View File

@@ -98,12 +98,6 @@ abstract class AppLocalizations {
Locale('en'), Locale('en'),
]; ];
/// No description provided for @add_team.
///
/// In en, this message translates to:
/// **'Add Team'**
String get add_team;
/// No description provided for @all_players. /// No description provided for @all_players.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -218,6 +212,12 @@ abstract class AppLocalizations {
/// **'Yellow'** /// **'Yellow'**
String get color_yellow; String get color_yellow;
/// No description provided for @confirm.
///
/// In en, this message translates to:
/// **'Confirm'**
String get confirm;
/// No description provided for @could_not_add_player. /// No description provided for @could_not_add_player.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -254,12 +254,6 @@ abstract class AppLocalizations {
/// **'Create new match'** /// **'Create new match'**
String get create_new_match; String get create_new_match;
/// No description provided for @create_teams.
///
/// In en, this message translates to:
/// **'Create teams'**
String get create_teams;
/// No description provided for @created_on. /// No description provided for @created_on.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -332,6 +326,12 @@ abstract class AppLocalizations {
/// **'Delete Match'** /// **'Delete Match'**
String get delete_match; String get delete_match;
/// No description provided for @delete_player.
///
/// In en, this message translates to:
/// **'Delete player?'**
String get delete_player;
/// No description provided for @description. /// No description provided for @description.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -362,6 +362,18 @@ abstract class AppLocalizations {
/// **'Edit Match'** /// **'Edit Match'**
String get edit_match; String get edit_match;
/// No description provided for @edit_name.
///
/// In en, this message translates to:
/// **'Edit name'**
String get edit_name;
/// No description provided for @edit_player.
///
/// In en, this message translates to:
/// **'Edit player'**
String get edit_player;
/// No description provided for @enter_points. /// No description provided for @enter_points.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -464,6 +476,12 @@ abstract class AppLocalizations {
/// **'Groups'** /// **'Groups'**
String get groups; String get groups;
/// No description provided for @groups_part_of.
///
/// In en, this message translates to:
/// **'Groups part of'**
String get groups_part_of;
/// No description provided for @highest_score. /// No description provided for @highest_score.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -542,12 +560,6 @@ abstract class AppLocalizations {
/// **'Lowest Score'** /// **'Lowest Score'**
String get lowest_score; String get lowest_score;
/// No description provided for @manage_members.
///
/// In en, this message translates to:
/// **'Manage Members'**
String get manage_members;
/// No description provided for @match_in_progress. /// No description provided for @match_in_progress.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -572,11 +584,23 @@ abstract class AppLocalizations {
/// **'Matches'** /// **'Matches'**
String get matches; String get matches;
/// No description provided for @member. /// No description provided for @matches_part_of.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Member'** /// **'Matches part of'**
String get member; String get matches_part_of;
/// No description provided for @matches_played.
///
/// In en, this message translates to:
/// **'Matches played'**
String get matches_played;
/// No description provided for @matches_won.
///
/// In en, this message translates to:
/// **'Matches won'**
String get matches_won;
/// No description provided for @members. /// No description provided for @members.
/// ///
@@ -632,11 +656,11 @@ abstract class AppLocalizations {
/// **'No matches created yet'** /// **'No matches created yet'**
String get no_matches_created_yet; String get no_matches_created_yet;
/// No description provided for @no_players_available. /// No description provided for @no_matches_played_yet.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'No players available'** /// **'No games played yet'**
String get no_players_available; String get no_matches_played_yet;
/// No description provided for @no_players_created_yet. /// No description provided for @no_players_created_yet.
/// ///
@@ -680,12 +704,6 @@ abstract class AppLocalizations {
/// **'No statistics available'** /// **'No statistics available'**
String get no_statistics_available; String get no_statistics_available;
/// No description provided for @no_teams_available.
///
/// In en, this message translates to:
/// **'No teams available'**
String get no_teams_available;
/// No description provided for @none. /// No description provided for @none.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -704,6 +722,12 @@ abstract class AppLocalizations {
/// **'Not available'** /// **'Not available'**
String get not_available; String get not_available;
/// No description provided for @not_part_of_any_group.
///
/// In en, this message translates to:
/// **'Not part of any group yet'**
String get not_part_of_any_group;
/// No description provided for @place. /// No description provided for @place.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -728,6 +752,12 @@ abstract class AppLocalizations {
/// **'Player name'** /// **'Player name'**
String get player_name; String get player_name;
/// No description provided for @player_profile.
///
/// In en, this message translates to:
/// **'Player Profile'**
String get player_profile;
/// No description provided for @players. /// No description provided for @players.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -764,12 +794,6 @@ abstract class AppLocalizations {
/// **'Recent Matches'** /// **'Recent Matches'**
String get recent_matches; String get recent_matches;
/// No description provided for @redistribute.
///
/// In en, this message translates to:
/// **'Redistribute'**
String get redistribute;
/// No description provided for @results. /// No description provided for @results.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -854,6 +878,12 @@ abstract class AppLocalizations {
/// **'Selected players'** /// **'Selected players'**
String get selected_players; String get selected_players;
/// No description provided for @set_name.
///
/// In en, this message translates to:
/// **'Set name'**
String get set_name;
/// No description provided for @settings. /// No description provided for @settings.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -890,24 +920,6 @@ abstract class AppLocalizations {
/// **'Successfully added player {playerName}'** /// **'Successfully added player {playerName}'**
String successfully_added_player(String playerName); String successfully_added_player(String playerName);
/// No description provided for @team.
///
/// In en, this message translates to:
/// **'Team'**
String get team;
/// No description provided for @team_match.
///
/// In en, this message translates to:
/// **'Team Match'**
String get team_match;
/// No description provided for @teams.
///
/// In en, this message translates to:
/// **'Teams'**
String get teams;
/// No description provided for @there_are_no_games_matching_your_search. /// No description provided for @there_are_no_games_matching_your_search.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -8,9 +8,6 @@ import 'app_localizations.dart';
class AppLocalizationsDe extends AppLocalizations { class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale); AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get add_team => 'Team hinzufügen';
@override @override
String get all_players => 'Alle Spieler:innen'; String get all_players => 'Alle Spieler:innen';
@@ -68,6 +65,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get color_yellow => 'Gelb'; String get color_yellow => 'Gelb';
@override
String get confirm => 'Bestätigen';
@override @override
String could_not_add_player(Object playerName) { String could_not_add_player(Object playerName) {
return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
@@ -88,9 +88,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get create_new_match => 'Neues Spiel erstellen'; String get create_new_match => 'Neues Spiel erstellen';
@override
String get create_teams => 'Teams erstellen';
@override @override
String get created_on => 'Erstellt am'; String get created_on => 'Erstellt am';
@@ -137,6 +134,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get delete_match => 'Spiel löschen'; String get delete_match => 'Spiel löschen';
@override
String get delete_player => 'Spieler:in löschen';
@override @override
String get description => 'Beschreibung'; String get description => 'Beschreibung';
@@ -152,6 +152,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get edit_match => 'Gruppe bearbeiten'; String get edit_match => 'Gruppe bearbeiten';
@override
String get edit_name => 'Name ändern';
@override
String get edit_player => 'Spieler bearbeiten';
@override @override
String get enter_points => 'Punkte eingeben'; String get enter_points => 'Punkte eingeben';
@@ -207,6 +213,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get groups => 'Gruppen'; String get groups => 'Gruppen';
@override
String get groups_part_of => 'Gruppen Teil von';
@override @override
String get highest_score => 'Höchste Punkte'; String get highest_score => 'Höchste Punkte';
@@ -246,9 +255,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get lowest_score => 'Niedrigste Punkte'; String get lowest_score => 'Niedrigste Punkte';
@override
String get manage_members => 'Mitglieder bearbeiten';
@override @override
String get match_in_progress => 'Spiel läuft...'; String get match_in_progress => 'Spiel läuft...';
@@ -262,7 +268,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get matches => 'Spiele'; String get matches => 'Spiele';
@override @override
String get member => 'Mitglied'; String get matches_part_of => 'Spiele Teil von';
@override
String get matches_played => 'Spiele gespielt';
@override
String get matches_won => 'Spiele gewonnen';
@override @override
String get members => 'Mitglieder'; String get members => 'Mitglieder';
@@ -292,7 +304,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get no_matches_created_yet => 'Noch keine Spiele erstellt'; String get no_matches_created_yet => 'Noch keine Spiele erstellt';
@override @override
String get no_players_available => 'Keine Spieler:innen verfügbar'; String get no_matches_played_yet => 'Noch kein Spiel gespielt';
@override @override
String get no_players_created_yet => 'Noch keine Spieler:in erstellt'; String get no_players_created_yet => 'Noch keine Spieler:in erstellt';
@@ -316,9 +328,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get no_statistics_available => 'Keine Statistiken verfügbar'; String get no_statistics_available => 'Keine Statistiken verfügbar';
@override
String get no_teams_available => 'Keine Teams verfügbar';
@override @override
String get none => 'Kein'; String get none => 'Kein';
@@ -328,6 +337,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get not_available => 'Nicht verfügbar'; String get not_available => 'Nicht verfügbar';
@override
String get not_part_of_any_group => 'Noch keiner Gruppe hinzugefügt';
@override @override
String get place => 'Platz'; String get place => 'Platz';
@@ -340,6 +352,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get player_name => 'Spieler:innenname'; String get player_name => 'Spieler:innenname';
@override
String get player_profile => 'Spieler:in-Profil';
@override @override
String get players => 'Spieler:innen'; String get players => 'Spieler:innen';
@@ -358,9 +373,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get recent_matches => 'Letzte Spiele'; String get recent_matches => 'Letzte Spiele';
@override
String get redistribute => 'Neu verteilen';
@override @override
String get results => 'Ergebnisse'; String get results => 'Ergebnisse';
@@ -408,6 +420,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get selected_players => 'Ausgewählte Spieler:innen'; String get selected_players => 'Ausgewählte Spieler:innen';
@override
String get set_name => 'Name setzen';
@override @override
String get settings => 'Einstellungen'; String get settings => 'Einstellungen';
@@ -428,15 +443,6 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Spieler:in $playerName erfolgreich hinzugefügt'; return 'Spieler:in $playerName erfolgreich hinzugefügt';
} }
@override
String get team => 'Team';
@override
String get team_match => 'Teamspiel';
@override
String get teams => 'Teams';
@override @override
String get there_are_no_games_matching_your_search => String get there_are_no_games_matching_your_search =>
'Es gibt keine Spielvorlagen, die deiner Suche entspricht'; 'Es gibt keine Spielvorlagen, die deiner Suche entspricht';

View File

@@ -8,9 +8,6 @@ import 'app_localizations.dart';
class AppLocalizationsEn extends AppLocalizations { class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale); AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get add_team => 'Add Team';
@override @override
String get all_players => 'All players'; String get all_players => 'All players';
@@ -68,6 +65,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get color_yellow => 'Yellow'; String get color_yellow => 'Yellow';
@override
String get confirm => 'Confirm';
@override @override
String could_not_add_player(Object playerName) { String could_not_add_player(Object playerName) {
return 'Could not add player'; return 'Could not add player';
@@ -88,9 +88,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get create_new_match => 'Create new match'; String get create_new_match => 'Create new match';
@override
String get create_teams => 'Create teams';
@override @override
String get created_on => 'Created on'; String get created_on => 'Created on';
@@ -137,6 +134,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get delete_match => 'Delete Match'; String get delete_match => 'Delete Match';
@override
String get delete_player => 'Delete player?';
@override @override
String get description => 'Description'; String get description => 'Description';
@@ -152,6 +152,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get edit_match => 'Edit Match'; String get edit_match => 'Edit Match';
@override
String get edit_name => 'Edit name';
@override
String get edit_player => 'Edit player';
@override @override
String get enter_points => 'Enter points'; String get enter_points => 'Enter points';
@@ -207,6 +213,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get groups => 'Groups'; String get groups => 'Groups';
@override
String get groups_part_of => 'Groups part of';
@override @override
String get highest_score => 'Highest Score'; String get highest_score => 'Highest Score';
@@ -246,9 +255,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get lowest_score => 'Lowest Score'; String get lowest_score => 'Lowest Score';
@override
String get manage_members => 'Manage Members';
@override @override
String get match_in_progress => 'Match in progress...'; String get match_in_progress => 'Match in progress...';
@@ -262,7 +268,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get matches => 'Matches'; String get matches => 'Matches';
@override @override
String get member => 'Member'; String get matches_part_of => 'Matches part of';
@override
String get matches_played => 'Matches played';
@override
String get matches_won => 'Matches won';
@override @override
String get members => 'Members'; String get members => 'Members';
@@ -292,7 +304,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get no_matches_created_yet => 'No matches created yet'; String get no_matches_created_yet => 'No matches created yet';
@override @override
String get no_players_available => 'No players available'; String get no_matches_played_yet => 'No games played yet';
@override @override
String get no_players_created_yet => 'No players created yet'; String get no_players_created_yet => 'No players created yet';
@@ -316,9 +328,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get no_statistics_available => 'No statistics available'; String get no_statistics_available => 'No statistics available';
@override
String get no_teams_available => 'No teams available';
@override @override
String get none => 'None'; String get none => 'None';
@@ -328,6 +337,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get not_available => 'Not available'; String get not_available => 'Not available';
@override
String get not_part_of_any_group => 'Not part of any group yet';
@override @override
String get place => 'place'; String get place => 'place';
@@ -340,6 +352,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get player_name => 'Player name'; String get player_name => 'Player name';
@override
String get player_profile => 'Player Profile';
@override @override
String get players => 'Players'; String get players => 'Players';
@@ -358,9 +373,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get recent_matches => 'Recent Matches'; String get recent_matches => 'Recent Matches';
@override
String get redistribute => 'Redistribute';
@override @override
String get results => 'Results'; String get results => 'Results';
@@ -408,6 +420,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get selected_players => 'Selected players'; String get selected_players => 'Selected players';
@override
String get set_name => 'Set name';
@override @override
String get settings => 'Settings'; String get settings => 'Settings';
@@ -428,15 +443,6 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Successfully added player $playerName'; return 'Successfully added player $playerName';
} }
@override
String get team => 'Team';
@override
String get team_match => 'Team Match';
@override
String get teams => 'Teams';
@override @override
String get there_are_no_games_matching_your_search => String get there_are_no_games_matching_your_search =>
'There are no games matching your search'; 'There are no games matching your search';

View File

@@ -8,7 +8,7 @@ import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/player_selection.dart'; import 'package:tallee/presentation/widgets/player_selection.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
@@ -89,6 +89,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
initialSelectedPlayers: initialSelectedPlayers, initialSelectedPlayers: initialSelectedPlayers,
onPlayerCreated: () => widget.onMembersChanged?.call(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectedPlayers = [...value]; selectedPlayers = [...value];
@@ -96,24 +97,19 @@ class _CreateGroupViewState extends State<CreateGroupView> {
}, },
), ),
), ),
Padding( CustomWidthButton(
padding: const EdgeInsets.symmetric(horizontal: 12), text: widget.groupToEdit == null
child: AnimatedDialogButton( ? loc.create_group
buttonConstraints: const BoxConstraints( : loc.edit_group,
minWidth: double.infinity, sizeRelativeToWidth: 0.95,
minHeight: 50, buttonType: ButtonType.primary,
), onPressed:
buttonText: widget.groupToEdit == null (_groupNameController.text.isEmpty ||
? loc.create_group (selectedPlayers.length < 2))
: loc.edit_group, ? null
buttonType: ButtonType.primary, : _saveGroup,
onPressed:
(_groupNameController.text.isEmpty ||
(selectedPlayers.length < 2))
? null
: _saveGroup,
),
), ),
const SizedBox(height: 20),
], ],
), ),
), ),
@@ -139,6 +135,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
if (!mounted) return; if (!mounted) return;
if (success) { if (success) {
widget.onMembersChanged?.call();
await HapticFeedback.successNotification(); await HapticFeedback.successNotification();
if (mounted) { if (mounted) {
Navigator.pop(context, updatedGroup); Navigator.pop(context, updatedGroup);
@@ -162,7 +159,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
final success = await db.groupDao.addGroup( final success = await db.groupDao.addGroup(
group: Group(name: groupName, members: selectedPlayers), group: Group(name: groupName, members: selectedPlayers),
); );
return success; return success;
} }

View File

@@ -150,6 +150,7 @@ class _GroupDetailViewState extends State<GroupDetailView> {
return TextIconTile( return TextIconTile(
text: member.name, text: member.name,
suffixText: getNameCountText(member), suffixText: getNameCountText(member),
iconEnabled: false,
); );
}).toList(), }).toList(),
), ),

View File

@@ -77,6 +77,7 @@ class _GroupViewState extends State<GroupView> {
); );
} }
return GroupTile( return GroupTile(
onPlayerChanged: loadGroups,
group: groups[index], group: groups[index],
onTap: () async { onTap: () async {
await Navigator.push( await Navigator.push(
@@ -106,13 +107,10 @@ class _GroupViewState extends State<GroupView> {
context, context,
adaptivePageRoute( adaptivePageRoute(
builder: (context) { builder: (context) {
return const CreateGroupView(); return CreateGroupView(onMembersChanged: loadGroups);
}, },
), ),
); );
setState(() {
loadGroups();
});
}, },
), ),
), ),

View File

@@ -51,9 +51,6 @@ class _ChooseGameViewState extends State<ChooseGameView> {
/// Games filtered according to the current search query /// Games filtered according to the current search query
late List<Game> filteredGames; late List<Game> filteredGames;
List<Game> get games =>
widget.games..sort((a, b) => a.name.compareTo(b.name));
@override @override
void initState() { void initState() {
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
@@ -62,7 +59,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
selectedGameId = widget.initialGameId; selectedGameId = widget.initialGameId;
// Start with all games visible // Start with all games visible
filteredGames = List<Game>.from(games); filteredGames = List<Game>.from(widget.games);
super.initState(); super.initState();
} }
@@ -80,7 +77,9 @@ class _ChooseGameViewState extends State<ChooseGameView> {
Navigator.of(context).pop( Navigator.of(context).pop(
selectedGameId == '' selectedGameId == ''
? null ? null
: games.firstWhere((game) => game.id == selectedGameId), : widget.games.firstWhere(
(game) => game.id == selectedGameId,
),
); );
}, },
), ),
@@ -100,7 +99,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
); );
if (result != null && result.game != null) { if (result != null && result.game != null) {
setState(() { setState(() {
games.insert(0, result.game); widget.games.insert(0, result.game);
}); });
_refreshFromSource(); _refreshFromSource();
} }
@@ -140,7 +139,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
child: Visibility( child: Visibility(
visible: filteredGames.isNotEmpty, visible: filteredGames.isNotEmpty,
replacement: Visibility( replacement: Visibility(
visible: games.isNotEmpty, visible: widget.games.isNotEmpty,
replacement: TopCenteredMessage( replacement: TopCenteredMessage(
icon: Icons.info, icon: Icons.info,
title: loc.info, title: loc.info,
@@ -161,7 +160,10 @@ class _ChooseGameViewState extends State<ChooseGameView> {
return GameTile( return GameTile(
title: game.name, title: game.name,
description: game.description, description: game.description,
subtitle: translateRulesetToString(game.ruleset, context), badgeText: translateRulesetToString(
game.ruleset,
context,
),
badgeColor: getColorFromGameColor(game.color), badgeColor: getColorFromGameColor(game.color),
isHighlighted: selectedGameId == game.id, isHighlighted: selectedGameId == game.id,
onTap: () async { onTap: () async {
@@ -188,7 +190,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
); );
if (result != null && result.game != null) { if (result != null && result.game != null) {
// Find the index in the original list to mutate // Find the index in the original list to mutate
final originalIndex = games.indexWhere( final originalIndex = widget.games.indexWhere(
(g) => g.id == game.id, (g) => g.id == game.id,
); );
if (originalIndex == -1) { if (originalIndex == -1) {
@@ -200,12 +202,12 @@ class _ChooseGameViewState extends State<ChooseGameView> {
if (selectedGameId == game.id) { if (selectedGameId == game.id) {
selectedGameId = ''; selectedGameId = '';
} }
games.removeAt(originalIndex); widget.games.removeAt(originalIndex);
widget.onGamesUpdated?.call(); widget.onGamesUpdated?.call();
}); });
} else { } else {
setState(() { setState(() {
games[originalIndex] = result.game; widget.games[originalIndex] = result.game;
}); });
} }
_refreshFromSource(); _refreshFromSource();
@@ -227,13 +229,13 @@ class _ChooseGameViewState extends State<ChooseGameView> {
final q = query.toLowerCase().trim(); final q = query.toLowerCase().trim();
if (q.isEmpty) { if (q.isEmpty) {
setState(() { setState(() {
filteredGames = List<Game>.from(games); filteredGames = List<Game>.from(widget.games);
}); });
return; return;
} }
setState(() { setState(() {
filteredGames = games.where((game) { filteredGames = widget.games.where((game) {
final name = game.name.toLowerCase(); final name = game.name.toLowerCase();
final description = game.description.toLowerCase(); final description = game.description.toLowerCase();
return name.contains(q) || description.contains(q); return name.contains(q) || description.contains(q);

View File

@@ -49,10 +49,10 @@ class _CreateGameViewState extends State<CreateGameView> {
late final AppDatabase db; late final AppDatabase db;
late List<(Ruleset, String)> _rulesets; late List<(Ruleset, String)> _rulesets;
late List<(AppColor, String)> _colors; late List<(GameColor, String)> _colors;
Ruleset? selectedRuleset = Ruleset.singleWinner; Ruleset? selectedRuleset = Ruleset.singleWinner;
AppColor? selectedColor = AppColor.orange; GameColor? selectedColor = GameColor.orange;
/// Controller for the game name input field. /// Controller for the game name input field.
final _gameNameController = TextEditingController(); final _gameNameController = TextEditingController();
@@ -87,10 +87,10 @@ class _CreateGameViewState extends State<CreateGameView> {
), ),
); );
_colors = List.generate( _colors = List.generate(
AppColor.values.length, GameColor.values.length,
(index) => ( (index) => (
AppColor.values[index], GameColor.values[index],
translateGameColorToString(AppColor.values[index], context), translateGameColorToString(GameColor.values[index], context),
), ),
); );

View File

@@ -12,9 +12,8 @@ import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_game_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_game_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_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/custom_width_button.dart';
import 'package:tallee/presentation/widgets/player_selection.dart'; import 'package:tallee/presentation/widgets/player_selection.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
import 'package:tallee/presentation/widgets/tiles/choose_tile.dart'; import 'package:tallee/presentation/widgets/tiles/choose_tile.dart';
@@ -60,7 +59,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
Group? selectedGroup; Group? selectedGroup;
Game? selectedGame; Game? selectedGame;
bool isTeamMatch = false;
List<Player> selectedPlayers = []; List<Player> selectedPlayers = [];
/// GlobalKey for ScaffoldMessenger to show snackbars /// GlobalKey for ScaffoldMessenger to show snackbars
@@ -137,7 +135,24 @@ class _CreateMatchViewState extends State<CreateMatchView> {
trailing: selectedGame == null trailing: selectedGame == null
? Text(loc.none_group) ? Text(loc.none_group)
: Text(selectedGame!.name), : Text(selectedGame!.name),
onPressed: () async => await onChoosingGame(), onPressed: () async {
selectedGame = await Navigator.of(context).push(
adaptivePageRoute(
builder: (context) => ChooseGameView(
games: gamesList,
initialGameId: selectedGame?.id ?? '',
onGamesUpdated: widget.onMatchesUpdated,
),
),
);
setState(() {
if (selectedGame != null) {
hintText = selectedGame!.name;
} else {
hintText = loc.match_name;
}
});
},
), ),
// Group selection tile. // Group selection tile.
@@ -146,25 +161,42 @@ class _CreateMatchViewState extends State<CreateMatchView> {
trailing: selectedGroup == null trailing: selectedGroup == null
? Text(loc.none_group) ? Text(loc.none_group)
: Text(selectedGroup!.name), : Text(selectedGroup!.name),
onPressed: () async => onChoosingGroup(), onPressed: () async {
), // Remove all players from the previously selected group from
// the selected players list, in case the user deselects the
// group or selects a different group.
selectedPlayers.removeWhere(
(player) =>
selectedGroup?.members.any(
(member) => member.id == player.id,
) ??
false,
);
selectedGroup = await Navigator.of(context).push(
adaptivePageRoute(
builder: (context) => ChooseGroupView(
groups: groupsList,
initialGroupId: selectedGroup?.id ?? '',
),
),
);
if (!isEditMode()) setState(() {
ChooseTile( if (selectedGroup != null) {
title: loc.team_match, setState(() {
trailing: Switch.adaptive( selectedPlayers += [...selectedGroup!.members];
activeTrackColor: CustomTheme.primaryColor, });
padding: const EdgeInsets.symmetric(vertical: -15), }
value: isTeamMatch, });
onChanged: (value) => setState(() => isTeamMatch = value), },
), ),
),
// Player selection widget. // Player selection widget.
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'), key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialSelectedPlayers: selectedPlayers, initialSelectedPlayers: selectedPlayers,
onPlayerCreated: () => widget.onMatchesUpdated?.call(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectedPlayers = value; selectedPlayers = value;
@@ -175,21 +207,15 @@ class _CreateMatchViewState extends State<CreateMatchView> {
), ),
// Create or save button. // Create or save button.
Padding( CustomWidthButton(
padding: const EdgeInsets.symmetric(horizontal: 12), text: buttonText,
child: AnimatedDialogButton( sizeRelativeToWidth: 0.95,
buttonConstraints: const BoxConstraints( buttonType: ButtonType.primary,
minWidth: double.infinity, onPressed: _enableCreateGameButton()
minHeight: 50, ? () {
), buttonNavigation(context);
buttonType: ButtonType.primary, }
onPressed: isSubmitButtonEnabled() : null,
? () {
submitButtonNavigation(context);
}
: null,
buttonText: buttonText,
),
), ),
], ],
), ),
@@ -202,86 +228,12 @@ class _CreateMatchViewState extends State<CreateMatchView> {
return widget.matchToEdit != null; return widget.matchToEdit != null;
} }
// 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;
selectedGame = match.game;
if (match.group != null) {
selectedGroup = match.group;
}
}
Future<void> onChoosingGame() async {
selectedGame = await Navigator.of(context).push(
adaptivePageRoute(
builder: (context) => ChooseGameView(
games: gamesList,
initialGameId: selectedGame?.id ?? '',
onGamesUpdated: widget.onMatchesUpdated,
),
),
);
setState(() {
if (selectedGame != null) {
hintText = selectedGame!.name;
} else {
hintText = AppLocalizations.of(context).match_name;
}
});
}
Future<void> onChoosingGroup() async {
// Remove all players from the previously selected group from
// the selected players list, in case the user deselects the
// group or selects a different group.
selectedPlayers.removeWhere(
(player) =>
selectedGroup?.members.any((member) => member.id == player.id) ??
false,
);
selectedGroup = await Navigator.of(context).push(
adaptivePageRoute(
builder: (context) => ChooseGroupView(
groups: groupsList,
initialGroupId: selectedGroup?.id ?? '',
),
),
);
setState(() {
if (selectedGroup != null) {
setState(() {
selectedPlayers += [...selectedGroup!.members];
});
}
});
}
// If none of the selected players are from the currently selected group,
// the group is also deselected.
Future<void> removeGroupWhenNoMemberLeft() async {
if (selectedGroup == null) return;
if (!selectedPlayers.any(
(player) =>
selectedGroup!.members.any((member) => member.id == player.id),
)) {
setState(() {
selectedGroup = null;
});
}
}
/// Determines whether the "Create Match" button should be enabled. /// Determines whether the "Create Match" button should be enabled.
/// ///
/// Returns `true` if: /// Returns `true` if:
/// - A game is selected AND /// - A game is selected AND
/// - Either a group is selected OR at least 2 players are selected. /// - Either a group is selected OR at least 2 players are selected.
bool isSubmitButtonEnabled() { bool _enableCreateGameButton() {
return ((selectedGroup != null || selectedPlayers.length > 1) && return ((selectedGroup != null || selectedPlayers.length > 1) &&
selectedGame != null); selectedGame != null);
} }
@@ -290,35 +242,20 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// ///
/// If a match is being edited, updates the match in the database. /// If a match is being edited, updates the match in the database.
/// Otherwise, creates a new match and navigates to the MatchResultView. /// Otherwise, creates a new match and navigates to the MatchResultView.
void submitButtonNavigation(BuildContext context) async { void buttonNavigation(BuildContext context) async {
if (isEditMode()) { if (isEditMode()) {
await updateMatch(); await updateMatch();
if (context.mounted) { if (context.mounted) {
Navigator.pop(context); Navigator.pop(context);
} }
}
final match = await createMatch();
if (isTeamMatch) {
if (context.mounted) {
Navigator.push(
context,
adaptivePageRoute(
fullscreenDialog: !isTeamMatch,
builder: (context) => CreateTeamsView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
),
),
);
}
} else { } else {
final match = await createMatch();
if (context.mounted) { if (context.mounted) {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
adaptivePageRoute( adaptivePageRoute(
fullscreenDialog: !isTeamMatch, fullscreenDialog: true,
builder: (context) => MatchResultView( builder: (context) => MatchResultView(
match: match, match: match,
onWinnerChanged: widget.onWinnerChanged, onWinnerChanged: widget.onWinnerChanged,
@@ -391,12 +328,36 @@ class _CreateMatchViewState extends State<CreateMatchView> {
createdAt: DateTime.now(), createdAt: DateTime.now(),
group: selectedGroup, group: selectedGroup,
players: selectedPlayers, players: selectedPlayers,
isTeamMatch: isTeamMatch,
game: selectedGame!, game: selectedGame!,
); );
await db.matchDao.addMatch(match: match);
// Team matches are saved in OrganizeTeamsView
if (!isTeamMatch) await db.matchDao.addMatch(match: match);
return match; return match;
} }
// 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;
selectedGame = match.game;
if (match.group != null) {
selectedGroup = match.group;
}
}
// If none of the selected players are from the currently selected group,
// the group is also deselected.
Future<void> removeGroupWhenNoMemberLeft() async {
if (selectedGroup == null) return;
if (!selectedPlayers.any(
(player) =>
selectedGroup!.members.any((member) => member.id == player.id),
)) {
setState(() {
selectedGroup = null;
});
}
}
} }

View File

@@ -1,190 +0,0 @@
import 'dart:math';
import 'package:flutter/material.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/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart';
class CreateTeamsView extends StatefulWidget {
const CreateTeamsView({super.key, required this.match, this.onWinnerChanged});
final Match match;
final VoidCallback? onWinnerChanged;
@override
State<CreateTeamsView> createState() => _CreateTeamsViewState();
}
class _CreateTeamsViewState extends State<CreateTeamsView> {
final Random random = Random();
List<Player> get matchPlayers => widget.match.players;
late List<Team> teams;
late List<TextEditingController> nameController;
final int initialTeamCount = 2;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final loc = AppLocalizations.of(context);
// Init the teams
teams = List.generate(
initialTeamCount,
(index) => Team(
name: '${loc.team} ${index + 1}',
color: getTeamColor(index),
members: [],
),
);
// Init the controllers
nameController = teams.map(getNewController).toList();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.create_teams)),
body: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: ListView.builder(
padding: const EdgeInsets.only(top: 12, bottom: 96),
itemCount: teams.length,
itemBuilder: (context, index) {
return TeamCreationTile(
color: teams[index].color,
controller: nameController[index],
hintText: '${loc.team} ${index + 1}',
onDelete: teams.length <= 2 ? null : () => removeTeam(index),
onColorSelection: (color) {
setState(() {
teams[index] = teams[index].copyWith(color: color);
});
},
);
},
),
),
// Button row
Positioned(
bottom: MediaQuery.paddingOf(context).bottom + 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Add new team
MainMenuButton(
icon: Icons.add,
text: loc.add_team,
onPressed: teams.length >= widget.match.players.length
? null
: addTeam,
),
const SizedBox(width: 15),
// Confirm teams
MainMenuButton(
icon: Icons.arrow_forward_sharp,
onPressed: teams.length >= 2
? () {
final match = widget.match.copyWith(teams: teams);
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => ManageMembersView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
),
),
);
}
: null,
),
],
),
),
],
),
);
}
/// Creates a new team with a default name and color based on the current number
Team getNewTeam() {
final loc = AppLocalizations.of(context);
return Team(
name: '${loc.team} ${teams.length + 1}',
color: getTeamColor(teams.length),
members: [],
);
}
/// Builds a [TextEditingController] for the given team and sets up a listener
/// to update the team's name whenever the text changes.
TextEditingController getNewController(Team team) {
final textController = TextEditingController(text: team.name);
textController.addListener(() {
final index = teams.indexWhere((t) => t.id == team.id);
if (index == -1) return;
teams[index] = teams[index].copyWith(name: textController.text);
});
return textController;
}
/// Adds a new team to the list of teams, creates a corresponding controller,
/// and redistributes the players among all teams.
void addTeam() {
setState(() {
final newTeam = getNewTeam();
teams.add(newTeam);
nameController.add(getNewController(newTeam));
});
}
/// Removes the team with the given index. If there are less than 2 teams the
/// removed team gets replaced with a new one
void removeTeam(int index) {
final loc = AppLocalizations.of(context);
setState(() {
teams.removeAt(index);
final removedController = nameController.removeAt(index);
removedController.dispose();
// Update index-based team names and default colors
for (int i = 0; i < nameController.length; i++) {
if (nameController[i].text.contains(
RegExp('^${RegExp.escape(loc.team)} \\d+\$'),
)) {
nameController[i].text = '${loc.team} ${i + 1}';
// Reset color to default if it was based on the index
final previousIndex = i < index ? i : i + 1;
if (teams[i].color == getTeamColor(previousIndex)) {
teams[i] = teams[i].copyWith(color: getTeamColor(i));
}
}
}
});
}
@override
void dispose() {
for (final c in nameController) {
c.dispose();
}
super.dispose();
}
}

View File

@@ -1,306 +0,0 @@
import 'dart:core' hide Match;
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_numeric_text/flutter_numeric_text.dart';
import 'package:fluttericon/rpg_awesome_icons.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_list_tile.dart';
/// Displays the given [teams] as a flat reorderable list where every team is
/// preceded by a header row and followed by its members. Members can be
/// dragged across team boundaries to be reassigned to another team.
class ManageMembersView extends StatefulWidget {
const ManageMembersView({
super.key,
required this.match,
required this.onWinnerChanged,
});
final Match match;
final VoidCallback? onWinnerChanged;
@override
State<ManageMembersView> createState() => _ManageMembersViewState();
}
class _ManageMembersViewState extends State<ManageMembersView> {
late AppDatabase db;
List<Team> get teams => widget.match.teams!;
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
redistributePlayers();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.manage_members)),
body: Stack(
alignment: AlignmentDirectional.center,
children: [
Positioned.fill(
child: ReorderableListView.builder(
padding: const EdgeInsets.fromLTRB(0, 12, 0, 96),
buildDefaultDragHandles: false,
itemCount: allItemsCount,
onReorderItem: onReorderItem,
proxyDecorator: (child, index, animation) =>
Material(type: MaterialType.transparency, child: child),
itemBuilder: (context, index) {
final teamIndex = teamIndexForFlat(index);
final memberIndex = memberIndexForFlat(index, teamIndex);
final team = teams[teamIndex];
if (memberIndex == -1) {
return buildTeamTile(team: team);
}
final player = team.members[memberIndex];
return ReorderableDelayedDragStartListener(
key: ValueKey('player_${player.id}'),
index: index,
child: TextIconListTile(
text: player.name,
suffixText: getNameCountText(player),
icon: Icons.drag_handle,
),
);
},
),
),
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MainMenuButton(
onPressed: () => setState(() {
redistributePlayers();
}),
icon: Icons.cached,
),
const SizedBox(width: 16),
MainMenuButton(
onPressed: allTeamsHaveMembers
? () async => submitMatch()
: null,
text: loc.create_match,
icon: RpgAwesome.clovers_card,
),
],
),
),
],
),
);
}
Widget buildTeamTile({required Team team}) {
final color = getColorFromGameColor(team.color);
final loc = AppLocalizations.of(context);
final length = team.members.length;
final memberText = length == 1 ? loc.member : loc.members;
return Padding(
key: ValueKey(team.id),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
child: Row(
children: [
// Color circle
Container(
width: 14,
height: 14,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 10),
// Team name
Expanded(
child: Text(
team.name,
style: const TextStyle(
color: CustomTheme.textColor,
fontSize: 17,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
// Member length
SizedBox(
width: 150,
child: NumericText(
'$length $memberText',
duration: const Duration(milliseconds: 200),
maxLines: 1,
textAlign: TextAlign.end,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: CustomTheme.hintColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
// Iterates through all teams and redistributes players randomly and
// as evenly as possible.
void redistributePlayers() {
for (final team in teams) {
team.members.clear();
}
var matchPlayers = widget.match.players;
Random random = Random();
if (matchPlayers.isEmpty || teams.isEmpty) {
return;
}
final shuffledPlayers = [...matchPlayers]..shuffle(random);
for (int i = 0; i < shuffledPlayers.length; i++) {
final teamIndex = i % teams.length;
teams[teamIndex].members.add(shuffledPlayers[i]);
}
}
/// Handles moving a member from one team to another
void onReorderItem(int oldIndex, int newIndex) {
final sourceTeamIndex = teamIndexForFlat(oldIndex);
final sourceMemberIndex = memberIndexForFlat(oldIndex, sourceTeamIndex);
// Headers themselves can't be reordered.
if (sourceMemberIndex == -1) return;
// When moving down, the target index is shifted by 1
// because the item is removed first.
var targetIndex = newIndex;
if (newIndex > oldIndex) targetIndex -= 1;
targetIndex = targetIndex.clamp(0, allItemsCount - 1);
// Resolve target location based on the item currently
// at targetIndex before the move.
int destTeamIndex;
int insertPositionInTeam;
if (targetIndex >= allItemsCount - 1 && newIndex >= allItemsCount) {
// dropped at the very end, append to the last team.
destTeamIndex = teams.length - 1;
insertPositionInTeam = teams[destTeamIndex].members.length;
} else {
destTeamIndex = teamIndexForFlat(targetIndex);
final anchorMemberIndex = memberIndexForFlat(targetIndex, destTeamIndex);
if (anchorMemberIndex == -1) {
// dropped on a header, direction decides which team the player gets added
// if moving down, insert as first member of that team.
// if moving UP, append to the previous team.
final isMovingDown = newIndex > oldIndex;
if (isMovingDown) {
insertPositionInTeam = 0;
} else {
final previousTeamIndex = destTeamIndex - 1;
if (previousTeamIndex < 0) {
// above the very first header, stay at top of team 0.
insertPositionInTeam = 0;
} else {
destTeamIndex = previousTeamIndex;
insertPositionInTeam = teams[destTeamIndex].members.length;
}
}
} else {
insertPositionInTeam = anchorMemberIndex;
}
}
setState(() {
final sourceMembers = teams[sourceTeamIndex].members;
final player = sourceMembers.removeAt(sourceMemberIndex);
// Adjust insert index if removed from before the insert point in the
// same team.
if (sourceTeamIndex == destTeamIndex &&
insertPositionInTeam > sourceMembers.length) {
insertPositionInTeam = sourceMembers.length;
}
teams[destTeamIndex].members.insert(insertPositionInTeam, player);
});
}
/// Total players + teams length
int get allItemsCount {
var count = 0;
for (final team in teams) {
count += 1 + team.members.length;
}
return count;
}
/// Returns the index of the team that owns the flat-list item at [flatIndex].
int teamIndexForFlat(int flatIndex) {
var remaining = flatIndex;
for (var i = 0; i < teams.length; i++) {
final size = 1 + teams[i].members.length;
if (remaining < size) return i;
remaining -= size;
}
return teams.length - 1;
}
/// Returns the member index within its team, or `-1` if the item at
/// [flatIndex] is the team header.
int memberIndexForFlat(int flatIndex, int teamIndex) {
var offset = 0;
for (var i = 0; i < teamIndex; i++) {
offset += 1 + teams[i].members.length;
}
// offset now points to the header of [teamIndex]. Anything beyond is a
// member of that team.
final localIndex = flatIndex - offset;
return localIndex == 0 ? -1 : localIndex - 1;
}
bool get allTeamsHaveMembers =>
teams.every((team) => team.members.isNotEmpty);
void submitMatch() async {
final match = widget.match;
await db.matchDao.addMatch(match: match);
if (mounted) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (_) => MatchResultView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
),
),
(route) => route.isFirst,
);
}
}
}

View File

@@ -1,89 +0,0 @@
import 'package:flutter/material.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart';
class LiveEditView extends StatefulWidget {
const LiveEditView({super.key, required this.match});
final Match match;
@override
State<LiveEditView> createState() => _LiveEditViewState();
}
class _LiveEditViewState extends State<LiveEditView> {
List<Team> get allTeams =>
(widget.match.teams ?? [])..sort((a, b) => a.name.compareTo(b.name));
List<Player> get allPlayers =>
widget.match.players..sort((a, b) => a.name.compareTo(b.name));
List<int> scores = [];
@override
void initState() {
super.initState();
if (widget.match.isTeamMatch) {
scores = List.generate(
allTeams.length,
(index) => allTeams[index].score ?? 0,
);
} else {
scores = List.generate(
allPlayers.length,
(index) => widget.match.scores[allPlayers[index].id]?.score ?? 0,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.match.name),
leading: HapticIconButton(
onPressed: () => Navigator.pop(context, scores),
icon: const Icon(Icons.close),
),
),
body: Column(
children: [
Expanded(child: buildLiveEditWidget(widget.match.isTeamMatch)),
],
),
);
}
Widget buildLiveEditWidget(bool isTeamMatch) {
if (isTeamMatch) {
return ListView.builder(
itemCount: allTeams.length,
itemBuilder: (context, index) {
return LiveEditListTile(
title: allTeams[index].name,
onChanged: (value) {
scores[index] = value;
},
value: scores[index],
);
},
);
} else {
return ListView.builder(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return LiveEditListTile(
title: allPlayers[index].name,
onChanged: (value) {
setState(() {
scores[index] = value;
});
},
value: scores[index],
);
},
);
}
}
}

View File

@@ -13,7 +13,6 @@ import 'package:tallee/presentation/views/main_menu/match_view/create_match/crea
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/cards/team_card.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
@@ -44,13 +43,13 @@ class MatchDetailView extends StatefulWidget {
class _MatchDetailViewState extends State<MatchDetailView> { class _MatchDetailViewState extends State<MatchDetailView> {
late final AppDatabase db; late final AppDatabase db;
late Match localMatch; late Match match;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
localMatch = widget.match; match = widget.match;
} }
@override @override
@@ -84,7 +83,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
), ),
).then((confirmed) async { ).then((confirmed) async {
if (confirmed! && context.mounted) { if (confirmed! && context.mounted) {
await db.matchDao.deleteMatch(matchId: localMatch.id); await db.matchDao.deleteMatch(matchId: match.id);
if (!context.mounted) return; if (!context.mounted) return;
Navigator.pop(context); Navigator.pop(context);
widget.onMatchUpdate.call(); widget.onMatchUpdate.call();
@@ -118,7 +117,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
// Match Name // Match Name
Text( Text(
localMatch.name, match.name,
style: const TextStyle( style: const TextStyle(
fontSize: 28, fontSize: 28,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -130,7 +129,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
// Creation Date // Creation Date
Text( Text(
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(localMatch.createdAt)}', '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}',
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: CustomTheme.textColor, color: CustomTheme.textColor,
@@ -140,14 +139,14 @@ class _MatchDetailViewState extends State<MatchDetailView> {
const SizedBox(height: 10), const SizedBox(height: 10),
// Group Name // Group Name
if (localMatch.group != null) ...[ if (match.group != null) ...[
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.group), const Icon(Icons.group),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'${localMatch.group!.name}${getExtraPlayerCount(localMatch)}', '${match.group!.name}${getExtraPlayerCount(match)}',
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
], ],
@@ -155,60 +154,25 @@ class _MatchDetailViewState extends State<MatchDetailView> {
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
// Teams or Players // Players
if (localMatch.isTeamMatch) ...[ InfoTile(
// Teams title: loc.players,
InfoTile( icon: Icons.people,
title: loc.teams, horizontalAlignment: CrossAxisAlignment.start,
icon: Icons.scoreboard, content: Wrap(
horizontalAlignment: CrossAxisAlignment.start, alignment: WrapAlignment.start,
content: crossAxisAlignment: WrapCrossAlignment.start,
localMatch.teams != null && localMatch.teams!.isNotEmpty spacing: 12,
? Wrap( runSpacing: 8,
alignment: WrapAlignment.start, children: match.players.map((player) {
crossAxisAlignment: WrapCrossAlignment.start, return TextIconTile(
spacing: 12, text: player.name,
runSpacing: 8, suffixText: getNameCountText(player),
children: (localMatch.teams ?? []).map((team) { iconEnabled: false,
return TeamCard(team: team); );
}).toList(), }).toList(),
)
: Text(
loc.no_teams_available,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
), ),
] else ...[ ),
// Players
InfoTile(
title: loc.players,
icon: Icons.people,
horizontalAlignment: CrossAxisAlignment.start,
content: localMatch.players.isNotEmpty
? Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: localMatch.players.map((player) {
return TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
);
}).toList(),
)
: Text(
loc.no_players_available,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
),
],
const SizedBox(height: 15), const SizedBox(height: 15),
// Game // Game
@@ -222,12 +186,12 @@ class _MatchDetailViewState extends State<MatchDetailView> {
horizontal: 8, horizontal: 8,
), ),
child: GameLabel( child: GameLabel(
title: localMatch.game.name, title: match.game.name,
description: translateRulesetToString( description: translateRulesetToString(
localMatch.game.ruleset, match.game.ruleset,
context, context,
), ),
color: localMatch.game.color, color: match.game.color,
), ),
), ),
), ),
@@ -258,7 +222,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
adaptivePageRoute( adaptivePageRoute(
fullscreenDialog: true, fullscreenDialog: true,
builder: (context) => CreateMatchView( builder: (context) => CreateMatchView(
matchToEdit: localMatch, matchToEdit: match,
onMatchUpdated: onMatchUpdated, onMatchUpdated: onMatchUpdated,
), ),
), ),
@@ -274,10 +238,12 @@ class _MatchDetailViewState extends State<MatchDetailView> {
adaptivePageRoute( adaptivePageRoute(
fullscreenDialog: true, fullscreenDialog: true,
builder: (context) => MatchResultView( builder: (context) => MatchResultView(
match: localMatch, match: match,
onWinnerChanged: () async { onWinnerChanged: () {
widget.onMatchUpdate.call(); widget.onMatchUpdate.call();
await updateScoresForCurrentMatch(); setState(() {
updateScoresForCurrentMatch();
});
}, },
), ),
), ),
@@ -297,7 +263,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
/// updates the match in this view /// updates the match in this view
void onMatchUpdated(Match editedMatch) { void onMatchUpdated(Match editedMatch) {
setState(() { setState(() {
localMatch = editedMatch; match = editedMatch;
}); });
widget.onMatchUpdate.call(); widget.onMatchUpdate.call();
} }
@@ -318,113 +284,95 @@ class _MatchDetailViewState extends State<MatchDetailView> {
/// Returns the result row for single winner/loser rulesets or a placeholder /// Returns the result row for single winner/loser rulesets or a placeholder
/// if no result is entered yet /// if no result is entered yet
List<Widget> getSingleResultRow(AppLocalizations loc) { List<Widget> getSingleResultRow(AppLocalizations loc) {
final ruleset = localMatch.game.ruleset; if (match.mvp.isNotEmpty) {
final ruleset = match.game.ruleset;
if (localMatch.mvp.isNotEmpty || localMatch.mvt.isNotEmpty) { if (ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser) {
// Single winner/loser, multiple winner return [
final names = localMatch.isTeamMatch Text(
? localMatch.mvt.map((t) => t.name).toList() ruleset == Ruleset.singleWinner ? loc.winner : loc.loser,
: localMatch.mvp.map((p) => p.name).toList(); style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
final mvpNames = names.length == 1 ? names.first : names.join(', '); ),
Text(
final label = ruleset == Ruleset.singleWinner match.mvp.first.name,
? loc.winner
: ruleset == Ruleset.singleLoser
? loc.loser
: loc.winners;
return [
Text(
label,
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
),
SizedBox(
width: 200,
child: Text(
mvpNames,
textAlign: TextAlign.end,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor, color: CustomTheme.primaryColor,
), ),
), ),
), ];
]; } else if (match.game.ruleset == Ruleset.multipleWinners) {
} else { return [
// No result yet Text(
return [ loc.winners,
Text( style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
loc.no_results_entered_yet, ),
style: const TextStyle(fontSize: 14, color: CustomTheme.textColor), Flexible(
), child: Container(
]; padding: const EdgeInsets.only(left: 10),
child: Text(
match.mvp.map((player) => player.name).join(', '),
textAlign: TextAlign.end,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
),
),
];
}
} }
// No results yet
return [
Text(
loc.no_results_entered_yet,
style: const TextStyle(fontSize: 14, color: CustomTheme.textColor),
),
];
} }
/// Returns the result widget for scores or placement /// Returns the result widget for scores or placement
Widget getMultiResultRows(AppLocalizations loc) { Widget getMultiResultRows(AppLocalizations loc) {
List<(String, int)> scores = getSortedScores(); List<(String, int)> playerScores = [];
for (var player in match.players) {
int score = match.scores[player.id]?.score ?? 0;
playerScores.add((player.name, score));
}
final ruleset = match.game.ruleset;
if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) {
playerScores.sort((a, b) => b.$2.compareTo(a.$2));
} else if (ruleset == Ruleset.lowestScore) {
playerScores.sort((a, b) => a.$2.compareTo(b.$2));
}
return Column( return Column(
children: [ children: [
for (var i = 0; i < scores.length; i++) for (var i = 0; i < playerScores.length; i++)
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
scores[i].$1, playerScores[i].$1,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
color: CustomTheme.textColor, color: CustomTheme.textColor,
), ),
), ),
getResultValueText(loc, i, scores[i].$2), getResultValueText(loc, i, playerScores[i].$2),
], ],
), ),
], ],
); );
} }
/// Returns a list of player/team names and their corresponding scores, sorted by score according to the ruleset
List<(String, int)> getSortedScores() {
List<(String, int)> namedScores = [];
if (localMatch.isTeamMatch) {
final teams = localMatch.teams ?? [];
for (var team in teams) {
int score = team.score ?? 0;
namedScores.add((team.name, score));
}
final ruleset = localMatch.game.ruleset;
if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) {
namedScores.sort((a, b) => b.$2.compareTo(a.$2));
} else if (ruleset == Ruleset.lowestScore) {
namedScores.sort((a, b) => a.$2.compareTo(b.$2));
}
} else {
final scores = localMatch.scores;
for (var player in localMatch.players) {
int score = scores[player.id]?.score ?? 0;
namedScores.add((player.name, score));
}
final ruleset = localMatch.game.ruleset;
if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) {
namedScores.sort((a, b) => b.$2.compareTo(a.$2));
} else if (ruleset == Ruleset.lowestScore) {
namedScores.sort((a, b) => a.$2.compareTo(b.$2));
}
}
return namedScores;
}
/// Returns the text widget for the score or placement value, styled according to the ruleset
Widget getResultValueText(AppLocalizations loc, int index, int score) { Widget getResultValueText(AppLocalizations loc, int index, int score) {
final ruleset = localMatch.game.ruleset; final ruleset = match.game.ruleset;
if (ruleset == Ruleset.placement) { if (ruleset == Ruleset.placement) {
return Text( return Text(
@@ -462,9 +410,9 @@ class _MatchDetailViewState extends State<MatchDetailView> {
// Returns if the result can be displayed in a single row // Returns if the result can be displayed in a single row
bool isSingleRowResult() { bool isSingleRowResult() {
return localMatch.game.ruleset == Ruleset.singleWinner || return match.game.ruleset == Ruleset.singleWinner ||
localMatch.game.ruleset == Ruleset.singleLoser || match.game.ruleset == Ruleset.singleLoser ||
localMatch.game.ruleset == Ruleset.multipleWinners; match.game.ruleset == Ruleset.multipleWinners;
} }
String getPlacementText(BuildContext context, int rank) { String getPlacementText(BuildContext context, int rank) {
@@ -495,19 +443,9 @@ class _MatchDetailViewState extends State<MatchDetailView> {
} }
} }
Future<void> updateScoresForCurrentMatch() async { void updateScoresForCurrentMatch() {
if (localMatch.isTeamMatch) { db.scoreEntryDao
final teams = await db.teamDao.getTeamsByMatchId(matchId: localMatch.id); .getAllMatchScores(matchId: match.id)
setState(() { .then((scores) => match.scores = scores);
localMatch = localMatch.copyWith(teams: teams);
});
} else {
final scores = await db.scoreEntryDao.getAllMatchScores(
matchId: localMatch.id,
);
setState(() {
localMatch = localMatch.copyWith(scores: scores);
});
}
} }
} }

View File

@@ -39,7 +39,7 @@ class _MatchViewState extends State<MatchView> {
game: Game( game: Game(
name: 'Game name', name: 'Game name',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
), ),
group: Group( group: Group(
@@ -97,6 +97,7 @@ class _MatchViewState extends State<MatchView> {
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 12.0), padding: const EdgeInsets.only(bottom: 12.0),
child: MatchTile( child: MatchTile(
onPlayerEdited: loadMatches,
width: MediaQuery.sizeOf(context).width * 0.95, width: MediaQuery.sizeOf(context).width * 0.95,
onTap: () async { onTap: () async {
Navigator.push( Navigator.push(

View File

@@ -0,0 +1,394 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class PlayerDetailView extends StatefulWidget {
const PlayerDetailView({
super.key,
required this.player,
required this.callback,
});
/// The player to display
final Player player;
final VoidCallback callback;
@override
State<PlayerDetailView> createState() => _PlayerDetailViewState();
}
class _PlayerDetailViewState extends State<PlayerDetailView> {
late final AppDatabase db;
late Player _player;
late String playerNameCount;
bool isLoading = true;
/// Total matches played by this player
int totalMatches = 0;
/// Total matches won by this player
int matchesWon = 0;
/// Total groups this player belongs to
int totalGroups = 0;
/// Full list of groups this player belongs to
List<Group> playerGroups = List.filled(
4,
Group(name: 'Skeleton group', members: []),
);
/// Full list of matches this player played in
List<Match> playerMatches = List.filled(
4,
Match(
name: 'Skeleton match',
game: Game(name: 'Game name', ruleset: Ruleset.singleWinner),
players: [],
),
);
TextEditingController nameController = TextEditingController();
@override
void initState() {
super.initState();
_player = widget.player;
db = Provider.of<AppDatabase>(context, listen: false);
playerNameCount = getNameCountText(_player);
_loadData();
}
@override
void dispose() {
nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(loc.player_profile),
actions: [
HapticIconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
showDialog<bool>(
context: context,
builder: (context) => CustomAlertDialog(
title: loc.delete_player,
content: Text(loc.this_cannot_be_undone),
actions: [
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(true),
text: loc.delete,
),
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
buttonType: ButtonType.secondary,
text: loc.cancel,
),
],
),
).then((confirmed) async {
if (confirmed! && context.mounted) {
//TODO: implement player deletion in db
if (!context.mounted) return;
Navigator.pop(context);
widget.callback();
}
});
},
),
],
),
body: SafeArea(
child: Stack(
alignment: Alignment.center,
children: [
ListView(
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 20,
bottom: 100,
),
children: [
const Center(
child: ColoredIconContainer(
icon: Icons.person,
containerSize: 55,
iconSize: 38,
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_player.name,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
textAlign: TextAlign.center,
),
Text(
playerNameCount,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor.withAlpha(120),
),
textAlign: TextAlign.center,
),
],
),
const SizedBox(height: 5),
Text(
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(_player.createdAt)}',
style: const TextStyle(
fontSize: 12,
color: CustomTheme.textColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
InfoTile(
title: '${loc.matches_part_of} ($totalMatches)',
icon: Icons.sports_esports,
horizontalAlignment: CrossAxisAlignment.start,
content: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
alignment: Alignment.topLeft,
child: playerMatches.isNotEmpty
? Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: playerMatches.map((match) {
return TextIconTile(
text: match.name,
iconEnabled: false,
);
}).toList(),
)
: Text(
loc.no_matches_played_yet,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
),
),
const SizedBox(height: 15),
InfoTile(
title: '${loc.groups_part_of} ($totalGroups)',
icon: Icons.people,
horizontalAlignment: CrossAxisAlignment.start,
content: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
alignment: Alignment.topLeft,
child: playerGroups.isNotEmpty
? Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: playerGroups.map((group) {
return TextIconTile(
text: group.name,
iconEnabled: false,
);
}).toList(),
)
: Text(
loc.not_part_of_any_group,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
),
),
const SizedBox(height: 15),
InfoTile(
title: loc.statistics,
icon: Icons.bar_chart,
content: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
child: Column(
children: [
_buildStatRow(
loc.matches_played,
totalMatches.toString(),
),
_buildStatRow(loc.matches_won, matchesWon.toString()),
_buildStatRow(
loc.winrate,
'${totalMatches == 0 ? 0 : ((matchesWon / totalMatches) * 100).round()}%',
),
],
),
),
),
],
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: MainMenuButton(
text: loc.edit_player,
icon: Icons.edit,
onPressed: () async {
nameController.text = _player.name;
showDialog<bool>(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) {
return CustomAlertDialog(
title: loc.edit_name,
content: TextInputField(
controller: nameController,
hintText: loc.set_name,
onChanged: (_) => setDialogState(() {}),
),
actions: [
CustomDialogAction(
onPressed: isConfirmButtonEnabled()
? () => Navigator.of(context).pop(true)
: null,
text: loc.confirm,
),
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
buttonType: ButtonType.secondary,
text: loc.cancel,
),
],
);
},
),
).then((confirmed) async {
if (confirmed! && context.mounted) {
final newName = nameController.text.trim();
if (newName != _player.name) {
final fetchedPlayerNameCount = await db.playerDao
.getNameCount(name: newName);
await db.playerDao.updatePlayerName(
playerId: _player.id,
name: newName,
);
widget.callback.call();
setState(() {
_player = Player(
name: newName,
createdAt: _player.createdAt,
id: _player.id,
nameCount: _player.nameCount,
description: _player.description,
);
// If there is already a player with the same name,
// the count of that player is 0, so we start counting from 2 to get the correct count for this player. If there are no players with the same name, we just show the name without a count.
playerNameCount = fetchedPlayerNameCount == 0
? ''
: ' #${fetchedPlayerNameCount + 1}';
});
}
}
});
},
),
),
],
),
),
);
}
/// Loads statistics for this player
Future<void> _loadData() async {
isLoading = true;
final fetchedMatches = await db.matchDao.getMatchesByPlayer(
playerId: _player.id,
);
final fetchedGroups = await db.groupDao.getGroupsByPlayer(
playerId: _player.id,
);
if (!mounted) return;
setState(() {
playerMatches = fetchedMatches;
totalMatches = fetchedMatches.length;
matchesWon = fetchedMatches
.where((match) => match.mvp.any((mvp) => mvp.id == _player.id))
.length;
playerGroups = fetchedGroups;
totalGroups = fetchedGroups.length;
isLoading = false;
});
}
/// Builds a single statistic row with a label and value
/// - [label]: The label of the statistic
/// - [value]: The value of the statistic
Widget _buildStatRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
label,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
],
),
Text(
value,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
);
}
bool isConfirmButtonEnabled() {
return nameController.text.trim().isNotEmpty;
}
}

View File

@@ -6,11 +6,13 @@ class AppSkeleton extends StatefulWidget {
/// - [child]: The widget tree to apply the skeleton effect to. /// - [child]: The widget tree to apply the skeleton effect to.
/// - [enabled]: A boolean to enable or disable the skeleton effect. /// - [enabled]: A boolean to enable or disable the skeleton effect.
/// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher. /// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher.
/// - [alignment]: The alignment used for the custom layout builder and optional Align wrapper. Defaults to [Alignment.center].
const AppSkeleton({ const AppSkeleton({
super.key, super.key,
required this.child, required this.child,
this.enabled = true, this.enabled = true,
this.fixLayoutBuilder = false, this.fixLayoutBuilder = false,
this.alignment = Alignment.center,
}); });
/// The widget tree to apply the skeleton effect to. /// The widget tree to apply the skeleton effect to.
@@ -22,6 +24,9 @@ class AppSkeleton extends StatefulWidget {
/// A boolean to fix the layout builder for AnimatedSwitcher. /// A boolean to fix the layout builder for AnimatedSwitcher.
final bool fixLayoutBuilder; final bool fixLayoutBuilder;
/// The alignment used for the custom layout builder and optional Align wrapper
final Alignment alignment;
@override @override
State<AppSkeleton> createState() => _AppSkeletonState(); State<AppSkeleton> createState() => _AppSkeletonState();
} }
@@ -45,13 +50,14 @@ class _AppSkeletonState extends State<AppSkeleton> {
layoutBuilder: !widget.fixLayoutBuilder layoutBuilder: !widget.fixLayoutBuilder
? AnimatedSwitcher.defaultLayoutBuilder ? AnimatedSwitcher.defaultLayoutBuilder
: (Widget? currentChild, List<Widget> previousChildren) { : (Widget? currentChild, List<Widget> previousChildren) {
return Stack( final children = <Widget>[...previousChildren];
alignment: Alignment.topCenter, if (currentChild != null) children.add(currentChild);
children: [...previousChildren, ?currentChild], return Stack(alignment: widget.alignment, children: children);
);
}, },
), ),
child: widget.child, child: widget.fixLayoutBuilder
? Align(alignment: widget.alignment, child: widget.child)
: widget.child,
); );
} }
} }

View File

@@ -11,11 +11,10 @@ class AnimatedDialogButton extends StatefulWidget {
const AnimatedDialogButton({ const AnimatedDialogButton({
super.key, super.key,
required this.buttonText, required this.buttonText,
required this.onPressed, this.onPressed,
this.buttonConstraints, this.buttonConstraints,
this.buttonType = ButtonType.primary, this.buttonType = ButtonType.primary,
this.isDescructive = false, this.isDescructive = false,
this.content,
}); });
final String buttonText; final String buttonText;
@@ -28,8 +27,6 @@ class AnimatedDialogButton extends StatefulWidget {
final bool isDescructive; final bool isDescructive;
final Widget? content;
@override @override
State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState(); State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState();
} }
@@ -41,12 +38,12 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textStyling = _getTextStyling(); final textStyling = _getTextStyling();
final buttonDecoration = _getButtonDecoration(); final buttonDecoration = _getButtonDecoration();
final isDisabled = widget.onPressed == null; bool isDisabled = widget.onPressed == null;
return IgnorePointer( return IgnorePointer(
ignoring: isDisabled, ignoring: isDisabled,
child: Opacity( child: Opacity(
opacity: isDisabled ? 0.4 : 1.0, opacity: isDisabled ? 0.5 : 1.0,
child: GestureDetector( child: GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true), onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false), onTapUp: (_) => setState(() => _isPressed = false),
@@ -67,13 +64,11 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
vertical: 12, vertical: 12,
), ),
margin: const EdgeInsets.symmetric(vertical: 8), margin: const EdgeInsets.symmetric(vertical: 8),
child: widget.buttonText == '' child: Text(
? widget.content! widget.buttonText,
: Text( style: textStyling,
widget.buttonText, textAlign: TextAlign.center,
style: textStyling, ),
textAlign: TextAlign.center,
),
), ),
), ),
), ),

View File

@@ -56,7 +56,6 @@ class CustomWidthButton extends StatelessWidget {
onPressed!.call(); onPressed!.call();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor, foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor, disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor, backgroundColor: buttonBackgroundColor,
@@ -92,7 +91,6 @@ class CustomWidthButton extends StatelessWidget {
onPressed!.call(); onPressed!.call();
}, },
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor, foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor, disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor, backgroundColor: buttonBackgroundColor,
@@ -130,7 +128,6 @@ class CustomWidthButton extends StatelessWidget {
onPressed!.call(); onPressed!.call();
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor, foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor, disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor, backgroundColor: buttonBackgroundColor,

View File

@@ -17,7 +17,7 @@ class MainMenuButton extends StatefulWidget {
}); });
/// The callback to be invoked when the button is pressed. /// The callback to be invoked when the button is pressed.
final void Function()? onPressed; final void Function() onPressed;
/// The icon of the button. /// The icon of the button.
final IconData icon; final IconData icon;
@@ -32,11 +32,9 @@ class MainMenuButton extends StatefulWidget {
} }
class _MainMenuButtonState extends State<MainMenuButton> class _MainMenuButtonState extends State<MainMenuButton>
with TickerProviderStateMixin { with SingleTickerProviderStateMixin {
late AnimationController _animationController; late AnimationController _animationController;
late AnimationController _disabledAnimationController;
late Animation<double> _scaleAnimation; late Animation<double> _scaleAnimation;
late Animation<double> _disabledScaleAnimation;
/// How long the button needs to be pressed to register it as long press /// How long the button needs to be pressed to register it as long press
Timer? _longPressTimer; Timer? _longPressTimer;
@@ -55,67 +53,45 @@ class _MainMenuButtonState extends State<MainMenuButton>
vsync: this, vsync: this,
); );
_disabledAnimationController = AnimationController(
duration: const Duration(milliseconds: 100),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate( _scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
); );
_disabledScaleAnimation = Tween<double>(begin: 1.0, end: 0.98).animate(
CurvedAnimation(
parent: _disabledAnimationController,
curve: Curves.easeInOut,
),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ScaleTransition( return ScaleTransition(
scale: widget.onPressed == null scale: _scaleAnimation,
? _disabledScaleAnimation
: _scaleAnimation,
child: GestureDetector( child: GestureDetector(
onTapDown: (_) { onTapDown: (_) {
if (widget.onPressed == null) { _animationController.forward();
_disabledAnimationController.forward(); if (widget.onLongPressed != null) {
} else { _longPressTimer = Timer(
_animationController.forward(); const Duration(milliseconds: 400),
if (widget.onLongPressed != null) { () async {
_longPressTimer = Timer( _isLongPressing = true;
const Duration(milliseconds: 400), widget.onLongPressed?.call();
() async { await HapticFeedback.heavyImpact();
_isLongPressing = true; _repeatTimer = Timer.periodic(
widget.onLongPressed?.call(); const Duration(milliseconds: 250),
await HapticFeedback.heavyImpact(); (_) async {
_repeatTimer = Timer.periodic( widget.onLongPressed?.call();
const Duration(milliseconds: 250), await HapticFeedback.heavyImpact();
(_) async { },
widget.onLongPressed?.call(); );
await HapticFeedback.heavyImpact(); },
}, );
);
},
);
}
} }
}, },
onTapUp: (_) async { onTapUp: (_) async {
if (widget.onPressed == null) { _cancelTimers();
_disabledAnimationController.reverse(); if (mounted && !_isLongPressing) {
} else { await HapticFeedback.selectionClick();
_cancelTimers(); widget.onPressed();
if (mounted && !_isLongPressing) {
await HapticFeedback.selectionClick();
widget.onPressed?.call();
}
_isLongPressing = false;
await Future.delayed(const Duration(milliseconds: 100));
await _animationController.reverse();
} }
_isLongPressing = false;
await Future.delayed(const Duration(milliseconds: 100));
await _animationController.reverse();
}, },
onTapCancel: () { onTapCancel: () {
_isLongPressing = false; _isLongPressing = false;
@@ -124,7 +100,7 @@ class _MainMenuButtonState extends State<MainMenuButton>
}, },
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: widget.onPressed == null ? Colors.grey : Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30),
), ),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
@@ -155,7 +131,6 @@ class _MainMenuButtonState extends State<MainMenuButton>
void dispose() { void dispose() {
_cancelTimers(); _cancelTimers();
_animationController.dispose(); _animationController.dispose();
_disabledAnimationController.dispose();
super.dispose(); super.dispose();
} }

View File

@@ -1,103 +0,0 @@
import 'package:flutter/material.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class TeamCard extends StatelessWidget {
const TeamCard({
super.key,
required this.team,
this.compact = false,
this.width = double.infinity,
});
final Team team;
final bool compact;
final double width;
@override
Widget build(BuildContext context) {
final teamColor = getColorFromGameColor(team.color);
if (compact) {
return Container(
width: width,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: teamColor.withAlpha(50),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: teamColor, width: 2),
),
child: Row(
children: [
Expanded(
child: Text(
team.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Container(
width: 1,
height: 14,
color: Colors.white.withValues(alpha: 0.35),
),
const SizedBox(width: 8),
const Icon(Icons.people_alt_rounded, size: 14, color: Colors.white),
const SizedBox(width: 4),
Text(
'${team.members.length}',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
);
} else {
return Container(
width: width,
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
decoration: BoxDecoration(
color: teamColor.withAlpha(50),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: teamColor, width: 2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 6,
children: [
Text(
team.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: CustomTheme.textColor,
),
),
Wrap(
spacing: 6,
runSpacing: 6,
children: team.members.map((player) {
return TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
);
}).toList(),
),
],
),
);
}
}
}

View File

@@ -19,7 +19,6 @@ class CustomAlertDialog extends StatelessWidget {
final String title; final String title;
final Widget content; final Widget content;
final List<CustomDialogAction> actions; final List<CustomDialogAction> actions;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(

View File

@@ -10,7 +10,7 @@ class CustomDialogAction extends StatelessWidget {
/// - [onPressed]: Callback function that is triggered when the button is pressed. /// - [onPressed]: Callback function that is triggered when the button is pressed.
const CustomDialogAction({ const CustomDialogAction({
super.key, super.key,
required this.onPressed, this.onPressed,
required this.text, required this.text,
this.buttonType = ButtonType.primary, this.buttonType = ButtonType.primary,
this.isDestructive = false, this.isDestructive = false,
@@ -20,17 +20,18 @@ class CustomDialogAction extends StatelessWidget {
final ButtonType buttonType; final ButtonType buttonType;
final VoidCallback onPressed; final VoidCallback? onPressed;
final bool isDestructive; final bool isDestructive;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedDialogButton( return AnimatedDialogButton(
onPressed: () async { onPressed: onPressed != null
await HapticFeedback.selectionClick(); ? () async {
onPressed.call(); await HapticFeedback.selectionClick();
}, onPressed?.call();
}
: null,
buttonText: text, buttonText: text,
buttonType: buttonType, buttonType: buttonType,
isDescructive: isDestructive, isDescructive: isDestructive,

View File

@@ -12,7 +12,7 @@ class GameLabel extends StatelessWidget {
final String title; final String title;
final String description; final String description;
final AppColor color; final GameColor color;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -26,6 +26,7 @@ class PlayerSelection extends StatefulWidget {
this.availablePlayers, this.availablePlayers,
this.initialSelectedPlayers, this.initialSelectedPlayers,
required this.onChanged, required this.onChanged,
this.onPlayerCreated,
}); });
/// An optional list of players to choose from. If null, all players from the database are used. /// An optional list of players to choose from. If null, all players from the database are used.
@@ -37,6 +38,9 @@ class PlayerSelection extends StatefulWidget {
/// A callback function that is invoked whenever the selection changes, /// A callback function that is invoked whenever the selection changes,
final Function(List<Player> value) onChanged; final Function(List<Player> value) onChanged;
/// A callback function that is invoked when a player was created in this widget
final VoidCallback? onPlayerCreated;
@override @override
State<PlayerSelection> createState() => _PlayerSelectionState(); State<PlayerSelection> createState() => _PlayerSelectionState();
} }
@@ -143,9 +147,9 @@ class _PlayerSelectionState extends State<PlayerSelection> {
child: TextIconTile( child: TextIconTile(
text: player.name, text: player.name,
suffixText: getNameCountText(player), suffixText: getNameCountText(player),
onIconTap: () async { onIconTap: () {
await HapticFeedback.selectionClick(); setState(() async {
setState(() { await HapticFeedback.selectionClick();
// Removes the player from the selection and notifies the parent. // Removes the player from the selection and notifies the parent.
selectedPlayers.remove(player); selectedPlayers.remove(player);
widget.onChanged([...selectedPlayers]); widget.onChanged([...selectedPlayers]);
@@ -252,9 +256,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
), ),
) )
.toList(); .toList();
suggestedPlayers = suggestedPlayers
.where((p) => !selectedPlayers.any((sp) => sp.id == p.id))
.toList();
} }
} else { } else {
// Otherwise, use the loaded players from the database. // Otherwise, use the loaded players from the database.
@@ -326,6 +327,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
/// Updates the state after successfully adding a new player. /// Updates the state after successfully adding a new player.
void _handleSuccessfulPlayerCreation(Player player) { void _handleSuccessfulPlayerCreation(Player player) {
widget.onPlayerCreated?.call();
selectedPlayers.insert(0, player); selectedPlayers.insert(0, player);
widget.onChanged([...selectedPlayers]); widget.onChanged([...selectedPlayers]);
allPlayers.add(player); allPlayers.add(player);

View File

@@ -57,6 +57,7 @@ class TextInputField extends StatelessWidget {
filled: true, filled: true,
fillColor: CustomTheme.boxColor, fillColor: CustomTheme.boxColor,
hintText: hintText, hintText: hintText,
hintStyle: const TextStyle(fontSize: 18),
counterText: showCounterText ? null : '', counterText: showCounterText ? null : '',
enabledBorder: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),

View File

@@ -7,7 +7,6 @@ import 'package:tallee/core/enums.dart';
class GameTile extends StatelessWidget { class GameTile extends StatelessWidget {
/// A list tile widget that displays a title and description, with optional highlighting and badge. /// A list tile widget that displays a title and description, with optional highlighting and badge.
/// - [title]: The title text displayed on the tile. /// - [title]: The title text displayed on the tile.
/// - [subtitle]: An optional subtitle displayed under the title.
/// - [description]: The description text displayed below the title. /// - [description]: The description text displayed below the title.
/// - [onTap]: The callback invoked when the tile is tapped. /// - [onTap]: The callback invoked when the tile is tapped.
/// - [onLongPress]: The callback invoked when the tile is tapped. /// - [onLongPress]: The callback invoked when the tile is tapped.
@@ -18,7 +17,6 @@ class GameTile extends StatelessWidget {
super.key, super.key,
required this.title, required this.title,
required this.description, required this.description,
this.subtitle,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
this.isHighlighted = false, this.isHighlighted = false,
@@ -26,20 +24,25 @@ class GameTile extends StatelessWidget {
this.badgeColor, this.badgeColor,
}); });
/// The title text displayed on the tile.
final String title; final String title;
final String? subtitle; /// The description text displayed below the title.
final String description; final String description;
/// The callback invoked when the tile is tapped.
final VoidCallback? onTap; final VoidCallback? onTap;
/// The callback invoked when the tile is long-pressed.
final VoidCallback? onLongPress; final VoidCallback? onLongPress;
/// A boolean to determine if the tile should be highlighted.
final bool isHighlighted; final bool isHighlighted;
/// Optional text to display in a badge on the right side of the title.
final String? badgeText; final String? badgeText;
/// Optional color for the badge background.
final Color? badgeColor; final Color? badgeColor;
@override @override
@@ -48,7 +51,7 @@ class GameTile extends StatelessWidget {
? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white) ? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white)
: Colors.white; : Colors.white;
final gameColor = badgeColor ?? getColorFromGameColor(AppColor.orange); final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange);
return GestureDetector( return GestureDetector(
onTap: () async { onTap: () async {
@@ -64,14 +67,13 @@ class GameTile extends StatelessWidget {
} }
}, },
child: AnimatedContainer( child: AnimatedContainer(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
decoration: !isHighlighted decoration: !isHighlighted
? CustomTheme.standardBoxDecoration ? CustomTheme.standardBoxDecoration
: CustomTheme.highlightedBoxDecoration.copyWith( : CustomTheme.highlightedBoxDecoration.copyWith(
border: Border.all( border: Border.all(
color: gameColor.withValues(alpha: 0.9), color: gameColor.withValues(alpha: 0.9),
width: 2, width: 2,
strokeAlign: BorderSide.strokeAlignCenter,
), ),
), ),
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
@@ -116,21 +118,6 @@ class GameTile extends StatelessWidget {
), ),
), ),
// Title
if (subtitle != null && subtitle!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
subtitle!,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.hintColor,
),
),
],
// Badge // Badge
if (badgeText != null) ...[ if (badgeText != null) ...[
const SizedBox(height: 5), const SizedBox(height: 5),

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class GroupTile extends StatefulWidget { class GroupTile extends StatefulWidget {
@@ -15,6 +17,7 @@ class GroupTile extends StatefulWidget {
required this.group, required this.group,
this.isHighlighted = false, this.isHighlighted = false,
this.onTap, this.onTap,
this.onPlayerChanged,
}); });
/// The group data to be displayed. /// The group data to be displayed.
@@ -26,6 +29,9 @@ class GroupTile extends StatefulWidget {
/// Callback function to be executed when the tile is tapped. /// Callback function to be executed when the tile is tapped.
final VoidCallback? onTap; final VoidCallback? onTap;
/// Callback function to be executed when the players in the group are changed.
final VoidCallback? onPlayerChanged;
@override @override
State<GroupTile> createState() => _GroupTileState(); State<GroupTile> createState() => _GroupTileState();
} }
@@ -91,6 +97,20 @@ class _GroupTileState extends State<GroupTile> {
TextIconTile( TextIconTile(
text: member.name, text: member.name,
suffixText: getNameCountText(member), suffixText: getNameCountText(member),
iconEnabled: false,
onTileTap: () {
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => PlayerDetailView(
player: member,
callback: () {
widget.onPlayerChanged?.call();
},
),
),
);
},
), ),
], ],
), ),

View File

@@ -5,12 +5,12 @@ import 'package:tallee/core/custom_theme.dart';
class CustomCheckboxListTile extends StatelessWidget { class CustomCheckboxListTile extends StatelessWidget {
const CustomCheckboxListTile({ const CustomCheckboxListTile({
super.key, super.key,
required this.content, required this.text,
required this.value, required this.value,
required this.onChanged, required this.onChanged,
}); });
final Widget content; final String text;
final bool value; final bool value;
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
@@ -39,7 +39,16 @@ class CustomCheckboxListTile extends StatelessWidget {
onChanged(v); onChanged(v);
}, },
), ),
Expanded(child: content), Expanded(
child: Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
], ],
), ),
), ),

View File

@@ -8,13 +8,13 @@ class CustomRadioListTile<T> extends StatelessWidget {
/// - [onContainerTap]: The callback invoked when the container is tapped. /// - [onContainerTap]: The callback invoked when the container is tapped.
const CustomRadioListTile({ const CustomRadioListTile({
super.key, super.key,
required this.content, required this.text,
required this.value, required this.value,
required this.onContainerTap, required this.onContainerTap,
}); });
/// The text to display next to the radio button. /// The text to display next to the radio button.
final Widget content; final String text;
/// The value associated with the radio button. /// The value associated with the radio button.
final T value; final T value;
@@ -37,7 +37,16 @@ class CustomRadioListTile<T> extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Radio<T>(value: value, toggleable: true), Radio<T>(value: value, toggleable: true),
Expanded(child: content), Expanded(
child: Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
], ],
), ),
), ),

View File

@@ -5,34 +5,36 @@ import 'package:tallee/l10n/generated/app_localizations.dart';
class ScoreListTile extends StatelessWidget { class ScoreListTile extends StatelessWidget {
/// A custom list tile widget that has a text field for inputting a score. /// A custom list tile widget that has a text field for inputting a score.
/// - [content]: The leading Widget to be displayed. /// - [text]: The leading text to be displayed.
/// - [controller]: The controller for the text field to input the score. /// - [controller]: The controller for the text field to input the score.
const ScoreListTile({ const ScoreListTile({
super.key, super.key,
required this.content, required this.text,
required this.controller, required this.controller,
this.horizontalPadding = 20,
}); });
final Widget content; /// The text to display next to the radio button.
final String text;
final TextEditingController controller; final TextEditingController controller;
final double horizontalPadding;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: EdgeInsets.symmetric(horizontal: horizontalPadding), padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: const BoxDecoration(color: CustomTheme.boxColor), decoration: const BoxDecoration(color: CustomTheme.boxColor),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
content, Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
),
SizedBox( SizedBox(
width: 100, width: 100,
height: 40, height: 40,

View File

@@ -3,12 +3,13 @@ import 'dart:core' hide Match;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/cards/team_card.dart'; import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/game_label.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -18,11 +19,14 @@ class MatchTile extends StatefulWidget {
/// - [match]: The match data to be displayed. /// - [match]: The match data to be displayed.
/// - [onTap]: The callback invoked when the tile is tapped. /// - [onTap]: The callback invoked when the tile is tapped.
/// - [width]: Optional width for the tile. /// - [width]: Optional width for the tile.
/// - [compact]: Whether to display the tile in a compact mode
const MatchTile({ const MatchTile({
super.key, super.key,
required this.match, required this.match,
required this.onTap, required this.onTap,
this.width, this.width,
this.compact = false,
this.onPlayerEdited,
}); });
/// The match data to be displayed. /// The match data to be displayed.
@@ -31,9 +35,15 @@ class MatchTile extends StatefulWidget {
/// The callback invoked when the tile is tapped. /// The callback invoked when the tile is tapped.
final VoidCallback onTap; final VoidCallback onTap;
/// The callback invoked when the players are edited
final VoidCallback? onPlayerEdited;
/// Optional width for the tile. /// Optional width for the tile.
final double? width; final double? width;
/// Whether to display the tile in a compact mode
final bool compact;
@override @override
State<MatchTile> createState() => _MatchTileState(); State<MatchTile> createState() => _MatchTileState();
} }
@@ -96,59 +106,40 @@ class _MatchTileState extends State<MatchTile> {
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
] else if (widget.compact) ...[
Row(
children: [
const Icon(Icons.person, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Expanded(
child: Text(
'${match.players.length} ${loc.players}',
style: const TextStyle(fontSize: 14, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 6),
] else ...[ ] else ...[
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
// Game + Ruleset Badge // Game + Ruleset Badge
GameLabel( if (!widget.compact)
title: match.game.name, GameLabel(
description: translateRulesetToString( title: match.game.name,
match.game.ruleset, description: translateRulesetToString(
context, match.game.ruleset,
context,
),
color: match.game.color,
), ),
color: match.game.color,
),
const SizedBox(height: 12), const SizedBox(height: 12),
// Winner / In Progress Info // Winner / In Progress Info
if (match.isTeamMatch && match.mvt.isNotEmpty) ...[ if (match.mvp.isNotEmpty) ...[
// MVT Display for team matches
Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.green.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
getMvpIcon(),
const SizedBox(width: 8),
Expanded(
child: Text(
getMvtText(loc),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: CustomTheme.textColor,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 12),
] else if (match.mvp.isNotEmpty) ...[
// MVP Display for player matches
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8, vertical: 8,
@@ -182,7 +173,6 @@ class _MatchTileState extends State<MatchTile> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
] else ...[ ] else ...[
// Match in progress display
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8, vertical: 8,
@@ -221,46 +211,8 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
if (match.teams != null && // Players List
match.teams!.isNotEmpty && if (players.isNotEmpty && widget.compact == false) ...[
match.isTeamMatch) ...[
// Team display
Text(
loc.teams,
style: const TextStyle(
fontSize: 13,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
LayoutBuilder(
builder: (context, constraints) {
final useSingleColumn = match.teams!.any(
(team) => team.name.length > 10,
);
const spacing = 8.0;
final itemWidth = useSingleColumn
? constraints.maxWidth
: (constraints.maxWidth - spacing) / 2;
return Wrap(
spacing: spacing,
runSpacing: spacing,
children: match.teams!.map((team) {
return TeamCard(
team: team,
compact: true,
width: itemWidth,
);
}).toList(),
);
},
),
const SizedBox(height: 12),
] else if (players.isNotEmpty) ...[
// Player display
Text( Text(
loc.players, loc.players,
style: const TextStyle( style: const TextStyle(
@@ -277,17 +229,23 @@ class _MatchTileState extends State<MatchTile> {
return TextIconTile( return TextIconTile(
text: player.name, text: player.name,
suffixText: getNameCountText(player), suffixText: getNameCountText(player),
iconEnabled: false,
onTileTap: () {
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => PlayerDetailView(
player: player,
callback: () {
widget.onPlayerEdited?.call();
},
),
),
);
},
); );
}).toList(), }).toList(),
), ),
] else ...[
Text(
loc.no_players_available,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.hintColor,
),
),
], ],
], ],
), ),
@@ -313,7 +271,6 @@ class _MatchTileState extends State<MatchTile> {
} }
} }
// Returns the appropriate text based on the match's ruleset and MVP.
String getMvpText(AppLocalizations loc) { String getMvpText(AppLocalizations loc) {
if (widget.match.mvp.isEmpty) return ''; if (widget.match.mvp.isEmpty) return '';
final ruleset = widget.match.game.ruleset; final ruleset = widget.match.game.ruleset;
@@ -337,41 +294,11 @@ class _MatchTileState extends State<MatchTile> {
return '${loc.winner}: n.A.'; return '${loc.winner}: n.A.';
} }
// Returns the appropriate text based on the match's ruleset and MVT.
String getMvtText(AppLocalizations loc) {
if (widget.match.mvt.isEmpty) return '';
final ruleset = widget.match.game.ruleset;
switch (ruleset) {
case Ruleset.singleWinner:
return '${loc.winner}: ${widget.match.mvt.first.name}';
case Ruleset.singleLoser:
return '${loc.loser}: ${widget.match.mvt.first.name}';
case Ruleset.highestScore:
case Ruleset.lowestScore:
final mvt = widget.match.mvt;
final mvtScore =
widget.match.teams!
.firstWhere((team) => team.id == mvt.first.id)
.score ??
0;
final mvtNames = mvt.map((team) => team.name).join(', ');
return '${loc.winner}: $mvtNames (${getPointLabel(loc, mvtScore)})';
case Ruleset.placement:
return '${loc.winner}: ${widget.match.mvt.first.name}';
case Ruleset.multipleWinners:
final mvtNames = widget.match.mvt.map((team) => team.name).join(', ');
return '${loc.winners}: $mvtNames';
}
}
// Returns the appropriate icon based on the match's ruleset.
Icon getMvpIcon() { Icon getMvpIcon() {
final icon = getRulesetIcon(widget.match.game.ruleset); final icon = getRulesetIcon(widget.match.game.ruleset);
switch (widget.match.game.ruleset) { switch (widget.match.game.ruleset) {
case Ruleset.singleWinner: case Ruleset.singleWinner:
case Ruleset.multipleWinners:
return Icon(icon, size: 20, color: Colors.amber); return Icon(icon, size: 20, color: Colors.amber);
case Ruleset.singleLoser: case Ruleset.singleLoser:
return Icon(icon, size: 20, color: Colors.blue); return Icon(icon, size: 20, color: Colors.blue);
@@ -379,6 +306,8 @@ class _MatchTileState extends State<MatchTile> {
return Icon(icon, size: 20, color: Colors.orange); return Icon(icon, size: 20, color: Colors.orange);
case Ruleset.highestScore: case Ruleset.highestScore:
return Icon(icon, size: 20, color: Colors.green); return Icon(icon, size: 20, color: Colors.green);
case Ruleset.multipleWinners:
return Icon(icon, size: 20, color: Colors.amber);
case Ruleset.placement: case Ruleset.placement:
return Icon(icon, size: 20, color: Colors.deepOrangeAccent); return Icon(icon, size: 20, color: Colors.deepOrangeAccent);
} }

View File

@@ -1,144 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fluttericon/font_awesome_icons.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
class TeamCreationTile extends StatefulWidget {
const TeamCreationTile({
super.key,
required this.color,
required this.controller,
required this.hintText,
this.onDelete,
this.onColorSelection,
});
final AppColor color;
final TextEditingController controller;
final String hintText;
final VoidCallback? onDelete;
final ValueChanged<AppColor>? onColorSelection;
@override
State<TeamCreationTile> createState() => _TeamCreationTileState();
}
class _TeamCreationTileState extends State<TeamCreationTile> {
final teamColors = List.generate(
AppColor.values.length,
(index) => getTeamColor(index),
);
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Container(
margin: CustomTheme.standardMargin,
decoration: CustomTheme.standardBoxDecoration,
clipBehavior: Clip.antiAlias,
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Name input + delete icon
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: TextInputField(
controller: widget.controller,
hintText: widget.hintText,
maxLength: Constants.MAX_TEAM_NAME_LENGTH,
),
),
HapticIconButton(
icon: const Icon(FontAwesome.trash),
color: CustomTheme.textColor,
iconSize: 25,
onPressed: widget.onDelete,
),
],
),
const SizedBox(height: 12),
// Color label
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
loc.color,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
),
),
const SizedBox(height: 8),
// Color picker
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: teamColors.map((color) {
final isSelected = widget.color == color;
return GestureDetector(
onTap: () {
widget.onColorSelection?.call(color);
},
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: getColorFromGameColor(color),
shape: BoxShape.circle,
border: Border.all(
color: isSelected
? Colors.white
: Colors.transparent,
width: 3,
),
),
child: isSelected
? const Icon(
Icons.check,
size: 18,
color: Colors.white,
)
: null,
),
);
}).toList(),
),
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -11,7 +11,6 @@ class TextIconListTile extends StatelessWidget {
required this.text, required this.text,
this.suffixText = '', this.suffixText = '',
this.icon, this.icon,
this.color,
this.onPressed, this.onPressed,
}); });
@@ -24,8 +23,6 @@ class TextIconListTile extends StatelessWidget {
/// The icon to display in the tile. /// The icon to display in the tile.
final IconData? icon; final IconData? icon;
final Color? color;
/// The callback to be invoked when the icon is pressed. /// The callback to be invoked when the icon is pressed.
final VoidCallback? onPressed; final VoidCallback? onPressed;
@@ -34,17 +31,7 @@ class TextIconListTile extends StatelessWidget {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration( decoration: CustomTheme.standardBoxDecoration,
color:
Color.lerp(CustomTheme.onBoxColor, color?.withAlpha(10), 0.1) ??
CustomTheme.boxColor,
border: Border.all(
color: color ?? CustomTheme.boxBorderColor,
width: color != null ? 2 : 1,
strokeAlign: BorderSide.strokeAlignCenter,
),
borderRadius: CustomTheme.standardBorderRadiusAll,
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,

View File

@@ -4,14 +4,15 @@ import 'package:tallee/core/custom_theme.dart';
class TextIconTile extends StatelessWidget { class TextIconTile extends StatelessWidget {
/// A tile widget that displays text with an optional icon that can be tapped. /// A tile widget that displays text with an optional icon that can be tapped.
/// - [text]: The text to display in the tile. /// - [text]: The text to display in the tile.
/// - [iconEnabled]: A boolean to determine if the icon should be displayed.
/// - [onIconTap]: The callback to be invoked when the icon is tapped. /// - [onIconTap]: The callback to be invoked when the icon is tapped.
/// - [icon]: Optional custom icon. Defaults to [Icons.close].
const TextIconTile({ const TextIconTile({
super.key, super.key,
required this.text, required this.text,
this.suffixText = '', this.suffixText = '',
this.iconEnabled = true,
this.onIconTap, this.onIconTap,
this.icon = Icons.close, this.onTileTap,
}); });
/// The text to display in the tile. /// The text to display in the tile.
@@ -19,57 +20,64 @@ class TextIconTile extends StatelessWidget {
final String suffixText; final String suffixText;
/// A boolean to determine if the icon should be displayed.
final bool iconEnabled;
/// The callback to be invoked when the icon is tapped. /// The callback to be invoked when the icon is tapped.
final VoidCallback? onIconTap; final VoidCallback? onIconTap;
/// The icon to display. Defaults to [Icons.close]. /// The callback to be invoked when the tile is tapped.
final IconData icon; final VoidCallback? onTileTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final iconEnabled = onIconTap != null; return GestureDetector(
onTap: onTileTap,
return Container( child: Container(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: CustomTheme.onBoxColor, color: CustomTheme.onBoxColor,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (iconEnabled) const SizedBox(width: 3), if (iconEnabled) const SizedBox(width: 3),
Flexible( Flexible(
child: RichText( child: RichText(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
text: TextSpan( text: TextSpan(
style: DefaultTextStyle.of(context).style, style: DefaultTextStyle.of(context).style,
children: [ children: [
TextSpan( TextSpan(
text: text, text: text,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
),
), ),
), TextSpan(
TextSpan( text: suffixText,
text: suffixText, style: TextStyle(
style: TextStyle( fontSize: 13,
fontSize: 13, fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, color: CustomTheme.textColor.withAlpha(120),
color: CustomTheme.textColor.withAlpha(120), ),
), ),
), ],
], ),
), ),
), ),
), if (iconEnabled) ...<Widget>[
if (iconEnabled) ...<Widget>[ const SizedBox(width: 3),
const SizedBox(width: 3), GestureDetector(
GestureDetector(onTap: onIconTap, child: Icon(icon, size: 20)), onTap: onIconTap,
child: const Icon(Icons.close, size: 20),
),
],
], ],
], ),
), ),
); );
} }

View File

@@ -200,9 +200,13 @@ class DataTransferService {
.map((id) => playerById[id]) .map((id) => playerById[id])
.whereType<Player>() .whereType<Player>()
.toList(); .toList();
final team = Team.fromJson(map);
return team.copyWith(members: members); return Team(
id: map['id'] as String,
name: map['name'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList(); }).toList();
} }
@@ -227,7 +231,6 @@ class DataTransferService {
final endedAt = map['endedAt'] != null final endedAt = map['endedAt'] != null
? DateTime.parse(map['endedAt'] as String) ? DateTime.parse(map['endedAt'] as String)
: null; : null;
final isTeamMatch = map['isTeamMatch'] as bool;
final notes = map['notes'] as String? ?? ''; final notes = map['notes'] as String? ?? '';
final scoresJson = map['scores'] as Map<String, dynamic>? ?? {}; final scoresJson = map['scores'] as Map<String, dynamic>? ?? {};
final scores = scoresJson.map( final scores = scoresJson.map(
@@ -259,7 +262,6 @@ class DataTransferService {
game: game, game: game,
group: group, group: group,
players: players, players: players,
isTeamMatch: isTeamMatch,
teams: teams.isEmpty ? null : teams, teams: teams.isEmpty ? null : teams,
createdAt: createdAt, createdAt: createdAt,
endedAt: endedAt, endedAt: endedAt,
@@ -276,7 +278,7 @@ class DataTransferService {
name: 'Unknown', name: 'Unknown',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: '', description: '',
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
); );
} }

View File

@@ -1,7 +1,7 @@
name: tallee name: tallee
description: "Tracking App for Card Games" description: "Tracking App for Card Games"
publish_to: 'none' publish_to: 'none'
version: 0.0.33+340 version: 0.0.33+273
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1

View File

@@ -194,6 +194,31 @@ void main() {
expect(allGroups, isEmpty); expect(allGroups, isEmpty);
}); });
test('getGroupsByPlayer() works correctly', () async {
await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup2],
);
final groups = await database.groupDao.getGroupsByPlayer(
playerId: testPlayer2.id,
);
expect(groups, hasLength(2));
expect(groups.any((group) => group.id == testGroup1.id), isTrue);
expect(groups.any((group) => group.id == testGroup2.id), isTrue);
});
test(
'getGroupsByPlayer() returns empty list for non-existent player',
() async {
final groups = await database.groupDao.getGroupsByPlayer(
playerId: 'non-existent-player-id',
);
expect(groups, isEmpty);
},
);
test('addGroupsAsList() with duplicate groups only adds once', () async { test('addGroupsAsList() with duplicate groups only adds once', () async {
await database.groupDao.addGroupsAsList( await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup1, testGroup1], groups: [testGroup1, testGroup1, testGroup1],

View File

@@ -56,7 +56,7 @@ void main() {
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A test game', description: 'A test game',
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(
@@ -260,6 +260,34 @@ void main() {
expect(match.group!.id, testGroup1.id); expect(match.group!.id, testGroup1.id);
}); });
test('getMatchesByPlayer() works correctly', () async {
await database.matchDao.addMatchesAsList(
matches: [testMatch1, testMatch2],
);
final matches = await database.matchDao.getMatchesByPlayer(
playerId: testPlayer1.id,
);
expect(matches, hasLength(1));
expect(matches.first.id, testMatch2.id);
expect(
matches.first.players.any((p) => p.id == testPlayer1.id),
isTrue,
);
});
test(
'getMatchesByPlayer() returns empty list for non-existent player',
() async {
final matches = await database.matchDao.getMatchesByPlayer(
playerId: 'non-existing-player-id',
);
expect(matches, isEmpty);
},
);
test('getMatchCount() works correctly', () async { test('getMatchCount() works correctly', () async {
var count = await database.matchDao.getMatchCount(); var count = await database.matchDao.getMatchCount();
expect(count, 0); expect(count, 0);
@@ -507,36 +535,34 @@ void main() {
deleted = await database.matchDao.deleteAllMatches(); deleted = await database.matchDao.deleteAllMatches();
expect(deleted, isFalse); expect(deleted, isFalse);
}); });
});
test('deleteMatchesByGame() deletes all matches for a game', () async { test('deleteMatchesByGame() deletes all matches for a game', () async {
await database.matchDao.addMatch(match: testMatch1); await database.matchDao.addMatch(match: testMatch1);
await database.matchDao.addMatch(match: testMatch2); await database.matchDao.addMatch(match: testMatch2);
var count = await database.matchDao.getMatchCountByGame( var count = await database.matchDao.getMatchCountByGame(
gameId: testGame.id, gameId: testGame.id,
); );
expect(count, 2); expect(count, 2);
final deletedCount = await database.matchDao.deleteMatchesByGame( final deletedCount = await database.matchDao.deleteMatchesByGame(
gameId: testGame.id, gameId: testGame.id,
); );
expect(deletedCount, 2); expect(deletedCount, 2);
count = await database.matchDao.getMatchCountByGame( count = await database.matchDao.getMatchCountByGame(gameId: testGame.id);
gameId: testGame.id, expect(count, 0);
);
expect(count, 0);
final allMatches = await database.matchDao.getAllMatches(); final allMatches = await database.matchDao.getAllMatches();
expect(allMatches, isEmpty); expect(allMatches, isEmpty);
}); });
test('deleteMatchesByGame() returns 0 for non-existent game', () async { test('deleteMatchesByGame() returns 0 for non-existent game', () async {
final deletedCount = await database.matchDao.deleteMatchesByGame( final deletedCount = await database.matchDao.deleteMatchesByGame(
gameId: 'non-existent-game-id', gameId: 'non-existent-game-id',
); );
expect(deletedCount, 0); expect(deletedCount, 0);
});
}); });
}); });
} }

View File

@@ -1,7 +1,7 @@
import 'dart:core' hide Match; import 'dart:core' hide Match;
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' hide isNotNull, isNull; import 'package:drift/drift.dart';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
@@ -49,7 +49,7 @@ void main() {
testGame = Game( testGame = Game(
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.highestScore, ruleset: Ruleset.highestScore,
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(
@@ -327,200 +327,5 @@ void main() {
expect(deleted, isFalse); expect(deleted, isFalse);
}); });
}); });
group('SCORE', () {
test('updateTeamScore() works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final updated = await database.teamDao.updateTeamScore(
teamId: testTeam1.id,
matchId: testMatch1.id,
score: 5,
);
expect(updated, isTrue);
final team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, 5);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 5);
}
});
test('set-/removeWinnerTeam() works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final set = await database.teamDao.setWinnerTeam(
teamId: testTeam1.id,
matchId: testMatch1.id,
);
expect(set, isTrue);
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, 1);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 1);
}
final removed = await database.teamDao.removeWinnerTeam(
matchId: testMatch1.id,
);
expect(removed, isTrue);
team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, isNull);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNull);
}
});
test('set-/removeLoserTeam() works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final set = await database.teamDao.setLoserTeam(
teamId: testTeam1.id,
matchId: testMatch1.id,
);
expect(set, isTrue);
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, 0);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 0);
}
final removed = await database.teamDao.removeLoserTeam(
matchId: testMatch1.id,
);
expect(removed, isTrue);
team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, isNull);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNull);
}
});
test('set-/removeWinnerTeams() works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final set = await database.teamDao.setWinnerTeams(
winners: [testTeam1, testTeam2],
matchId: testMatch1.id,
);
expect(set, isTrue);
// check both teams got the winner score
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, 1);
team = await database.teamDao.getTeamById(teamId: testTeam2.id);
expect(team.score, 1);
// check all members of both teams got the winner score
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 1);
}
for (final member in testTeam2.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 1);
}
final removed = await database.teamDao.removeWinnerTeam(
matchId: testMatch1.id,
);
expect(removed, isTrue);
team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, isNull);
team = await database.teamDao.getTeamById(teamId: testTeam2.id);
expect(team.score, isNull);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNull);
}
for (final member in testTeam2.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNull);
}
});
test('setTeamPlacements() works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final set = await database.teamDao.setTeamPlacements(
teams: [testTeam1, testTeam2],
matchId: testMatch1.id,
);
expect(set, isTrue);
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, 2);
team = await database.teamDao.getTeamById(teamId: testTeam2.id);
expect(team.score, 1);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 2);
}
for (final member in testTeam2.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 1);
}
});
});
}); });
} }

View File

@@ -28,7 +28,7 @@ void main() {
name: 'Chess', name: 'Chess',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A classic strategy game', description: 'A classic strategy game',
color: AppColor.blue, color: GameColor.blue,
icon: 'chess_icon', icon: 'chess_icon',
); );
testGame2 = Game( testGame2 = Game(
@@ -36,7 +36,7 @@ void main() {
name: 'Poker', name: 'Poker',
ruleset: Ruleset.multipleWinners, ruleset: Ruleset.multipleWinners,
description: 'Card game with multiple winners', description: 'Card game with multiple winners',
color: AppColor.red, color: GameColor.red,
icon: 'poker_icon', icon: 'poker_icon',
); );
testGame3 = Game( testGame3 = Game(
@@ -44,7 +44,7 @@ void main() {
name: 'Monopoly', name: 'Monopoly',
ruleset: Ruleset.highestScore, ruleset: Ruleset.highestScore,
description: 'A board game about real estate', description: 'A board game about real estate',
color: AppColor.orange, color: GameColor.orange,
icon: '', icon: '',
); );
}); });
@@ -124,7 +124,7 @@ void main() {
name: 'Game\'s & "Special" <Name>', name: 'Game\'s & "Special" <Name>',
ruleset: Ruleset.multipleWinners, ruleset: Ruleset.multipleWinners,
description: 'Description with émojis 🎮🎲', description: 'Description with émojis 🎮🎲',
color: AppColor.purple, color: GameColor.purple,
icon: '', icon: '',
); );
await database.gameDao.addGame(game: specialGame); await database.gameDao.addGame(game: specialGame);
@@ -280,19 +280,19 @@ void main() {
await database.gameDao.updateGameColor( await database.gameDao.updateGameColor(
gameId: testGame1.id, gameId: testGame1.id,
color: AppColor.green, color: GameColor.green,
); );
final updatedGame = await database.gameDao.getGameById( final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id, gameId: testGame1.id,
); );
expect(updatedGame.color, AppColor.green); expect(updatedGame.color, GameColor.green);
}); });
test('updateGameColor() does nothing for non-existent game', () async { test('updateGameColor() does nothing for non-existent game', () async {
final updated = await database.gameDao.updateGameColor( final updated = await database.gameDao.updateGameColor(
gameId: 'non-existent-id', gameId: 'non-existent-id',
color: AppColor.green, color: GameColor.green,
); );
expect(updated, isFalse); expect(updated, isFalse);
@@ -336,7 +336,7 @@ void main() {
name: newName, name: newName,
); );
const newGameColor = AppColor.teal; const newGameColor = GameColor.teal;
await database.gameDao.updateGameColor( await database.gameDao.updateGameColor(
gameId: testGame1.id, gameId: testGame1.id,
color: newGameColor, color: newGameColor,

View File

@@ -233,6 +233,95 @@ void main() {
expect(allPlayers, isEmpty); expect(allPlayers, isEmpty);
}); });
test('updatePlayerName() sets correct nameCount with 2 player', () async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer2);
final newName = testPlayer1.name;
await database.playerDao.updatePlayerName(
playerId: testPlayer2.id,
name: newName,
);
var player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 1);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 2);
await database.playerDao.updatePlayerName(
playerId: testPlayer1.id,
name: 'different name',
);
player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 0);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 0);
});
test('updatePlayerName() sets correct nameCount with 3 player', () async {
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2, testPlayer3],
);
// Changing both names to player 1's name
final newName = testPlayer1.name;
await database.playerDao.updatePlayerName(
playerId: testPlayer2.id,
name: newName,
);
await database.playerDao.updatePlayerName(
playerId: testPlayer3.id,
name: newName,
);
var player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 1);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 2);
player = await database.playerDao.getPlayerById(
playerId: testPlayer3.id,
);
expect(player.nameCount, 3);
// Changing the middle players name
await database.playerDao.updatePlayerName(
playerId: testPlayer2.id,
name: 'different name',
);
player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 1);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 0);
player = await database.playerDao.getPlayerById(
playerId: testPlayer3.id,
);
expect(player.nameCount, 2);
});
test('updatePlayerDescription() works correctly', () async { test('updatePlayerDescription() works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
@@ -372,14 +461,22 @@ void main() {
final player1 = Player(name: testPlayer1.name, description: ''); final player1 = Player(name: testPlayer1.name, description: '');
await database.playerDao.addPlayer(player: player1); await database.playerDao.addPlayer(player: player1);
final player2 = Player(name: testPlayer1.name, description: '');
await database.playerDao.addPlayer(player: player2);
var players = await database.playerDao.getAllPlayers(); var players = await database.playerDao.getAllPlayers();
expect(players.length, 2); expect(players.length, 3);
players.sort((a, b) => a.nameCount.compareTo(b.nameCount)); players.sort((a, b) => a.nameCount.compareTo(b.nameCount));
for (int i = 0; i < players.length - 1; i++) { for (int i = 0; i < players.length - 1; i++) {
expect(players[i].nameCount, i + 1); expect(players[i].nameCount, i + 1);
} }
// ids are correct in the right order
expect(players[0].id, testPlayer1.id);
expect(players[1].id, player1.id);
expect(players[2].id, player2.id);
}, },
); );
@@ -404,24 +501,62 @@ void main() {
for (int i = 0; i < players.length - 1; i++) { for (int i = 0; i < players.length - 1; i++) {
expect(players[i].nameCount, i + 1); expect(players[i].nameCount, i + 1);
} }
// ids are correct in the right order
expect(players[0].id, testPlayer1.id);
expect(players[1].id, player1.id);
expect(players[2].id, player2.id);
expect(players[3].id, player3.id);
}, },
); );
test('getNameCount works correctly', () async { test('getNameCount works correctly', () async {
final player1 = Player(name: testPlayer1.name);
final player2 = Player(name: testPlayer1.name); final player2 = Player(name: testPlayer1.name);
final player3 = Player(name: testPlayer1.name);
await database.playerDao.addPlayersAsList( await database.playerDao.addPlayer(player: testPlayer1);
players: [testPlayer1, player2, player3],
var nameCount = await database.playerDao.getNameCount(
name: testPlayer1.name,
); );
final nameCount = await database.playerDao.getNameCount( expect(nameCount, 1);
await database.playerDao.addPlayersAsList(players: [player1, player2]);
nameCount = await database.playerDao.getNameCount(
name: testPlayer1.name, name: testPlayer1.name,
); );
expect(nameCount, 3); expect(nameCount, 3);
}); });
test('calculateNameCount works correctly', () async {
final player1 = Player(name: testPlayer1.name);
final player2 = Player(name: testPlayer1.name);
// Case 1: No existing players with the name
var nameCount = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(nameCount, 0);
// Case 2: One existing player with the name. Should return 2 for
// the new player
await database.playerDao.addPlayer(player: testPlayer1);
nameCount = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(nameCount, 2);
// Case 3: Multiple existing players with the name. Should return count + 1
await database.playerDao.addPlayersAsList(players: [player1, player2]);
nameCount = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(nameCount, 4);
});
test('updateNameCount works correctly', () async { test('updateNameCount works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
@@ -441,14 +576,24 @@ void main() {
final player2 = Player(name: testPlayer1.name, description: ''); final player2 = Player(name: testPlayer1.name, description: '');
final player3 = Player(name: testPlayer1.name, description: ''); final player3 = Player(name: testPlayer1.name, description: '');
await database.playerDao.addPlayersAsList( await database.playerDao.addPlayer(player: testPlayer1);
players: [testPlayer1, player2, player3], var player = await database.playerDao.getPlayerWithHighestNameCount(
);
final player = await database.playerDao.getPlayerWithHighestNameCount(
name: testPlayer1.name, name: testPlayer1.name,
); );
expect(player, isNotNull);
expect(player!.nameCount, 0);
await database.playerDao.addPlayer(player: player2);
player = await database.playerDao.getPlayerWithHighestNameCount(
name: testPlayer1.name,
);
expect(player, isNotNull);
expect(player!.nameCount, 2);
await database.playerDao.addPlayer(player: player3);
player = await database.playerDao.getPlayerWithHighestNameCount(
name: testPlayer1.name,
);
expect(player, isNotNull); expect(player, isNotNull);
expect(player!.nameCount, 3); expect(player!.nameCount, 3);
}); });
@@ -460,32 +605,6 @@ void main() {
expect(player, isNull); expect(player, isNull);
}); });
test('calculateNameCount works correctly', () async {
// Case 1: No existing players with the name
var count = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(count, 0);
// Case 2: One existing player with the name. Should update that
// player's nameCount to 1 and return 2 for the new player
await database.playerDao.addPlayer(player: testPlayer1);
count = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(count, 2);
// Case 3: Multiple existing players with the name.
final player2 = Player(name: testPlayer1.name, nameCount: count);
await database.playerDao.addPlayer(player: player2);
count = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(count, 3);
});
test('getPlayerWithHighestNameCount with non existing player', () async { test('getPlayerWithHighestNameCount with non existing player', () async {
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.initializeNameCount(name: testPlayer1.name); await database.playerDao.initializeNameCount(name: testPlayer1.name);

View File

@@ -42,7 +42,7 @@ void main() {
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A test game', description: 'A test game',
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(

View File

@@ -40,7 +40,7 @@ void main() {
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A test game', description: 'A test game',
color: AppColor.blue, color: GameColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(

View File

@@ -45,7 +45,7 @@ void main() {
name: 'Chess', name: 'Chess',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'Strategic board game', description: 'Strategic board game',
color: AppColor.blue, color: GameColor.blue,
icon: 'chess_icon', icon: 'chess_icon',
); );
@@ -55,12 +55,7 @@ void main() {
members: [testPlayer1, testPlayer2], members: [testPlayer1, testPlayer2],
); );
testTeam = Team( testTeam = Team(name: 'Test Team', members: [testPlayer1, testPlayer2]);
name: 'Test Team',
color: AppColor.yellow,
score: 5,
members: [testPlayer1, testPlayer2],
);
testMatch = Match( testMatch = Match(
name: 'Test Match', name: 'Test Match',
@@ -142,6 +137,9 @@ void main() {
await database.playerDao.addPlayer(player: testPlayer2); await database.playerDao.addPlayer(player: testPlayer2);
await database.gameDao.addGame(game: testGame); await database.gameDao.addGame(game: testGame);
await database.groupDao.addGroup(group: testGroup); await database.groupDao.addGroup(group: testGroup);
/*
await database.teamDao.addTeam(team: testTeam);
*/
await database.matchDao.addMatch(match: testMatch); await database.matchDao.addMatch(match: testMatch);
final ctx = await getContext(tester); final ctx = await getContext(tester);
@@ -450,19 +448,19 @@ void main() {
Game( Game(
name: 'Red Game', name: 'Red Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: AppColor.red, color: GameColor.red,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Blue Game', name: 'Blue Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: AppColor.blue, color: GameColor.blue,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Green Game', name: 'Green Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: AppColor.green, color: GameColor.green,
icon: 'icon', icon: 'icon',
), ),
]; ];
@@ -486,19 +484,19 @@ void main() {
Game( Game(
name: 'Highest Score Game', name: 'Highest Score Game',
ruleset: Ruleset.highestScore, ruleset: Ruleset.highestScore,
color: AppColor.blue, color: GameColor.blue,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Lowest Score Game', name: 'Lowest Score Game',
ruleset: Ruleset.lowestScore, ruleset: Ruleset.lowestScore,
color: AppColor.blue, color: GameColor.blue,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Single Winner', name: 'Single Winner',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: AppColor.blue, color: GameColor.blue,
icon: 'icon', icon: 'icon',
), ),
]; ];
@@ -671,8 +669,6 @@ void main() {
'name': testTeam.name, 'name': testTeam.name,
'memberIds': [testPlayer1.id], 'memberIds': [testPlayer1.id],
'createdAt': testTeam.createdAt.toIso8601String(), 'createdAt': testTeam.createdAt.toIso8601String(),
'color': testTeam.color.name,
'score': testTeam.score,
}, },
]; ];
@@ -686,8 +682,6 @@ void main() {
expect(teams[0].name, testTeam.name); expect(teams[0].name, testTeam.name);
expect(teams[0].members.length, 1); expect(teams[0].members.length, 1);
expect(teams[0].members[0].id, testPlayer1.id); expect(teams[0].members[0].id, testPlayer1.id);
expect(teams[0].color, testTeam.color);
expect(teams[0].score, testTeam.score);
}); });
test('parseTeamsFromJson() empty list', () { test('parseTeamsFromJson() empty list', () {
@@ -724,9 +718,6 @@ void main() {
'gameId': testGame.id, 'gameId': testGame.id,
'groupId': testGroup.id, 'groupId': testGroup.id,
'playerIds': [testPlayer1.id, testPlayer2.id], 'playerIds': [testPlayer1.id, testPlayer2.id],
'isTeamMatch': false,
'teams': null,
'scores': null,
'notes': testMatch.notes, 'notes': testMatch.notes,
'createdAt': testMatch.createdAt.toIso8601String(), 'createdAt': testMatch.createdAt.toIso8601String(),
}, },
@@ -782,9 +773,6 @@ void main() {
'name': testMatch.name, 'name': testMatch.name,
'gameId': 'non-existent-game-id', 'gameId': 'non-existent-game-id',
'playerIds': [testPlayer1.id], 'playerIds': [testPlayer1.id],
'isTeamMatch': false,
'teams': null,
'scores': null,
'notes': '', 'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(), 'createdAt': testMatch.createdAt.toIso8601String(),
}, },
@@ -816,9 +804,6 @@ void main() {
'gameId': testGame.id, 'gameId': testGame.id,
'groupId': null, 'groupId': null,
'playerIds': [testPlayer1.id], 'playerIds': [testPlayer1.id],
'isTeamMatch': false,
'teams': null,
'scores': null,
'notes': '', 'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(), 'createdAt': testMatch.createdAt.toIso8601String(),
}, },
@@ -849,9 +834,6 @@ void main() {
'name': testMatch.name, 'name': testMatch.name,
'gameId': testGame.id, 'gameId': testGame.id,
'playerIds': [testPlayer1.id], 'playerIds': [testPlayer1.id],
'isTeamMatch': false,
'teams': null,
'scores': null,
'notes': '', 'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(), 'createdAt': testMatch.createdAt.toIso8601String(),
'endedAt': endedDate.toIso8601String(), 'endedAt': endedDate.toIso8601String(),
@@ -871,7 +853,7 @@ void main() {
}); });
}); });
test('validateJsonSchema() works correctly', () async { test('validateJsonSchema()', () async {
final validJson = json.encode({ final validJson = json.encode({
'players': [ 'players': [
{ {
@@ -915,15 +897,6 @@ void main() {
}, },
'createdAt': testMatch.createdAt.toIso8601String(), 'createdAt': testMatch.createdAt.toIso8601String(),
'endedAt': null, 'endedAt': null,
'isTeamMatch': true,
'teams': [
{
'id': testTeam.id,
'name': testTeam.name,
'memberIds': [testPlayer1.id, testPlayer2.id],
'createdAt': testTeam.createdAt.toIso8601String(),
},
],
}, },
], ],
}); });
@@ -931,28 +904,5 @@ void main() {
final isValid = await DataTransferService.validateJsonSchema(validJson); final isValid = await DataTransferService.validateJsonSchema(validJson);
expect(isValid, true); expect(isValid, true);
}); });
testWidgets('validateJsonSchema() validates exported json file', (
tester,
) async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer2);
await database.gameDao.addGame(game: testGame);
await database.groupDao.addGroup(group: testGroup);
await database.matchDao.addMatch(match: testMatch);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
expect(jsonString, isNotEmpty);
// Schema validation requires real async operations (rootBundle,
// HttpClient within json_schema). These must run via
// tester.runAsync, otherwise the test hangs due to a pending timer.
final isValid = await tester.runAsync(
() => DataTransferService.validateJsonSchema(jsonString),
);
expect(isValid, true);
});
}); });
} }