1 Commits

Author SHA1 Message Date
f0379e97c7 Merge pull request 'MVP' (#141) from development into main
Reviewed-on: #141
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
2026-01-09 12:55:50 +00:00
37 changed files with 652 additions and 1092 deletions

View File

@@ -1,19 +0,0 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
Route<T> adaptivePageRoute<T>({
required Widget Function(BuildContext) builder,
bool fullscreenDialog = false,
}) {
if (Platform.isIOS) {
return CupertinoPageRoute<T>(
builder: builder,
fullscreenDialog: fullscreenDialog,
);
}
return MaterialPageRoute<T>(
builder: builder,
fullscreenDialog: fullscreenDialog,
);
}

View File

@@ -1,179 +0,0 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/dto/game.dart';
part 'game_dao.g.dart';
@DriftAccessor(tables: [GameTable])
class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
GameDao(super.db);
/// Retrieves all games from the database.
Future<List<Game>> getAllGames() async {
final query = select(gameTable);
final result = await query.get();
return result
.map(
(row) => Game(
id: row.id,
name: row.name,
ruleset: row.ruleset,
description: row.description,
color: row.color != null ? int.tryParse(row.color!) : null,
icon: row.icon,
createdAt: row.createdAt,
),
)
.toList();
}
/// Retrieves a [Game] by its [gameId].
Future<Game> getGameById({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId));
final result = await query.getSingle();
return Game(
id: result.id,
name: result.name,
ruleset: result.ruleset,
description: result.description,
color: result.color != null ? int.tryParse(result.color!) : null,
icon: result.icon,
createdAt: result.createdAt,
);
}
/// Adds a new [game] to the database.
/// If a game with the same ID already exists, no action is taken.
/// Returns `true` if the game was added, `false` otherwise.
Future<bool> addGame({required Game game}) async {
if (!await gameExists(gameId: game.id)) {
await into(gameTable).insert(
GameTableCompanion.insert(
id: game.id,
name: game.name,
ruleset: game.ruleset ?? '',
description: Value(game.description),
color: Value(game.color?.toString()),
icon: Value(game.icon),
createdAt: game.createdAt,
),
mode: InsertMode.insertOrReplace,
);
return true;
}
return false;
}
/// Adds multiple [games] to the database in a batch operation.
/// Uses insertOrIgnore to avoid overwriting existing games.
Future<bool> addGamesAsList({required List<Game> games}) async {
if (games.isEmpty) return false;
await db.batch(
(b) => b.insertAll(
gameTable,
games
.map(
(game) => GameTableCompanion.insert(
id: game.id,
name: game.name,
ruleset: game.ruleset ?? '',
description: Value(game.description),
color: Value(game.color?.toString()),
icon: Value(game.icon),
createdAt: game.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrIgnore,
),
);
return true;
}
/// Deletes the game with the given [gameId] from the database.
/// Returns `true` if the game was deleted, `false` if the game did not exist.
Future<bool> deleteGame({required String gameId}) async {
final query = delete(gameTable)..where((g) => g.id.equals(gameId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Checks if a game with the given [gameId] exists in the database.
/// Returns `true` if the game exists, `false` otherwise.
Future<bool> gameExists({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId));
final result = await query.getSingleOrNull();
return result != null;
}
/// Updates the name of the game with the given [gameId] to [newName].
Future<void> updateGameName({
required String gameId,
required String newName,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(name: Value(newName)),
);
}
/// Updates the ruleset of the game with the given [gameId].
Future<void> updateGameRuleset({
required String gameId,
required String newRuleset,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(ruleset: Value(newRuleset)),
);
}
/// Updates the description of the game with the given [gameId].
Future<void> updateGameDescription({
required String gameId,
required String? newDescription,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(description: Value(newDescription)),
);
}
/// Updates the color of the game with the given [gameId].
Future<void> updateGameColor({
required String gameId,
required int? newColor,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(color: Value(newColor?.toString())),
);
}
/// Updates the icon of the game with the given [gameId].
Future<void> updateGameIcon({
required String gameId,
required String? newIcon,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(icon: Value(newIcon)),
);
}
/// Retrieves the total count of games in the database.
Future<int> getGameCount() async {
final count =
await (selectOnly(gameTable)..addColumns([gameTable.id.count()]))
.map((row) => row.read(gameTable.id.count()))
.getSingle();
return count ?? 0;
}
/// Deletes all games from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteAllGames() async {
final query = delete(gameTable);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
}

View File

@@ -23,7 +23,6 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
return Group(
id: groupData.id,
name: groupData.name,
description: groupData.description,
members: members,
createdAt: groupData.createdAt,
);
@@ -43,7 +42,6 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
return Group(
id: result.id,
name: result.name,
description: result.description,
members: members,
createdAt: result.createdAt,
);
@@ -58,7 +56,6 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
GroupTableCompanion.insert(
id: group.id,
name: group.name,
description: Value(group.description),
createdAt: group.createdAt,
),
mode: InsertMode.insertOrReplace,
@@ -108,7 +105,6 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
(group) => GroupTableCompanion.insert(
id: group.id,
name: group.name,
description: Value(group.description),
createdAt: group.createdAt,
),
)
@@ -136,7 +132,6 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
(p) => PlayerTableCompanion.insert(
id: p.id,
name: p.name,
description: Value(p.description),
createdAt: p.createdAt,
),
)

View File

@@ -1,46 +1,50 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/group_match_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/dto/group.dart';
part 'group_match_dao.g.dart';
@DriftAccessor(tables: [GroupMatchTable, GroupTable])
@DriftAccessor(tables: [GroupMatchTable])
class GroupMatchDao extends DatabaseAccessor<AppDatabase>
with _$GroupMatchDaoMixin {
GroupMatchDao(super.db);
/// Adds a group to a match by inserting a record into the [GroupMatchTable].
/// Associates a group with a match by inserting a record into the
/// [GroupMatchTable].
Future<void> addGroupToMatch({
required String matchId,
required String groupId,
}) async {
if (await matchHasGroup(matchId: matchId)) {
throw Exception('Match already has a group');
}
await into(groupMatchTable).insert(
GroupMatchTableCompanion.insert(
matchId: matchId,
groupId: groupId,
),
GroupMatchTableCompanion.insert(groupId: groupId, matchId: matchId),
mode: InsertMode.insertOrIgnore,
);
}
/// Retrieves the [Group] associated with the given [matchId].
/// Returns null if no group is found.
/// Returns `null` if no group is found.
Future<Group?> getGroupOfMatch({required String matchId}) async {
final query = select(groupMatchTable)
..where((gm) => gm.matchId.equals(matchId));
final result = await query.getSingleOrNull();
final result = await (select(
groupMatchTable,
)..where((g) => g.matchId.equals(matchId))).getSingleOrNull();
if (result == null) return null;
return db.groupDao.getGroupById(groupId: result.groupId);
if (result == null) {
return null;
}
/// Checks if a match has a group associated with it.
/// Returns `true` if the match has a group, otherwise `false`.
final group = await db.groupDao.getGroupById(groupId: result.groupId);
return group;
}
/// Checks if there is a group associated with the given [matchId].
/// Returns `true` if there is a group, otherwise `false`.
Future<bool> matchHasGroup({required String matchId}) async {
final count = await (selectOnly(groupMatchTable)
final count =
await (selectOnly(groupMatchTable)
..where(groupMatchTable.matchId.equals(matchId))
..addColumns([groupMatchTable.groupId.count()]))
.map((row) => row.read(groupMatchTable.groupId.count()))
@@ -54,55 +58,41 @@ class GroupMatchDao extends DatabaseAccessor<AppDatabase>
required String matchId,
required String groupId,
}) async {
final query = select(groupMatchTable)
final count =
await (selectOnly(groupMatchTable)
..where(
(gm) => gm.matchId.equals(matchId) & gm.groupId.equals(groupId),
);
final result = await query.getSingleOrNull();
return result != null;
groupMatchTable.matchId.equals(matchId) &
groupMatchTable.groupId.equals(groupId),
)
..addColumns([groupMatchTable.groupId.count()]))
.map((row) => row.read(groupMatchTable.groupId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Removes the association of a group with a match by deleting the record
/// from the [GroupMatchTable].
/// Removes the association of a group from a match based on [groupId] and
/// [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeGroupFromMatch({
required String matchId,
required String groupId,
}) async {
final query = delete(groupMatchTable)
..where((gm) => gm.matchId.equals(matchId) & gm.groupId.equals(groupId));
..where((g) => g.matchId.equals(matchId) & g.groupId.equals(groupId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Updates the group associated with a match.
/// Removes the existing group association and adds the new one.
Future<void> updateGroupOfMatch({
/// Updates the group associated with a match to [newGroupId] based on
/// [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGroupOfMatch({
required String matchId,
required String newGroupId,
}) async {
await db.transaction(() async {
// Remove existing group association
await (delete(groupMatchTable)
..where((gm) => gm.matchId.equals(matchId)))
.go();
// Add new group association
await into(groupMatchTable).insert(
GroupMatchTableCompanion.insert(
matchId: matchId,
groupId: newGroupId,
),
);
});
}
/// Retrieves all matches associated with a specific group.
Future<List<String>> getMatchIdsForGroup({required String groupId}) async {
final query = select(groupMatchTable)
..where((gm) => gm.groupId.equals(groupId));
final result = await query.get();
return result.map((row) => row.matchId).toList();
final updatedRows =
await (update(groupMatchTable)..where((g) => g.matchId.equals(matchId)))
.write(GroupMatchTableCompanion(groupId: Value(newGroupId)));
return updatedRows > 0;
}
}

View File

@@ -0,0 +1,10 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'group_match_dao.dart';
// ignore_for_file: type=lint
mixin _$GroupMatchDaoMixin on DatabaseAccessor<AppDatabase> {
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$GroupMatchTableTable get groupMatchTable => attachedDatabase.groupMatchTable;
}

View File

@@ -1,17 +1,13 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_match_table.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
part 'match_dao.g.dart';
@DriftAccessor(tables: [MatchTable, GameTable, GroupTable, PlayerMatchTable])
@DriftAccessor(tables: [MatchTable])
class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
MatchDao(super.db);
@@ -22,22 +18,20 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return Future.wait(
result.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 group = await db.groupMatchDao.getGroupOfMatch(matchId: row.id);
final players = await db.playerMatchDao.getPlayersOfMatch(
matchId: row.id,
);
final winner = row.winnerId != null
? await db.playerDao.getPlayerById(playerId: row.winnerId!)
: null;
return Match(
id: row.id,
name: row.name ?? '',
game: game,
name: row.name,
group: group,
players: players,
notes: row.notes,
createdAt: row.createdAt,
winner: winner,
);
}),
);
@@ -48,110 +42,100 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
final query = select(matchTable)..where((g) => g.id.equals(matchId));
final result = await query.getSingle();
final game = await db.gameDao.getGameById(gameId: result.gameId);
Group? group;
if (result.groupId != null) {
group = await db.groupDao.getGroupById(groupId: result.groupId!);
}
List<Player>? players;
if (await db.playerMatchDao.matchHasPlayers(matchId: matchId)) {
players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
}
Group? group;
if (await db.groupMatchDao.matchHasGroup(matchId: matchId)) {
group = await db.groupMatchDao.getGroupOfMatch(matchId: matchId);
}
Player? winner;
if (result.winnerId != null) {
winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
}
return Match(
id: result.id,
name: result.name ?? '',
game: game,
group: group,
name: result.name,
players: players,
notes: result.notes,
group: group,
winner: winner,
createdAt: result.createdAt,
);
}
/// Adds a new [Match] to the database. Also adds players associations.
/// This method assumes that the game and group (if any) are already present
/// in the database.
/// Adds a new [Match] to the database. Also adds players and group
/// associations. This method assumes that the players and groups added to
/// this match are already present in the database.
Future<void> addMatch({required Match match}) async {
if (match.game == null) {
throw ArgumentError('Match must have a game associated with it');
}
await db.transaction(() async {
await into(matchTable).insert(
MatchTableCompanion.insert(
id: match.id,
gameId: match.game!.id,
groupId: Value(match.group?.id),
name: Value(match.name),
notes: Value(match.notes),
name: match.name,
winnerId: Value(match.winner?.id),
createdAt: match.createdAt,
),
mode: InsertMode.insertOrReplace,
);
if (match.players != null) {
for (final p in match.players!) {
for (final p in match.players ?? []) {
await db.playerMatchDao.addPlayerToMatch(
matchId: match.id,
playerId: p.id,
);
}
}
if (match.group != null) {
await db.groupMatchDao.addGroupToMatch(
matchId: match.id,
groupId: match.group!.id,
);
}
});
}
/// Adds multiple [Match]es to the database in a batch operation.
/// Adds multiple [Match]s to the database in a batch operation.
/// Also adds associated players and groups if they exist.
/// If the [matches] list is empty, the method returns immediately.
/// This method should only be used to import matches from a different device.
/// This Method should only be used to import matches from a different device.
Future<void> addMatchAsList({required List<Match> matches}) async {
if (matches.isEmpty) return;
await db.transaction(() async {
// Add all games first (deduplicated)
final uniqueGames = <String, Game>{};
for (final match in matches) {
if (match.game != null) {
uniqueGames[match.game!.id] = match.game!;
}
}
if (uniqueGames.isNotEmpty) {
// Add all matches in batch
await db.batch(
(b) => b.insertAll(
db.gameTable,
uniqueGames.values
matchTable,
matches
.map(
(game) => GameTableCompanion.insert(
id: game.id,
name: game.name,
ruleset: game.ruleset ?? '',
description: Value(game.description),
color: Value(game.color?.toString()),
icon: Value(game.icon),
createdAt: game.createdAt,
(match) => MatchTableCompanion.insert(
id: match.id,
name: match.name,
createdAt: match.createdAt,
winnerId: Value(match.winner?.id),
),
)
.toList(),
mode: InsertMode.insertOrIgnore,
mode: InsertMode.insertOrReplace,
),
);
}
// Add all groups of the matches in batch
// Using insertOrIgnore to avoid overwriting existing groups (which would
// trigger cascade deletes on player_group associations)
await db.batch(
(b) => b.insertAll(
db.groupTable,
matches
.where((match) => match.group != null)
.map(
(match) => GroupTableCompanion.insert(
id: match.group!.id,
name: match.group!.name,
description: Value(match.group!.description),
createdAt: match.group!.createdAt,
(matches) => GroupTableCompanion.insert(
id: matches.group!.id,
name: matches.group!.name,
createdAt: matches.group!.createdAt,
),
)
.toList(),
@@ -159,27 +143,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
),
);
// Add all matches in batch
await db.batch(
(b) => b.insertAll(
matchTable,
matches
.where((match) => match.game != null)
.map(
(match) => MatchTableCompanion.insert(
id: match.id,
gameId: match.game!.id,
groupId: Value(match.group?.id),
name: Value(match.name),
notes: Value(match.notes),
createdAt: match.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
// Add all players of the matches in batch (unique)
final uniquePlayers = <String, Player>{};
for (final match in matches) {
@@ -197,6 +160,8 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
}
if (uniquePlayers.isNotEmpty) {
// Using insertOrIgnore to avoid triggering cascade deletes on
// player_group/player_match associations when players already exist
await db.batch(
(b) => b.insertAll(
db.playerTable,
@@ -205,7 +170,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
(p) => PlayerTableCompanion.insert(
id: p.id,
name: p.name,
description: Value(p.description),
createdAt: p.createdAt,
),
)
@@ -219,13 +183,12 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
await db.batch((b) {
for (final match in matches) {
if (match.players != null) {
for (final p in match.players!) {
for (final p in match.players ?? []) {
b.insert(
db.playerMatchTable,
PlayerMatchTableCompanion.insert(
matchId: match.id,
playerId: p.id,
score: 0,
),
mode: InsertMode.insertOrIgnore,
);
@@ -251,6 +214,22 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
}
}
});
// Add all group-match associations in batch
await db.batch((b) {
for (final match in matches) {
if (match.group != null) {
b.insert(
db.groupMatchTable,
GroupMatchTableCompanion.insert(
matchId: match.id,
groupId: match.group!.id,
),
mode: InsertMode.insertOrIgnore,
);
}
}
});
});
}
@@ -287,20 +266,52 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return rowsAffected > 0;
}
/// Updates the notes of the match with the given [matchId].
/// Sets the winner of the match with the given [matchId] to the player with
/// the given [winnerId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchNotes({
Future<bool> setWinner({
required String matchId,
required String? notes,
required String winnerId,
}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
MatchTableCompanion(notes: Value(notes)),
MatchTableCompanion(winnerId: Value(winnerId)),
);
return rowsAffected > 0;
}
/// Changes the name of the match with the given [matchId] to [newName].
/// Retrieves the winner of the match with the given [matchId].
/// Returns the [Player] who won the match, or `null` if no winner is set.
Future<Player?> getWinner({required String matchId}) async {
final query = select(matchTable)..where((g) => g.id.equals(matchId));
final result = await query.getSingleOrNull();
if (result == null || result.winnerId == null) {
return null;
}
final winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
return winner;
}
/// Removes the winner of the match with the given [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeWinner({required String matchId}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
const MatchTableCompanion(winnerId: Value(null)),
);
return rowsAffected > 0;
}
/// Checks if the match with the given [matchId] has a winner set.
/// Returns `true` if a winner is set, otherwise `false`.
Future<bool> hasWinner({required String matchId}) async {
final query = select(matchTable)
..where((g) => g.id.equals(matchId) & g.winnerId.isNotNull());
final result = await query.getSingleOrNull();
return result != null;
}
/// Changes the title of the match with the given [matchId] to [newName].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchName({
required String matchId,
@@ -312,31 +323,4 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
);
return rowsAffected > 0;
}
/// Updates the game of the match with the given [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchGame({
required String matchId,
required String gameId,
}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
MatchTableCompanion(gameId: Value(gameId)),
);
return rowsAffected > 0;
}
/// Updates the group of the match with the given [matchId].
/// Pass null to remove the group association.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchGroup({
required String matchId,
required String? groupId,
}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
MatchTableCompanion(groupId: Value(groupId)),
);
return rowsAffected > 0;
}
}

