diff --git a/assets/schema.json b/assets/schema.json new file mode 100644 index 0000000..1883122 --- /dev/null +++ b/assets/schema.json @@ -0,0 +1,180 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "games": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "name": { + "type": "string" + }, + "players": { + "type": [ + "array", + "null" + ], + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "createdAt", + "name" + ] + } + }, + "group": { + "type": [ + "object", + "null" + ], + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "name": { + "type": "string" + }, + "members": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "createdAt", + "name" + ] + } + ] + } + }, + "required": [ + "id", + "createdAt", + "name", + "members" + ] + }, + "winner": { + "type": ["string","null"] + }, + "required": [ + "id", + "createdAt", + "name", + "winner" + ] + } + ] + }, + "groups": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "name": { + "type": "string" + }, + "members": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "createdAt", + "name" + ] + } + ] + } + }, + "required": [ + "id", + "createdAt", + "name", + "members" + ] + } + ] + }, + "players": { + "type": "array", + "items": [ + { + "type": [ + "object", + "null" + ], + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "createdAt", + "name" + ] + } + ] + } + } +} + diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 320eaf7..8c809b0 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -1,2 +1,24 @@ /// Button types used for styling the [CustomWidthButton] enum ButtonType { primary, secondary, tertiary } + +/// Result types for import operations in the [SettingsView] +/// - [ImportResult.success]: The import operation was successful. +/// - [ImportResult.canceled]: The import operation was canceled by the user. +/// - [ImportResult.fileReadError]: There was an error reading the selected file. +/// - [ImportResult.invalidSchema]: The JSON schema of the imported data is invalid. +/// - [ImportResult.formatException]: A format exception occurred during import. +/// - [ImportResult.unknownException]: An exception occurred during import. +enum ImportResult { + success, + canceled, + fileReadError, + invalidSchema, + formatException, + unknownException, +} + +/// Result types for export operations in the [SettingsView] +/// - [ExportResult.success]: The export operation was successful. +/// - [ExportResult.canceled]: The export operation was canceled by the user. +/// - [ExportResult.unknownException]: An exception occurred during export. +enum ExportResult { success, canceled, unknownException } diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index 283c02f..a211849 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -60,23 +60,157 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /// Also adds associated players and group if they exist. Future addGame({required Game game}) async { await db.transaction(() async { - for (final p in game.players ?? []) { - await db.playerDao.addPlayer(player: p); - await db.playerGameDao.addPlayerToGame(gameId: game.id, playerId: p.id); - } - if (game.group != null) { - await db.groupDao.addGroup(group: game.group!); - await db.groupGameDao.addGroupToGame(game.id, game.group!.id); - } await into(gameTable).insert( GameTableCompanion.insert( id: game.id, name: game.name, - winnerId: game.winner, + winnerId: Value(game.winner), createdAt: game.createdAt, ), mode: InsertMode.insertOrReplace, ); + + if (game.players != null) { + await db.playerDao.addPlayers(players: game.players!); + for (final p in game.players ?? []) { + await db.playerGameDao.addPlayerToGame( + gameId: game.id, + playerId: p.id, + ); + } + } + + if (game.group != null) { + await db.groupDao.addGroup(group: game.group!); + await db.groupGameDao.addGroupToGame(game.id, game.group!.id); + } + }); + } + + Future addGames({required List games}) async { + if (games.isEmpty) return; + await db.transaction(() async { + // Add all games in batch + await db.batch( + (b) => b.insertAll( + gameTable, + games + .map( + (game) => GameTableCompanion.insert( + id: game.id, + name: game.name, + createdAt: game.createdAt, + winnerId: Value(game.winner), + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + + // Add all groups of the games in batch + await db.batch( + (b) => b.insertAll( + db.groupTable, + games + .where((game) => game.group != null) + .map( + (game) => GroupTableCompanion.insert( + id: game.group!.id, + name: game.group!.name, + createdAt: game.group!.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + + // Add all players of the games in batch (unique) + final uniquePlayers = {}; + for (final game in games) { + if (game.players != null) { + for (final p in game.players!) { + uniquePlayers[p.id] = p; + } + } + // Also include members of groups + if (game.group != null) { + for (final m in game.group!.members) { + uniquePlayers[m.id] = m; + } + } + } + + if (uniquePlayers.isNotEmpty) { + await db.batch( + (b) => b.insertAll( + db.playerTable, + uniquePlayers.values + .map( + (p) => PlayerTableCompanion.insert( + id: p.id, + name: p.name, + createdAt: p.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + } + + // Add all player-game associations in batch + await db.batch((b) { + for (final game in games) { + if (game.players != null) { + for (final p in game.players ?? []) { + b.insert( + db.playerGameTable, + PlayerGameTableCompanion.insert( + gameId: game.id, + playerId: p.id, + ), + mode: InsertMode.insertOrReplace, + ); + } + } + } + }); + + // Add all player-group associations in batch + await db.batch((b) { + for (final game in games) { + if (game.group != null) { + for (final m in game.group!.members) { + b.insert( + db.playerGroupTable, + PlayerGroupTableCompanion.insert( + playerId: m.id, + groupId: game.group!.id, + ), + mode: InsertMode.insertOrReplace, + ); + } + } + } + }); + + // Add all group-game associations in batch + await db.batch((b) { + for (final game in games) { + if (game.group != null) { + b.insert( + db.groupGameTable, + GroupGameTableCompanion.insert( + gameId: game.id, + groupId: game.group!.id, + ), + mode: InsertMode.insertOrReplace, + ); + } + } + }); }); } @@ -104,4 +238,12 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { final result = await query.getSingleOrNull(); return result != null; } + + /// Deletes all games from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllGames() async { + final query = delete(gameTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } } diff --git a/lib/data/dao/game_dao.g.dart b/lib/data/dao/game_dao.g.dart index ebf5524..b5a29fe 100644 --- a/lib/data/dao/game_dao.g.dart +++ b/lib/data/dao/game_dao.g.dart @@ -4,6 +4,5 @@ part of 'game_dao.dart'; // ignore_for_file: type=lint mixin _$GameDaoMixin on DatabaseAccessor { - $PlayerTableTable get playerTable => attachedDatabase.playerTable; $GameTableTable get gameTable => attachedDatabase.gameTable; } diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 9b16801..fbb4d6f 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -57,6 +57,10 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { name: group.name, createdAt: group.createdAt, ), + mode: InsertMode.insertOrReplace, + ); + await Future.wait( + group.members.map((player) => db.playerDao.addPlayer(player: player)), ); await db.batch( (b) => b.insertAll( @@ -69,17 +73,94 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { ), ) .toList(), + mode: InsertMode.insertOrReplace, ), ); - await Future.wait( - group.members.map((player) => db.playerDao.addPlayer(player: player)), - ); }); return true; } return false; } + /// Adds multiple groups to the database. + /// Also adds the group's members to the [PlayerGroupTable]. + Future addGroups({required List groups}) async { + if (groups.isEmpty) return; + await db.transaction(() async { + // Deduplicate groups by id - keep first occurrence + final Map uniqueGroups = {}; + for (final g in groups) { + uniqueGroups.putIfAbsent(g.id, () => g); + } + + // Insert unique groups in batch + await db.batch( + (b) => b.insertAll( + groupTable, + uniqueGroups.values + .map( + (group) => GroupTableCompanion.insert( + id: group.id, + name: group.name, + createdAt: group.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + + // Collect unique players from all groups + final uniquePlayers = {}; + for (final g in uniqueGroups.values) { + for (final m in g.members) { + uniquePlayers[m.id] = m; + } + } + + if (uniquePlayers.isNotEmpty) { + await db.batch( + (b) => b.insertAll( + db.playerTable, + uniquePlayers.values + .map( + (p) => PlayerTableCompanion.insert( + id: p.id, + name: p.name, + createdAt: p.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + } + + // Prepare all player-group associations in one list (unique pairs) + final Set seenPairs = {}; + final List pgRows = []; + for (final g in uniqueGroups.values) { + for (final m in g.members) { + final key = '${m.id}|${g.id}'; + if (!seenPairs.contains(key)) { + seenPairs.add(key); + pgRows.add( + PlayerGroupTableCompanion.insert(playerId: m.id, groupId: g.id), + ); + } + } + } + + if (pgRows.isNotEmpty) { + await db.batch((b) { + for (final pg in pgRows) { + b.insert(db.playerGroupTable, pg, mode: InsertMode.insertOrReplace); + } + }); + } + }); + } + /// Deletes the group with the given [id] from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteGroup({required String groupId}) async { @@ -117,4 +198,12 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { final result = await query.getSingleOrNull(); return result != null; } + + /// Deletes all groups from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllGroups() async { + final query = delete(groupTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } } diff --git a/lib/data/dao/group_game_dao.g.dart b/lib/data/dao/group_game_dao.g.dart index 426f192..735a35f 100644 --- a/lib/data/dao/group_game_dao.g.dart +++ b/lib/data/dao/group_game_dao.g.dart @@ -5,7 +5,6 @@ part of 'group_game_dao.dart'; // ignore_for_file: type=lint mixin _$GroupGameDaoMixin on DatabaseAccessor { $GroupTableTable get groupTable => attachedDatabase.groupTable; - $PlayerTableTable get playerTable => attachedDatabase.playerTable; $GameTableTable get gameTable => attachedDatabase.gameTable; $GroupGameTableTable get groupGameTable => attachedDatabase.groupGameTable; } diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 36f9305..53e251f 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -42,12 +42,36 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { name: player.name, createdAt: player.createdAt, ), + mode: InsertMode.insertOrReplace, ); return true; } return false; } + /// Adds multiple [players] to the database in a batch operation. + Future addPlayers({required List players}) async { + if (players.isEmpty) return false; + + await db.batch( + (b) => b.insertAll( + playerTable, + players + .map( + (player) => PlayerTableCompanion.insert( + id: player.id, + name: player.name, + createdAt: player.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + + return true; + } + /// Deletes the player with the given [id] from the database. /// Returns `true` if the player was deleted, `false` if the player did not exist. Future deletePlayer({required String playerId}) async { @@ -82,4 +106,12 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { .getSingle(); return count ?? 0; } + + /// Deletes all players from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllPlayers() async { + final query = delete(playerTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } } diff --git a/lib/data/db/database.dart b/lib/data/db/database.dart index 73ad73e..704e1f0 100644 --- a/lib/data/db/database.dart +++ b/lib/data/db/database.dart @@ -40,6 +40,15 @@ class AppDatabase extends _$AppDatabase { @override int get schemaVersion => 1; + @override + MigrationStrategy get migration { + return MigrationStrategy( + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON'); + }, + ); + } + static QueryExecutor _openConnection() { return driftDatabase( name: 'gametracker_db', diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index 3f10169..f211d0c 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -552,12 +552,9 @@ class $GameTableTable extends GameTable late final GeneratedColumn winnerId = GeneratedColumn( 'winner_id', aliasedName, - false, + true, type: DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES player_table (id) ON DELETE CASCADE', - ), + requiredDuringInsert: false, ); static const VerificationMeta _createdAtMeta = const VerificationMeta( 'createdAt', @@ -602,8 +599,6 @@ class $GameTableTable extends GameTable _winnerIdMeta, winnerId.isAcceptableOrUnknown(data['winner_id']!, _winnerIdMeta), ); - } else if (isInserting) { - context.missing(_winnerIdMeta); } if (data.containsKey('created_at')) { context.handle( @@ -633,7 +628,7 @@ class $GameTableTable extends GameTable winnerId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}winner_id'], - )!, + ), createdAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}created_at'], @@ -650,12 +645,12 @@ class $GameTableTable extends GameTable class GameTableData extends DataClass implements Insertable { final String id; final String name; - final String winnerId; + final String? winnerId; final DateTime createdAt; const GameTableData({ required this.id, required this.name, - required this.winnerId, + this.winnerId, required this.createdAt, }); @override @@ -663,7 +658,9 @@ class GameTableData extends DataClass implements Insertable { final map = {}; map['id'] = Variable(id); map['name'] = Variable(name); - map['winner_id'] = Variable(winnerId); + if (!nullToAbsent || winnerId != null) { + map['winner_id'] = Variable(winnerId); + } map['created_at'] = Variable(createdAt); return map; } @@ -672,7 +669,9 @@ class GameTableData extends DataClass implements Insertable { return GameTableCompanion( id: Value(id), name: Value(name), - winnerId: Value(winnerId), + winnerId: winnerId == null && nullToAbsent + ? const Value.absent() + : Value(winnerId), createdAt: Value(createdAt), ); } @@ -685,7 +684,7 @@ class GameTableData extends DataClass implements Insertable { return GameTableData( id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), - winnerId: serializer.fromJson(json['winnerId']), + winnerId: serializer.fromJson(json['winnerId']), createdAt: serializer.fromJson(json['createdAt']), ); } @@ -695,7 +694,7 @@ class GameTableData extends DataClass implements Insertable { return { 'id': serializer.toJson(id), 'name': serializer.toJson(name), - 'winnerId': serializer.toJson(winnerId), + 'winnerId': serializer.toJson(winnerId), 'createdAt': serializer.toJson(createdAt), }; } @@ -703,12 +702,12 @@ class GameTableData extends DataClass implements Insertable { GameTableData copyWith({ String? id, String? name, - String? winnerId, + Value winnerId = const Value.absent(), DateTime? createdAt, }) => GameTableData( id: id ?? this.id, name: name ?? this.name, - winnerId: winnerId ?? this.winnerId, + winnerId: winnerId.present ? winnerId.value : this.winnerId, createdAt: createdAt ?? this.createdAt, ); GameTableData copyWithCompanion(GameTableCompanion data) { @@ -746,7 +745,7 @@ class GameTableData extends DataClass implements Insertable { class GameTableCompanion extends UpdateCompanion { final Value id; final Value name; - final Value winnerId; + final Value winnerId; final Value createdAt; final Value rowid; const GameTableCompanion({ @@ -759,12 +758,11 @@ class GameTableCompanion extends UpdateCompanion { GameTableCompanion.insert({ required String id, required String name, - required String winnerId, + this.winnerId = const Value.absent(), required DateTime createdAt, this.rowid = const Value.absent(), }) : id = Value(id), name = Value(name), - winnerId = Value(winnerId), createdAt = Value(createdAt); static Insertable custom({ Expression? id, @@ -785,7 +783,7 @@ class GameTableCompanion extends UpdateCompanion { GameTableCompanion copyWith({ Value? id, Value? name, - Value? winnerId, + Value? winnerId, Value? createdAt, Value? rowid, }) { @@ -1538,13 +1536,6 @@ abstract class _$AppDatabase extends GeneratedDatabase { ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ - WritePropagation( - on: TableUpdateQuery.onTableName( - 'player_table', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('game_table', kind: UpdateKind.delete)], - ), WritePropagation( on: TableUpdateQuery.onTableName( 'player_table', @@ -1609,24 +1600,6 @@ final class $$PlayerTableTableReferences extends BaseReferences<_$AppDatabase, $PlayerTableTable, PlayerTableData> { $$PlayerTableTableReferences(super.$_db, super.$_table, super.$_typedResult); - static MultiTypedResultKey<$GameTableTable, List> - _gameTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( - db.gameTable, - aliasName: $_aliasNameGenerator(db.playerTable.id, db.gameTable.winnerId), - ); - - $$GameTableTableProcessedTableManager get gameTableRefs { - final manager = $$GameTableTableTableManager( - $_db, - $_db.gameTable, - ).filter((f) => f.winnerId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull(_gameTableRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } - static MultiTypedResultKey<$PlayerGroupTableTable, List> _playerGroupTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( db.playerGroupTable, @@ -1698,31 +1671,6 @@ class $$PlayerTableTableFilterComposer builder: (column) => ColumnFilters(column), ); - Expression gameTableRefs( - Expression Function($$GameTableTableFilterComposer f) f, - ) { - final $$GameTableTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.gameTable, - getReferencedColumn: (t) => t.winnerId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$GameTableTableFilterComposer( - $db: $db, - $table: $db.gameTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - Expression playerGroupTableRefs( Expression Function($$PlayerGroupTableTableFilterComposer f) f, ) { @@ -1817,31 +1765,6 @@ class $$PlayerTableTableAnnotationComposer GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); - Expression gameTableRefs( - Expression Function($$GameTableTableAnnotationComposer a) f, - ) { - final $$GameTableTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.gameTable, - getReferencedColumn: (t) => t.winnerId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$GameTableTableAnnotationComposer( - $db: $db, - $table: $db.gameTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - Expression playerGroupTableRefs( Expression Function($$PlayerGroupTableTableAnnotationComposer a) f, ) { @@ -1907,7 +1830,6 @@ class $$PlayerTableTableTableManager (PlayerTableData, $$PlayerTableTableReferences), PlayerTableData, PrefetchHooks Function({ - bool gameTableRefs, bool playerGroupTableRefs, bool playerGameTableRefs, }) @@ -1956,42 +1878,16 @@ class $$PlayerTableTableTableManager ) .toList(), prefetchHooksCallback: - ({ - gameTableRefs = false, - playerGroupTableRefs = false, - playerGameTableRefs = false, - }) { + ({playerGroupTableRefs = false, playerGameTableRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ - if (gameTableRefs) db.gameTable, if (playerGroupTableRefs) db.playerGroupTable, if (playerGameTableRefs) db.playerGameTable, ], addJoins: null, getPrefetchedDataCallback: (items) async { return [ - if (gameTableRefs) - await $_getPrefetchedData< - PlayerTableData, - $PlayerTableTable, - GameTableData - >( - currentTable: table, - referencedTable: $$PlayerTableTableReferences - ._gameTableRefsTable(db), - managerFromTypedResult: (p0) => - $$PlayerTableTableReferences( - db, - table, - p0, - ).gameTableRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.winnerId == item.id, - ), - typedResults: items, - ), if (playerGroupTableRefs) await $_getPrefetchedData< PlayerTableData, @@ -2055,7 +1951,6 @@ typedef $$PlayerTableTableProcessedTableManager = (PlayerTableData, $$PlayerTableTableReferences), PlayerTableData, PrefetchHooks Function({ - bool gameTableRefs, bool playerGroupTableRefs, bool playerGameTableRefs, }) @@ -2436,7 +2331,7 @@ typedef $$GameTableTableCreateCompanionBuilder = GameTableCompanion Function({ required String id, required String name, - required String winnerId, + Value winnerId, required DateTime createdAt, Value rowid, }); @@ -2444,7 +2339,7 @@ typedef $$GameTableTableUpdateCompanionBuilder = GameTableCompanion Function({ Value id, Value name, - Value winnerId, + Value winnerId, Value createdAt, Value rowid, }); @@ -2453,25 +2348,6 @@ final class $$GameTableTableReferences extends BaseReferences<_$AppDatabase, $GameTableTable, GameTableData> { $$GameTableTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $PlayerTableTable _winnerIdTable(_$AppDatabase db) => - db.playerTable.createAlias( - $_aliasNameGenerator(db.gameTable.winnerId, db.playerTable.id), - ); - - $$PlayerTableTableProcessedTableManager get winnerId { - final $_column = $_itemColumn('winner_id')!; - - final manager = $$PlayerTableTableTableManager( - $_db, - $_db.playerTable, - ).filter((f) => f.id.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_winnerIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item]), - ); - } - static MultiTypedResultKey<$PlayerGameTableTable, List> _playerGameTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( db.playerGameTable, @@ -2530,34 +2406,16 @@ class $$GameTableTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get winnerId => $composableBuilder( + column: $table.winnerId, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column), ); - $$PlayerTableTableFilterComposer get winnerId { - final $$PlayerTableTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.winnerId, - referencedTable: $db.playerTable, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$PlayerTableTableFilterComposer( - $db: $db, - $table: $db.playerTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } - Expression playerGameTableRefs( Expression Function($$PlayerGameTableTableFilterComposer f) f, ) { @@ -2628,33 +2486,15 @@ class $$GameTableTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get winnerId => $composableBuilder( + column: $table.winnerId, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column), ); - - $$PlayerTableTableOrderingComposer get winnerId { - final $$PlayerTableTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.winnerId, - referencedTable: $db.playerTable, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$PlayerTableTableOrderingComposer( - $db: $db, - $table: $db.playerTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } } class $$GameTableTableAnnotationComposer @@ -2672,32 +2512,12 @@ class $$GameTableTableAnnotationComposer GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); + GeneratedColumn get winnerId => + $composableBuilder(column: $table.winnerId, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); - $$PlayerTableTableAnnotationComposer get winnerId { - final $$PlayerTableTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.winnerId, - referencedTable: $db.playerTable, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$PlayerTableTableAnnotationComposer( - $db: $db, - $table: $db.playerTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } - Expression playerGameTableRefs( Expression Function($$PlayerGameTableTableAnnotationComposer a) f, ) { @@ -2763,7 +2583,6 @@ class $$GameTableTableTableManager (GameTableData, $$GameTableTableReferences), GameTableData, PrefetchHooks Function({ - bool winnerId, bool playerGameTableRefs, bool groupGameTableRefs, }) @@ -2783,7 +2602,7 @@ class $$GameTableTableTableManager ({ Value id = const Value.absent(), Value name = const Value.absent(), - Value winnerId = const Value.absent(), + Value winnerId = const Value.absent(), Value createdAt = const Value.absent(), Value rowid = const Value.absent(), }) => GameTableCompanion( @@ -2797,7 +2616,7 @@ class $$GameTableTableTableManager ({ required String id, required String name, - required String winnerId, + Value winnerId = const Value.absent(), required DateTime createdAt, Value rowid = const Value.absent(), }) => GameTableCompanion.insert( @@ -2816,49 +2635,14 @@ class $$GameTableTableTableManager ) .toList(), prefetchHooksCallback: - ({ - winnerId = false, - playerGameTableRefs = false, - groupGameTableRefs = false, - }) { + ({playerGameTableRefs = false, groupGameTableRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ if (playerGameTableRefs) db.playerGameTable, if (groupGameTableRefs) db.groupGameTable, ], - addJoins: - < - T extends TableManagerState< - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic - > - >(state) { - if (winnerId) { - state = - state.withJoin( - currentTable: table, - currentColumn: table.winnerId, - referencedTable: $$GameTableTableReferences - ._winnerIdTable(db), - referencedColumn: $$GameTableTableReferences - ._winnerIdTable(db) - .id, - ) - as T; - } - - return state; - }, + addJoins: null, getPrefetchedDataCallback: (items) async { return [ if (playerGameTableRefs) @@ -2924,7 +2708,6 @@ typedef $$GameTableTableProcessedTableManager = (GameTableData, $$GameTableTableReferences), GameTableData, PrefetchHooks Function({ - bool winnerId, bool playerGameTableRefs, bool groupGameTableRefs, }) diff --git a/lib/data/db/tables/game_table.dart b/lib/data/db/tables/game_table.dart index 0fe5a3c..1a37a73 100644 --- a/lib/data/db/tables/game_table.dart +++ b/lib/data/db/tables/game_table.dart @@ -1,11 +1,9 @@ import 'package:drift/drift.dart'; -import 'package:game_tracker/data/db/tables/player_table.dart'; class GameTable extends Table { TextColumn get id => text()(); TextColumn get name => text()(); - TextColumn get winnerId => - text().references(PlayerTable, #id, onDelete: KeyAction.cascade)(); + late final winnerId = text().nullable()(); DateTimeColumn get createdAt => dateTime()(); @override diff --git a/lib/data/dto/game.dart b/lib/data/dto/game.dart index 96e9d73..4188bc4 100644 --- a/lib/data/dto/game.dart +++ b/lib/data/dto/game.dart @@ -5,11 +5,11 @@ import 'package:uuid/uuid.dart'; class Game { final String id; + final DateTime createdAt; final String name; final List? players; final Group? group; - final String winner; - final DateTime createdAt; + final String? winner; Game({ String? id, @@ -25,4 +25,27 @@ class Game { String toString() { return 'Game{\n\tid: $id,\n\tname: $name,\n\tplayers: $players,\n\tgroup: $group,\n\twinner: $winner\n}'; } + + /// Creates a Game instance from a JSON object. + Game.fromJson(Map json) + : id = json['id'], + name = json['name'], + createdAt = DateTime.parse(json['createdAt']), + players = json['players'] != null + ? (json['players'] as List) + .map((playerJson) => Player.fromJson(playerJson)) + .toList() + : null, + group = json['group'] != null ? Group.fromJson(json['group']) : null, + winner = json['winner'] ?? ''; + + /// Converts the Game instance to a JSON object. + Map toJson() => { + 'id': id, + 'createdAt': createdAt.toIso8601String(), + 'name': name, + 'players': players?.map((player) => player.toJson()).toList(), + 'group': group?.toJson(), + 'winner': winner, + }; } diff --git a/lib/data/dto/group.dart b/lib/data/dto/group.dart index 46c6f91..92dbd09 100644 --- a/lib/data/dto/group.dart +++ b/lib/data/dto/group.dart @@ -4,9 +4,9 @@ import 'package:uuid/uuid.dart'; class Group { final String id; + final DateTime createdAt; final String name; final List members; - final DateTime createdAt; Group({ String? id, @@ -20,4 +20,21 @@ class Group { String toString() { return 'Group{id: $id, name: $name,members: $members}'; } + + /// Creates a Group instance from a JSON object. + Group.fromJson(Map json) + : id = json['id'], + createdAt = DateTime.parse(json['createdAt']), + name = json['name'], + members = (json['members'] as List) + .map((memberJson) => Player.fromJson(memberJson)) + .toList(); + + /// Converts the Group instance to a JSON object. + Map toJson() => { + 'id': id, + 'createdAt': createdAt.toIso8601String(), + 'name': name, + 'members': members.map((member) => member.toJson()).toList(), + }; } diff --git a/lib/data/dto/player.dart b/lib/data/dto/player.dart index 4ef58b1..cfb4f4b 100644 --- a/lib/data/dto/player.dart +++ b/lib/data/dto/player.dart @@ -3,8 +3,8 @@ import 'package:uuid/uuid.dart'; class Player { final String id; - final String name; final DateTime createdAt; + final String name; Player({String? id, DateTime? createdAt, required this.name}) : id = id ?? const Uuid().v4(), @@ -14,4 +14,17 @@ class Player { String toString() { return 'Player{id: $id,name: $name}'; } + + /// Creates a Player instance from a JSON object. + Player.fromJson(Map json) + : id = json['id'], + createdAt = DateTime.parse(json['createdAt']), + name = json['name']; + + /// Converts the Player instance to a JSON object. + Map toJson() => { + 'id': id, + 'createdAt': createdAt.toIso8601String(), + 'name': name, + }; } diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 20ced7a..9dd0658 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -17,12 +17,7 @@ class CustomNavigationBar extends StatefulWidget { class _CustomNavigationBarState extends State with SingleTickerProviderStateMixin { int currentIndex = 0; - final List tabs = [ - const HomeView(), - const GameHistoryView(), - const GroupsView(), - const StatisticsView(), - ]; + int tabKeyCount = 0; @override void initState() { @@ -31,6 +26,22 @@ class _CustomNavigationBarState extends State @override Widget build(BuildContext context) { + // Pretty ugly but works + final List tabs = [ + KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()), + KeyedSubtree( + key: ValueKey('games_$tabKeyCount'), + child: const GameHistoryView(), + ), + KeyedSubtree( + key: ValueKey('groups_$tabKeyCount'), + child: const GroupsView(), + ), + KeyedSubtree( + key: ValueKey('stats_$tabKeyCount'), + child: const StatisticsView(), + ), + ]; return Scaffold( appBar: AppBar( centerTitle: true, @@ -42,10 +53,15 @@ class _CustomNavigationBarState extends State scrolledUnderElevation: 0, actions: [ IconButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => const SettingsView()), - ), + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute(builder: (_) => const SettingsView()), + ); + setState(() { + tabKeyCount++; + }); + }, icon: const Icon(Icons.settings), ), ], diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index c3e75f3..6ebb7fb 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -1,13 +1,191 @@ import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; +import 'package:game_tracker/services/data_transfer_service.dart'; -class SettingsView extends StatelessWidget { +class SettingsView extends StatefulWidget { const SettingsView({super.key}); + @override + State createState() => _SettingsViewState(); +} + +class _SettingsViewState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Einstellungen')), - body: const Center(child: Text('Settings View')), + appBar: AppBar(backgroundColor: CustomTheme.backgroundColor), + backgroundColor: CustomTheme.backgroundColor, + body: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) => + SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(24, 0, 24, 10), + child: Text( + textAlign: TextAlign.start, + 'Menu', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10), + child: Text( + textAlign: TextAlign.start, + 'Settings', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ), + SettingsListTile( + title: 'Export data', + icon: Icons.upload_outlined, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () async { + final String json = + await DataTransferService.getAppDataAsJson(context); + final result = await DataTransferService.exportData( + json, + 'game_tracker-data', + ); + if (!context.mounted) return; + showExportSnackBar(context: context, result: result); + }, + ), + SettingsListTile( + title: 'Import data', + icon: Icons.download_outlined, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () async { + final result = await DataTransferService.importData( + context, + ); + if (!context.mounted) return; + showImportSnackBar(context: context, result: result); + }, + ), + SettingsListTile( + title: 'Delete all data', + icon: Icons.download_outlined, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete all data?'), + content: const Text('This can\'t be undone'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Abbrechen'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Löschen'), + ), + ], + ), + ).then((confirmed) { + if (confirmed == true && context.mounted) { + DataTransferService.deleteAllData(context); + showSnackbar( + context: context, + message: 'Daten erfolgreich gelöscht', + ); + } + }); + }, + ), + ], + ), + ), + ), + ); + } + + /// Displays a snackbar based on the import result. + /// + /// [context] The BuildContext to show the snackbar in. + /// [result] The result of the import operation. + void showImportSnackBar({ + required BuildContext context, + required ImportResult result, + }) { + switch (result) { + case ImportResult.success: + showSnackbar(context: context, message: 'Data successfully imported'); + case ImportResult.invalidSchema: + showSnackbar(context: context, message: 'Invalid Schema'); + case ImportResult.fileReadError: + showSnackbar(context: context, message: 'Error reading file'); + case ImportResult.canceled: + showSnackbar(context: context, message: 'Import canceled'); + case ImportResult.formatException: + showSnackbar( + context: context, + message: 'Format Exception (see console)', + ); + case ImportResult.unknownException: + showSnackbar( + context: context, + message: 'Unknown Exception (see console)', + ); + } + } + + /// Displays a snackbar based on the export result. + /// + /// [context] The BuildContext to show the snackbar in. + /// [result] The result of the export operation. + void showExportSnackBar({ + required BuildContext context, + required ExportResult result, + }) { + switch (result) { + case ExportResult.success: + showSnackbar(context: context, message: 'Data successfully exported'); + case ExportResult.canceled: + showSnackbar(context: context, message: 'Export canceled'); + case ExportResult.unknownException: + showSnackbar( + context: context, + message: 'Unknown Exception (see console)', + ); + } + } + + /// Displays a snackbar with the given message and optional action. + /// + /// [context] The BuildContext to show the snackbar in. + /// [message] The message to display in the snackbar. + /// [duration] The duration for which the snackbar is displayed. + /// [action] An optional callback function to execute when the action button is pressed. + void showSnackbar({ + required BuildContext context, + required String message, + Duration duration = const Duration(seconds: 3), + VoidCallback? action, + }) { + final messenger = ScaffoldMessenger.of(context); + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message, style: const TextStyle(color: Colors.white)), + backgroundColor: CustomTheme.onBoxColor, + duration: duration, + action: action != null + ? SnackBarAction(label: 'Rückgängig', onPressed: action) + : null, + ), ); } } diff --git a/lib/presentation/widgets/tiles/settings_list_tile.dart b/lib/presentation/widgets/tiles/settings_list_tile.dart new file mode 100644 index 0000000..d5c421f --- /dev/null +++ b/lib/presentation/widgets/tiles/settings_list_tile.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +class SettingsListTile extends StatelessWidget { + final VoidCallback? onPressed; + final IconData icon; + final String title; + final Widget? suffixWidget; + const SettingsListTile({ + super.key, + required this.title, + required this.icon, + this.suffixWidget, + this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Center( + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.95, + child: GestureDetector( + onTap: onPressed ?? () {}, + child: Container( + margin: EdgeInsets.zero, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorder), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: CustomTheme.primaryColor, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 24), + ), + const SizedBox(width: 16), + Text(title, style: const TextStyle(fontSize: 18)), + ], + ), + if (suffixWidget != null) + suffixWidget! + else + const SizedBox.shrink(), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart new file mode 100644 index 0000000..eaa9633 --- /dev/null +++ b/lib/services/data_transfer_service.dart @@ -0,0 +1,161 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/data/db/database.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:json_schema/json_schema.dart'; +import 'package:provider/provider.dart'; + +class DataTransferService { + /// Deletes all data from the database. + static Future deleteAllData(BuildContext context) async { + final db = Provider.of(context, listen: false); + await db.gameDao.deleteAllGames(); + await db.groupDao.deleteAllGroups(); + await db.playerDao.deleteAllPlayers(); + } + + /// Retrieves all application data and converts it to a JSON string. + /// Returns the JSON string representation of the data. + static Future getAppDataAsJson(BuildContext context) async { + final db = Provider.of(context, listen: false); + final games = await db.gameDao.getAllGames(); + final groups = await db.groupDao.getAllGroups(); + final players = await db.playerDao.getAllPlayers(); + + // Construct a JSON representation of the data + final Map jsonMap = { + 'games': games.map((game) => game.toJson()).toList(), + 'groups': groups.map((group) => group.toJson()).toList(), + 'players': players.map((player) => player.toJson()).toList(), + }; + + return json.encode(jsonMap); + } + + /// Exports the given JSON string to a file with the specified name. + /// Returns an [ExportResult] indicating the outcome. + /// + /// [jsonString] The JSON string to be exported. + /// [fileName] The desired name for the exported file (without extension). + static Future exportData( + String jsonString, + String fileName, + ) async { + try { + final bytes = Uint8List.fromList(utf8.encode(jsonString)); + final path = await FilePicker.platform.saveFile( + fileName: '$fileName.json', + bytes: bytes, + ); + if (path == null) { + return ExportResult.canceled; + } else { + return ExportResult.success; + } + } catch (e, stack) { + print('[exportData] $e'); + print(stack); + return ExportResult.unknownException; + } + } + + /// Imports data from a selected JSON file into the database. + static Future importData(BuildContext context) async { + final db = Provider.of(context, listen: false); + + final path = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + ); + + if (path == null) { + return ImportResult.canceled; + } + + try { + final jsonString = await _readFileContent(path.files.single); + if (jsonString == null) { + return ImportResult.fileReadError; + } + + if (await _validateJsonSchema(jsonString)) { + final Map jsonData = + json.decode(jsonString) as Map; + + final List? gamesJson = jsonData['games'] as List?; + final List? groupsJson = jsonData['groups'] as List?; + final List? playersJson = + jsonData['players'] as List?; + + final List importedGames = + gamesJson + ?.map((g) => Game.fromJson(g as Map)) + .toList() ?? + []; + final List importedGroups = + groupsJson + ?.map((g) => Group.fromJson(g as Map)) + .toList() ?? + []; + final List importedPlayers = + playersJson + ?.map((p) => Player.fromJson(p as Map)) + .toList() ?? + []; + + await db.playerDao.addPlayers(players: importedPlayers); + await db.groupDao.addGroups(groups: importedGroups); + await db.gameDao.addGames(games: importedGames); + } else { + return ImportResult.invalidSchema; + } + return ImportResult.success; + } on FormatException catch (e, stack) { + print('[importData] FormatException'); + print('[importData] $e'); + print(stack); + return ImportResult.formatException; + } on Exception catch (e, stack) { + print('[importData] Exception'); + print('[importData] $e'); + print(stack); + return ImportResult.unknownException; + } + } + + /// Helper method to read file content from either bytes or path + static Future _readFileContent(PlatformFile file) async { + if (file.bytes != null) return utf8.decode(file.bytes!); + if (file.path != null) return await File(file.path!).readAsString(); + return null; + } + + /// Validates the given JSON string against the predefined schema. + static Future _validateJsonSchema(String jsonString) async { + final String schemaString; + + schemaString = await rootBundle.loadString('assets/schema.json'); + + try { + final schema = JsonSchema.create(json.decode(schemaString)); + final jsonData = json.decode(jsonString); + final result = schema.validate(jsonData); + + if (result.isValid) { + return true; + } + return false; + } catch (e, stack) { + print('[validateJsonSchema] $e'); + print(stack); + return false; + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index b17f409..fa4c213 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,9 @@ dependencies: provider: ^6.1.5 skeletonizer: ^2.1.0+1 uuid: ^4.5.2 + file_picker: ^10.3.6 + json_schema: ^5.2.2 + file_saver: ^0.3.1 clock: ^1.1.2 dev_dependencies: @@ -31,3 +34,5 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - assets/schema.json diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index 7590a97..0d33c1e 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -102,11 +102,9 @@ void main() { }); test('Adding and fetching multiple games works correctly', () async { - // TODO: Use upcoming addGames() method - await database.gameDao.addGame(game: testGame1); - await database.gameDao.addGame(game: testGame2); - await database.gameDao.addGame(game: testGameOnlyGroup); - await database.gameDao.addGame(game: testGameOnlyPlayers); + await database.gameDao.addGames( + games: [testGame1, testGame2, testGameOnlyGroup, testGameOnlyPlayers], + ); final allGames = await database.gameDao.getAllGames(); expect(allGames.length, 4); diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart index 6c8e6c9..a1832c1 100644 --- a/test/db_tests/group_test.dart +++ b/test/db_tests/group_test.dart @@ -81,11 +81,9 @@ void main() { }); test('Adding and fetching multiple groups works correctly', () async { - // TODO: Use upcoming addGroups() method - await database.groupDao.addGroup(group: testGroup1); - await database.groupDao.addGroup(group: testGroup2); - await database.groupDao.addGroup(group: testGroup3); - await database.groupDao.addGroup(group: testGroup4); + await database.groupDao.addGroups( + groups: [testGroup1, testGroup2, testGroup3, testGroup4], + ); final allGroups = await database.groupDao.getAllGroups(); expect(allGroups.length, 2); diff --git a/test/db_tests/player_game_test.dart b/test/db_tests/player_game_test.dart index f50afc1..e8fd707 100644 --- a/test/db_tests/player_game_test.dart +++ b/test/db_tests/player_game_test.dart @@ -63,7 +63,7 @@ void main() { expect(gameHasPlayers, false); - database.playerGameDao.addPlayerToGame( + await database.playerGameDao.addPlayerToGame( gameId: testGameOnlyGroup.id, playerId: testPlayer1.id, ); diff --git a/test/db_tests/player_test.dart b/test/db_tests/player_test.dart index 30d9a14..9a1ba16 100644 --- a/test/db_tests/player_test.dart +++ b/test/db_tests/player_test.dart @@ -56,11 +56,9 @@ void main() { }); test('Adding and fetching multiple players works correctly', () async { - // TODO: Use upcoming addPlayers() method - await database.playerDao.addPlayer(player: testPlayer1); - await database.playerDao.addPlayer(player: testPlayer2); - await database.playerDao.addPlayer(player: testPlayer3); - await database.playerDao.addPlayer(player: testPlayer4); + await database.playerDao.addPlayers( + players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], + ); final allPlayers = await database.playerDao.getAllPlayers(); expect(allPlayers.length, 4);