diff --git a/lib/data/dao/group_match_dao.dart b/lib/data/dao/group_match_dao.dart index 3c16c83..d428fb5 100644 --- a/lib/data/dao/group_match_dao.dart +++ b/lib/data/dao/group_match_dao.dart @@ -21,7 +21,7 @@ class GroupMatchDao extends DatabaseAccessor } await into(groupMatchTable).insert( GroupMatchTableCompanion.insert(groupId: groupId, matchId: matchId), - mode: InsertMode.insertOrReplace, + mode: InsertMode.insertOrIgnore, ); } diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 32bd323..b56a38b 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -65,8 +65,9 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); } - /// Adds a new [Match] to the database. - /// Also adds associated players and group if they exist. + /// 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 addMatch({required Match match}) async { await db.transaction(() async { await into(matchTable).insert( @@ -80,7 +81,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); if (match.players != null) { - await db.playerDao.addPlayersAsList(players: match.players!); for (final p in match.players ?? []) { await db.playerMatchDao.addPlayerToMatch( matchId: match.id, @@ -90,7 +90,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { } if (match.group != null) { - await db.groupDao.addGroup(group: match.group!); await db.groupMatchDao.addGroupToMatch( matchId: match.id, groupId: match.group!.id, @@ -102,6 +101,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// 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. Future addMatchAsList({required List matches}) async { if (matches.isEmpty) return; await db.transaction(() async { @@ -186,7 +186,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { matchId: match.id, playerId: p.id, ), - mode: InsertMode.insertOrReplace, + mode: InsertMode.insertOrIgnore, ); } } @@ -204,7 +204,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { playerId: m.id, groupId: match.group!.id, ), - mode: InsertMode.insertOrReplace, + mode: InsertMode.insertOrIgnore, ); } } @@ -221,7 +221,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { matchId: match.id, groupId: match.group!.id, ), - mode: InsertMode.insertOrReplace, + mode: InsertMode.insertOrIgnore, ); } } diff --git a/lib/data/dao/player_match_dao.dart b/lib/data/dao/player_match_dao.dart index 9e242b7..7ebaee6 100644 --- a/lib/data/dao/player_match_dao.dart +++ b/lib/data/dao/player_match_dao.dart @@ -18,7 +18,7 @@ class PlayerMatchDao extends DatabaseAccessor }) async { await into(playerMatchTable).insert( PlayerMatchTableCompanion.insert(playerId: playerId, matchId: matchId), - mode: InsertMode.insertOrReplace, + mode: InsertMode.insertOrIgnore, ); } @@ -121,7 +121,7 @@ class PlayerMatchDao extends DatabaseAccessor inserts.map( (c) => into( playerMatchTable, - ).insert(c, mode: InsertMode.insertOrReplace), + ).insert(c, mode: InsertMode.insertOrIgnore), ), ); } diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart index c30d6ef..37096b9 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart @@ -74,10 +74,18 @@ class _ChooseGroupViewState extends State { Expanded( child: Visibility( visible: filteredGroups.isNotEmpty, - replacement: const TopCenteredMessage( - icon: Icons.info, - title: 'Info', - message: 'There is no group matching your search', + replacement: Visibility( + visible: widget.groups.isNotEmpty, + replacement: const TopCenteredMessage( + icon: Icons.info, + title: 'Info', + message: 'You have no groups created yet', + ), + child: const TopCenteredMessage( + icon: Icons.info, + title: 'Info', + message: 'There is no group matching your search', + ), ), child: ListView.builder( padding: const EdgeInsets.only(bottom: 85), diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 787d200..f3b4d79 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -28,8 +28,8 @@ class _CreateMatchViewState extends State { /// Reference to the app database late final AppDatabase db; - /// Controller for the game name input field - final TextEditingController _gameNameController = TextEditingController(); + /// Controller for the match name input field + final TextEditingController _matchNameController = TextEditingController(); /// List of all groups from the database List groupsList = []; @@ -95,7 +95,7 @@ class _CreateMatchViewState extends State { @override void initState() { super.initState(); - _gameNameController.addListener(() { + _matchNameController.addListener(() { setState(() {}); }); @@ -119,7 +119,7 @@ class _CreateMatchViewState extends State { backgroundColor: CustomTheme.backgroundColor, scrolledUnderElevation: 0, title: const Text( - 'Create new game', + 'Create new match', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), centerTitle: true, @@ -131,8 +131,8 @@ class _CreateMatchViewState extends State { Container( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), child: TextInputField( - controller: _gameNameController, - hintText: 'Game name', + controller: _matchNameController, + hintText: 'Match name', ), ), ChooseTile( @@ -222,13 +222,13 @@ class _CreateMatchViewState extends State { ), ), CustomWidthButton( - text: 'Create game', + text: 'Create match', sizeRelativeToWidth: 0.95, buttonType: ButtonType.primary, onPressed: _enableCreateGameButton() ? () async { Match match = Match( - name: _gameNameController.text.trim(), + name: _matchNameController.text.trim(), createdAt: DateTime.now(), group: selectedGroup, players: selectedPlayers, @@ -239,7 +239,7 @@ class _CreateMatchViewState extends State { context, CupertinoPageRoute( fullscreenDialog: true, - builder: (context) => GameResultView( + builder: (context) => MatchResultView( match: match, onWinnerChanged: widget.onWinnerChanged, ), @@ -258,7 +258,7 @@ class _CreateMatchViewState extends State { /// Determines whether the "Create Game" button should be enabled based on /// the current state of the input fields. bool _enableCreateGameButton() { - return _gameNameController.text.isNotEmpty && + return _matchNameController.text.isNotEmpty && (selectedGroup != null || (selectedPlayers != null && selectedPlayers!.length > 1)) && selectedRuleset != null; diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 58ff9ce..e8075f6 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -6,17 +6,17 @@ import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart'; import 'package:provider/provider.dart'; -class GameResultView extends StatefulWidget { +class MatchResultView extends StatefulWidget { final Match match; final VoidCallback? onWinnerChanged; - const GameResultView({super.key, required this.match, this.onWinnerChanged}); + const MatchResultView({super.key, required this.match, this.onWinnerChanged}); @override - State createState() => _GameResultViewState(); + State createState() => _MatchResultViewState(); } -class _GameResultViewState extends State { +class _MatchResultViewState extends State { late final List allPlayers; late final AppDatabase db; Player? _selectedPlayer; @@ -38,6 +38,13 @@ class _GameResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + widget.onWinnerChanged?.call(); + Navigator.of(context).pop(); + }, + ), backgroundColor: CustomTheme.backgroundColor, scrolledUnderElevation: 0, title: Text( @@ -135,12 +142,12 @@ class _GameResultViewState extends State { widget.onWinnerChanged?.call(); } - List getAllPlayers(Match game) { - if (game.group == null && game.players != null) { - return [...game.players!]; - } else if (game.group != null && game.players != null) { - return [...game.players!, ...game.group!.members]; + List getAllPlayers(Match match) { + if (match.group == null && match.players != null) { + return [...match.players!]; + } else if (match.group != null && match.players != null) { + return [...match.players!, ...match.group!.members]; } - return [...game.group!.members]; + return [...match.group!.members]; } } diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 462e1b5..e7d29c0 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -80,7 +80,7 @@ class _MatchViewState extends State { context, CupertinoPageRoute( fullscreenDialog: true, - builder: (context) => GameResultView( + builder: (context) => MatchResultView( match: matches[index], onWinnerChanged: loadGames, ), diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 01c0338..eb70ae0 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -133,33 +133,42 @@ class _PlayerSelectionState extends State { style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), - Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 8.0, - runSpacing: 8.0, - children: [ - // Generates a TextIconTile for each selected player. - for (var player in selectedPlayers) - TextIconTile( - text: player.name, - onIconTap: () { - setState(() { - // Removes the player from the selection and notifies the parent. - final currentSearch = _searchBarController.text - .toLowerCase(); - selectedPlayers.remove(player); - widget.onChanged([...selectedPlayers]); - // If the player matches the current search query (or search is empty), - // they are added back to the suggestions and the list is re-sorted. - if (currentSearch.isEmpty || - player.name.toLowerCase().contains(currentSearch)) { - suggestedPlayers.add(player); - } - }); - }, - ), - ], + SizedBox( + height: 50, + child: selectedPlayers.isEmpty + ? const Center(child: Text('No players selected')) + : SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (var player in selectedPlayers) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: TextIconTile( + text: player.name, + onIconTap: () { + setState(() { + // Removes the player from the selection and notifies the parent. + final currentSearch = _searchBarController + .text + .toLowerCase(); + selectedPlayers.remove(player); + widget.onChanged([...selectedPlayers]); + // If the player matches the current search query (or search is empty), + // they are added back to the suggestions and the list is re-sorted. + if (currentSearch.isEmpty || + player.name.toLowerCase().contains( + currentSearch, + )) { + suggestedPlayers.add(player); + } + }); + }, + ), + ), + ], + ), + ), ), const SizedBox(height: 10), const Text( diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index ca86a60..0ec2cfc 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -23,7 +23,7 @@ void main() { final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); - setUp(() { + setUp(() async { database = AppDatabase( DatabaseConnection( NativeDatabase.memory(), @@ -68,6 +68,16 @@ void main() { group: testGroup2, ); }); + await database.playerDao.addPlayersAsList( + players: [ + testPlayer1, + testPlayer2, + testPlayer3, + testPlayer4, + testPlayer5, + ], + ); + await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]); }); tearDown(() async { await database.close(); @@ -253,7 +263,7 @@ void main() { expect(matchCount, 0); }); - test('Checking if match has winner works correclty', () async { + test('Checking if match has winner works correctly', () async { await database.matchDao.addMatch(match: testMatch1); await database.matchDao.addMatch(match: testMatchOnlyGroup); diff --git a/test/db_tests/group_game_test.dart b/test/db_tests/group_match_test.dart similarity index 82% rename from test/db_tests/group_game_test.dart rename to test/db_tests/group_match_test.dart index a4fa146..7d812bd 100644 --- a/test/db_tests/group_game_test.dart +++ b/test/db_tests/group_match_test.dart @@ -1,5 +1,5 @@ import 'package:clock/clock.dart'; -import 'package:drift/drift.dart'; +import 'package:drift/drift.dart' hide isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:game_tracker/data/db/database.dart'; @@ -21,7 +21,7 @@ void main() { final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); - setUp(() { + setUp(() async { database = AppDatabase( DatabaseConnection( NativeDatabase.memory(), @@ -53,6 +53,16 @@ void main() { group: testGroup1, ); }); + await database.playerDao.addPlayersAsList( + players: [ + testPlayer1, + testPlayer2, + testPlayer3, + testPlayer4, + testPlayer5, + ], + ); + await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]); }); tearDown(() async { await database.close(); @@ -179,5 +189,33 @@ void main() { } } }); + + test('Adding the same group to seperate matches works correctly', () async { + final match1 = Match(name: 'Match 1', group: testGroup1); + final match2 = Match(name: 'Match 2', group: testGroup1); + + await Future.wait([ + database.matchDao.addMatch(match: match1), + database.matchDao.addMatch(match: match2), + ]); + + final group1 = await database.groupMatchDao.getGroupOfMatch( + matchId: match1.id, + ); + final group2 = await database.groupMatchDao.getGroupOfMatch( + matchId: match2.id, + ); + + expect(group1, isNotNull); + expect(group2, isNotNull); + + final groups = [group1!, group2!]; + for (final group in groups) { + expect(group.members.length, testGroup1.members.length); + expect(group.id, testGroup1.id); + expect(group.name, testGroup1.name); + expect(group.createdAt, testGroup1.createdAt); + } + }); }); } diff --git a/test/db_tests/player_game_test.dart b/test/db_tests/player_match_test.dart similarity index 79% rename from test/db_tests/player_game_test.dart rename to test/db_tests/player_match_test.dart index e2501eb..8a4f569 100644 --- a/test/db_tests/player_game_test.dart +++ b/test/db_tests/player_match_test.dart @@ -1,5 +1,5 @@ import 'package:clock/clock.dart'; -import 'package:drift/drift.dart'; +import 'package:drift/drift.dart' hide isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:game_tracker/data/db/database.dart'; @@ -21,7 +21,7 @@ void main() { final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); - setUp(() { + setUp(() async { database = AppDatabase( DatabaseConnection( NativeDatabase.memory(), @@ -50,6 +50,17 @@ void main() { players: [testPlayer4, testPlayer5, testPlayer6], ); }); + await database.playerDao.addPlayersAsList( + players: [ + testPlayer1, + testPlayer2, + testPlayer3, + testPlayer4, + testPlayer5, + testPlayer6, + ], + ); + await database.groupDao.addGroup(group: testgroup); }); tearDown(() async { await database.close(); @@ -185,5 +196,42 @@ void main() { expect(player.createdAt, testPlayer.createdAt); } }); + + test( + 'Adding the same player to seperate matches works correctly', + () async { + final playersList = [testPlayer1, testPlayer2, testPlayer3]; + final match1 = Match(name: 'Match 1', players: playersList); + final match2 = Match(name: 'Match 2', players: playersList); + + await Future.wait([ + database.matchDao.addMatch(match: match1), + database.matchDao.addMatch(match: match2), + ]); + + final players1 = await database.playerMatchDao.getPlayersOfMatch( + matchId: match1.id, + ); + final players2 = await database.playerMatchDao.getPlayersOfMatch( + matchId: match2.id, + ); + + expect(players1, isNotNull); + expect(players2, isNotNull); + + expect( + players1!.map((p) => p.id).toList(), + equals(players2!.map((p) => p.id).toList()), + ); + expect( + players1.map((p) => p.name).toList(), + equals(players2.map((p) => p.name).toList()), + ); + expect( + players1.map((p) => p.createdAt).toList(), + equals(players2.map((p) => p.createdAt).toList()), + ); + }, + ); }); }