View File

@@ -1,10 +1,12 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:game_tracker/data/dao/group_dao.dart';
import 'package:game_tracker/data/dao/group_match_dao.dart';
import 'package:game_tracker/data/dao/match_dao.dart';
import 'package:game_tracker/data/dao/player_dao.dart';
import 'package:game_tracker/data/dao/player_group_dao.dart';
import 'package:game_tracker/data/dao/player_match_dao.dart';
import 'package:game_tracker/data/db/tables/group_match_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_group_table.dart';
@@ -20,14 +22,16 @@ part 'database.g.dart';
GroupTable,
MatchTable,
PlayerGroupTable,
PlayerMatchTable
PlayerMatchTable,
GroupMatchTable,
],
daos: [
PlayerDao,
GroupDao,
MatchDao,
PlayerGroupDao,
PlayerMatchDao
PlayerMatchDao,
GroupMatchDao,
],
)
class AppDatabase extends _$AppDatabase {

View File

@@ -1,14 +0,0 @@
import 'package:drift/drift.dart';
class GameTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get ruleset => text()();
TextColumn get description => text().nullable()();
TextColumn get color => text().nullable()();
TextColumn get icon => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override
Set<Column<Object>> get primaryKey => {id};
}

View File

@@ -0,0 +1,13 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
class GroupMatchTable extends Table {
TextColumn get groupId =>
text().references(GroupTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
@override
Set<Column<Object>> get primaryKey => {groupId, matchId};
}

View File

@@ -3,7 +3,6 @@ import 'package:drift/drift.dart';
class GroupTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get description => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override

View File

@@ -1,15 +1,9 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
class MatchTable extends Table {
TextColumn get id => text()();
TextColumn get gameId =>
text().references(GameTable, #id, onDelete: KeyAction.cascade)();
TextColumn get groupId =>
text().references(GroupTable, #id, onDelete: KeyAction.cascade).nullable()(); // Nullable if not part of a group
TextColumn get name => text().nullable()();
TextColumn get notes => text().nullable()();
TextColumn get name => text()();
late final winnerId = text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override

View File

@@ -1,16 +1,12 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart';
import 'package:game_tracker/data/db/tables/team_table.dart';
class PlayerMatchTable extends Table {
TextColumn get playerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
TextColumn get teamId =>
text().references(TeamTable, #id).nullable()();
IntColumn get score => integer()();
@override
Set<Column<Object>> get primaryKey => {playerId, matchId};

View File

@@ -3,7 +3,6 @@ import 'package:drift/drift.dart';
class PlayerTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get description => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override

View File

@@ -1,16 +0,0 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart';
class ScoreTable extends Table {
TextColumn get playerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
IntColumn get roundNumber => integer()();
IntColumn get score => integer()();
IntColumn get change => integer()();
@override
Set<Column<Object>> get primaryKey => {playerId, matchId, roundNumber};
}

View File

@@ -1,10 +0,0 @@
import 'package:drift/drift.dart';
class TeamTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
DateTimeColumn get createdAt => dateTime()();
@override
Set<Column<Object>> get primaryKey => {id};
}

View File

@@ -1,50 +0,0 @@
import 'package:clock/clock.dart';
import 'package:uuid/uuid.dart';
class Game {
final String id;
final DateTime createdAt;
final String name;
final String? ruleset;
final String? description;
final int? color;
final String? icon;
Game({
String? id,
DateTime? createdAt,
required this.name,
this.ruleset,
this.description,
this.color,
this.icon,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@override
String toString() {
return 'Game{id: $id, name: $name, ruleset: $ruleset, description: $description, color: $color, icon: $icon}';
}
/// Creates a Game instance from a JSON object.
Game.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'],
ruleset = json['ruleset'],
description = json['description'],
color = json['color'],
icon = json['icon'];
/// Converts the Game instance to a JSON object.
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'name': name,
'ruleset': ruleset,
'description': description,
'color': color,
'icon': icon,
};
}

View File

@@ -4,23 +4,21 @@ import 'package:uuid/uuid.dart';
class Group {
final String id;
final String name;
final String? description;
final DateTime createdAt;
final String name;
final List<Player> members;
Group({
String? id,
DateTime? createdAt,
required this.name,
this.description,
required this.members,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@override
String toString() {
return 'Group{id: $id, name: $name, description: $description, members: $members}';
return 'Group{id: $id, name: $name,members: $members}';
}
/// Creates a Group instance from a JSON object.
@@ -28,7 +26,6 @@ class Group {
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'],
description = json['description'],
members = (json['members'] as List)
.map((memberJson) => Player.fromJson(memberJson))
.toList();
@@ -38,7 +35,6 @@ class Group {
'id': id,
'createdAt': createdAt.toIso8601String(),
'name': name,
'description': description,
'members': members.map((member) => member.toJson()).toList(),
};
}

View File

@@ -1,5 +1,4 @@
import 'package:clock/clock.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:uuid/uuid.dart';
@@ -8,49 +7,45 @@ class Match {
final String id;
final DateTime createdAt;
final String name;
final Game? game;
final Group? group;
final List<Player>? players;
final String? notes;
final Group? group;
final Player? winner;
Match({
String? id,
DateTime? createdAt,
required this.name,
this.game,
this.group,
this.players,
this.notes,
this.group,
this.winner,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@override
String toString() {
return 'Match{id: $id, name: $name, game: $game, group: $group, players: $players, notes: $notes}';
return 'Match{\n\tid: $id,\n\tname: $name,\n\tplayers: $players,\n\tgroup: $group,\n\twinner: $winner\n}';
}
/// Creates a Match instance from a JSON object.
Match.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'],
game = json['game'] != null ? Game.fromJson(json['game']) : null,
group = json['group'] != null ? Group.fromJson(json['group']) : null,
createdAt = DateTime.parse(json['createdAt']),
players = json['players'] != null
? (json['players'] as List)
.map((playerJson) => Player.fromJson(playerJson))
.toList()
: null,
notes = json['notes'];
group = json['group'] != null ? Group.fromJson(json['group']) : null,
winner = json['winner'] != null ? Player.fromJson(json['winner']) : null;
/// Converts the Match instance to a JSON object.
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'name': name,
'game': game?.toJson(),
'group': group?.toJson(),
'players': players?.map((player) => player.toJson()).toList(),
'notes': notes,
'group': group?.toJson(),
'winner': winner?.toJson(),
};
}

View File

@@ -1,16 +0,0 @@
import 'package:game_tracker/data/dto/team.dart';
class Pair extends Team {
Pair({
super.id,
super.createdAt,
required super.members,
});
@override
String toString() {
return 'Pair{id: $id, members: $members}';
}
}

View File

@@ -5,33 +5,26 @@ class Player {
final String id;
final DateTime createdAt;
final String name;
final String? description;
Player({
String? id,
DateTime? createdAt,
required this.name,
this.description,
}) : id = id ?? const Uuid().v4(),
Player({String? id, DateTime? createdAt, required this.name})
: id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@override
String toString() {
return 'Player{id: $id, name: $name, description: $description}';
return 'Player{id: $id,name: $name}';
}
/// Creates a Player instance from a JSON object.
Player.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'],
description = json['description'];
name = json['name'];
/// Converts the Player instance to a JSON object.
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'name': name,
'description': description,
};
}

View File

@@ -1,37 +0,0 @@
import 'package:clock/clock.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:uuid/uuid.dart';
class Team {
final String id;
final DateTime createdAt;
final List<Player> members;
Team({
String? id,
DateTime? createdAt,
required this.members,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@override
String toString() {
return 'Team{id: $id, members: $members}';
}
/// Creates a Team instance from a JSON object.
Team.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
members = (json['members'] as List)
.map((memberJson) => Player.fromJson(memberJson))
.toList();
/// Converts the Team instance to a JSON object.
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'members': members.map((member) => member.toJson()).toList(),
};
}

View File

@@ -17,7 +17,7 @@
"data_successfully_imported": "Daten erfolgreich importiert",
"days_ago": "vor {count} Tagen",
"delete": "Löschen",
"delete_all_data": "Alle Daten löschen",
"delete_all_data": "Alle Daten löschen?",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
"error_reading_file": "Fehler beim Lesen der Datei",
"export_canceled": "Export abgebrochen",

View File

@@ -277,7 +277,7 @@
"data_successfully_imported": "Data successfully imported",
"days_ago": "{count} days ago",
"delete": "Delete",
"delete_all_data": "Delete all data",
"delete_all_data": "Delete all data?",
"error_creating_group": "Error while creating group, please try again",
"error_reading_file": "Error reading file",
"export_canceled": "Export canceled",

View File

@@ -209,7 +209,7 @@ abstract class AppLocalizations {
/// Confirmation dialog for deleting all data
///
/// In en, this message translates to:
/// **'Delete all data'**
/// **'Delete all data?'**
String get delete_all_data;
/// Error message when group creation fails

View File

@@ -67,7 +67,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get delete => 'Löschen';
@override
String get delete_all_data => 'Alle Daten löschen';
String get delete_all_data => 'Alle Daten löschen?';
@override
String get error_creating_group =>

View File

@@ -67,7 +67,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get delete => 'Delete';
@override
String get delete_all_data => 'Delete all data';
String get delete_all_data => 'Delete all data?';
@override
String get error_creating_group =>

View File

@@ -44,12 +44,6 @@ class GameTracker extends StatelessWidget {
seedColor: CustomTheme.primaryColor,
brightness: Brightness.dark,
).copyWith(surface: CustomTheme.backgroundColor),
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
},
),
),
home: const CustomNavigationBar(),
);

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/group_view/groups_view.dart';
@@ -57,7 +56,7 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
onPressed: () async {
await Navigator.push(
context,
adaptivePageRoute(builder: (_) => const SettingsView()),
MaterialPageRoute(builder: (_) => const SettingsView()),
);
setState(() {
tabKeyCount++;

View File

@@ -44,8 +44,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return ScaffoldMessenger(
child: Scaffold(
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.create_new_group)),
body: SafeArea(
@@ -107,7 +106,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
],
),
),
),
);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.dart';
@@ -86,7 +85,7 @@ class _GroupsViewState extends State<GroupsView> {
onPressed: () async {
await Navigator.push(
context,
adaptivePageRoute(
MaterialPageRoute(
builder: (context) {
return const CreateGroupView();
},

View File

@@ -1,16 +1,14 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/match_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/match_summary_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart';
import 'package:provider/provider.dart';
@@ -45,6 +43,7 @@ class _HomeViewState extends State<HomeView> {
Player(name: 'Skeleton Player 2'),
],
),
winner: Player(name: 'Skeleton Player 1'),
),
);
@@ -91,43 +90,74 @@ class _HomeViewState extends State<HomeView> {
child: InfoTile(
width: constraints.maxWidth * 0.95,
title: loc.recent_matches,
icon: Icons.history_rounded,
content: Column(
icon: Icons.timer,
content: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: Visibility(
visible: !isLoading && loadedRecentMatches.isNotEmpty,
replacement: Center(
heightFactor: 12,
child: Text(
AppLocalizations.of(
context,
).no_recent_matches_available,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (recentMatches.isNotEmpty)
for (Match match in recentMatches)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 6.0,
MatchSummaryTile(
matchTitle: recentMatches[0].name,
game: 'Winner',
ruleset: 'Ruleset',
players: _getPlayerText(
recentMatches[0],
context,
),
child: MatchTile(
compact: true,
width: constraints.maxWidth * 0.9,
match: match,
onTap: () async {
await Navigator.of(context).push(
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) =>
MatchResultView(match: match),
winner: recentMatches[0].winner == null
? AppLocalizations.of(
context,
).match_in_progress
: recentMatches[0].winner!.name,
),
);
await updatedWinnerinRecentMatches(match.id);
},
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Divider(),
),
)
else
if (loadedRecentMatches.length > 1) ...[
MatchSummaryTile(
matchTitle: recentMatches[1].name,
game: 'Winner',
ruleset: 'Ruleset',
players: _getPlayerText(
recentMatches[1],
context,
),
winner: recentMatches[1].winner == null
? AppLocalizations.of(
context,
).match_in_progress
: recentMatches[1].winner!.name,
),
const SizedBox(height: 8),
] else ...[
Center(
heightFactor: 5,
child: Text(loc.no_recent_matches_available),
heightFactor: 5.35,
child: Text(
AppLocalizations.of(
context,
).no_second_match_available,
),
),
],
],
),
),
),
Padding(
padding: EdgeInsets.zero,
child: InfoTile(
),
),
InfoTile(
width: constraints.maxWidth * 0.95,
title: loc.quick_create,
icon: Icons.add_box_rounded,
@@ -175,8 +205,6 @@ class _HomeViewState extends State<HomeView> {
],
),
),
),
SizedBox(height: MediaQuery.paddingOf(context).bottom),
],
),
),
@@ -187,7 +215,7 @@ class _HomeViewState extends State<HomeView> {
/// Loads the data for the HomeView from the database.
/// This includes the match count, group count, and recent matches.
Future<void> loadHomeViewData() async {
void loadHomeViewData() {
final db = Provider.of<AppDatabase>(context, listen: false);
Future.wait([
db.matchDao.getMatchCount(),
@@ -203,6 +231,11 @@ class _HomeViewState extends State<HomeView> {
..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2)
.toList();
if (loadedRecentMatches.length < 2) {
recentMatches.add(
Match(name: 'Dummy Match', winner: null, group: null, players: null),
);
}
if (mounted) {
setState(() {
isLoading = false;
@@ -211,15 +244,18 @@ class _HomeViewState extends State<HomeView> {
});
}
/// Updates the winner information for a specific match in the recent matches list.
Future<void> updatedWinnerinRecentMatches(String matchId) async {
final db = Provider.of<AppDatabase>(context, listen: false);
final winner = await db.matchDao.getWinner(matchId: matchId);
final matchIndex = recentMatches.indexWhere((match) => match.id == matchId);
if (matchIndex != -1) {
setState(() {
recentMatches[matchIndex].winner = winner;
});
}
/// Generates a text representation of the players in the match.
/// If the match has a group, it returns the group name and the number of additional players.
/// If there is no group, it returns the count of players.
String _getPlayerText(Match game, context) {
final loc = AppLocalizations.of(context);
if (game.group == null) {
final playerCount = game.players?.length ?? 0;
return loc.players_count(playerCount);
}
if (game.players == null || game.players!.isEmpty) {
return game.group!.name;
}
return '${game.group!.name} + ${game.players!.length}';
}
}

View File

@@ -140,11 +140,7 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
filteredGroups.clear();
filteredGroups.addAll(
widget.groups.where(
(group) =>
group.name.toLowerCase().contains(query.toLowerCase()) ||
group.members.any(
(player) => player.name.toLowerCase().contains(query.toLowerCase()),
),
(group) => group.name.toLowerCase().contains(query.toLowerCase()),
),
);
}

View File

@@ -1,5 +1,5 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/db/database.dart';
@@ -119,8 +119,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return ScaffoldMessenger(
child: Scaffold(
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.create_new_match)),
body: SafeArea(
@@ -141,7 +140,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
: games[selectedGameIndex].$1,
onPressed: () async {
selectedGameIndex = await Navigator.of(context).push(
adaptivePageRoute(
MaterialPageRoute(
builder: (context) => ChooseGameView(
games: games,
initialGameIndex: selectedGameIndex,
@@ -169,7 +168,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
: translateRulesetToString(selectedRuleset!, context),
onPressed: () async {
selectedRuleset = await Navigator.of(context).push(
adaptivePageRoute(
MaterialPageRoute(
builder: (context) => ChooseRulesetView(
rulesets: _rulesets,
initialRulesetIndex: selectedRulesetIndex,
@@ -191,7 +190,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
: selectedGroup!.name,
onPressed: () async {
selectedGroup = await Navigator.of(context).push(
adaptivePageRoute(
MaterialPageRoute(
builder: (context) => ChooseGroupView(
groups: groupsList,
initialGroupId: selectedGroupId,
@@ -241,7 +240,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
if (context.mounted) {
Navigator.pushReplacement(
context,
adaptivePageRoute(
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) => MatchResultView(
match: match,
@@ -256,7 +255,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
],
),
),
),
);
}

View File

@@ -1,7 +1,7 @@
import 'dart:core' hide Match;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.dart';
@@ -78,15 +78,11 @@ class _MatchViewState extends State<MatchView> {
height: MediaQuery.paddingOf(context).bottom - 20,
);
}
return Center(
child: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: MatchTile(
width: MediaQuery.sizeOf(context).width * 0.95,
return MatchTile(
onTap: () async {
Navigator.push(
context,
adaptivePageRoute(
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) => MatchResultView(
match: matches[index],
@@ -96,8 +92,6 @@ class _MatchViewState extends State<MatchView> {
);
},
match: matches[index],
),
),
);
},
),
@@ -111,7 +105,7 @@ class _MatchViewState extends State<MatchView> {
onPressed: () async {
Navigator.push(
context,
adaptivePageRoute(
MaterialPageRoute(
builder: (context) =>
CreateMatchView(onWinnerChanged: loadGames),
),

View File

@@ -4,7 +4,6 @@ import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart';
import 'package:game_tracker/services/data_transfer_service.dart';
import 'package:package_info_plus/package_info_plus.dart';
class SettingsView extends StatefulWidget {
const SettingsView({super.key});
@@ -14,26 +13,21 @@ class SettingsView extends StatefulWidget {
}
class _SettingsViewState extends State<SettingsView> {
PackageInfo _packageInfo = PackageInfo(
appName: 'n.A.',
packageName: 'n.A.',
version: 'n.A.',
buildNumber: 'n.A.',
);
@override
void initState() {
super.initState();
_initPackageInfo();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return ScaffoldMessenger(
child: Scaffold(
return Scaffold(
appBar: AppBar(backgroundColor: CustomTheme.backgroundColor),
backgroundColor: CustomTheme.backgroundColor,
body: Column(
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) =>
SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -49,7 +43,10 @@ class _SettingsViewState extends State<SettingsView> {
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 10,
),
child: Text(
textAlign: TextAlign.start,
loc.settings,
@@ -64,9 +61,8 @@ class _SettingsViewState extends State<SettingsView> {
icon: Icons.upload_rounded,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () async {
final String json = await DataTransferService.getAppDataAsJson(
context,
);
final String json =
await DataTransferService.getAppDataAsJson(context);
final result = await DataTransferService.exportData(
json,
'game_tracker-data',
@@ -80,7 +76,9 @@ class _SettingsViewState extends State<SettingsView> {
icon: Icons.download_rounded,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () async {
final result = await DataTransferService.importData(context);
final result = await DataTransferService.importData(
context,
);
if (!context.mounted) return;
showImportSnackBar(context: context, result: result);
},
@@ -93,7 +91,7 @@ class _SettingsViewState extends State<SettingsView> {
showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('${loc.delete_all_data}?'),
title: Text(loc.delete_all_data),
content: Text(loc.this_cannot_be_undone),
actions: [
TextButton(
@@ -119,23 +117,10 @@ class _SettingsViewState extends State<SettingsView> {
});
},
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: Text(
'Version ${_packageInfo.version} (${_packageInfo.buildNumber})',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
);
}
@@ -209,11 +194,4 @@ class _SettingsViewState extends State<SettingsView> {
),
);
}
Future<void> _initPackageInfo() async {
final info = await PackageInfo.fromPlatform();
setState(() {
_packageInfo = info;
});
}
}

View File

@@ -10,16 +10,8 @@ import 'package:intl/intl.dart';
/// creation date, associated group, winner, and players.
/// - [match]: The match data to be displayed.
/// - [onTap]: The callback invoked when the tile is tapped.
/// - [width]: Optional width for the tile.
/// - [compact]: Whether to display the tile in a compact mode
class MatchTile extends StatefulWidget {
const MatchTile({
super.key,
required this.match,
required this.onTap,
this.width,
this.compact = false,
});
const MatchTile({super.key, required this.match, required this.onTap});
/// The match data to be displayed.
final Match match;
@@ -27,12 +19,6 @@ class MatchTile extends StatefulWidget {
/// The callback invoked when the tile is tapped.
final VoidCallback onTap;
/// Optional width for the tile.
final double? width;
/// Whether to display the tile in a compact mode
final bool compact;
@override
State<MatchTile> createState() => _MatchTileState();
}
@@ -55,10 +41,13 @@ class _MatchTileState extends State<MatchTile> {
return GestureDetector(
onTap: widget.onTap,
child: Container(
margin: EdgeInsets.zero,
width: widget.width,
margin: CustomTheme.tileMargin,
padding: const EdgeInsets.all(12),
decoration: CustomTheme.standardBoxDecoration,
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -91,22 +80,7 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(width: 6),
Expanded(
child: Text(
'${group.name}${widget.match.players != null ? ' + ${widget.match.players?.length}' : ''}',
style: const TextStyle(fontSize: 14, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 12),
] else if (widget.compact) ...[
Row(
children: [
const Icon(Icons.person, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Expanded(
child: Text(
'${widget.match.players!.length} ${loc.players}',
group.name,
style: const TextStyle(fontSize: 14, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
@@ -153,46 +127,9 @@ class _MatchTileState extends State<MatchTile> {
),
),
const SizedBox(height: 12),
] else ...[
Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.amber.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
const Icon(
Icons.watch_later,
size: 20,
color: Colors.amber,
),
const SizedBox(width: 8),
Expanded(
child: Text(
loc.match_in_progress,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: CustomTheme.textColor,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 12),
],
if (_allPlayers.isNotEmpty && widget.compact == false) ...[
if (_allPlayers.isNotEmpty) ...[
Text(
loc.players,
style: const TextStyle(

View File

@@ -1,7 +1,7 @@
name: game_tracker
description: "Game Tracking App for Card Games"
publish_to: 'none'
version: 0.0.4+101
version: 0.0.1+21
environment:
sdk: ^3.8.1
@@ -9,6 +9,11 @@ environment:
dependencies:
flutter:
sdk: flutter
material_symbols_icons: ^4.2815.1
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
drift: ^2.27.0
drift_flutter: ^0.2.4
path_provider: ^2.1.5
@@ -22,7 +27,6 @@ dependencies:
intl: any
flutter_localizations:
sdk: flutter
package_info_plus: ^9.0.0
dev_dependencies:
flutter_test: