From e8c50b2f32aba70216942ec56cab68dc2ce8a5bb Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 24 Apr 2026 13:10:34 +0200 Subject: [PATCH 001/127] Renamed file --- test/db_tests/values/{score_test.dart => score_entry_test.dart} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/db_tests/values/{score_test.dart => score_entry_test.dart} (100%) diff --git a/test/db_tests/values/score_test.dart b/test/db_tests/values/score_entry_test.dart similarity index 100% rename from test/db_tests/values/score_test.dart rename to test/db_tests/values/score_entry_test.dart From 8df5c24fa8d886567fb97559d187fe3be9e7335a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 24 Apr 2026 13:51:20 +0200 Subject: [PATCH 002/127] Updated group dao + tests --- lib/data/dao/group_dao.dart | 132 ++--- test/db_tests/aggregates/group_test.dart | 617 ++++++++++++----------- 2 files changed, 402 insertions(+), 347 deletions(-) diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 0d66ef6..552b138 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -12,43 +12,7 @@ part 'group_dao.g.dart'; class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { GroupDao(super.db); - /// Retrieves all groups from the database. - Future> getAllGroups() async { - final query = select(groupTable); - final result = await query.get(); - return Future.wait( - result.map((groupData) async { - final members = await db.playerGroupDao.getPlayersOfGroup( - groupId: groupData.id, - ); - return Group( - id: groupData.id, - name: groupData.name, - description: groupData.description, - members: members, - createdAt: groupData.createdAt, - ); - }), - ); - } - - /// Retrieves a [Group] by its [groupId], including its members. - Future getGroupById({required String groupId}) async { - final query = select(groupTable)..where((g) => g.id.equals(groupId)); - final result = await query.getSingle(); - - List members = await db.playerGroupDao.getPlayersOfGroup( - groupId: groupId, - ); - - return Group( - id: result.id, - name: result.name, - description: result.description, - members: members, - createdAt: result.createdAt, - ); - } + /* Create */ /// Adds a new group with the given [id] and [name] to the database. /// This method also adds the group's members to the [PlayerGroupTable]. @@ -172,6 +136,65 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { }); } + /* Read */ + + /// Retrieves all groups from the database. + Future> getAllGroups() async { + final query = select(groupTable); + final result = await query.get(); + return Future.wait( + result.map((groupData) async { + final members = await db.playerGroupDao.getPlayersOfGroup( + groupId: groupData.id, + ); + return Group( + id: groupData.id, + name: groupData.name, + description: groupData.description, + members: members, + createdAt: groupData.createdAt, + ); + }), + ); + } + + /// Retrieves a [Group] by its [groupId], including its members. + Future getGroupById({required String groupId}) async { + final query = select(groupTable)..where((g) => g.id.equals(groupId)); + final result = await query.getSingle(); + + List members = await db.playerGroupDao.getPlayersOfGroup( + groupId: groupId, + ); + + return Group( + id: result.id, + name: result.name, + description: result.description, + members: members, + createdAt: result.createdAt, + ); + } + + /// Retrieves the number of groups in the database. + Future getGroupCount() async { + final count = + await (selectOnly(groupTable)..addColumns([groupTable.id.count()])) + .map((row) => row.read(groupTable.id.count())) + .getSingle(); + return count ?? 0; + } + + /// Checks if a group with the given [groupId] exists in the database. + /// Returns `true` if the group exists, `false` otherwise. + Future groupExists({required String groupId}) async { + final query = select(groupTable)..where((g) => g.id.equals(groupId)); + final result = await query.getSingleOrNull(); + return result != null; + } + + /* Delete */ + /// 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 { @@ -180,6 +203,16 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { return rowsAffected > 0; } + /// 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; + } + + /* Update */ + /// Updates the name of the group with the given [id] to [newName]. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateGroupName({ @@ -206,31 +239,6 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { return rowsAffected > 0; } - /// Retrieves the number of groups in the database. - Future getGroupCount() async { - final count = - await (selectOnly(groupTable)..addColumns([groupTable.id.count()])) - .map((row) => row.read(groupTable.id.count())) - .getSingle(); - return count ?? 0; - } - - /// Checks if a group with the given [groupId] exists in the database. - /// Returns `true` if the group exists, `false` otherwise. - Future groupExists({required String groupId}) async { - final query = select(groupTable)..where((g) => g.id.equals(groupId)); - 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; - } - /// Replaces all players in a group with the provided list of players. /// Removes all existing players from the group and adds the new players. /// Also adds any new players to the player table if they don't exist. diff --git a/test/db_tests/aggregates/group_test.dart b/test/db_tests/aggregates/group_test.dart index 3d51a06..9040daf 100644 --- a/test/db_tests/aggregates/group_test.dart +++ b/test/db_tests/aggregates/group_test.dart @@ -35,25 +35,23 @@ void main() { testPlayer4 = Player(name: 'Diana'); testGroup1 = Group( name: 'Test Group', - description: '', members: [testPlayer1, testPlayer2, testPlayer3], + description: 'description of the test group 1', ); testGroup2 = Group( id: 'gr2', name: 'Second Group', - description: '', members: [testPlayer2, testPlayer3, testPlayer4], + description: 'description of the test group 2', ); testGroup3 = Group( id: 'gr2', name: 'Second Group', - description: '', members: [testPlayer2, testPlayer4], ); testGroup4 = Group( id: 'gr2', name: 'Second Group', - description: '', members: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); }); @@ -62,312 +60,361 @@ void main() { await database.close(); }); group('Group Tests', () { - // Verifies that a single group can be added and retrieved with all fields and members intact. - test('Adding and fetching a single group works correctly', () async { - await database.groupDao.addGroup(group: testGroup1); + group('CREATE', () { + test('Adding and fetching a single group works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); - final fetchedGroup = await database.groupDao.getGroupById( - groupId: testGroup1.id, - ); - - expect(fetchedGroup.id, testGroup1.id); - expect(fetchedGroup.name, testGroup1.name); - expect(fetchedGroup.createdAt, testGroup1.createdAt); - - expect(fetchedGroup.members.length, testGroup1.members.length); - for (int i = 0; i < testGroup1.members.length; i++) { - expect(fetchedGroup.members[i].id, testGroup1.members[i].id); - expect(fetchedGroup.members[i].name, testGroup1.members[i].name); - expect( - fetchedGroup.members[i].createdAt, - testGroup1.members[i].createdAt, + final fetchedGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, ); - } - }); - // Verifies that multiple groups can be added and retrieved with correct members. - test('Adding and fetching multiple groups works correctly', () async { - await database.groupDao.addGroupsAsList( - groups: [testGroup1, testGroup2, testGroup3, testGroup4], - ); + expect(fetchedGroup.id, testGroup1.id); + expect(fetchedGroup.name, testGroup1.name); + expect(fetchedGroup.createdAt, testGroup1.createdAt); - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups.length, 2); - - final testGroups = {testGroup1.id: testGroup1, testGroup2.id: testGroup2}; - - for (final group in allGroups) { - final testGroup = testGroups[group.id]!; - - expect(group.id, testGroup.id); - expect(group.name, testGroup.name); - expect(group.createdAt, testGroup.createdAt); - - expect(group.members.length, testGroup.members.length); - for (int i = 0; i < testGroup.members.length; i++) { - expect(group.members[i].id, testGroup.members[i].id); - expect(group.members[i].name, testGroup.members[i].name); - expect(group.members[i].createdAt, testGroup.members[i].createdAt); + expect(fetchedGroup.members.length, testGroup1.members.length); + for (int i = 0; i < testGroup1.members.length; i++) { + expect(fetchedGroup.members[i].id, testGroup1.members[i].id); + expect(fetchedGroup.members[i].name, testGroup1.members[i].name); + expect( + fetchedGroup.members[i].createdAt, + testGroup1.members[i].createdAt, + ); } - } + }); + + test('Adding the same group twice does not create duplicates', () async { + await database.groupDao.addGroup(group: testGroup1); + await database.groupDao.addGroup(group: testGroup1); + + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 1); + + final fetchedGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(fetchedGroup.id, testGroup1.id); + expect(fetchedGroup.members.length, testGroup1.members.length); + }); + + test('addGroup() returns false when group already exists', () async { + final firstAdd = await database.groupDao.addGroup(group: testGroup1); + expect(firstAdd, true); + + final secondAdd = await database.groupDao.addGroup(group: testGroup1); + expect(secondAdd, false); + + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 1); + }); + + test('Adding and fetching multiple groups works correctly', () async { + await database.groupDao.addGroupsAsList( + groups: [testGroup1, testGroup2, testGroup3, testGroup4], + ); + + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 2); + + final testGroups = { + testGroup1.id: testGroup1, + testGroup2.id: testGroup2, + }; + + for (final group in allGroups) { + final testGroup = testGroups[group.id]!; + + expect(group.id, testGroup.id); + expect(group.name, testGroup.name); + expect(group.createdAt, testGroup.createdAt); + + expect(group.members.length, testGroup.members.length); + for (int i = 0; i < testGroup.members.length; i++) { + expect(group.members[i].id, testGroup.members[i].id); + expect(group.members[i].name, testGroup.members[i].name); + expect(group.members[i].createdAt, testGroup.members[i].createdAt); + } + } + }); + + test('addGroupsAsList() handles empty list correctly', () async { + await database.groupDao.addGroupsAsList(groups: []); + + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 0); + }); }); - // Verifies that adding the same group twice does not create duplicates. - test('Adding the same group twice does not create duplicates', () async { - await database.groupDao.addGroup(group: testGroup1); - await database.groupDao.addGroup(group: testGroup1); + group('READ', () { + test('groupExists() works correctly', () async { + var groupExists = await database.groupDao.groupExists( + groupId: testGroup1.id, + ); + expect(groupExists, false); - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups.length, 1); + await database.groupDao.addGroup(group: testGroup1); + + groupExists = await database.groupDao.groupExists( + groupId: testGroup1.id, + ); + expect(groupExists, true); + }); + + test('getGroupCount() works correctly', () async { + var count = await database.groupDao.getGroupCount(); + expect(count, 0); + + var added = await database.groupDao.addGroup(group: testGroup1); + expect(added, true); + count = await database.groupDao.getGroupCount(); + expect(count, 1); + + added = await database.groupDao.addGroup(group: testGroup2); + expect(added, true); + count = await database.groupDao.getGroupCount(); + expect(count, 2); + + final removed = await database.groupDao.deleteGroup( + groupId: testGroup1.id, + ); + expect(removed, true); + count = await database.groupDao.getGroupCount(); + expect(count, 1); + }); + + test('getGroupById() throws exception for non-existent group', () async { + expect( + () => database.groupDao.getGroupById(groupId: 'non-existent-id'), + throwsA(isA()), + ); + }); + + test('getAllGroups() returns empty list when no groups exist', () async { + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups, isEmpty); + }); + + test('addGroupsAsList() with duplicate groups only adds once', () async { + await database.groupDao.addGroupsAsList( + groups: [testGroup1, testGroup1, testGroup1], + ); + + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 1); + }); }); - // Verifies that groupExists returns correct boolean based on group presence. - test('Group existence check works correctly', () async { - var groupExists = await database.groupDao.groupExists( - groupId: testGroup1.id, - ); - expect(groupExists, false); + group('UPDATE', () { + test('updateGroupName() works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); - await database.groupDao.addGroup(group: testGroup1); + const newName = 'New name'; + await database.groupDao.updateGroupName( + groupId: testGroup1.id, + newName: newName, + ); - groupExists = await database.groupDao.groupExists(groupId: testGroup1.id); - expect(groupExists, true); - }); + final result = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(result.name, newName); + }); - // Verifies that deleteGroup removes the group and returns true. - test('Deleting a group works correctly', () async { - await database.groupDao.addGroup(group: testGroup1); - - final groupDeleted = await database.groupDao.deleteGroup( - groupId: testGroup1.id, - ); - expect(groupDeleted, true); - - final groupExists = await database.groupDao.groupExists( - groupId: testGroup1.id, - ); - expect(groupExists, false); - }); - - // Verifies that updateGroupName correctly updates only the name field. - test('Updating a group name works correctly', () async { - await database.groupDao.addGroup(group: testGroup1); - - const newGroupName = 'new group name'; - - await database.groupDao.updateGroupName( - groupId: testGroup1.id, - newName: newGroupName, - ); - - final result = await database.groupDao.getGroupById( - groupId: testGroup1.id, - ); - expect(result.name, newGroupName); - }); - - // Verifies that getGroupCount returns correct count through add/delete operations. - test('Getting the group count works correctly', () async { - final initialCount = await database.groupDao.getGroupCount(); - expect(initialCount, 0); - - await database.groupDao.addGroup(group: testGroup1); - - final groupAdded = await database.groupDao.getGroupCount(); - expect(groupAdded, 1); - - final groupRemoved = await database.groupDao.deleteGroup( - groupId: testGroup1.id, - ); - expect(groupRemoved, true); - - final finalCount = await database.groupDao.getGroupCount(); - expect(finalCount, 0); - }); - - // Verifies that getAllGroups returns an empty list when no groups exist. - test('getAllGroups returns empty list when no groups exist', () async { - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups, isEmpty); - }); - - // Verifies that getGroupById throws StateError for non-existent group ID. - test('getGroupById throws exception for non-existent group', () async { - expect( - () => database.groupDao.getGroupById(groupId: 'non-existent-id'), - throwsA(isA()), - ); - }); - - // Verifies that addGroup returns false when trying to add a duplicate group. - test('addGroup returns false when group already exists', () async { - final firstAdd = await database.groupDao.addGroup(group: testGroup1); - expect(firstAdd, true); - - final secondAdd = await database.groupDao.addGroup(group: testGroup1); - expect(secondAdd, false); - - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups.length, 1); - }); - - // Verifies that addGroupsAsList handles an empty list without errors. - test('addGroupsAsList handles empty list correctly', () async { - await database.groupDao.addGroupsAsList(groups: []); - - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups.length, 0); - }); - - // Verifies that deleteGroup returns false for a non-existent group ID. - test('deleteGroup returns false for non-existent group', () async { - final deleted = await database.groupDao.deleteGroup( - groupId: 'non-existent-id', - ); - expect(deleted, false); - }); - - // Verifies that updateGroupName returns false for a non-existent group ID. - test('updateGroupName returns false for non-existent group', () async { - final updated = await database.groupDao.updateGroupName( - groupId: 'non-existent-id', - newName: 'New Name', - ); - expect(updated, false); - }); - - // Verifies that updateGroupDescription correctly updates the description field. - test('Updating a group description works correctly', () async { - await database.groupDao.addGroup(group: testGroup1); - - const newDescription = 'This is a new description'; - - final updated = await database.groupDao.updateGroupDescription( - groupId: testGroup1.id, - newDescription: newDescription, - ); - expect(updated, true); - - final result = await database.groupDao.getGroupById( - groupId: testGroup1.id, - ); - expect(result.description, newDescription); - }); - - // Verifies that updateGroupDescription can set the description to null. - test('updateGroupDescription can set description to null', () async { - final groupWithDescription = Group( - name: 'Group with description', - description: 'Initial description', - members: [testPlayer1], - ); - await database.groupDao.addGroup(group: groupWithDescription); - - final updated = await database.groupDao.updateGroupDescription( - groupId: groupWithDescription.id, - newDescription: 'Updated description', - ); - expect(updated, true); - - final result = await database.groupDao.getGroupById( - groupId: groupWithDescription.id, - ); - expect(result.description, 'Updated description'); - }); - - // Verifies that updateGroupDescription returns false for a non-existent group. - test( - 'updateGroupDescription returns false for non-existent group', - () async { - final updated = await database.groupDao.updateGroupDescription( + test('updateGroupName() returns false for non-existent group', () async { + final updated = await database.groupDao.updateGroupName( groupId: 'non-existent-id', - newDescription: 'New Description', + newName: 'New name', ); expect(updated, false); - }, - ); + }); - // Verifies that deleteAllGroups removes all groups from the database. - test('deleteAllGroups removes all groups', () async { - await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]); + test('updateGroupDescription() works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); - final countBefore = await database.groupDao.getGroupCount(); - expect(countBefore, 2); + const newDescription = 'New description'; + final updated = await database.groupDao.updateGroupDescription( + groupId: testGroup1.id, + newDescription: newDescription, + ); + expect(updated, true); - final deleted = await database.groupDao.deleteAllGroups(); - expect(deleted, true); + final group = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(group.description, newDescription); + }); - final countAfter = await database.groupDao.getGroupCount(); - expect(countAfter, 0); + test( + 'updateGroupDescription() returns false for non-existent group', + () async { + final updated = await database.groupDao.updateGroupDescription( + groupId: 'non-existent-id', + newDescription: 'New description', + ); + expect(updated, false); + }, + ); + + test('Multiple updates to the same group work correctly', () async { + await database.groupDao.addGroup(group: testGroup1); + const newName = 'New name'; + const newDescription = 'New description'; + + await database.groupDao.updateGroupName( + groupId: testGroup1.id, + newName: newName, + ); + await database.groupDao.updateGroupDescription( + groupId: testGroup1.id, + newDescription: newDescription, + ); + + final updatedGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(updatedGroup.name, newName); + expect(updatedGroup.description, newDescription); + }); + + test('replaceGroupPlayers() works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); + + final initialGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(initialGroup.members.length, 3); + expect( + initialGroup.members + .map((p) => p.id) + .toList() + .contains(testPlayer1.id), + true, + ); + expect( + initialGroup.members + .map((p) => p.id) + .toList() + .contains(testPlayer2.id), + true, + ); + expect( + initialGroup.members + .map((p) => p.id) + .toList() + .contains(testPlayer3.id), + true, + ); + + final newPlayers = [testPlayer2, testPlayer4]; + final replaced = await database.groupDao.replaceGroupPlayers( + groupId: testGroup1.id, + newPlayers: newPlayers, + ); + expect(replaced, true); + + final updatedGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(updatedGroup.members.length, 2); + + final memberIds = updatedGroup.members.map((p) => p.id).toList(); + expect(memberIds.contains(testPlayer2.id), true); + expect(memberIds.contains(testPlayer4.id), true); + expect(memberIds.contains(testPlayer1.id), false); + expect(memberIds.contains(testPlayer3.id), false); + }); + + test('replaceGroupPlayers() with empty list works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); + + final initialGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(initialGroup.members.length, 3); + + final replaced = await database.groupDao.replaceGroupPlayers( + groupId: testGroup1.id, + newPlayers: [], + ); + expect(replaced, true); + + final updatedGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(updatedGroup.members.length, 0); + expect(updatedGroup.members, isEmpty); + }); + + test( + 'replaceGroupPlayers() returns false for non-existent group', + () async { + final replaced = await database.groupDao.replaceGroupPlayers( + groupId: 'non-existent-id', + newPlayers: [testPlayer1], + ); + expect(replaced, false); + }, + ); }); - // Verifies that deleteAllGroups returns false when no groups exist. - test('deleteAllGroups returns false when no groups exist', () async { - final deleted = await database.groupDao.deleteAllGroups(); - expect(deleted, false); + group('DELETE', () { + test('deleteGroup() works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); + + final groupDeleted = await database.groupDao.deleteGroup( + groupId: testGroup1.id, + ); + expect(groupDeleted, true); + + final groupExists = await database.groupDao.groupExists( + groupId: testGroup1.id, + ); + expect(groupExists, false); + }); + + test('deleteGroup() returns false for non-existent group', () async { + final deleted = await database.groupDao.deleteGroup( + groupId: 'non-existent-id', + ); + expect(deleted, false); + }); + + test('deleteAllGroups() works correctly', () async { + await database.groupDao.addGroupsAsList( + groups: [testGroup1, testGroup2], + ); + + var count = await database.groupDao.getGroupCount(); + expect(count, 2); + + final deleted = await database.groupDao.deleteAllGroups(); + expect(deleted, true); + + count = await database.groupDao.getGroupCount(); + expect(count, 0); + }); + + test('deleteAllGroups() returns false when no groups exist', () async { + final deleted = await database.groupDao.deleteAllGroups(); + expect(deleted, false); + }); }); - // Verifies that groups with special characters (quotes, emojis) are stored correctly. - test('Group with special characters in name is stored correctly', () async { - final specialGroup = Group( - name: 'Group\'s & "Special" ', - description: 'Description with émojis 🎮🎲', - members: [testPlayer1], - ); - await database.groupDao.addGroup(group: specialGroup); + group('Edge Cases', () { + test('Group with special characters is stored correctly', () async { + final specialGroup = Group( + name: 'Group\'s & "Special" ', + description: 'Description with émojis 🎮🎲', + members: [testPlayer1], + ); + await database.groupDao.addGroup(group: specialGroup); - final fetchedGroup = await database.groupDao.getGroupById( - groupId: specialGroup.id, - ); - expect(fetchedGroup.name, 'Group\'s & "Special" '); - expect(fetchedGroup.description, 'Description with émojis 🎮🎲'); - }); - - // Verifies that a group with an empty members list can be stored and retrieved. - test('Group with empty members list is stored correctly', () async { - final emptyGroup = Group( - name: 'Empty Group', - description: '', - members: [], - ); - await database.groupDao.addGroup(group: emptyGroup); - - final fetchedGroup = await database.groupDao.getGroupById( - groupId: emptyGroup.id, - ); - expect(fetchedGroup.name, 'Empty Group'); - expect(fetchedGroup.members, isEmpty); - }); - - // Verifies that multiple sequential updates to the same group work correctly. - test('Multiple updates to the same group work correctly', () async { - await database.groupDao.addGroup(group: testGroup1); - - await database.groupDao.updateGroupName( - groupId: testGroup1.id, - newName: 'Updated Name', - ); - await database.groupDao.updateGroupDescription( - groupId: testGroup1.id, - newDescription: 'Updated Description', - ); - - final updatedGroup = await database.groupDao.getGroupById( - groupId: testGroup1.id, - ); - expect(updatedGroup.name, 'Updated Name'); - expect(updatedGroup.description, 'Updated Description'); - expect(updatedGroup.members.length, testGroup1.members.length); - }); - - // Verifies that addGroupsAsList with duplicate groups only adds unique ones. - test('addGroupsAsList with duplicate groups only adds once', () async { - await database.groupDao.addGroupsAsList( - groups: [testGroup1, testGroup1, testGroup1], - ); - - final allGroups = await database.groupDao.getAllGroups(); - expect(allGroups.length, 1); + final fetchedGroup = await database.groupDao.getGroupById( + groupId: specialGroup.id, + ); + expect(fetchedGroup.name, 'Group\'s & "Special" '); + expect(fetchedGroup.description, 'Description with émojis 🎮🎲'); + }); }); }); } From 95cad71ea0550a1571affb71f4c032b064fe627f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 24 Apr 2026 14:29:01 +0200 Subject: [PATCH 003/127] Updated player dao + tests --- lib/data/dao/player_dao.dart | 118 +++-- test/db_tests/entities/player_test.dart | 611 +++++++++++------------- 2 files changed, 357 insertions(+), 372 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 5d46343..dd46e17 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -10,35 +10,7 @@ part 'player_dao.g.dart'; class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { PlayerDao(super.db); - /// Retrieves all players from the database. - Future> getAllPlayers() async { - final query = select(playerTable); - final result = await query.get(); - return result - .map( - (row) => Player( - id: row.id, - name: row.name, - description: row.description, - createdAt: row.createdAt, - nameCount: row.nameCount, - ), - ) - .toList(); - } - - /// Retrieves a [Player] by their [id]. - Future getPlayerById({required String playerId}) async { - final query = select(playerTable)..where((p) => p.id.equals(playerId)); - final result = await query.getSingle(); - return Player( - id: result.id, - name: result.name, - description: result.description, - createdAt: result.createdAt, - nameCount: result.nameCount, - ); - } + /* Create */ /// Adds a new [player] to the database. /// If a player with the same ID already exists, updates their name to @@ -135,12 +107,15 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { 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 { - final query = delete(playerTable)..where((p) => p.id.equals(playerId)); - final rowsAffected = await query.go(); - return rowsAffected > 0; + /* Read */ + + /// Retrieves the total count of players in the database. + Future getPlayerCount() async { + final count = + await (selectOnly(playerTable)..addColumns([playerTable.id.count()])) + .map((row) => row.read(playerTable.id.count())) + .getSingle(); + return count ?? 0; } /// Checks if a player with the given [playerId] exists in the database. @@ -151,8 +126,40 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return result != null; } + /// Retrieves all players from the database. + Future> getAllPlayers() async { + final query = select(playerTable); + final result = await query.get(); + return result + .map( + (row) => Player( + id: row.id, + name: row.name, + description: row.description, + createdAt: row.createdAt, + nameCount: row.nameCount, + ), + ) + .toList(); + } + + /// Retrieves a [Player] by their [id]. + Future getPlayerById({required String playerId}) async { + final query = select(playerTable)..where((p) => p.id.equals(playerId)); + final result = await query.getSingle(); + return Player( + id: result.id, + name: result.name, + description: result.description, + createdAt: result.createdAt, + nameCount: result.nameCount, + ); + } + + /* Update */ + /// Updates the name of the player with the given [playerId] to [newName]. - Future updatePlayerName({ + Future updatePlayerName({ required String playerId, required String newName, }) async { @@ -164,9 +171,10 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { ''; final previousNameCount = await getNameCount(name: previousPlayerName); - await (update(playerTable)..where((p) => p.id.equals(playerId))).write( - PlayerTableCompanion(name: Value(newName)), - ); + final rowsAffected = + await (update(playerTable)..where((p) => p.id.equals(playerId))).write( + PlayerTableCompanion(name: Value(newName)), + ); // Update name count for the new name final count = await calculateNameCount(name: newName); @@ -188,17 +196,35 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { ); } } + return rowsAffected > 0; } - /// Retrieves the total count of players in the database. - Future getPlayerCount() async { - final count = - await (selectOnly(playerTable)..addColumns([playerTable.id.count()])) - .map((row) => row.read(playerTable.id.count())) - .getSingle(); - return count ?? 0; + /// Updates the description of the player with the given [playerId] to + /// [newDescription]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future updatePlayerDescription({ + required String playerId, + required String newDescription, + }) async { + final rowsAffected = + await (update(playerTable)..where((g) => g.id.equals(playerId))).write( + PlayerTableCompanion(description: Value(newDescription)), + ); + return rowsAffected > 0; } + /* Delete */ + + /// 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 { + final query = delete(playerTable)..where((p) => p.id.equals(playerId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + + /* Name count management */ + /// Retrieves the count of players with the given [name]. Future getNameCount({required String name}) async { final query = select(playerTable)..where((p) => p.name.equals(name)); diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index 1aab348..963d10e 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -24,8 +24,8 @@ void main() { ); withClock(fakeClock, () { - testPlayer1 = Player(name: 'Test Player'); - testPlayer2 = Player(name: 'Second Player'); + testPlayer1 = Player(name: 'Anna', description: 'First test player'); + testPlayer2 = Player(name: 'Bob', description: 'Second test player'); testPlayer3 = Player(name: 'Charlie'); testPlayer4 = Player(name: 'Diana'); }); @@ -35,355 +35,314 @@ void main() { }); group('Player Tests', () { - // Verifies that players can be added and retrieved with all fields intact. - test('Adding and fetching single player works correctly', () async { - await database.playerDao.addPlayer(player: testPlayer1); - await database.playerDao.addPlayer(player: testPlayer2); + group('CREATE', () { + test('Adding and fetching single player works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer2); - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers.length, 2); + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 2); - final fetchedPlayer1 = allPlayers.firstWhere( - (g) => g.id == testPlayer1.id, - ); - expect(fetchedPlayer1.name, testPlayer1.name); - expect(fetchedPlayer1.createdAt, testPlayer1.createdAt); + final fetchedPlayer1 = allPlayers.firstWhere( + (g) => g.id == testPlayer1.id, + ); + expect(fetchedPlayer1.name, testPlayer1.name); + expect(fetchedPlayer1.createdAt, testPlayer1.createdAt); + expect(fetchedPlayer1.description, testPlayer1.description); - final fetchedPlayer2 = allPlayers.firstWhere( - (g) => g.id == testPlayer2.id, - ); - expect(fetchedPlayer2.name, testPlayer2.name); - expect(fetchedPlayer2.createdAt, testPlayer2.createdAt); - }); + final fetchedPlayer2 = allPlayers.firstWhere( + (g) => g.id == testPlayer2.id, + ); + expect(fetchedPlayer2.name, testPlayer2.name); + expect(fetchedPlayer2.createdAt, testPlayer2.createdAt); + expect(fetchedPlayer2.description, testPlayer2.description); + }); - // Verifies that multiple players can be added at once and retrieved correctly. - test('Adding and fetching multiple players works correctly', () async { - await database.playerDao.addPlayersAsList( - players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], - ); - - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers.length, 4); - - // Map for connecting fetched players with expected players - final testPlayers = { - testPlayer1.id: testPlayer1, - testPlayer2.id: testPlayer2, - testPlayer3.id: testPlayer3, - testPlayer4.id: testPlayer4, - }; - - for (final player in allPlayers) { - final testPlayer = testPlayers[player.id]!; - - expect(player.id, testPlayer.id); - expect(player.name, testPlayer.name); - expect(player.createdAt, testPlayer.createdAt); - } - }); - - // Verifies that adding the same player twice does not create duplicates. - test('Adding the same player twice does not create duplicates', () async { - await database.playerDao.addPlayer(player: testPlayer1); - await database.playerDao.addPlayer(player: testPlayer1); - - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers.length, 1); - }); - - // Verifies that playerExists returns correct boolean based on player presence. - test('Player existence check works correctly', () async { - var playerExists = await database.playerDao.playerExists( - playerId: testPlayer1.id, - ); - expect(playerExists, false); - - await database.playerDao.addPlayer(player: testPlayer1); - - playerExists = await database.playerDao.playerExists( - playerId: testPlayer1.id, - ); - expect(playerExists, true); - }); - - // Verifies that deletePlayer removes the player and returns true. - test('Deleting a player works correctly', () async { - await database.playerDao.addPlayer(player: testPlayer1); - final playerDeleted = await database.playerDao.deletePlayer( - playerId: testPlayer1.id, - ); - expect(playerDeleted, true); - - final playerExists = await database.playerDao.playerExists( - playerId: testPlayer1.id, - ); - expect(playerExists, false); - }); - - // Verifies that updatePlayerName correctly updates only the name field. - test('Updating a player name works correctly', () async { - await database.playerDao.addPlayer(player: testPlayer1); - - const newPlayerName = 'new player name'; - - await database.playerDao.updatePlayerName( - playerId: testPlayer1.id, - newName: newPlayerName, - ); - - final result = await database.playerDao.getPlayerById( - playerId: testPlayer1.id, - ); - expect(result.name, newPlayerName); - }); - - // Verifies that getPlayerCount returns correct count through add/delete operations. - test('Getting the player count works correctly', () async { - var playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 0); - - await database.playerDao.addPlayer(player: testPlayer1); - - playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 1); - - await database.playerDao.addPlayer(player: testPlayer2); - - playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 2); - - await database.playerDao.deletePlayer(playerId: testPlayer1.id); - - playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 1); - - await database.playerDao.deletePlayer(playerId: testPlayer2.id); - - playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 0); - }); - - // Verifies that getAllPlayers returns an empty list when no players exist. - test('getAllPlayers returns empty list when no players exist', () async { - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers, isEmpty); - }); - - // Verifies that getPlayerById returns the correct player. - test('getPlayerById returns correct player', () async { - await database.playerDao.addPlayer(player: testPlayer1); - await database.playerDao.addPlayer(player: testPlayer2); - - final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: testPlayer1.id, - ); - - expect(fetchedPlayer.id, testPlayer1.id); - expect(fetchedPlayer.name, testPlayer1.name); - expect(fetchedPlayer.createdAt, testPlayer1.createdAt); - expect(fetchedPlayer.description, testPlayer1.description); - }); - - // Verifies that getPlayerById throws StateError for non-existent player ID. - test('getPlayerById throws exception for non-existent player', () async { - expect( - () => database.playerDao.getPlayerById(playerId: 'non-existent-id'), - throwsA(isA()), - ); - }); - - // Verifies that addPlayer returns false when trying to add a duplicate player. - test('addPlayer returns false when player already exists', () async { - final firstAdd = await database.playerDao.addPlayer(player: testPlayer1); - expect(firstAdd, true); - - final secondAdd = await database.playerDao.addPlayer(player: testPlayer1); - expect(secondAdd, false); - }); - - // Verifies that addPlayersAsList handles empty list correctly. - test('addPlayersAsList handles empty list correctly', () async { - final result = await database.playerDao.addPlayersAsList(players: []); - expect(result, false); - - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers, isEmpty); - }); - - // Verifies that addPlayersAsList ignores duplicate player IDs. - test('addPlayersAsList with duplicate IDs ignores duplicates', () async { - await database.playerDao.addPlayersAsList( - players: [testPlayer1, testPlayer1, testPlayer2], - ); - - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers.length, 2); - }); - - // Verifies that deletePlayer returns false for non-existent player. - test('deletePlayer returns false for non-existent player', () async { - final result = await database.playerDao.deletePlayer( - playerId: 'non-existent-id', - ); - expect(result, false); - }); - - // Verifies that updatePlayerName does nothing for non-existent player (no exception). - test('updatePlayerName does nothing for non-existent player', () async { - // Should not throw, just do nothing - await database.playerDao.updatePlayerName( - playerId: 'non-existent-id', - newName: 'New Name', - ); - - final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers, isEmpty); - }); - - // Verifies that deleteAllPlayers removes all players. - test('deleteAllPlayers removes all players', () async { - await database.playerDao.addPlayersAsList( - players: [testPlayer1, testPlayer2, testPlayer3], - ); - - var playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 3); - - final result = await database.playerDao.deleteAllPlayers(); - expect(result, true); - - playerCount = await database.playerDao.getPlayerCount(); - expect(playerCount, 0); - }); - - // Verifies that deleteAllPlayers returns false when no players exist. - test('deleteAllPlayers returns false when no players exist', () async { - final result = await database.playerDao.deleteAllPlayers(); - expect(result, false); - }); - - // Verifies that a player with special characters in name is stored correctly. - test( - 'Player with special characters in name is stored correctly', - () async { - final specialPlayer = Player( - name: 'Test!@#\$%^&*()_+-=[]{}|;\':",.<>?/`~', - description: '', + test('Adding and fetching multiple players works correctly', () async { + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); - await database.playerDao.addPlayer(player: specialPlayer); + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 4); + + // Map for connecting fetched players with expected players + final testPlayers = { + testPlayer1.id: testPlayer1, + testPlayer2.id: testPlayer2, + testPlayer3.id: testPlayer3, + testPlayer4.id: testPlayer4, + }; + + for (final player in allPlayers) { + final testPlayer = testPlayers[player.id]!; + + expect(player.id, testPlayer.id); + expect(player.name, testPlayer.name); + expect(player.createdAt, testPlayer.createdAt); + expect(player.description, testPlayer.description); + } + }); + + test('Adding the same player twice does not create duplicates', () async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer1); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 1); + }); + + test('addPlayer() returns false when player already exists', () async { + var added = await database.playerDao.addPlayer(player: testPlayer1); + expect(added, true); + + added = await database.playerDao.addPlayer(player: testPlayer1); + expect(added, false); + }); + + test('addPlayersAsList() handles empty list correctly', () async { + final added = await database.playerDao.addPlayersAsList(players: []); + expect(added, false); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers, isEmpty); + }); + + test( + 'addPlayersAsList() with duplicate IDs ignores duplicates', + () async { + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer1, testPlayer2], + ); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 2); + }, + ); + + test( + 'Player with special characters in name is stored correctly', + () async { + final specialPlayer = Player( + name: 'Test!@#\$%^&*()_+-=[]{}|;\':"😎,.<>?/`~', + ); + + await database.playerDao.addPlayer(player: specialPlayer); + + final fetchedPlayer = await database.playerDao.getPlayerById( + playerId: specialPlayer.id, + ); + expect(fetchedPlayer.name, specialPlayer.name); + }, + ); + }); + + group('READ', () { + test('getPlayerCount() works correctly', () async { + var playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 0); + + await database.playerDao.addPlayer(player: testPlayer1); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 1); + + await database.playerDao.addPlayer(player: testPlayer2); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 2); + + await database.playerDao.deletePlayer(playerId: testPlayer1.id); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 1); + + await database.playerDao.deletePlayer(playerId: testPlayer2.id); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 0); + }); + + test('playerExists() works correctly', () async { + var playerExists = await database.playerDao.playerExists( + playerId: testPlayer1.id, + ); + expect(playerExists, false); + + await database.playerDao.addPlayer(player: testPlayer1); + playerExists = await database.playerDao.playerExists( + playerId: testPlayer1.id, + ); + expect(playerExists, true); + }); + + test( + 'getAllPlayers() returns empty list when no players exist', + () async { + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers, isEmpty); + }, + ); + + test('getPlayerById() returns correct player', () async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer2); final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: specialPlayer.id, + playerId: testPlayer1.id, ); - expect(fetchedPlayer.name, specialPlayer.name); - }, - ); - // Verifies that a player with description is stored correctly. - test('Player with description is stored correctly', () async { - final playerWithDescription = Player( - name: 'Described Player', - description: 'This is a test description', + expect(fetchedPlayer.id, testPlayer1.id); + expect(fetchedPlayer.name, testPlayer1.name); + expect(fetchedPlayer.createdAt, testPlayer1.createdAt); + expect(fetchedPlayer.description, testPlayer1.description); + }); + + test( + 'getPlayerById() throws exception for non-existent player', + () async { + expect( + () => database.playerDao.getPlayerById(playerId: 'non-existent-id'), + throwsA(isA()), + ); + }, ); - - await database.playerDao.addPlayer(player: playerWithDescription); - - final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: playerWithDescription.id, - ); - expect(fetchedPlayer.name, playerWithDescription.name); - expect(fetchedPlayer.description, playerWithDescription.description); }); - // Verifies that a player with null description is stored correctly. - test('Player with null description is stored correctly', () async { - final playerWithoutDescription = Player( - name: 'No Description Player', - description: '', + group('UPDATE', () { + test('updatePlayerName() works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + + const newName = 'New name'; + + await database.playerDao.updatePlayerName( + playerId: testPlayer1.id, + newName: newName, + ); + + final player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.name, newName); + }); + + test('updatePlayerName() does nothing for non-existent player', () async { + final updated = await database.playerDao.updatePlayerName( + playerId: 'non-existent-id', + newName: 'New name', + ); + expect(updated, false); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers, isEmpty); + }); + + test('updatePlayerDescription() works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + + const newDescription = 'New description'; + + final updated = await database.playerDao.updatePlayerDescription( + playerId: testPlayer1.id, + newDescription: newDescription, + ); + expect(updated, true); + + final player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.description, newDescription); + }); + + test( + 'updatePlayerDescription() does nothing for non-existent player', + () async { + final updated = await database.playerDao.updatePlayerDescription( + playerId: 'non-existent-id', + newDescription: 'New description', + ); + expect(updated, false); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers, isEmpty); + }, ); - await database.playerDao.addPlayer(player: playerWithoutDescription); + test('Multiple updates to the same player work correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); - final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: playerWithoutDescription.id, - ); - expect(fetchedPlayer.description, ''); + await database.playerDao.updatePlayerName( + playerId: testPlayer1.id, + newName: 'First Update', + ); + + var fetchedPlayer = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(fetchedPlayer.name, 'First Update'); + + await database.playerDao.updatePlayerName( + playerId: testPlayer1.id, + newName: 'Second Update', + ); + + fetchedPlayer = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(fetchedPlayer.name, 'Second Update'); + + await database.playerDao.updatePlayerDescription( + playerId: testPlayer1.id, + newDescription: 'Third Update', + ); + + fetchedPlayer = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(fetchedPlayer.description, 'Third Update'); + }); }); - // Verifies that multiple updates to the same player work correctly. - test('Multiple updates to the same player work correctly', () async { - await database.playerDao.addPlayer(player: testPlayer1); + group('DELETE', () { + test('deletePlayer() works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + final playerDeleted = await database.playerDao.deletePlayer( + playerId: testPlayer1.id, + ); + expect(playerDeleted, true); - await database.playerDao.updatePlayerName( - playerId: testPlayer1.id, - newName: 'First Update', - ); + final playerExists = await database.playerDao.playerExists( + playerId: testPlayer1.id, + ); + expect(playerExists, false); + }); - var fetchedPlayer = await database.playerDao.getPlayerById( - playerId: testPlayer1.id, - ); - expect(fetchedPlayer.name, 'First Update'); + test('deletePlayer() returns false for non-existent player', () async { + final deleted = await database.playerDao.deletePlayer( + playerId: 'non-existent-id', + ); + expect(deleted, false); + }); - await database.playerDao.updatePlayerName( - playerId: testPlayer1.id, - newName: 'Second Update', - ); + test('deleteAllPlayers() removes all players', () async { + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2, testPlayer3], + ); - fetchedPlayer = await database.playerDao.getPlayerById( - playerId: testPlayer1.id, - ); - expect(fetchedPlayer.name, 'Second Update'); + var playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 3); - await database.playerDao.updatePlayerName( - playerId: testPlayer1.id, - newName: 'Third Update', - ); + final deleted = await database.playerDao.deleteAllPlayers(); + expect(deleted, true); - fetchedPlayer = await database.playerDao.getPlayerById( - playerId: testPlayer1.id, - ); - expect(fetchedPlayer.name, 'Third Update'); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 0); + }); + + test('deleteAllPlayers() returns false when no players exist', () async { + final deleted = await database.playerDao.deleteAllPlayers(); + expect(deleted, false); + }); }); - // Verifies that a player with empty string name is stored correctly. - test('Player with empty string name is stored correctly', () async { - final emptyNamePlayer = Player(name: ''); - - await database.playerDao.addPlayer(player: emptyNamePlayer); - - final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: emptyNamePlayer.id, - ); - expect(fetchedPlayer.name, ''); - }); - - // Verifies that a player with very long name is stored correctly. - test('Player with very long name is stored correctly', () async { - final longName = 'A' * 1000; - final longNamePlayer = Player(name: longName); - - await database.playerDao.addPlayer(player: longNamePlayer); - - final fetchedPlayer = await database.playerDao.getPlayerById( - playerId: longNamePlayer.id, - ); - expect(fetchedPlayer.name, longName); - }); - - // Verifies that addPlayer returns true on first add. - test('addPlayer returns true when player is added successfully', () async { - final result = await database.playerDao.addPlayer(player: testPlayer1); - expect(result, true); - - final playerExists = await database.playerDao.playerExists( - playerId: testPlayer1.id, - ); - expect(playerExists, true); - }); - - group('Name Count Tests', () { - test('Single player gets initialized wih name count 0', () async { + group('NAME COUNT', () { + test('Single player gets initialized wih name count 0', () async { await database.playerDao.addPlayer(player: testPlayer1); final player = await database.playerDao.getPlayerById( @@ -392,7 +351,7 @@ void main() { expect(player.nameCount, 0); }); - test('Multiple players get initialized wih name count 0', () async { + test('Multiple players get initialized wih name count 0', () async { await database.playerDao.addPlayersAsList( players: [testPlayer1, testPlayer2], ); From 92577e4ed96dc4a655130ab895cf9de912291295 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 24 Apr 2026 16:01:14 +0200 Subject: [PATCH 004/127] Updated games dao + tests --- lib/data/dao/game_dao.dart | 214 +++---- test/db_tests/entities/game_test.dart | 799 +++++++++++--------------- 2 files changed, 446 insertions(+), 567 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index f07e2c7..099c501 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -10,39 +10,7 @@ part 'game_dao.g.dart'; class GameDao extends DatabaseAccessor with _$GameDaoMixin { GameDao(super.db); - /// Retrieves all games from the database. - Future> getAllGames() async { - final query = select(gameTable); - final result = await query.get(); - return result - .map( - (row) => Game( - id: row.id, - name: row.name, - ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), - description: row.description, - color: GameColor.values.firstWhere((e) => e.name == row.color), - icon: row.icon, - createdAt: row.createdAt, - ), - ) - .toList(); - } - - /// Retrieves a [Game] by its [gameId]. - Future 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: Ruleset.values.firstWhere((e) => e.name == result.ruleset), - description: result.description, - color: GameColor.values.firstWhere((e) => e.name == result.color), - icon: result.icon, - createdAt: result.createdAt, - ); - } + /* Create */ /// Adds a new [game] to the database. /// If a game with the same ID already exists, no action is taken. @@ -94,71 +62,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { 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 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 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 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 updateGameRuleset({ - required String gameId, - required Ruleset newRuleset, - }) async { - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(ruleset: Value(newRuleset.name)), - ); - } - - /// Updates the description of the game with the given [gameId]. - Future 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 updateGameColor({ - required String gameId, - required GameColor newColor, - }) async { - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(color: Value(newColor.name)), - ); - } - - /// Updates the icon of the game with the given [gameId]. - Future updateGameIcon({ - required String gameId, - required String newIcon, - }) async { - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(icon: Value(newIcon)), - ); - } + /* Read */ /// Retrieves the total count of games in the database. Future getGameCount() async { @@ -169,6 +73,120 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { return count ?? 0; } + /// Checks if a game with the given [gameId] exists in the database. + /// Returns `true` if the game exists, `false` otherwise. + Future gameExists({required String gameId}) async { + final query = select(gameTable)..where((g) => g.id.equals(gameId)); + final result = await query.getSingleOrNull(); + return result != null; + } + + /// Retrieves all games from the database. + Future> getAllGames() async { + final query = select(gameTable); + final result = await query.get(); + return result + .map( + (row) => Game( + id: row.id, + name: row.name, + ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), + description: row.description, + color: GameColor.values.firstWhere((e) => e.name == row.color), + icon: row.icon, + createdAt: row.createdAt, + ), + ) + .toList(); + } + + /// Retrieves a [Game] by its [gameId]. + Future 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: Ruleset.values.firstWhere((e) => e.name == result.ruleset), + description: result.description, + color: GameColor.values.firstWhere((e) => e.name == result.color), + icon: result.icon, + createdAt: result.createdAt, + ); + } + + /* Update */ + + /// Updates the name of the game with the given [gameId] to [newName]. + Future updateGameName({ + required String gameId, + required String newName, + }) async { + final rowsAffected = + await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + GameTableCompanion(name: Value(newName)), + ); + return rowsAffected > 0; + } + + /// Updates the ruleset of the game with the given [gameId]. + Future updateGameRuleset({ + required String gameId, + required Ruleset newRuleset, + }) async { + final rowsAffected = + await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + GameTableCompanion(ruleset: Value(newRuleset.name)), + ); + return rowsAffected > 0; + } + + /// Updates the description of the game with the given [gameId]. + Future updateGameDescription({ + required String gameId, + required String newDescription, + }) async { + final rowsAffected = + await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + GameTableCompanion(description: Value(newDescription)), + ); + return rowsAffected > 0; + } + + /// Updates the color of the game with the given [gameId]. + Future updateGameColor({ + required String gameId, + required GameColor newColor, + }) async { + final rowsAffected = + await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + GameTableCompanion(color: Value(newColor.name)), + ); + return rowsAffected > 0; + } + + /// Updates the icon of the game with the given [gameId]. + Future updateGameIcon({ + required String gameId, + required String newIcon, + }) async { + final rowsAffected = + await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + GameTableCompanion(icon: Value(newIcon)), + ); + return rowsAffected > 0; + } + + /* Delete */ + + /// 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 deleteGame({required String gameId}) async { + final query = delete(gameTable)..where((g) => g.id.equals(gameId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + /// Deletes all games from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteAllGames() async { diff --git a/test/db_tests/entities/game_test.dart b/test/db_tests/entities/game_test.dart index b00dba1..9f94910 100644 --- a/test/db_tests/entities/game_test.dart +++ b/test/db_tests/entities/game_test.dart @@ -54,492 +54,353 @@ void main() { }); group('Game Tests', () { - // Verifies that getAllGames returns an empty list when the database has no games. - test('getAllGames returns empty list when no games exist', () async { - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); + group('CREATE', () { + test('Adding and fetching a single game works correctly', () async { + final added = await database.gameDao.addGame(game: testGame1); + expect(added, true); - // Verifies that a single game can be added and retrieved with all fields intact. - test('Adding and fetching a single game works correctly', () async { - await database.gameDao.addGame(game: testGame1); + final game = await database.gameDao.getGameById(gameId: testGame1.id); + expect(game.id, testGame1.id); + expect(game.name, testGame1.name); + expect(game.ruleset, testGame1.ruleset); + expect(game.description, testGame1.description); + expect(game.color, testGame1.color); + expect(game.icon, testGame1.icon); + expect(game.createdAt, testGame1.createdAt); + }); - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 1); - expect(allGames.first.id, testGame1.id); - expect(allGames.first.name, testGame1.name); - expect(allGames.first.ruleset, testGame1.ruleset); - expect(allGames.first.description, testGame1.description); - expect(allGames.first.color, testGame1.color); - expect(allGames.first.icon, testGame1.icon); - expect(allGames.first.createdAt, testGame1.createdAt); - }); + test('Adding and fetching multiple games works correctly', () async { + final added = await database.gameDao.addGamesAsList( + games: [testGame1, testGame2, testGame3], + ); + expect(added, true); - // Verifies that multiple games can be added and retrieved correctly. - test('Adding and fetching multiple games works correctly', () async { - await database.gameDao.addGame(game: testGame1); - await database.gameDao.addGame(game: testGame2); - await database.gameDao.addGame(game: testGame3); + final allGames = await database.gameDao.getAllGames(); + expect(allGames.length, 3); - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 3); + // Map for connecting fetched games with expected games + final testGames = { + testGame1.id: testGame1, + testGame2.id: testGame2, + testGame3.id: testGame3, + }; - final names = allGames.map((g) => g.name).toList(); - expect(names, containsAll(['Chess', 'Poker', 'Monopoly'])); - }); + for (final game in allGames) { + final testGame = testGames[game.id]!; - // Verifies that getGameById returns the correct game with all properties. - test('getGameById returns correct game', () async { - await database.gameDao.addGame(game: testGame1); - await database.gameDao.addGame(game: testGame2); + expect(game.id, testGame.id); + expect(game.name, testGame.name); + expect(game.createdAt, testGame.createdAt); + expect(game.description, testGame.description); + expect(game.ruleset, testGame.ruleset); + expect(game.color, testGame.color); + expect(game.icon, testGame.icon); + } + }); - final game = await database.gameDao.getGameById(gameId: testGame2.id); - expect(game.id, testGame2.id); - expect(game.name, testGame2.name); - expect(game.ruleset, testGame2.ruleset); - expect(game.description, testGame2.description); - expect(game.color, testGame2.color); - expect(game.icon, testGame2.icon); - }); + test('addGamesAsList() returns false for empty list', () async { + final result = await database.gameDao.addGamesAsList(games: []); + expect(result, false); - // Verifies that getGameById throws a StateError when the game doesn't exist. - test('getGameById throws exception for non-existent game', () async { - expect( - () => database.gameDao.getGameById(gameId: 'non-existent-id'), - throwsA(isA()), + final allGames = await database.gameDao.getAllGames(); + expect(allGames.length, 0); + }); + + test('addGamesAsList() ignores duplicate games', () async { + final added = await database.gameDao.addGamesAsList( + games: [testGame1, testGame2, testGame1], + ); + expect(added, true); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames.length, 2); + }); + + test( + 'Game with special characters in name is stored correctly', + () async { + final specialGame = Game( + name: 'Game\'s & "Special" ', + ruleset: Ruleset.multipleWinners, + description: 'Description with émojis 🎮🎲', + color: GameColor.purple, + icon: '', + ); + await database.gameDao.addGame(game: specialGame); + + final fetchedGame = await database.gameDao.getGameById( + gameId: specialGame.id, + ); + expect(fetchedGame.name, 'Game\'s & "Special" '); + expect(fetchedGame.description, 'Description with émojis 🎮🎲'); + }, ); }); - // Verifies that addGame returns true when a game is successfully added. - test('addGame returns true when game is added successfully', () async { - final result = await database.gameDao.addGame(game: testGame1); - expect(result, true); + group('READ', () { + test('getGameById() works correctly', () async { + await database.gameDao.addGame(game: testGame1); - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 1); + final game = await database.gameDao.getGameById(gameId: testGame1.id); + expect(game.id, testGame2.id); + expect(game.name, testGame2.name); + expect(game.ruleset, testGame2.ruleset); + expect(game.description, testGame2.description); + expect(game.color, testGame2.color); + expect(game.icon, testGame2.icon); + }); + + test('getGameById() throws exception for non-existent game', () async { + expect( + () => database.gameDao.getGameById(gameId: 'non-existent-id'), + throwsA(isA()), + ); + }); + + test('gameExists() works correctly', () async { + var exists = await database.gameDao.gameExists(gameId: testGame1.id); + expect(exists, false); + + await database.gameDao.addGame(game: testGame1); + exists = await database.gameDao.gameExists(gameId: testGame1.id); + expect(exists, true); + }); + + test('getAllGames() returns empty list when no games exist', () async { + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test('getGameCount() works correctly', () async { + var count = await database.gameDao.getGameCount(); + expect(count, 0); + + await database.gameDao.addGame(game: testGame1); + count = await database.gameDao.getGameCount(); + expect(count, 1); + + await database.gameDao.addGame(game: testGame2); + count = await database.gameDao.getGameCount(); + expect(count, 2); + + await database.gameDao.deleteGame(gameId: testGame1.id); + count = await database.gameDao.getGameCount(); + expect(count, 1); + }); }); - // Verifies that addGame returns false when trying to add a duplicate game. - test('addGame returns false when game already exists', () async { - final firstAdd = await database.gameDao.addGame(game: testGame1); - expect(firstAdd, true); + group('UPDATE', () { + test('updateGameName() updates the name correctly', () async { + await database.gameDao.addGame(game: testGame1); + const newName = 'New name'; - final secondAdd = await database.gameDao.addGame(game: testGame1); - expect(secondAdd, false); + final updated = await database.gameDao.updateGameName( + gameId: testGame1.id, + newName: newName, + ); + expect(updated, true); - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 1); + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + expect(updatedGame.name, newName); + }); + + test('updateGameName() does nothing for non-existent game', () async { + final updated = await database.gameDao.updateGameName( + gameId: 'non-existent-id', + newName: 'New name', + ); + expect(updated, false); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test('updateGameRuleset() updates the ruleset correctly', () async { + await database.gameDao.addGame(game: testGame1); + const ruleset = Ruleset.highestScore; + + final updated = await database.gameDao.updateGameRuleset( + gameId: testGame1.id, + newRuleset: ruleset, + ); + expect(updated, true); + + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + expect(updatedGame.ruleset, ruleset); + }); + + test('updateGameRuleset() does nothing for non-existent game', () async { + final updated = await database.gameDao.updateGameRuleset( + gameId: 'non-existent-id', + newRuleset: Ruleset.lowestScore, + ); + expect(updated, false); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test( + 'updateGameDescription() updates the description correctly', + () async { + await database.gameDao.addGame(game: testGame1); + const newDescription = 'New description'; + + final updated = await database.gameDao.updateGameDescription( + gameId: testGame1.id, + newDescription: newDescription, + ); + expect(updated, true); + + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + expect(updatedGame.description, newDescription); + }, + ); + + test( + 'updateGameDescription() does nothing for non-existent game', + () async { + final updated = await database.gameDao.updateGameDescription( + gameId: 'non-existent-id', + newDescription: 'New description', + ); + expect(updated, false); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }, + ); + + test('updateGameColor() works correctly', () async { + await database.gameDao.addGame(game: testGame1); + + await database.gameDao.updateGameColor( + gameId: testGame1.id, + newColor: GameColor.green, + ); + + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + expect(updatedGame.color, GameColor.green); + }); + + test('updateGameColor() does nothing for non-existent game', () async { + final updated = await database.gameDao.updateGameColor( + gameId: 'non-existent-id', + newColor: GameColor.green, + ); + expect(updated, false); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test('updateGameIcon() works correctly', () async { + await database.gameDao.addGame(game: testGame1); + const newIcon = 'new_chess_icon'; + + final updated = await database.gameDao.updateGameIcon( + gameId: testGame1.id, + newIcon: newIcon, + ); + expect(updated, true); + + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + expect(updatedGame.icon, newIcon); + }); + + test('updateGameIcon() does nothing for non-existent game', () async { + final updated = await database.gameDao.updateGameIcon( + gameId: 'non-existent-id', + newIcon: 'New icon', + ); + expect(updated, false); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test('Multiple updates to the same game work correctly', () async { + await database.gameDao.addGame(game: testGame1); + + const newName = 'New name'; + await database.gameDao.updateGameName( + gameId: testGame1.id, + newName: newName, + ); + + const newGameColor = GameColor.teal; + await database.gameDao.updateGameColor( + gameId: testGame1.id, + newColor: newGameColor, + ); + + const newDescription = 'New description'; + await database.gameDao.updateGameDescription( + gameId: testGame1.id, + newDescription: newDescription, + ); + + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + + // Changed values + expect(updatedGame.name, newName); + expect(updatedGame.color, newGameColor); + expect(updatedGame.description, newDescription); + + // Staying the same + expect(updatedGame.ruleset, testGame1.ruleset); + expect(updatedGame.icon, testGame1.icon); + }); }); - - // Verifies that a game with empty optional fields can be added and retrieved. - test('addGame handles game with null optional fields', () async { - final gameWithNulls = Game( - name: 'Simple Game', - ruleset: Ruleset.lowestScore, - description: 'A simple game', - color: GameColor.green, - icon: '', - ); - final result = await database.gameDao.addGame(game: gameWithNulls); - expect(result, true); - - final fetchedGame = await database.gameDao.getGameById( - gameId: gameWithNulls.id, - ); - expect(fetchedGame.name, 'Simple Game'); - expect(fetchedGame.description, 'A simple game'); - expect(fetchedGame.color, GameColor.green); - expect(fetchedGame.icon, ''); - }); - - // Verifies that multiple games can be added at once using addGamesAsList. - test('addGamesAsList adds multiple games correctly', () async { - final result = await database.gameDao.addGamesAsList( - games: [testGame1, testGame2, testGame3], - ); - expect(result, true); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 3); - }); - - // Verifies that addGamesAsList returns false when given an empty list. - test('addGamesAsList returns false for empty list', () async { - final result = await database.gameDao.addGamesAsList(games: []); - expect(result, false); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 0); - }); - - // Verifies that addGamesAsList ignores duplicate games when adding. - test('addGamesAsList ignores duplicate games', () async { - await database.gameDao.addGame(game: testGame1); - - final result = await database.gameDao.addGamesAsList( - games: [testGame1, testGame2], - ); - expect(result, true); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 2); - }); - - // Verifies that deleteGame returns true and removes the game from database. - test('deleteGame returns true when game is deleted', () async { - await database.gameDao.addGame(game: testGame1); - - final result = await database.gameDao.deleteGame(gameId: testGame1.id); - expect(result, true); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that deleteGame returns false for a non-existent game ID. - test('deleteGame returns false for non-existent game', () async { - final result = await database.gameDao.deleteGame( - gameId: 'non-existent-id', - ); - expect(result, false); - }); - - // Verifies that deleteGame only removes the specified game, leaving others intact. - test('deleteGame only deletes the specified game', () async { - await database.gameDao.addGamesAsList( - games: [testGame1, testGame2, testGame3], - ); - - await database.gameDao.deleteGame(gameId: testGame2.id); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 2); - expect(allGames.any((g) => g.id == testGame2.id), false); - expect(allGames.any((g) => g.id == testGame1.id), true); - expect(allGames.any((g) => g.id == testGame3.id), true); - }); - - // Verifies that gameExists returns true when the game exists in database. - test('gameExists returns true for existing game', () async { - await database.gameDao.addGame(game: testGame1); - - final exists = await database.gameDao.gameExists(gameId: testGame1.id); - expect(exists, true); - }); - - // Verifies that gameExists returns false for a non-existent game ID. - test('gameExists returns false for non-existent game', () async { - final exists = await database.gameDao.gameExists( - gameId: 'non-existent-id', - ); - expect(exists, false); - }); - - // Verifies that gameExists returns false after a game has been deleted. - test('gameExists returns false after game is deleted', () async { - await database.gameDao.addGame(game: testGame1); - await database.gameDao.deleteGame(gameId: testGame1.id); - - final exists = await database.gameDao.gameExists(gameId: testGame1.id); - expect(exists, false); - }); - - // Verifies that updateGameName correctly updates only the name field. - test('updateGameName updates the name correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameName( - gameId: testGame1.id, - newName: 'Updated Chess', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.name, 'Updated Chess'); - expect(updatedGame.ruleset, testGame1.ruleset); - }); - - // Verifies that updateGameName does nothing when game doesn't exist. - test('updateGameName does nothing for non-existent game', () async { - await database.gameDao.updateGameName( - gameId: 'non-existent-id', - newName: 'New Name', - ); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that updateGameRuleset correctly updates only the ruleset field. - test('updateGameRuleset updates the ruleset correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameRuleset( - gameId: testGame1.id, - newRuleset: Ruleset.highestScore, - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.ruleset, Ruleset.highestScore); - expect(updatedGame.name, testGame1.name); - }); - - // Verifies that updateGameRuleset does nothing when game doesn't exist. - test('updateGameRuleset does nothing for non-existent game', () async { - await database.gameDao.updateGameRuleset( - gameId: 'non-existent-id', - newRuleset: Ruleset.lowestScore, - ); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that updateGameDescription correctly updates the description. - test('updateGameDescription updates the description correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameDescription( - gameId: testGame1.id, - newDescription: 'An updated description', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.description, 'An updated description'); - }); - - // Verifies that updateGameDescription can set the description to an empty string. - test('updateGameDescription can set description to empty string', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameDescription( - gameId: testGame1.id, - newDescription: '', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.description, ''); - }); - - // Verifies that updateGameDescription does nothing when game doesn't exist. - test('updateGameDescription does nothing for non-existent game', () async { - await database.gameDao.updateGameDescription( - gameId: 'non-existent-id', - newDescription: 'New Description', - ); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that updateGameColor correctly updates the color value. - test('updateGameColor updates the color correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameColor( - gameId: testGame1.id, - newColor: GameColor.green, - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.color, GameColor.green); - }); - - // Verifies that updateGameColor does nothing when game doesn't exist. - test('updateGameColor does nothing for non-existent game', () async { - await database.gameDao.updateGameColor( - gameId: 'non-existent-id', - newColor: GameColor.green, - ); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that updateGameIcon correctly updates the icon value. - test('updateGameIcon updates the icon correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameIcon( - gameId: testGame1.id, - newIcon: 'new_chess_icon', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.icon, 'new_chess_icon'); - }); - - // Verifies that updateGameIcon can update the icon. - test('updateGameIcon updates icon correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameIcon( - gameId: testGame1.id, - newIcon: 'new_icon', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.icon, 'new_icon'); - }); - - // Verifies that updateGameIcon does nothing when game doesn't exist. - test('updateGameIcon does nothing for non-existent game', () async { - await database.gameDao.updateGameIcon( - gameId: 'non-existent-id', - newIcon: 'some_icon', - ); - - final allGames = await database.gameDao.getAllGames(); - expect(allGames, isEmpty); - }); - - // Verifies that getGameCount returns 0 when no games exist. - test('getGameCount returns 0 when no games exist', () async { - final count = await database.gameDao.getGameCount(); - expect(count, 0); - }); - - // Verifies that getGameCount returns the correct count after adding games. - test('getGameCount returns correct count after adding games', () async { - await database.gameDao.addGamesAsList( - games: [testGame1, testGame2, testGame3], - ); - - final count = await database.gameDao.getGameCount(); - expect(count, 3); - }); - - // Verifies that getGameCount updates correctly after deleting a game. - test('getGameCount updates correctly after deletion', () async { - await database.gameDao.addGamesAsList(games: [testGame1, testGame2]); - - final countBefore = await database.gameDao.getGameCount(); - expect(countBefore, 2); - - await database.gameDao.deleteGame(gameId: testGame1.id); - - final countAfter = await database.gameDao.getGameCount(); - expect(countAfter, 1); - }); - - // Verifies that deleteAllGames removes all games from the database. - test('deleteAllGames removes all games', () async { - await database.gameDao.addGamesAsList( - games: [testGame1, testGame2, testGame3], - ); - - final countBefore = await database.gameDao.getGameCount(); - expect(countBefore, 3); - - final result = await database.gameDao.deleteAllGames(); - expect(result, true); - - final countAfter = await database.gameDao.getGameCount(); - expect(countAfter, 0); - }); - - // Verifies that deleteAllGames returns false when no games exist. - test('deleteAllGames returns false when no games exist', () async { - final result = await database.gameDao.deleteAllGames(); - expect(result, false); - }); - - // Verifies that games with special characters (quotes, emojis) are stored correctly. - test('Game with special characters in name is stored correctly', () async { - final specialGame = Game( - name: 'Game\'s & "Special" ', - ruleset: Ruleset.multipleWinners, - description: 'Description with émojis 🎮🎲', - color: GameColor.purple, - icon: '', - ); - await database.gameDao.addGame(game: specialGame); - - final fetchedGame = await database.gameDao.getGameById( - gameId: specialGame.id, - ); - expect(fetchedGame.name, 'Game\'s & "Special" '); - expect(fetchedGame.description, 'Description with émojis 🎮🎲'); - }); - - // Verifies that games with empty string fields are stored and retrieved correctly. - test('Game with empty string fields is stored correctly', () async { - final emptyGame = Game( - name: '', - ruleset: Ruleset.singleWinner, - description: '', - icon: '', - color: GameColor.red, - ); - await database.gameDao.addGame(game: emptyGame); - - final fetchedGame = await database.gameDao.getGameById( - gameId: emptyGame.id, - ); - expect(fetchedGame.name, ''); - expect(fetchedGame.ruleset, Ruleset.singleWinner); - expect(fetchedGame.description, ''); - expect(fetchedGame.icon, ''); - }); - - // Verifies that games with very long strings (10000 chars) are handled correctly. - test('Game with very long strings is stored correctly', () async { - final longString = 'A' * 10000; - final longGame = Game( - name: longString, - description: longString, - ruleset: Ruleset.multipleWinners, - color: GameColor.yellow, - icon: '', - ); - await database.gameDao.addGame(game: longGame); - - final fetchedGame = await database.gameDao.getGameById( - gameId: longGame.id, - ); - expect(fetchedGame.name.length, 10000); - expect(fetchedGame.description.length, 10000); - expect(fetchedGame.ruleset, Ruleset.multipleWinners); - }); - - // Verifies that multiple sequential updates to the same game work correctly. - test('Multiple updates to the same game work correctly', () async { - await database.gameDao.addGame(game: testGame1); - - await database.gameDao.updateGameName( - gameId: testGame1.id, - newName: 'Updated Name', - ); - await database.gameDao.updateGameColor( - gameId: testGame1.id, - newColor: GameColor.teal, - ); - await database.gameDao.updateGameDescription( - gameId: testGame1.id, - newDescription: 'Updated Description', - ); - - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.name, 'Updated Name'); - expect(updatedGame.color, GameColor.teal); - expect(updatedGame.description, 'Updated Description'); - expect(updatedGame.ruleset, testGame1.ruleset); - expect(updatedGame.icon, testGame1.icon); + group('DELETE', () { + test('deleteGame() works correctly', () async { + await database.gameDao.addGame(game: testGame1); + + final deleted = await database.gameDao.deleteGame(gameId: testGame1.id); + expect(deleted, true); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames, isEmpty); + }); + + test('deleteGame() returns false for non-existent game', () async { + final deleted = await database.gameDao.deleteGame( + gameId: 'non-existent-id', + ); + expect(deleted, false); + }); + + test('deleteAllGames() removes all games', () async { + await database.gameDao.addGamesAsList( + games: [testGame1, testGame2, testGame3], + ); + + var count = await database.gameDao.getGameCount(); + expect(count, 3); + + final deleted = await database.gameDao.deleteAllGames(); + expect(deleted, true); + + count = await database.gameDao.getGameCount(); + expect(count, 0); + }); + + test('deleteAllGames() returns false when no games exist', () async { + final deleted = await database.gameDao.deleteAllGames(); + expect(deleted, false); + }); }); }); } From 99b894c58091fc1ab3789357628bc3c1f2228626 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 24 Apr 2026 16:36:27 +0200 Subject: [PATCH 005/127] Updated score dao + tests --- lib/data/dao/score_entry_dao.dart | 72 ++-- .../match_view/match_result_view.dart | 4 +- test/db_tests/values/score_entry_test.dart | 378 ++++++++---------- 3 files changed, 215 insertions(+), 239 deletions(-) diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index 566b9d1..0d04e33 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -13,6 +13,8 @@ class ScoreEntryDao extends DatabaseAccessor with _$ScoreEntryDaoMixin { ScoreEntryDao(super.db); + /* Create */ + /// Adds a score entry to the database. Future addScore({ required String playerId, @@ -58,6 +60,8 @@ class ScoreEntryDao extends DatabaseAccessor }); } + /* Read */ + /// Retrieves the score for a specific round. Future getScore({ required String playerId, @@ -126,6 +130,34 @@ class ScoreEntryDao extends DatabaseAccessor ); } + /// Gets the highest (latest) round number for a match. + /// Returns `null` if there are no scores for the match. + Future getLatestRoundNumber({required String matchId}) async { + final query = selectOnly(scoreEntryTable) + ..where(scoreEntryTable.matchId.equals(matchId)) + ..addColumns([scoreEntryTable.roundNumber.max()]); + final result = await query.getSingle(); + return result.read(scoreEntryTable.roundNumber.max()); + } + + /// Aggregates the total score for a player in a match by summing all their + /// score entry changes. Returns `0` if there are no scores for the player + /// in the match. + Future getTotalScoreForPlayer({ + required String playerId, + required String matchId, + }) async { + final scores = await getAllPlayerScoresInMatch( + playerId: playerId, + matchId: matchId, + ); + if (scores.isEmpty) return 0; + // Return the sum of all score changes + return scores.fold(0, (sum, element) => sum + element.change); + } + + /* Update */ + /// Updates a score entry. Future updateScore({ required String playerId, @@ -148,6 +180,8 @@ class ScoreEntryDao extends DatabaseAccessor return rowsAffected > 0; } + /* Delete */ + /// Deletes a score entry. Future deleteScore({ required String playerId, @@ -182,31 +216,7 @@ class ScoreEntryDao extends DatabaseAccessor return rowsAffected > 0; } - /// Gets the highest (latest) round number for a match. - /// Returns `null` if there are no scores for the match. - Future getLatestRoundNumber({required String matchId}) async { - final query = selectOnly(scoreEntryTable) - ..where(scoreEntryTable.matchId.equals(matchId)) - ..addColumns([scoreEntryTable.roundNumber.max()]); - final result = await query.getSingle(); - return result.read(scoreEntryTable.roundNumber.max()); - } - - /// Aggregates the total score for a player in a match by summing all their - /// score entry changes. Returns `0` if there are no scores for the player - /// in the match. - Future getTotalScoreForPlayer({ - required String playerId, - required String matchId, - }) async { - final scores = await getAllPlayerScoresInMatch( - playerId: playerId, - matchId: matchId, - ); - if (scores.isEmpty) return 0; - // Return the sum of all score changes - return scores.fold(0, (sum, element) => sum + element.change); - } + /* Winner handling */ Future hasWinner({required String matchId}) async { return await getWinner(matchId: matchId) != null; @@ -275,12 +285,14 @@ class ScoreEntryDao extends DatabaseAccessor } } - Future hasLooser({required String matchId}) async { - return await getLooser(matchId: matchId) != null; + /* Loser handling */ + + Future hasLoser({required String matchId}) async { + return await getLoser(matchId: matchId) != null; } // Setting the looser for a game and clearing previous looser if exists. - Future setLooser({ + Future setLoser({ required String matchId, required String playerId, }) async { @@ -304,7 +316,7 @@ class ScoreEntryDao extends DatabaseAccessor /// Retrieves the looser of a match by looking for a score entry where score /// is 0. Returns `null` if no player found, else the first with the score. - Future getLooser({required String matchId}) async { + Future getLoser({required String matchId}) async { final query = select(scoreEntryTable).join([ innerJoin( @@ -332,7 +344,7 @@ class ScoreEntryDao extends DatabaseAccessor /// /// Returns `true` if the looser was removed, `false` if there are multiple /// scores or if the looser cannot be removed. - Future removeLooser({required String matchId}) async { + Future removeLoser({required String matchId}) async { final scores = await getAllMatchScores(matchId: matchId); if (scores.length > 1) { 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 8b41920..1fd6780 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 @@ -242,9 +242,9 @@ class _MatchResultViewState extends State { /// Handles saving or removing the loser in the database. Future _handleLoser() async { if (_selectedPlayer == null) { - return await db.scoreEntryDao.removeLooser(matchId: widget.match.id); + return await db.scoreEntryDao.removeLoser(matchId: widget.match.id); } else { - return await db.scoreEntryDao.setLooser( + return await db.scoreEntryDao.setLoser( matchId: widget.match.id, playerId: _selectedPlayer!.id, ); diff --git a/test/db_tests/values/score_entry_test.dart b/test/db_tests/values/score_entry_test.dart index d550995..bb41a9a 100644 --- a/test/db_tests/values/score_entry_test.dart +++ b/test/db_tests/values/score_entry_test.dart @@ -17,6 +17,9 @@ void main() { late Game testGame; late Match testMatch1; late Match testMatch2; + ScoreEntry entryRound1 = ScoreEntry(roundNumber: 1, score: 10, change: 10); + ScoreEntry entryRound2 = ScoreEntry(roundNumber: 2, score: 25, change: 15); + ScoreEntry entryRound3 = ScoreEntry(roundNumber: 3, score: 30, change: 5); final fixedDate = DateTime(2025, 11, 19, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -65,13 +68,12 @@ void main() { }); group('Score Tests', () { - group('Adding and Fetching scores', () { - test('Single Score', () async { - ScoreEntry entry = ScoreEntry(roundNumber: 1, score: 10, change: 10); + group('CREATE', () { + test('Adding and fetching single score works correctly', () async { await database.scoreEntryDao.addScore( playerId: testPlayer1.id, matchId: testMatch1.id, - entry: entry, + entry: entryRound1, ); final score = await database.scoreEntryDao.getScore( @@ -81,41 +83,37 @@ void main() { ); expect(score, isNotNull); - expect(score!.roundNumber, 1); - expect(score.score, 10); - expect(score.change, 10); + expect(score!.roundNumber, entryRound1.roundNumber); + expect(score.score, entryRound1.score); + expect(score.change, entryRound1.change); }); - test('Multiple Scores', () async { - final entryList = [ - ScoreEntry(roundNumber: 1, score: 5, change: 5), - ScoreEntry(roundNumber: 2, score: 12, change: 7), - ScoreEntry(roundNumber: 3, score: 18, change: 6), - ]; - + test('Adding and fetching single score works correctly', () async { await database.scoreEntryDao.addScoresAsList( - entrys: entryList, + entrys: [entryRound1, entryRound2, entryRound3], playerId: testPlayer1.id, matchId: testMatch1.id, ); - final scores = await database.scoreEntryDao.getAllPlayerScoresInMatch( + final entrys = await database.scoreEntryDao.getAllPlayerScoresInMatch( playerId: testPlayer1.id, matchId: testMatch1.id, ); - expect(scores, isNotNull); + expect(entrys, isNotEmpty); - // Scores should be returned in order of round number - for (int i = 0; i < entryList.length; i++) { - expect(scores[i].roundNumber, entryList[i].roundNumber); - expect(scores[i].score, entryList[i].score); - expect(scores[i].change, entryList[i].change); + // Map for connecting fetched entry with expected entrys + final testScores = {1: entryRound1, 2: entryRound2, 3: entryRound3}; + + for (final entry in entrys) { + final testEntry = testScores[entry.roundNumber]!; + + expect(entry.roundNumber, testEntry.roundNumber); + expect(entry.score, testEntry.score); + expect(entry.change, testEntry.change); } }); - }); - group('Undesirable values', () { test('Score & Round can have negative values', () async { ScoreEntry entry = ScoreEntry(roundNumber: -2, score: -10, change: -10); await database.scoreEntryDao.addScore( @@ -155,6 +153,31 @@ void main() { expect(score.change, 0); }); + test('Adding the same score twice replaces the existing one', () async { + await database.scoreEntryDao.addScore( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entry: entryRound1, + ); + await database.scoreEntryDao.addScore( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entry: entryRound1, + ); + + final score = await database.scoreEntryDao.getScore( + playerId: testPlayer1.id, + matchId: testMatch1.id, + roundNumber: 1, + ); + + expect(score, isNotNull); + expect(score!.score, entryRound1.score); + expect(score.change, entryRound1.change); + }); + }); + + group('READ', () { test('Getting score for a non-existent entities returns null', () async { var score = await database.scoreEntryDao.getScore( playerId: testPlayer1.id, @@ -201,10 +224,8 @@ void main() { expect(score, isNull); }); - }); - group('Scores in matches', () { - test('getAllMatchScores()', () async { + test('getAllMatchScores() works correctly', () async { ScoreEntry entry1 = ScoreEntry(roundNumber: 1, score: 10, change: 10); ScoreEntry entry2 = ScoreEntry(roundNumber: 1, score: 20, change: 20); ScoreEntry entry3 = ScoreEntry(roundNumber: 2, score: 25, change: 15); @@ -241,7 +262,7 @@ void main() { expect(scores.isEmpty, true); }); - test('getAllPlayerScoresInMatch()', () async { + test('getAllPlayerScoresInMatch() works correctly', () async { ScoreEntry entry1 = ScoreEntry(roundNumber: 1, score: 10, change: 10); ScoreEntry entry2 = ScoreEntry(roundNumber: 2, score: 25, change: 15); ScoreEntry entry3 = ScoreEntry(roundNumber: 1, score: 30, change: 30); @@ -315,28 +336,106 @@ void main() { expect(match2Scores[0].score, 50); expect(match2Scores[0].change, 50); }); + + test('getLatestRoundNumber() works correctly', () async { + var latestRound = await database.scoreEntryDao.getLatestRoundNumber( + matchId: testMatch1.id, + ); + expect(latestRound, isNull); + + await database.scoreEntryDao.addScore( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entry: entryRound1, + ); + + latestRound = await database.scoreEntryDao.getLatestRoundNumber( + matchId: testMatch1.id, + ); + expect(latestRound, 1); + + await database.scoreEntryDao.addScore( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entry: entryRound2, + ); + + latestRound = await database.scoreEntryDao.getLatestRoundNumber( + matchId: testMatch1.id, + ); + expect(latestRound, 2); + }); + + test('getLatestRoundNumber() with non-consecutive rounds', () async { + await database.scoreEntryDao.addScoresAsList( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entrys: [entryRound1, entryRound3], + ); + + final latestRound = await database.scoreEntryDao.getLatestRoundNumber( + matchId: testMatch1.id, + ); + + expect(latestRound, 3); + }); + + test('getTotalScoreForPlayer() works correctly', () async { + var totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( + playerId: testPlayer1.id, + matchId: testMatch1.id, + ); + expect(totalScore, 0); + + await database.scoreEntryDao.addScoresAsList( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entrys: [entryRound1, entryRound2, entryRound3], + ); + + totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( + playerId: testPlayer1.id, + matchId: testMatch1.id, + ); + final expectedTotal = + entryRound1.change + entryRound2.change + entryRound3.change; + expect(totalScore, expectedTotal); + }); + + test('getTotalScoreForPlayer() ignores round score', () async { + await database.scoreEntryDao.addScoresAsList( + playerId: testPlayer1.id, + matchId: testMatch1.id, + entrys: [ + ScoreEntry(roundNumber: 2, score: 25, change: 25), + ScoreEntry(roundNumber: 1, score: 25, change: 10), + ScoreEntry(roundNumber: 3, score: 25, change: 25), + ], + ); + + final totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( + playerId: testPlayer1.id, + matchId: testMatch1.id, + ); + + // Should return the sum of all changes + expect(totalScore, 60); + }); }); - group('Updating scores', () { - test('updateScore()', () async { - ScoreEntry entry1 = ScoreEntry(roundNumber: 1, score: 10, change: 10); - ScoreEntry entry2 = ScoreEntry(roundNumber: 2, score: 15, change: 5); - await database.scoreEntryDao.addScore( + group('UPDATE', () { + test('updateScore() works correctly', () async { + await database.scoreEntryDao.addScoresAsList( playerId: testPlayer1.id, matchId: testMatch1.id, - entry: entry1, - ); - - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: entry2, + entrys: [entryRound1, entryRound2], ); + final newEntry = ScoreEntry(roundNumber: 2, score: 50, change: 40); final updated = await database.scoreEntryDao.updateScore( playerId: testPlayer1.id, matchId: testMatch1.id, - newEntry: ScoreEntry(roundNumber: 2, score: 50, change: 40), + newEntry: newEntry, ); expect(updated, true); @@ -348,23 +447,23 @@ void main() { ); expect(score, isNotNull); - expect(score!.score, 50); - expect(score.change, 40); + expect(score!.score, newEntry.score); + expect(score.change, newEntry.change); }); test('Updating a non-existent score returns false', () async { final updated = await database.scoreEntryDao.updateScore( playerId: testPlayer1.id, matchId: testMatch1.id, - newEntry: ScoreEntry(roundNumber: 1, score: 20, change: 20), + newEntry: entryRound1, ); expect(updated, false); }); }); - group('Deleting scores', () { - test('deleteScore() ', () async { + group('DELETE', () { + test('deleteScore() works correctly', () async { await database.scoreEntryDao.addScore( playerId: testPlayer1.id, matchId: testMatch1.id, @@ -399,20 +498,25 @@ void main() { }); test('deleteAllScoresForMatch() works correctly', () async { + final score1 = ScoreEntry(roundNumber: 1, score: 10, change: 10); await database.scoreEntryDao.addScore( playerId: testPlayer1.id, matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), + entry: score1, ); + + final score2 = ScoreEntry(roundNumber: 1, score: 20, change: 20); await database.scoreEntryDao.addScore( playerId: testPlayer2.id, matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 20, change: 20), + entry: score2, ); + + final score3 = ScoreEntry(roundNumber: 1, score: 15, change: 15); await database.scoreEntryDao.addScore( playerId: testPlayer1.id, matchId: testMatch2.id, - entry: ScoreEntry(roundNumber: 1, score: 15, change: 15), + entry: score3, ); final deleted = await database.scoreEntryDao.deleteAllScoresForMatch( @@ -433,22 +537,16 @@ void main() { }); test('deleteAllScoresForPlayerInMatch() works correctly', () async { - await database.scoreEntryDao.addScore( + await database.scoreEntryDao.addScoresAsList( playerId: testPlayer1.id, matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), - ); - - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 2, score: 15, change: 5), + entrys: [entryRound1, entryRound2], ); await database.scoreEntryDao.addScore( playerId: testPlayer2.id, matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 6, change: 6), + entry: entryRound1, ); final deleted = await database.scoreEntryDao @@ -475,141 +573,7 @@ void main() { }); }); - group('Score Aggregations & Edge Cases', () { - test('getLatestRoundNumber()', () async { - var latestRound = await database.scoreEntryDao.getLatestRoundNumber( - matchId: testMatch1.id, - ); - expect(latestRound, isNull); - - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), - ); - - latestRound = await database.scoreEntryDao.getLatestRoundNumber( - matchId: testMatch1.id, - ); - expect(latestRound, 1); - - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 5, score: 50, change: 40), - ); - - latestRound = await database.scoreEntryDao.getLatestRoundNumber( - matchId: testMatch1.id, - ); - expect(latestRound, 5); - }); - - test('getLatestRoundNumber() with non-consecutive rounds', () async { - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 5, score: 50, change: 40), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 3, score: 30, change: 20), - ); - - final latestRound = await database.scoreEntryDao.getLatestRoundNumber( - matchId: testMatch1.id, - ); - - expect(latestRound, 5); - }); - - test('getTotalScoreForPlayer()', () async { - var totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( - playerId: testPlayer1.id, - matchId: testMatch1.id, - ); - expect(totalScore, 0); - - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 2, score: 25, change: 15), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 3, score: 40, change: 15), - ); - - totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( - playerId: testPlayer1.id, - matchId: testMatch1.id, - ); - expect(totalScore, 40); - }); - - test('getTotalScoreForPlayer() ignores round score', () async { - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 2, score: 25, change: 25), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 25, change: 10), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 3, score: 25, change: 25), - ); - - final totalScore = await database.scoreEntryDao.getTotalScoreForPlayer( - playerId: testPlayer1.id, - matchId: testMatch1.id, - ); - - // Should return the sum of all changes - expect(totalScore, 60); - }); - - test('Adding the same score twice replaces the existing one', () async { - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 10, change: 10), - ); - await database.scoreEntryDao.addScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - entry: ScoreEntry(roundNumber: 1, score: 20, change: 20), - ); - - final score = await database.scoreEntryDao.getScore( - playerId: testPlayer1.id, - matchId: testMatch1.id, - roundNumber: 1, - ); - - expect(score, isNotNull); - expect(score!.score, 20); - expect(score.change, 20); - }); - }); - - group('Handling Winner', () { + group('WINNER', () { test('hasWinner() works correctly', () async { var hasWinner = await database.scoreEntryDao.hasWinner( matchId: testMatch1.id, @@ -667,58 +631,58 @@ void main() { }); }); - group('Handling Looser', () { - test('hasLooser() works correctly', () async { - var hasLooser = await database.scoreEntryDao.hasLooser( + group('LOSER', () { + test('hasLoser() works correctly', () async { + var hasLooser = await database.scoreEntryDao.hasLoser( matchId: testMatch1.id, ); expect(hasLooser, false); - await database.scoreEntryDao.setLooser( + await database.scoreEntryDao.setLoser( playerId: testPlayer1.id, matchId: testMatch1.id, ); - hasLooser = await database.scoreEntryDao.hasLooser( + hasLooser = await database.scoreEntryDao.hasLoser( matchId: testMatch1.id, ); expect(hasLooser, true); }); - test('getLooser() returns correct winner', () async { - var looser = await database.scoreEntryDao.getLooser( + test('getLoser() returns correct winner', () async { + var looser = await database.scoreEntryDao.getLoser( matchId: testMatch1.id, ); expect(looser, isNull); - await database.scoreEntryDao.setLooser( + await database.scoreEntryDao.setLoser( playerId: testPlayer1.id, matchId: testMatch1.id, ); - looser = await database.scoreEntryDao.getLooser(matchId: testMatch1.id); + looser = await database.scoreEntryDao.getLoser(matchId: testMatch1.id); expect(looser, isNotNull); expect(looser!.id, testPlayer1.id); }); - test('removeLooser() works correctly', () async { - var removed = await database.scoreEntryDao.removeLooser( + test('removeLoser() works correctly', () async { + var removed = await database.scoreEntryDao.removeLoser( matchId: testMatch1.id, ); expect(removed, false); - await database.scoreEntryDao.setLooser( + await database.scoreEntryDao.setLoser( playerId: testPlayer1.id, matchId: testMatch1.id, ); - removed = await database.scoreEntryDao.removeLooser( + removed = await database.scoreEntryDao.removeLoser( matchId: testMatch1.id, ); expect(removed, true); - var looser = await database.scoreEntryDao.getLooser( + var looser = await database.scoreEntryDao.getLoser( matchId: testMatch1.id, ); expect(looser, isNull); From 785873d3c88fee9f74e7fbd711a2905db4889f38 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 24 Apr 2026 16:36:36 +0200 Subject: [PATCH 006/127] Fixed error in game tests --- test/db_tests/entities/game_test.dart | 46 +++++++++++++-------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/test/db_tests/entities/game_test.dart b/test/db_tests/entities/game_test.dart index 9f94910..f3e99b6 100644 --- a/test/db_tests/entities/game_test.dart +++ b/test/db_tests/entities/game_test.dart @@ -24,6 +24,7 @@ void main() { withClock(fakeClock, () { testGame1 = Game( + id: 'game1', name: 'Chess', ruleset: Ruleset.singleWinner, description: 'A classic strategy game', @@ -142,12 +143,12 @@ void main() { await database.gameDao.addGame(game: testGame1); final game = await database.gameDao.getGameById(gameId: testGame1.id); - expect(game.id, testGame2.id); - expect(game.name, testGame2.name); - expect(game.ruleset, testGame2.ruleset); - expect(game.description, testGame2.description); - expect(game.color, testGame2.color); - expect(game.icon, testGame2.icon); + expect(game.id, testGame1.id); + expect(game.name, testGame1.name); + expect(game.ruleset, testGame1.ruleset); + expect(game.description, testGame1.description); + expect(game.color, testGame1.color); + expect(game.icon, testGame1.icon); }); test('getGameById() throws exception for non-existent game', () async { @@ -190,7 +191,7 @@ void main() { }); group('UPDATE', () { - test('updateGameName() updates the name correctly', () async { + test('updateGameName() works correctly', () async { await database.gameDao.addGame(game: testGame1); const newName = 'New name'; @@ -217,7 +218,7 @@ void main() { expect(allGames, isEmpty); }); - test('updateGameRuleset() updates the ruleset correctly', () async { + test('updateGameRuleset() works correctly', () async { await database.gameDao.addGame(game: testGame1); const ruleset = Ruleset.highestScore; @@ -244,24 +245,21 @@ void main() { expect(allGames, isEmpty); }); - test( - 'updateGameDescription() updates the description correctly', - () async { - await database.gameDao.addGame(game: testGame1); - const newDescription = 'New description'; + test('updateGameDescription() works correctly', () async { + await database.gameDao.addGame(game: testGame1); + const newDescription = 'New description'; - final updated = await database.gameDao.updateGameDescription( - gameId: testGame1.id, - newDescription: newDescription, - ); - expect(updated, true); + final updated = await database.gameDao.updateGameDescription( + gameId: testGame1.id, + newDescription: newDescription, + ); + expect(updated, true); - final updatedGame = await database.gameDao.getGameById( - gameId: testGame1.id, - ); - expect(updatedGame.description, newDescription); - }, - ); + final updatedGame = await database.gameDao.getGameById( + gameId: testGame1.id, + ); + expect(updatedGame.description, newDescription); + }); test( 'updateGameDescription() does nothing for non-existent game', From 2f5b9e5ff2a6ac9738bb37a0082132ec97631b3e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 28 Apr 2026 15:27:52 +0200 Subject: [PATCH 007/127] Cherry picked changes from 119-implementierung-der-games --- lib/core/common.dart | 47 ++- lib/core/constants.dart | 3 + lib/data/models/game.dart | 11 +- lib/l10n/arb/app_de.arb | 15 + lib/l10n/arb/app_en.arb | 36 ++ lib/l10n/generated/app_localizations.dart | 90 +++++ lib/l10n/generated/app_localizations_de.dart | 46 +++ lib/l10n/generated/app_localizations_en.dart | 46 +++ .../create_match/choose_game_view.dart | 123 +++++- .../create_game/choose_color_view.dart | 78 ++++ .../create_game/choose_ruleset_view.dart | 99 +++++ .../create_game/create_game_view.dart | 352 ++++++++++++++++++ .../create_match/create_match_view.dart | 31 +- .../widgets/text_input/text_input_field.dart | 21 +- .../tiles/title_description_list_tile.dart | 14 +- 15 files changed, 978 insertions(+), 34 deletions(-) create mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart create mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart create mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart diff --git a/lib/core/common.dart b/lib/core/common.dart index 8027180..4c02350 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; @@ -21,6 +21,51 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) { } } +/// Translates a [GameColor] enum value to its corresponding localized string. +String translateGameColorToString(GameColor color, BuildContext context) { + final loc = AppLocalizations.of(context); + switch (color) { + case GameColor.red: + return loc.color_red; + case GameColor.blue: + return loc.color_blue; + case GameColor.green: + return loc.color_green; + case GameColor.yellow: + return loc.color_yellow; + case GameColor.purple: + return loc.color_purple; + case GameColor.orange: + return loc.color_orange; + case GameColor.pink: + return loc.color_pink; + case GameColor.teal: + return loc.color_teal; + } +} + +/// Returns the [Color] object corresponding to a [GameColor] enum value. +Color getColorFromGameColor(GameColor color) { + switch (color) { + case GameColor.red: + return Colors.red; + case GameColor.blue: + return Colors.blue; + case GameColor.green: + return Colors.green; + case GameColor.yellow: + return Colors.yellow; + case GameColor.purple: + return Colors.purple; + case GameColor.orange: + return Colors.orange; + case GameColor.pink: + return Colors.pink; + case GameColor.teal: + return Colors.teal; + } +} + /// Counts how many players in the match are not part of the group /// Returns the count as a string, or an empty string if there is no group String getExtraPlayerCount(Match match) { diff --git a/lib/core/constants.dart b/lib/core/constants.dart index c1bc0fe..86e3ad7 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -19,4 +19,7 @@ class Constants { /// Maximum length for team names static const int MAX_TEAM_NAME_LENGTH = 32; + + /// Maximum length for game descriptions + static const int MAX_GAME_DESCRIPTION_LENGTH = 256; } diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index 607db0a..4888df4 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -12,16 +12,17 @@ class Game { final String icon; Game({ - String? id, - DateTime? createdAt, required this.name, required this.ruleset, - String? description, required this.color, - required this.icon, + String? id, + DateTime? createdAt, + String? description, + String? icon, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? clock.now(), - description = description ?? ''; + description = description ?? '', + icon = icon ?? ''; @override String toString() { diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 46c780a..ba4fe38 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -6,10 +6,21 @@ "app_name": "Tallee", "best_player": "Beste:r Spieler:in", "cancel": "Abbrechen", + "choose_color": "Farbe wählen", "choose_game": "Spielvorlage wählen", "choose_group": "Gruppe wählen", "choose_ruleset": "Regelwerk wählen", + "color": "Farbe", + "color_blue": "Blau", + "color_green": "Grün", + "color_orange": "Orange", + "color_pink": "Rosa", + "color_purple": "Lila", + "color_red": "Rot", + "color_teal": "Türkis", + "color_yellow": "Gelb", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", + "create_game": "Spielvorlage erstellen", "create_group": "Gruppe erstellen", "create_match": "Spiel erstellen", "create_new_group": "Neue Gruppe erstellen", @@ -22,13 +33,17 @@ "days_ago": "vor {count} Tagen", "delete": "Löschen", "delete_all_data": "Alle Daten löschen", + "delete_game": "Spielvorlage löschen", "delete_group": "Gruppe löschen", "delete_match": "Spiel löschen", + "description": "Beschreibung", + "edit_game": "Spielvorlage bearbeiten", "edit_group": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten", "enter_points": "Punkte eingeben", "enter_results": "Ergebnisse eintragen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", + "error_deleting_game": "Fehler beim Löschen der Spielvorlage, bitte erneut versuchen", "error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen", "error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen", "error_reading_file": "Fehler beim Lesen der Datei", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a85e1b0..98d1c38 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -18,6 +18,9 @@ "@cancel": { "description": "Cancel button text" }, + "@choose_color": { + "description": "Label for choosing a color" + }, "@choose_game": { "description": "Label for choosing a game" }, @@ -27,9 +30,15 @@ "@choose_ruleset": { "description": "Label for choosing a ruleset" }, + "@color": { + "description": "Color label" + }, "@could_not_add_player": { "description": "Error message when adding a player fails" }, + "@create_game": { + "description": "Button text to create a game" + }, "@create_group": { "description": "Button text to create a group" }, @@ -71,12 +80,21 @@ "@delete_all_data": { "description": "Confirmation dialog for deleting all data" }, + "@delete_game": { + "description": "Button text to delete a game" + }, "@delete_group": { "description": "Confirmation dialog for deleting a group" }, "@delete_match": { "description": "Button text to delete a match" }, + "@description": { + "description": "Description label" + }, + "@edit_game": { + "description": "Button text to edit a game" + }, "@edit_group": { "description": "Button & Appbar label for editing a group" }, @@ -92,6 +110,9 @@ "@error_creating_group": { "description": "Error message when group creation fails" }, + "@error_deleting_game": { + "description": "Error message when game deletion fails" + }, "@error_deleting_group": { "description": "Error message when group deletion fails" }, @@ -340,10 +361,21 @@ "app_name": "Tallee", "best_player": "Best Player", "cancel": "Cancel", + "choose_color": "Choose Color", "choose_game": "Choose Game", "choose_group": "Choose Group", "choose_ruleset": "Choose Ruleset", + "color": "Color", + "color_blue": "Blue", + "color_green": "Green", + "color_orange": "Orange", + "color_pink": "Pink", + "color_purple": "Purple", + "color_red": "Red", + "color_teal": "Teal", + "color_yellow": "Yellow", "could_not_add_player": "Could not add player", + "create_game": "Create Game", "create_group": "Create Group", "create_match": "Create match", "create_new_group": "Create new group", @@ -356,13 +388,17 @@ "days_ago": "{count} days ago", "delete": "Delete", "delete_all_data": "Delete all data", + "delete_game": "Delete Game", "delete_group": "Delete Group", "delete_match": "Delete Match", + "description": "Description", + "edit_game": "Edit Game", "edit_group": "Edit Group", "edit_match": "Edit Match", "enter_points": "Enter points", "enter_results": "Enter Results", "error_creating_group": "Error while creating group, please try again", + "error_deleting_game": "Error while deleting game, please try again", "error_deleting_group": "Error while deleting group, please try again", "error_editing_group": "Error while editing group, please try again", "error_reading_file": "Error reading file", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 99c9317..8e44e7b 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -134,6 +134,12 @@ abstract class AppLocalizations { /// **'Cancel'** String get cancel; + /// Label for choosing a color + /// + /// In en, this message translates to: + /// **'Choose Color'** + String get choose_color; + /// Label for choosing a game /// /// In en, this message translates to: @@ -152,12 +158,72 @@ abstract class AppLocalizations { /// **'Choose Ruleset'** String get choose_ruleset; + /// Color label + /// + /// In en, this message translates to: + /// **'Color'** + String get color; + + /// No description provided for @color_blue. + /// + /// In en, this message translates to: + /// **'Blue'** + String get color_blue; + + /// No description provided for @color_green. + /// + /// In en, this message translates to: + /// **'Green'** + String get color_green; + + /// No description provided for @color_orange. + /// + /// In en, this message translates to: + /// **'Orange'** + String get color_orange; + + /// No description provided for @color_pink. + /// + /// In en, this message translates to: + /// **'Pink'** + String get color_pink; + + /// No description provided for @color_purple. + /// + /// In en, this message translates to: + /// **'Purple'** + String get color_purple; + + /// No description provided for @color_red. + /// + /// In en, this message translates to: + /// **'Red'** + String get color_red; + + /// No description provided for @color_teal. + /// + /// In en, this message translates to: + /// **'Teal'** + String get color_teal; + + /// No description provided for @color_yellow. + /// + /// In en, this message translates to: + /// **'Yellow'** + String get color_yellow; + /// Error message when adding a player fails /// /// In en, this message translates to: /// **'Could not add player'** String could_not_add_player(Object playerName); + /// Button text to create a game + /// + /// In en, this message translates to: + /// **'Create Game'** + String get create_game; + /// Button text to create a group /// /// In en, this message translates to: @@ -230,6 +296,12 @@ abstract class AppLocalizations { /// **'Delete all data'** String get delete_all_data; + /// Button text to delete a game + /// + /// In en, this message translates to: + /// **'Delete Game'** + String get delete_game; + /// Confirmation dialog for deleting a group /// /// In en, this message translates to: @@ -242,6 +314,18 @@ abstract class AppLocalizations { /// **'Delete Match'** String get delete_match; + /// Description label + /// + /// In en, this message translates to: + /// **'Description'** + String get description; + + /// Button text to edit a game + /// + /// In en, this message translates to: + /// **'Edit Game'** + String get edit_game; + /// Button & Appbar label for editing a group /// /// In en, this message translates to: @@ -272,6 +356,12 @@ abstract class AppLocalizations { /// **'Error while creating group, please try again'** String get error_creating_group; + /// Error message when game deletion fails + /// + /// In en, this message translates to: + /// **'Error while deleting game, please try again'** + String get error_deleting_game; + /// Error message when group deletion fails /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 51b4c62..3c2b4e3 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -26,6 +26,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get cancel => 'Abbrechen'; + @override + String get choose_color => 'Farbe wählen'; + @override String get choose_game => 'Spielvorlage wählen'; @@ -35,11 +38,41 @@ class AppLocalizationsDe extends AppLocalizations { @override String get choose_ruleset => 'Regelwerk wählen'; + @override + String get color => 'Farbe'; + + @override + String get color_blue => 'Blau'; + + @override + String get color_green => 'Grün'; + + @override + String get color_orange => 'Orange'; + + @override + String get color_pink => 'Rosa'; + + @override + String get color_purple => 'Lila'; + + @override + String get color_red => 'Rot'; + + @override + String get color_teal => 'Türkis'; + + @override + String get color_yellow => 'Gelb'; + @override String could_not_add_player(Object playerName) { return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; } + @override + String get create_game => 'Spielvorlage erstellen'; + @override String get create_group => 'Gruppe erstellen'; @@ -78,12 +111,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get delete_all_data => 'Alle Daten löschen'; + @override + String get delete_game => 'Spielvorlage löschen'; + @override String get delete_group => 'Gruppe löschen'; @override String get delete_match => 'Spiel löschen'; + @override + String get description => 'Beschreibung'; + + @override + String get edit_game => 'Spielvorlage bearbeiten'; + @override String get edit_group => 'Gruppe bearbeiten'; @@ -100,6 +142,10 @@ class AppLocalizationsDe extends AppLocalizations { String get error_creating_group => 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; + @override + String get error_deleting_game => + 'Fehler beim Löschen der Spielvorlage, bitte erneut versuchen'; + @override String get error_deleting_group => 'Fehler beim Löschen der Gruppe, bitte erneut versuchen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 2b42e47..e14b7a0 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -26,6 +26,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cancel => 'Cancel'; + @override + String get choose_color => 'Choose Color'; + @override String get choose_game => 'Choose Game'; @@ -35,11 +38,41 @@ class AppLocalizationsEn extends AppLocalizations { @override String get choose_ruleset => 'Choose Ruleset'; + @override + String get color => 'Color'; + + @override + String get color_blue => 'Blue'; + + @override + String get color_green => 'Green'; + + @override + String get color_orange => 'Orange'; + + @override + String get color_pink => 'Pink'; + + @override + String get color_purple => 'Purple'; + + @override + String get color_red => 'Red'; + + @override + String get color_teal => 'Teal'; + + @override + String get color_yellow => 'Yellow'; + @override String could_not_add_player(Object playerName) { return 'Could not add player'; } + @override + String get create_game => 'Create Game'; + @override String get create_group => 'Create Group'; @@ -78,12 +111,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get delete_all_data => 'Delete all data'; + @override + String get delete_game => 'Delete Game'; + @override String get delete_group => 'Delete Group'; @override String get delete_match => 'Delete Match'; + @override + String get description => 'Description'; + + @override + String get edit_game => 'Edit Game'; + @override String get edit_group => 'Edit Group'; @@ -100,6 +142,10 @@ class AppLocalizationsEn extends AppLocalizations { String get error_creating_group => 'Error while creating group, please try again'; + @override + String get error_deleting_game => + 'Error while deleting game, please try again'; + @override String get error_deleting_group => 'Error while deleting group, please try again'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 51512f9..8f3e06e 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -1,19 +1,23 @@ import 'package:flutter/material.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; class ChooseGameView extends StatefulWidget { /// A view that allows the user to choose a game from a list of available games - /// - [games]: A list of tuples containing the game name, description and ruleset - /// - [initialGameIndex]: The index of the initially selected game + /// - [games]: The list of available games + /// - [initialGameId]: The id of the initially selected game + /// - [onGamesUpdated]: Optional callback invoked when the games are updated const ChooseGameView({ super.key, required this.games, required this.initialGameId, + this.onGamesUpdated, }); /// A list of tuples containing the game name, description and ruleset @@ -22,6 +26,9 @@ class ChooseGameView extends StatefulWidget { /// The id of the initially selected game final String initialGameId; + /// Optional callback invoked when the games are updated + final VoidCallback? onGamesUpdated; + @override State createState() => _ChooseGameViewState(); } @@ -33,9 +40,16 @@ class _ChooseGameViewState extends State { /// Currently selected game index late String selectedGameId; + /// Games filtered according to the current search query + late List filteredGames; + @override void initState() { selectedGameId = widget.initialGameId; + + // Start with all games visible + filteredGames = List.from(widget.games); + super.initState(); } @@ -58,6 +72,30 @@ class _ChooseGameViewState extends State { ); }, ), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () async { + final result = await Navigator.push( + context, + adaptivePageRoute( + builder: (context) => CreateGameView( + onGameChanged: () { + widget.onGamesUpdated?.call(); + }, + ), + ), + ); + if (result != null && result.game != null) { + setState(() { + widget.games.insert(0, result.game); + }); + _refreshFromSource(); + } + }, + ), + ], + title: Text(loc.choose_game), ), body: PopScope( @@ -77,30 +115,63 @@ class _ChooseGameViewState extends State { child: CustomSearchBar( controller: searchBarController, hintText: loc.game_name, + onChanged: (value) { + _applySearchFilter(value); + }, ), ), const SizedBox(height: 5), Expanded( child: ListView.builder( - itemCount: widget.games.length, + itemCount: filteredGames.length, itemBuilder: (BuildContext context, int index) { + final game = filteredGames[index]; return TitleDescriptionListTile( - title: widget.games[index].name, - description: widget.games[index].description, - badgeText: translateRulesetToString( - widget.games[index].ruleset, - context, - ), - isHighlighted: selectedGameId == widget.games[index].id, - onPressed: () async { + title: game.name, + description: game.description, + badgeText: translateRulesetToString(game.ruleset, context), + isHighlighted: selectedGameId == game.id, + onTap: () async { setState(() { - if (selectedGameId != widget.games[index].id) { - selectedGameId = widget.games[index].id; - } else { + if (selectedGameId == game.id) { selectedGameId = ''; + } else { + selectedGameId = game.id; } }); }, + onLongPress: () async { + final result = await Navigator.push( + context, + adaptivePageRoute( + builder: (context) => CreateGameView( + gameToEdit: game, + onGameChanged: () { + widget.onGamesUpdated?.call(); + }, + ), + ), + ); + if (result != null && result.game != null) { + // Find the index in the original list to mutate + final originalIndex = widget.games.indexWhere( + (g) => g.id == game.id, + ); + if (originalIndex == -1) { + return; + } + if (result.delete) { + setState(() { + widget.games.removeAt(originalIndex); + }); + } else { + setState(() { + widget.games[originalIndex] = result.game; + }); + } + _refreshFromSource(); + } + }, ); }, ), @@ -110,4 +181,28 @@ class _ChooseGameViewState extends State { ), ); } + + /// Applies the search filter to the games list based on [query]. + void _applySearchFilter(String query) { + final q = query.toLowerCase().trim(); + if (q.isEmpty) { + setState(() { + filteredGames = List.from(widget.games); + }); + return; + } + + setState(() { + filteredGames = widget.games.where((game) { + final name = game.name.toLowerCase(); + final description = game.description.toLowerCase(); + return name.contains(q) || description.contains(q); + }).toList(); + }); + } + + /// Re-applies the current filter after the underlying games list changed. + void _refreshFromSource() { + _applySearchFilter(searchBarController.text); + } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart new file mode 100644 index 0000000..bf764ad --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; + +class ChooseColorView extends StatefulWidget { + /// A view that allows the user to choose a color from a list of available game colors + /// - [initialColor]: The initially selected color + const ChooseColorView({super.key, this.initialColor}); + + /// The initially selected color + final GameColor? initialColor; + + @override + State createState() => _ChooseColorViewState(); +} + +class _ChooseColorViewState extends State { + /// Currently selected color + GameColor? selectedColor; + + @override + void initState() { + selectedColor = widget.initialColor; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + const colors = GameColor.values; + + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () { + Navigator.of(context).pop(selectedColor); + }, + ), + title: Text(loc.choose_color), + ), + body: PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) { + if (didPop) return; + Navigator.of(context).pop(selectedColor); + }, + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: colors.length, + itemBuilder: (BuildContext context, int index) { + final color = colors[index]; + return TitleDescriptionListTile( + onTap: () { + setState(() { + if (selectedColor == color) { + selectedColor = null; + } else { + selectedColor = color; + } + }); + }, + title: translateGameColorToString(color, context), + description: '', + isHighlighted: selectedColor == color, + badgeText: ' ', //Breite für Color Badge + badgeColor: getColorFromGameColor(color), + ); + }, + ), + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart new file mode 100644 index 0000000..6b69b22 --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; + +class ChooseRulesetView extends StatefulWidget { + /// A view that allows the user to choose a ruleset from a list of available rulesets + /// - [rulesets]: A list of tuples containing the ruleset and its description + /// - [initialRulesetIndex]: The index of the initially selected ruleset + const ChooseRulesetView({ + super.key, + required this.rulesets, + required this.initialRulesetIndex, + }); + + /// A list of tuples containing the ruleset and its description + final List<(Ruleset, String)> rulesets; + + /// The index of the initially selected ruleset + final int initialRulesetIndex; + @override + State createState() => _ChooseRulesetViewState(); +} + +class _ChooseRulesetViewState extends State { + /// Currently selected ruleset index + late int selectedRulesetIndex; + + @override + void initState() { + selectedRulesetIndex = widget.initialRulesetIndex; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return DefaultTabController( + length: 2, + initialIndex: 0, + child: Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () { + Navigator.of(context).pop( + selectedRulesetIndex == -1 + ? null + : widget.rulesets[selectedRulesetIndex].$1, + ); + }, + ), + title: Text(loc.choose_ruleset), + ), + body: PopScope( + // This fixes that the Android Back Gesture didn't return the + // selectedRulesetIndex and therefore the selected Ruleset wasn't saved + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) { + if (didPop) { + return; + } + Navigator.of(context).pop( + selectedRulesetIndex == -1 + ? null + : widget.rulesets[selectedRulesetIndex].$1, + ); + }, + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: widget.rulesets.length, + itemBuilder: (BuildContext context, int index) { + return TitleDescriptionListTile( + onTap: () async { + setState(() { + if (selectedRulesetIndex == index) { + selectedRulesetIndex = -1; + } else { + selectedRulesetIndex = index; + } + }); + }, + title: translateRulesetToString( + widget.rulesets[index].$1, + context, + ), + description: widget.rulesets[index].$2, + isHighlighted: selectedRulesetIndex == index, + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart new file mode 100644 index 0000000..2907720 --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -0,0 +1,352 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart'; +import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; +import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; +import 'package:tallee/presentation/widgets/tiles/choose_tile.dart'; + +/// A stateful widget for creating or editing a game. +/// - [gameToEdit] An optional game to prefill the fields +/// - [onGameChanged] Callback to invoke when the game is created or edited +class CreateGameView extends StatefulWidget { + const CreateGameView({ + super.key, + this.gameToEdit, + required this.onGameChanged, + }); + + /// An optional game to prefill the fields + final Game? gameToEdit; + + /// Callback to invoke when the game is created or edited + final VoidCallback onGameChanged; + + @override + State createState() => _CreateGameViewState(); +} + +class _CreateGameViewState extends State { + /// GlobalKey for ScaffoldMessenger to show snackbars + final _scaffoldMessengerKey = GlobalKey(); + + /// The database instance for accessing game data. + late final AppDatabase db; + + /// The currently selected ruleset for the game. + Ruleset? selectedRuleset; + + /// The index of the currently selected ruleset. + int selectedRulesetIndex = -1; + + /// A list of available rulesets and their localized names. + late List<(Ruleset, String)> _rulesets; + + /// The currently selected color for the game. + GameColor? selectedColor; + + /// Controller for the game name input field. + final _gameNameController = TextEditingController(); + + /// Controller for the game description input field. + final _descriptionController = TextEditingController(); + + /// The ID of the currently selected group. + late String selectedGroupId; + + /// A controller for the search bar input field. + final TextEditingController controller = TextEditingController(); + + /// A list of groups filtered based on the search query. + late final List filteredGroups; + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + _gameNameController.addListener(() => setState(() {})); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _rulesets = [ + ( + Ruleset.singleWinner, + translateRulesetToString(Ruleset.singleWinner, context), + ), + ( + Ruleset.singleLoser, + translateRulesetToString(Ruleset.singleLoser, context), + ), + ( + Ruleset.highestScore, + translateRulesetToString(Ruleset.highestScore, context), + ), + ( + Ruleset.lowestScore, + translateRulesetToString(Ruleset.lowestScore, context), + ), + ( + Ruleset.multipleWinners, + translateRulesetToString(Ruleset.multipleWinners, context), + ), + ]; + + if (widget.gameToEdit != null) { + _gameNameController.text = widget.gameToEdit!.name; + _descriptionController.text = widget.gameToEdit!.description; + selectedRuleset = widget.gameToEdit!.ruleset; + selectedColor = widget.gameToEdit!.color; + + selectedRulesetIndex = _rulesets.indexWhere( + (r) => r.$1 == selectedRuleset, + ); + } + } + + @override + void dispose() { + _gameNameController.dispose(); + _descriptionController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var loc = AppLocalizations.of(context); + final isEditing = widget.gameToEdit != null; + + return ScaffoldMessenger( + child: Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + title: Text(isEditing ? loc.edit_game : loc.create_game), + actions: widget.gameToEdit == null + ? [] + : [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + if (widget.gameToEdit != null) { + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: loc.delete_game, + content: Text(loc.this_cannot_be_undone), + actions: [ + CustomDialogAction( + onPressed: () => + Navigator.of(context).pop(false), + text: loc.cancel, + ), + CustomDialogAction( + onPressed: () => + Navigator.of(context).pop(true), + text: loc.delete, + ), + ], + ), + ).then((confirmed) async { + if (confirmed == true && context.mounted) { + bool success = await db.gameDao.deleteGame( + gameId: widget.gameToEdit!.id, + ); + if (!context.mounted) return; + if (success) { + widget.onGameChanged.call(); + Navigator.of( + context, + ).pop((game: widget.gameToEdit, delete: true)); + } else { + if (!mounted) return; + showSnackbar(message: loc.error_deleting_game); + } + } + }); + } + }, + ), + ], + ), + body: SafeArea( + child: Column( + children: [ + Container( + margin: CustomTheme.tileMargin, + child: TextInputField( + controller: _gameNameController, + maxLength: Constants.MAX_MATCH_NAME_LENGTH, + hintText: loc.game_name, + ), + ), + ChooseTile( + title: loc.ruleset, + trailingText: selectedRuleset == null + ? loc.none + : translateRulesetToString(selectedRuleset!, context), + onPressed: () async { + final result = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => ChooseRulesetView( + rulesets: _rulesets, + initialRulesetIndex: selectedRulesetIndex, + ), + ), + ); + if (mounted) { + setState(() { + selectedRuleset = result; + selectedRulesetIndex = result == null + ? -1 + : _rulesets.indexWhere((r) => r.$1 == result); + }); + } + }, + ), + ChooseTile( + title: loc.color, + trailingText: selectedColor == null + ? loc.none + : translateGameColorToString(selectedColor!, context), + onPressed: () async { + final result = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => + ChooseColorView(initialColor: selectedColor), + ), + ); + if (mounted) { + setState(() { + selectedColor = result; + }); + } + }, + ), + Container( + margin: CustomTheme.tileMargin, + child: TextInputField( + controller: _descriptionController, + hintText: loc.description, + minLines: 6, + maxLines: 6, + maxLength: Constants.MAX_GAME_DESCRIPTION_LENGTH, + showCounterText: true, + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(12.0), + child: CustomWidthButton( + text: isEditing ? loc.edit_game : loc.create_game, + sizeRelativeToWidth: 1, + buttonType: ButtonType.primary, + onPressed: + _gameNameController.text.trim().isNotEmpty && + selectedRulesetIndex != -1 && + selectedColor != null + ? () async { + Game newGame = Game( + name: _gameNameController.text.trim(), + description: _descriptionController.text.trim(), + ruleset: selectedRuleset!, + color: selectedColor!, + ); + if (isEditing) { + await handleGameUpdate(newGame); + } else { + await handleGameCreation(newGame); + } + widget.onGameChanged.call(); + if (context.mounted) { + Navigator.of( + context, + ).pop((game: newGame, delete: false)); + } + } + : null, + ), + ), + ], + ), + ), + ), + ); + } + + /// Handles updating an existing game in the database. + /// + /// [newGame] The updated game object. + Future handleGameUpdate(Game newGame) async { + final oldGame = widget.gameToEdit!; + + if (oldGame.name != newGame.name) { + await db.gameDao.updateGameName( + gameId: oldGame.id, + newName: newGame.name, + ); + } + + if (oldGame.description != newGame.description) { + await db.gameDao.updateGameDescription( + gameId: oldGame.id, + newDescription: newGame.description, + ); + } + + if (oldGame.ruleset != newGame.ruleset) { + await db.gameDao.updateGameRuleset( + gameId: oldGame.id, + newRuleset: newGame.ruleset, + ); + } + + if (oldGame.color != newGame.color) { + await db.gameDao.updateGameColor( + gameId: oldGame.id, + newColor: newGame.color, + ); + } + + if (oldGame.icon != newGame.icon) { + await db.gameDao.updateGameIcon( + gameId: oldGame.id, + newIcon: newGame.icon, + ); + } + } + + /// Handles creating a new game in the database. + /// + /// [newGame] The game object to be created. + Future handleGameCreation(Game newGame) async { + await db.gameDao.addGame(game: newGame); + } + + /// Displays a snackbar with the given message and optional action. + /// + /// [message] The message to display in the snackbar. + void showSnackbar({required String message}) { + final messenger = _scaffoldMessengerKey.currentState; + if (messenger != null) { + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message, style: const TextStyle(color: Colors.white)), + backgroundColor: CustomTheme.boxColor, + ), + ); + } + } +} 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 1a04c78..b042ebb 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 @@ -115,6 +115,7 @@ class _CreateMatchViewState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ + // Match name input field. Container( margin: CustomTheme.tileMargin, child: TextInputField( @@ -123,6 +124,8 @@ class _CreateMatchViewState extends State { maxLength: Constants.MAX_MATCH_NAME_LENGTH, ), ), + + // Game selection tile. ChooseTile( title: loc.game, trailingText: selectedGame == null @@ -146,6 +149,8 @@ class _CreateMatchViewState extends State { }); }, ), + + // Group selection tile. ChooseTile( title: loc.group, trailingText: selectedGroup == null @@ -181,6 +186,8 @@ class _CreateMatchViewState extends State { }); }, ), + + // Player selection widget. Expanded( child: PlayerSelection( key: ValueKey(selectedGroup?.id ?? 'no_group'), @@ -193,6 +200,8 @@ class _CreateMatchViewState extends State { }, ), ), + + // Create or save button. CustomWidthButton( text: buttonText, sizeRelativeToWidth: 0.95, @@ -218,16 +227,16 @@ class _CreateMatchViewState extends State { /// /// Returns `true` if: /// - A ruleset is selected AND - /// - Either a group is selected OR at least 2 players are selected + /// - Either a group is selected OR at least 2 players are selected. bool _enableCreateGameButton() { return (selectedGroup != null || (selectedPlayers.length > 1) && selectedGame != null); } - // If a match was provided to the view, it updates the match in the database - // and navigates back to the previous screen. - // If no match was provided, it creates a new match in the database and - // navigates to the MatchResultView for the newly created match. + /// Handles navigation when the create or save button is pressed. + /// + /// If a match is being edited, updates the match in the database. + /// Otherwise, creates a new match and navigates to the MatchResultView. void buttonNavigation(BuildContext context) async { if (isEditMode()) { await updateMatch(); @@ -252,8 +261,7 @@ class _CreateMatchViewState extends State { } } - /// Updates attributes of the existing match in the database based on the - /// changes made in the edit view. + /// Updates the existing match in the database. Future updateMatch() async { final updatedMatch = Match( id: widget.matchToEdit!.id, @@ -262,7 +270,7 @@ class _CreateMatchViewState extends State { : _matchNameController.text.trim(), group: selectedGroup, players: selectedPlayers, - game: widget.matchToEdit!.game, + game: selectedGame!, createdAt: widget.matchToEdit!.createdAt, endedAt: widget.matchToEdit!.endedAt, notes: widget.matchToEdit!.notes, @@ -282,6 +290,13 @@ class _CreateMatchViewState extends State { ); } + if (widget.matchToEdit!.game.id != updatedMatch.game.id) { + await db.matchDao.updateMatchGame( + matchId: widget.matchToEdit!.id, + gameId: updatedMatch.game.id, + ); + } + // Add players who are in updatedMatch but not in the original match for (var player in updatedMatch.players) { if (!widget.matchToEdit!.players.any((p) => p.id == player.id)) { diff --git a/lib/presentation/widgets/text_input/text_input_field.dart b/lib/presentation/widgets/text_input/text_input_field.dart index 541ae6f..b074638 100644 --- a/lib/presentation/widgets/text_input/text_input_field.dart +++ b/lib/presentation/widgets/text_input/text_input_field.dart @@ -8,12 +8,18 @@ class TextInputField extends StatelessWidget { /// - [onChanged]: Optional callback invoked when the text in the field changes. /// - [hintText]: The hint text displayed in the text input field when it is empty /// - [maxLength]: Optional parameter for maximum length of the input text. + /// - [maxLines]: The maximum number of lines for the text input field. Defaults to 1. + /// - [minLines]: The minimum number of lines for the text input field. Defaults to 1. + /// - [showCounterText]: Whether to show the counter text in the text input field. Defaults to false. const TextInputField({ super.key, required this.controller, required this.hintText, this.onChanged, this.maxLength, + this.maxLines = 1, + this.minLines = 1, + this.showCounterText = false, }); /// The controller for the text input field. @@ -28,6 +34,15 @@ class TextInputField extends StatelessWidget { /// Optional parameter for maximum length of the input text. final int? maxLength; + /// The maximum number of lines for the text input field. + final int? maxLines; + + /// The minimum number of lines for the text input field. + final int? minLines; + + /// Whether to show the counter text in the text input field. + final bool showCounterText; + @override Widget build(BuildContext context) { return TextField( @@ -35,13 +50,15 @@ class TextInputField extends StatelessWidget { onChanged: onChanged, maxLength: maxLength, maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, + maxLines: maxLines, + minLines: minLines, + decoration: InputDecoration( filled: true, fillColor: CustomTheme.boxColor, hintText: hintText, hintStyle: const TextStyle(fontSize: 18), - // Hides the character counter - counterText: '', + counterText: showCounterText ? null : '', enabledBorder: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), borderSide: BorderSide(color: CustomTheme.boxBorderColor), diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart index 9dc8f33..95163f2 100644 --- a/lib/presentation/widgets/tiles/title_description_list_tile.dart +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -5,7 +5,8 @@ class TitleDescriptionListTile extends StatelessWidget { /// A list tile widget that displays a title and description, with optional highlighting and badge. /// - [title]: The title text displayed on the tile. /// - [description]: The description text displayed below the title. - /// - [onPressed]: The callback invoked when the tile is tapped. + /// - [onTap]: The callback invoked when the tile is tapped. + /// - [onLongPress]: The callback invoked when the tile is tapped. /// - [isHighlighted]: A boolean to determine if the tile should be highlighted. /// - [badgeText]: Optional text to display in a badge on the right side of the title. /// - [badgeColor]: Optional color for the badge background. @@ -13,7 +14,8 @@ class TitleDescriptionListTile extends StatelessWidget { super.key, required this.title, required this.description, - this.onPressed, + this.onTap, + this.onLongPress, this.isHighlighted = false, this.badgeText, this.badgeColor, @@ -26,7 +28,10 @@ class TitleDescriptionListTile extends StatelessWidget { final String description; /// The callback invoked when the tile is tapped. - final VoidCallback? onPressed; + final VoidCallback? onTap; + + /// The callback invoked when the tile is long-pressed. + final VoidCallback? onLongPress; /// A boolean to determine if the tile should be highlighted. final bool isHighlighted; @@ -40,7 +45,8 @@ class TitleDescriptionListTile extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: onPressed, + onTap: onTap, + onLongPress: onLongPress, child: AnimatedContainer( margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), From be6b968a4321325330a5e01a172a12973cdf1401 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 1 May 2026 11:57:03 +0200 Subject: [PATCH 008/127] Updated match dao + tests --- lib/data/dao/match_dao.dart | 242 +++++----- lib/services/data_transfer_service.dart | 2 +- test/db_tests/aggregates/match_test.dart | 541 ++++++++++++++--------- 3 files changed, 438 insertions(+), 347 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 93df7d7..3df8b9d 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -15,74 +15,13 @@ part 'match_dao.g.dart'; class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { MatchDao(super.db); - /// Retrieves all matches from the database. - Future> getAllMatches() async { - final query = select(matchTable); - final result = await query.get(); - - 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 players = - await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; - - final scores = await db.scoreEntryDao.getAllMatchScores( - matchId: row.id, - ); - - return Match( - id: row.id, - name: row.name, - game: game, - group: group, - players: players, - notes: row.notes ?? '', - createdAt: row.createdAt, - endedAt: row.endedAt, - scores: scores, - ); - }), - ); - } - - /// Retrieves a [Match] by its [matchId]. - Future getMatchById({required String matchId}) async { - 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!); - } - - final players = - await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; - - final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId); - - return Match( - id: result.id, - name: result.name, - game: game, - group: group, - players: players, - notes: result.notes ?? '', - createdAt: result.createdAt, - endedAt: result.endedAt, - scores: scores, - ); - } + /* Create */ /// 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. - Future addMatch({required Match match}) async { + Future addMatch({required Match match}) async { + if (await matchExists(matchId: match.id)) return false; await db.transaction(() async { await into(matchTable).insert( MatchTableCompanion.insert( @@ -114,15 +53,17 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); } } + print('Return true'); }); + return true; } /// Adds multiple [Match]es 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; + Future addMatchesAsList({required List matches}) async { + if (matches.isEmpty) return false; await db.transaction(() async { // Add all games first (deduplicated) final uniqueGames = {}; @@ -280,14 +221,17 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { } }); }); + return true; } - /// Deletes the match with the given [matchId] from the database. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future deleteMatch({required String matchId}) async { - final query = delete(matchTable)..where((g) => g.id.equals(matchId)); - final rowsAffected = await query.go(); - return rowsAffected > 0; + /* Read */ + + /// Checks if a match with the given [matchId] exists in the database. + /// Returns `true` if the match exists, otherwise `false`. + Future matchExists({required String matchId}) async { + final query = select(matchTable)..where((g) => g.id.equals(matchId)); + final result = await query.getSingleOrNull(); + return result != null; } /// Retrieves the number of matches in the database. @@ -299,6 +243,70 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return count ?? 0; } + /// Retrieves all matches from the database. + Future> getAllMatches() async { + final query = select(matchTable); + final result = await query.get(); + + 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 players = + await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; + + final scores = await db.scoreEntryDao.getAllMatchScores( + matchId: row.id, + ); + + return Match( + id: row.id, + name: row.name, + game: game, + group: group, + players: players, + notes: row.notes ?? '', + createdAt: row.createdAt, + endedAt: row.endedAt, + scores: scores, + ); + }), + ); + } + + /// Retrieves a [Match] by its [matchId]. + Future getMatchById({required String matchId}) async { + 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!); + } + + final players = + await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; + + final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId); + + return Match( + id: result.id, + name: result.name, + game: game, + group: group, + players: players, + notes: result.notes ?? '', + createdAt: result.createdAt, + endedAt: result.endedAt, + scores: scores, + ); + } + /// Retrieves all matches associated with the given [groupId]. /// Queries the database directly, filtering by [groupId]. Future> getGroupMatches({required String groupId}) async { @@ -325,34 +333,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); } - /// Checks if a match with the given [matchId] exists in the database. - /// Returns `true` if the match exists, otherwise `false`. - Future matchExists({required String matchId}) async { - final query = select(matchTable)..where((g) => g.id.equals(matchId)); - final result = await query.getSingleOrNull(); - return result != null; - } - - /// Deletes all matches from the database. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future deleteAllMatches() async { - final query = delete(matchTable); - final rowsAffected = await query.go(); - return rowsAffected > 0; - } - - /// Updates the notes of the match with the given [matchId]. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future updateMatchNotes({ - required String matchId, - required String? notes, - }) async { - final query = update(matchTable)..where((g) => g.id.equals(matchId)); - final rowsAffected = await query.write( - MatchTableCompanion(notes: Value(notes)), - ); - return rowsAffected > 0; - } + /* Update */ /// Changes the name of the match with the given [matchId] to [newName]. /// Returns `true` if more than 0 rows were affected, otherwise `false`. @@ -367,19 +348,6 @@ class MatchDao extends DatabaseAccessor 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 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]. /// Replaces the existing group association with the new group specified by [newGroupId]. /// Pass null to remove the group association. @@ -395,6 +363,19 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return rowsAffected > 0; } + /// Updates the notes of the match with the given [matchId]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future updateMatchNotes({ + required String matchId, + required String notes, + }) async { + final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final rowsAffected = await query.write( + MatchTableCompanion(notes: Value(notes)), + ); + return rowsAffected > 0; + } + /// Removes the group association of the match with the given [matchId]. /// Sets the groupId to null. /// Returns `true` if more than 0 rows were affected, otherwise `false`. @@ -406,25 +387,12 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return rowsAffected > 0; } - /// Updates the createdAt timestamp of the match with the given [matchId]. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future updateMatchCreatedAt({ - required String matchId, - required DateTime createdAt, - }) async { - final query = update(matchTable)..where((g) => g.id.equals(matchId)); - final rowsAffected = await query.write( - MatchTableCompanion(createdAt: Value(createdAt)), - ); - return rowsAffected > 0; - } - /// Updates the endedAt timestamp of the match with the given [matchId]. /// Pass null to remove the ended time (mark match as ongoing). /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateMatchEndedAt({ required String matchId, - required DateTime? endedAt, + required DateTime endedAt, }) async { final query = update(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.write( @@ -436,7 +404,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Replaces all players in a match with the provided list of players. /// Removes all existing players from the match and adds the new players. /// Also adds any new players to the player table if they don't exist. - Future replaceMatchPlayers({ + Future updateMatchPlayers({ required String matchId, required List newPlayers, }) async { @@ -466,4 +434,22 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); }); } + + /* Delete */ + + /// Deletes the match with the given [matchId] from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteMatch({required String matchId}) async { + final query = delete(matchTable)..where((g) => g.id.equals(matchId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + + /// Deletes all matches from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllMatches() async { + final query = delete(matchTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } } diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index daf4768..e47b220 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -143,7 +143,7 @@ class DataTransferService { await db.gameDao.addGamesAsList(games: importedGames); await db.groupDao.addGroupsAsList(groups: importedGroups); await db.teamDao.addTeamsAsList(teams: importedTeams); - await db.matchDao.addMatchAsList(matches: importedMatches); + await db.matchDao.addMatchesAsList(matches: importedMatches); } /* Parsing Methods */ diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 3305b9a..a056b2c 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -101,212 +101,221 @@ void main() { }); group('Match Tests', () { - // Verifies that a single match can be added and retrieved with all fields, group, and players intact. - test('Adding and fetching single match works correctly', () async { - await database.matchDao.addMatch(match: testMatch1); + group('CREATE', () { + test('Adding and fetching single match works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); - final result = await database.matchDao.getMatchById( - matchId: testMatch1.id, - ); + final result = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); - expect(result.id, testMatch1.id); - expect(result.name, testMatch1.name); - expect(result.createdAt, testMatch1.createdAt); + expect(result.id, testMatch1.id); + expect(result.name, testMatch1.name); + expect(result.createdAt, testMatch1.createdAt); - if (result.group != null) { - expect(result.group!.members.length, testGroup1.members.length); + if (result.group != null) { + expect(result.group!.members.length, testGroup1.members.length); - for (int i = 0; i < testGroup1.members.length; i++) { - expect(result.group!.members[i].id, testGroup1.members[i].id); - expect(result.group!.members[i].name, testGroup1.members[i].name); - } - } else { - fail('Group is null'); - } - expect(result.players.length, testMatch1.players.length); - - for (int i = 0; i < testMatch1.players.length; i++) { - expect(result.players[i].id, testMatch1.players[i].id); - expect(result.players[i].name, testMatch1.players[i].name); - expect(result.players[i].createdAt, testMatch1.players[i].createdAt); - } - }); - - // Verifies that multiple matches can be added and retrieved with correct groups and players. - test('Adding and fetching multiple matches works correctly', () async { - await database.matchDao.addMatchAsList( - matches: [ - testMatch1, - testMatch2, - testMatchOnlyGroup, - testMatchOnlyPlayers, - ], - ); - - final allMatches = await database.matchDao.getAllMatches(); - expect(allMatches.length, 4); - - final testMatches = { - testMatch1.id: testMatch1, - testMatch2.id: testMatch2, - testMatchOnlyGroup.id: testMatchOnlyGroup, - testMatchOnlyPlayers.id: testMatchOnlyPlayers, - }; - - for (final match in allMatches) { - final testMatch = testMatches[match.id]!; - - // Match-Checks - expect(match.id, testMatch.id); - expect(match.name, testMatch.name); - expect(match.createdAt, testMatch.createdAt); - - // Group-Checks - if (testMatch.group != null) { - expect(match.group!.id, testMatch.group!.id); - expect(match.group!.name, testMatch.group!.name); - expect(match.group!.createdAt, testMatch.group!.createdAt); - - // Group Members-Checks - expect(match.group!.members.length, testMatch.group!.members.length); - for (int i = 0; i < testMatch.group!.members.length; i++) { - expect(match.group!.members[i].id, testMatch.group!.members[i].id); - expect( - match.group!.members[i].name, - testMatch.group!.members[i].name, - ); - expect( - match.group!.members[i].createdAt, - testMatch.group!.members[i].createdAt, - ); + for (int i = 0; i < testGroup1.members.length; i++) { + expect(result.group!.members[i].id, testGroup1.members[i].id); + expect(result.group!.members[i].name, testGroup1.members[i].name); } } else { - expect(match.group, null); + fail('Group is null'); } + expect(result.players.length, testMatch1.players.length); - // Players-Checks - expect(match.players.length, testMatch.players.length); - for (int i = 0; i < testMatch.players.length; i++) { - expect(match.players[i].id, testMatch.players[i].id); - expect(match.players[i].name, testMatch.players[i].name); - expect(match.players[i].createdAt, testMatch.players[i].createdAt); + for (int i = 0; i < testMatch1.players.length; i++) { + expect(result.players[i].id, testMatch1.players[i].id); + expect(result.players[i].name, testMatch1.players[i].name); + expect(result.players[i].createdAt, testMatch1.players[i].createdAt); } - } + }); + + test('Adding and fetching multiple matches works correctly', () async { + await database.matchDao.addMatchesAsList( + matches: [ + testMatch1, + testMatch2, + testMatchOnlyGroup, + testMatchOnlyPlayers, + ], + ); + + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches.length, 4); + + final testMatches = { + testMatch1.id: testMatch1, + testMatch2.id: testMatch2, + testMatchOnlyGroup.id: testMatchOnlyGroup, + testMatchOnlyPlayers.id: testMatchOnlyPlayers, + }; + + for (final match in allMatches) { + final testMatch = testMatches[match.id]!; + + // Match-Checks + expect(match.id, testMatch.id); + expect(match.name, testMatch.name); + expect(match.createdAt, testMatch.createdAt); + + // Group-Checks + if (testMatch.group != null) { + expect(match.group!.id, testMatch.group!.id); + expect(match.group!.name, testMatch.group!.name); + expect(match.group!.createdAt, testMatch.group!.createdAt); + + // Group Members-Checks + expect( + match.group!.members.length, + testMatch.group!.members.length, + ); + for (int i = 0; i < testMatch.group!.members.length; i++) { + expect( + match.group!.members[i].id, + testMatch.group!.members[i].id, + ); + expect( + match.group!.members[i].name, + testMatch.group!.members[i].name, + ); + expect( + match.group!.members[i].createdAt, + testMatch.group!.members[i].createdAt, + ); + } + } else { + expect(match.group, null); + } + + // Players-Checks + expect(match.players.length, testMatch.players.length); + for (int i = 0; i < testMatch.players.length; i++) { + expect(match.players[i].id, testMatch.players[i].id); + expect(match.players[i].name, testMatch.players[i].name); + expect(match.players[i].createdAt, testMatch.players[i].createdAt); + } + } + }); + + test('addMatch() ignores duplicate games', () async { + var added = await database.matchDao.addMatch(match: testMatch1); + expect(added, isTrue); + + added = await database.matchDao.addMatch(match: testMatch1); + expect(added, isFalse); + + final matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 1); + }); + + test('addMatchesAsList() returns isFalse for empty list', () async { + var added = await database.matchDao.addMatchesAsList(matches: []); + expect(added, isFalse); + }); + + test('addMatchesAsList() ignores duplicate games', () async { + final added = await database.matchDao.addMatchesAsList( + matches: [testMatch1, testMatch2, testMatch1], + ); + expect(added, isTrue); + + final matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 2); + }); }); - // Verifies that adding the same match twice does not create duplicates. - test('Adding the same match twice does not create duplicates', () async { - await database.matchDao.addMatch(match: testMatch1); - await database.matchDao.addMatch(match: testMatch1); + group('READ', () { + test('matchExists() works correctly', () async { + var matchExists = await database.matchDao.matchExists( + matchId: testMatch1.id, + ); + expect(matchExists, isFalse); - final matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); + await database.matchDao.addMatch(match: testMatch1); + + matchExists = await database.matchDao.matchExists( + matchId: testMatch1.id, + ); + expect(matchExists, isTrue); + }); + + test('getGroupMatches() works correctly', () async { + var matches = await database.matchDao.getGroupMatches( + groupId: 'non-existing-id', + ); + + expect(matches, isEmpty); + + await database.matchDao.addMatch(match: testMatch1); + matches = await database.matchDao.getGroupMatches( + groupId: testGroup1.id, + ); + expect(matches, isNotEmpty); + + final match = matches.first; + expect(match.id, testMatch1.id); + expect(match.group, isNotNull); + expect(match.group!.id, testGroup1.id); + }); }); - // Verifies that matchExists returns correct boolean based on match presence. - test('Match existence check works correctly', () async { - var matchExists = await database.matchDao.matchExists( - matchId: testMatch1.id, - ); - expect(matchExists, false); + group('UPDATE', () { + test('updateMatchName() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); - await database.matchDao.addMatch(match: testMatch1); + const newName = 'New name'; + await database.matchDao.updateMatchName( + matchId: testMatch1.id, + newName: newName, + ); - matchExists = await database.matchDao.matchExists(matchId: testMatch1.id); - expect(matchExists, true); - }); + final fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(fetchedMatch.name, newName); + }); - // Verifies that deleteMatch removes the match and returns true. - test('Deleting a match works correctly', () async { - await database.matchDao.addMatch(match: testMatch1); + test('updateMatchName() does nothing for non-existent match', () async { + final updated = await database.matchDao.updateMatchName( + matchId: 'non-existing-id', + newName: 'New Name', + ); + expect(updated, isFalse); - final matchDeleted = await database.matchDao.deleteMatch( - matchId: testMatch1.id, - ); - expect(matchDeleted, true); + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); - final matchExists = await database.matchDao.matchExists( - matchId: testMatch1.id, - ); - expect(matchExists, false); - }); + test('updateMatchGroup() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.groupDao.addGroup(group: testGroup2); - // Verifies that getMatchCount returns correct count through add/delete operations. - test('Getting the match count works correctly', () async { - var matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); + await database.matchDao.updateMatchGroup( + matchId: testMatch1.id, + newGroupId: testGroup2.id, + ); - await database.matchDao.addMatch(match: testMatch1); + final fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(fetchedMatch.group?.id, testGroup2.id); + }); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); + test('updateMatchGroup() does nothing for non-existent match', () async { + final updated = await database.matchDao.updateMatchGroup( + matchId: 'non-existing-id', + newGroupId: 'group-id', + ); + expect(updated, isFalse); - await database.matchDao.addMatch(match: testMatch2); + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 2); - - await database.matchDao.deleteMatch(matchId: testMatch1.id); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); - - await database.matchDao.deleteMatch(matchId: testMatch2.id); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); - }); - - // Verifies that updateMatchName correctly updates only the name field. - test('Renaming a match works correctly', () async { - await database.matchDao.addMatch(match: testMatch1); - - var fetchedMatch = await database.matchDao.getMatchById( - matchId: testMatch1.id, - ); - expect(fetchedMatch.name, testMatch1.name); - - const newName = 'Updated Match Name'; - await database.matchDao.updateMatchName( - matchId: testMatch1.id, - newName: newName, - ); - - fetchedMatch = await database.matchDao.getMatchById( - matchId: testMatch1.id, - ); - expect(fetchedMatch.name, newName); - }); - - test('Fetching a winner works correctly', () async { - await database.matchDao.addMatch(match: testMatch1); - - var fetchedMatch = await database.matchDao.getMatchById( - matchId: testMatch1.id, - ); - - expect(fetchedMatch.mvp, isNotNull); - expect(fetchedMatch.mvp.first.id, testPlayer4.id); - }); - - test('Setting a winner works correctly', () async { - await database.matchDao.addMatch(match: testMatch1); - - await database.scoreEntryDao.setWinner( - matchId: testMatch1.id, - playerId: testPlayer5.id, - ); - - final fetchedMatch = await database.matchDao.getMatchById( - matchId: testMatch1.id, - ); - expect(fetchedMatch.mvp, isNotNull); - expect(fetchedMatch.mvp.first.id, testPlayer5.id); - }); - - test( - 'removeMatchGroup removes group from match with existing group', - () async { + test('removeMatchGroup() works correctly', () async { + expect(testMatch1.group, isNotNull); await database.matchDao.addMatch(match: testMatch1); final removed = await database.matchDao.removeMatchGroup( @@ -318,53 +327,149 @@ void main() { matchId: testMatch1.id, ); expect(updatedMatch.group, null); - expect(updatedMatch.game.id, testMatch1.game.id); - expect(updatedMatch.name, testMatch1.name); - expect(updatedMatch.notes, testMatch1.notes); - }, - ); + }); - test( - 'removeMatchGroup on match that already has no group still succeeds', - () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); + test( + 'removeMatchGroup() on match that already has no group still succeeds', + () async { + await database.matchDao.addMatch(match: testMatchOnlyPlayers); - final removed = await database.matchDao.removeMatchGroup( - matchId: testMatchOnlyPlayers.id, - ); - expect(removed, isTrue); + final removed = await database.matchDao.removeMatchGroup( + matchId: testMatchOnlyPlayers.id, + ); + expect(removed, isTrue); - final updatedMatch = await database.matchDao.getMatchById( - matchId: testMatchOnlyPlayers.id, - ); - expect(updatedMatch.group, null); - }, - ); - - test('removeMatchGroup on non-existing match returns false', () async { - final removed = await database.matchDao.removeMatchGroup( - matchId: 'non-existing-id', + final updatedMatch = await database.matchDao.getMatchById( + matchId: testMatchOnlyPlayers.id, + ); + expect(updatedMatch.group, null); + }, ); - expect(removed, isFalse); + + test( + 'removeMatchGroup() on non-existing match returns isFalse', + () async { + final removed = await database.matchDao.removeMatchGroup( + matchId: 'non-existing-id', + ); + expect(removed, isFalse); + }, + ); + + test('updateMatchNotes() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + const newName = 'New name'; + await database.matchDao.updateMatchName( + matchId: testMatch1.id, + newName: newName, + ); + + final fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(fetchedMatch.name, newName); + }); + + test('updateMatchNotes() does nothing for non-existent game', () async { + final updated = await database.matchDao.updateMatchNotes( + matchId: 'non-existing-id', + notes: 'New notes', + ); + expect(updated, isFalse); + + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); + + test('updateMatchEndedAt() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + DateTime newEndedAt = DateTime(2030, 1, 1, 12, 0, 0); + print(newEndedAt); + await database.matchDao.updateMatchEndedAt( + matchId: testMatch1.id, + endedAt: newEndedAt, + ); + + final fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(fetchedMatch.endedAt, newEndedAt); + }); + + test('updateMatchEndedAt() does nothing for non-existent game', () async { + final updated = await database.matchDao.updateMatchEndedAt( + matchId: 'non-existing-id', + endedAt: DateTime.now(), + ); + expect(updated, isFalse); + + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); + + test('Getting the match count works correctly', () async { + var matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 0); + + await database.matchDao.addMatch(match: testMatch1); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 1); + + await database.matchDao.addMatch(match: testMatch2); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 2); + + await database.matchDao.deleteMatch(matchId: testMatch1.id); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 1); + + await database.matchDao.deleteMatch(matchId: testMatch2.id); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 0); + }); }); - test('Fetching all matches related to a group', () async { - var matches = await database.matchDao.getGroupMatches( - groupId: 'non-existing-id', - ); + group('DELETE', () { + test('deleteMatch() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); - expect(matches, isEmpty); + var deleted = await database.matchDao.deleteMatch( + matchId: testMatch1.id, + ); + expect(deleted, isTrue); - await database.matchDao.addMatch(match: testMatch1); + final matchExists = await database.matchDao.matchExists( + matchId: testMatch1.id, + ); + expect(matchExists, isFalse); - matches = await database.matchDao.getGroupMatches(groupId: testGroup1.id); + deleted = await database.matchDao.deleteMatch(matchId: testMatch1.id); + expect(deleted, isFalse); + }); - expect(matches, isNotEmpty); + test('deleteAllMatches() works correctly', () async { + await database.matchDao.addMatchesAsList( + matches: [testMatch1, testMatch2, testMatchOnlyPlayers], + ); - final match = matches.first; - expect(match.id, testMatch1.id); - expect(match.group, isNotNull); - expect(match.group!.id, testGroup1.id); + var count = await database.matchDao.getMatchCount(); + expect(count, 3); + + var deleted = await database.matchDao.deleteAllMatches(); + expect(deleted, isTrue); + + count = await database.matchDao.getMatchCount(); + expect(count, 0); + + deleted = await database.matchDao.deleteAllMatches(); + expect(deleted, isFalse); + }); }); }); } From f49440367cb3b5d33a780ac3c4c9ebf8c92104ec Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 1 May 2026 14:31:15 +0200 Subject: [PATCH 009/127] Finalized and updated team implementation, updated team dao, tests, and ex-/import --- lib/data/dao/match_dao.dart | 50 +- lib/data/dao/team_dao.dart | 184 +++-- lib/data/dao/team_dao.g.dart | 19 + lib/data/db/database.g.dart | 9 +- lib/data/db/tables/player_match_table.dart | 4 +- lib/data/models/match.dart | 5 + lib/services/data_transfer_service.dart | 14 +- test/db_tests/aggregates/team_test.dart | 704 +++++++----------- test/services/data_transfer_service_test.dart | 44 +- 9 files changed, 480 insertions(+), 553 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 3df8b9d..faa0227 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -8,6 +8,7 @@ import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/team.dart'; part 'match_dao.g.dart'; @@ -17,7 +18,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /* Create */ - /// Adds a new [Match] to the database. Also adds players associations. + /// Adds a new [Match] to the database. Also adds players associations and teams. /// This method assumes that the game and group (if any) are already present /// in the database. Future addMatch({required Match match}) async { @@ -36,6 +37,11 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { mode: InsertMode.insertOrReplace, ); + // Add teams + if (match.teams != null && match.teams!.isNotEmpty) { + await db.teamDao.addTeamsAsList(teams: match.teams!, matchId: match.id); + } + for (final p in match.players) { await db.playerMatchDao.addPlayerToMatch( matchId: match.id, @@ -53,7 +59,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); } } - print('Return true'); }); return true; } @@ -220,6 +225,16 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { } } }); + + // Add teams for matches + for (final match in matches) { + if (match.teams != null && match.teams!.isNotEmpty) { + await db.teamDao.addTeamsAsList( + teams: match.teams!, + matchId: match.id, + ); + } + } }); return true; } @@ -262,12 +277,15 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { matchId: row.id, ); + final teams = await _getMatchTeams(matchId: row.id); + return Match( id: row.id, name: row.name, game: game, group: group, players: players, + teams: teams.isEmpty ? null : teams, notes: row.notes ?? '', createdAt: row.createdAt, endedAt: row.endedAt, @@ -294,12 +312,15 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId); + final teams = await _getMatchTeams(matchId: matchId); + return Match( id: result.id, name: result.name, game: game, group: group, players: players, + teams: teams.isEmpty ? null : teams, notes: result.notes ?? '', createdAt: result.createdAt, endedAt: result.endedAt, @@ -319,12 +340,14 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { final group = await db.groupDao.getGroupById(groupId: groupId); final players = await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; + final teams = await _getMatchTeams(matchId: row.id); return Match( id: row.id, name: row.name, game: game, group: group, players: players, + teams: teams.isEmpty ? null : teams, notes: row.notes ?? '', createdAt: row.createdAt, endedAt: row.endedAt, @@ -333,6 +356,29 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); } + /// Helper method to retrieve teams for a specific match + Future> _getMatchTeams({required String matchId}) async { + // Get all unique team IDs from PlayerMatchTable for this match + final playerMatchQuery = select(db.playerMatchTable) + ..where((pm) => pm.matchId.equals(matchId) & pm.teamId.isNotNull()); + final playerMatches = await playerMatchQuery.get(); + + if (playerMatches.isEmpty) return []; + + final teamIds = playerMatches + .map((pm) => pm.teamId) + .whereType() + .toSet() + .toList(); + + // Fetch all teams + final teams = await Future.wait( + teamIds.map((teamId) => db.teamDao.getTeamById(teamId: teamId)), + ); + + return teams; + } + /* Update */ /// Changes the name of the match with the given [matchId] to [newName]. diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index 01dc724..708b475 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -1,17 +1,105 @@ import 'package:drift/drift.dart'; import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/team_table.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/team.dart'; part 'team_dao.g.dart'; -@DriftAccessor(tables: [TeamTable]) +@DriftAccessor(tables: [TeamTable, PlayerMatchTable]) class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { TeamDao(super.db); + /* Create */ + + /// Adds a new [team] to the database. + /// Returns `true` if the team was added, `false` otherwise. + Future addTeam({required Team team, required String matchId}) async { + if (await teamExists(teamId: team.id)) return false; + await into(teamTable).insert( + TeamTableCompanion.insert( + id: team.id, + name: team.name, + createdAt: team.createdAt, + ), + mode: InsertMode.insertOrReplace, + ); + await db.batch((batch) async { + for (final player in team.members) { + await into(playerMatchTable).insert( + PlayerMatchTableCompanion.insert( + playerId: player.id, + matchId: matchId, + teamId: Value(team.id), + ), + mode: InsertMode.insertOrReplace, + ); + } + }); + return true; + } + + /// Adds multiple [teams] to the database in a batch operation. + Future addTeamsAsList({ + required List teams, + required String matchId, + }) async { + if (teams.isEmpty) return false; + + await db.batch( + (b) => b.insertAll( + teamTable, + teams + .map( + (team) => TeamTableCompanion.insert( + id: team.id, + name: team.name, + createdAt: team.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrIgnore, + ), + ); + + for (final team in teams) { + await db.batch((batch) async { + for (final player in team.members) { + await into(db.playerMatchTable).insert( + PlayerMatchTableCompanion.insert( + playerId: player.id, + matchId: matchId, + teamId: Value(team.id), + ), + mode: InsertMode.insertOrReplace, + ); + } + }); + } + return true; + } + + /* Read */ + + /// Retrieves the total count of teams in the database. + Future getTeamCount() async { + final count = + await (selectOnly(teamTable)..addColumns([teamTable.id.count()])) + .map((row) => row.read(teamTable.id.count())) + .getSingle(); + return count ?? 0; + } + + /// Checks if a team with the given [teamId] exists in the database. + /// Returns `true` if the team exists, `false` otherwise. + Future teamExists({required String teamId}) async { + final query = select(teamTable)..where((t) => t.id.equals(teamId)); + final result = await query.getSingleOrNull(); + return result != null; + } + /// Retrieves all teams from the database. - /// Note: This returns teams without their members. Use getTeamById for full team data. Future> getAllTeams() async { final query = select(teamTable); final result = await query.get(); @@ -41,8 +129,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { ); } - /// Helper method to get team members from player_match_table. - /// This assumes team members are tracked via the player_match_table. + /// Helper method to get team members from PlayerMatchTable. Future> _getTeamMembers({required String teamId}) async { // Get all player_match entries with this teamId final playerMatchQuery = select(db.playerMatchTable) @@ -61,44 +148,28 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { return players; } - /// Adds a new [team] to the database. - /// Returns `true` if the team was added, `false` otherwise. - Future addTeam({required Team team}) async { - if (!await teamExists(teamId: team.id)) { - await into(teamTable).insert( - TeamTableCompanion.insert( - id: team.id, - name: team.name, - createdAt: team.createdAt, - ), - mode: InsertMode.insertOrReplace, - ); - return true; - } - return false; + /* Update */ + + /// Updates the name of the team with the given [teamId]. + Future updateTeamName({ + required String teamId, + required String newName, + }) async { + final rowsAffected = + await (update(teamTable)..where((t) => t.id.equals(teamId))).write( + TeamTableCompanion(name: Value(newName)), + ); + return rowsAffected > 0; } - /// Adds multiple [teams] to the database in a batch operation. - Future addTeamsAsList({required List teams}) async { - if (teams.isEmpty) return false; + /* Delete */ - await db.batch( - (b) => b.insertAll( - teamTable, - teams - .map( - (team) => TeamTableCompanion.insert( - id: team.id, - name: team.name, - createdAt: team.createdAt, - ), - ) - .toList(), - mode: InsertMode.insertOrIgnore, - ), - ); - - return true; + /// Deletes all teams from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllTeams() async { + final query = delete(teamTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; } /// Deletes the team with the given [teamId] from the database. @@ -108,39 +179,4 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { final rowsAffected = await query.go(); return rowsAffected > 0; } - - /// Checks if a team with the given [teamId] exists in the database. - /// Returns `true` if the team exists, `false` otherwise. - Future teamExists({required String teamId}) async { - final query = select(teamTable)..where((t) => t.id.equals(teamId)); - final result = await query.getSingleOrNull(); - return result != null; - } - - /// Updates the name of the team with the given [teamId]. - Future updateTeamName({ - required String teamId, - required String newName, - }) async { - await (update(teamTable)..where((t) => t.id.equals(teamId))).write( - TeamTableCompanion(name: Value(newName)), - ); - } - - /// Retrieves the total count of teams in the database. - Future getTeamCount() async { - final count = - await (selectOnly(teamTable)..addColumns([teamTable.id.count()])) - .map((row) => row.read(teamTable.id.count())) - .getSingle(); - return count ?? 0; - } - - /// Deletes all teams from the database. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future deleteAllTeams() async { - final query = delete(teamTable); - final rowsAffected = await query.go(); - return rowsAffected > 0; - } } diff --git a/lib/data/dao/team_dao.g.dart b/lib/data/dao/team_dao.g.dart index 3b78c03..7b468dd 100644 --- a/lib/data/dao/team_dao.g.dart +++ b/lib/data/dao/team_dao.g.dart @@ -5,6 +5,12 @@ part of 'team_dao.dart'; // ignore_for_file: type=lint mixin _$TeamDaoMixin on DatabaseAccessor { $TeamTableTable get teamTable => attachedDatabase.teamTable; + $PlayerTableTable get playerTable => attachedDatabase.playerTable; + $GameTableTable get gameTable => attachedDatabase.gameTable; + $GroupTableTable get groupTable => attachedDatabase.groupTable; + $MatchTableTable get matchTable => attachedDatabase.matchTable; + $PlayerMatchTableTable get playerMatchTable => + attachedDatabase.playerMatchTable; TeamDaoManager get managers => TeamDaoManager(this); } @@ -13,4 +19,17 @@ class TeamDaoManager { TeamDaoManager(this._db); $$TeamTableTableTableManager get teamTable => $$TeamTableTableTableManager(_db.attachedDatabase, _db.teamTable); + $$PlayerTableTableTableManager get playerTable => + $$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable); + $$GameTableTableTableManager get gameTable => + $$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable); + $$GroupTableTableTableManager get groupTable => + $$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable); + $$MatchTableTableTableManager get matchTable => + $$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable); + $$PlayerMatchTableTableTableManager get playerMatchTable => + $$PlayerMatchTableTableTableManager( + _db.attachedDatabase, + _db.playerMatchTable, + ); } diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index 2190c3d..b4f3702 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -2122,7 +2122,7 @@ class $PlayerMatchTableTable extends PlayerMatchTable type: DriftSqlType.string, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES team_table (id)', + 'REFERENCES team_table (id) ON DELETE SET NULL', ), ); @override @@ -2820,6 +2820,13 @@ abstract class _$AppDatabase extends GeneratedDatabase { ), result: [TableUpdate('player_match_table', kind: UpdateKind.delete)], ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'team_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('player_match_table', kind: UpdateKind.update)], + ), WritePropagation( on: TableUpdateQuery.onTableName( 'player_table', diff --git a/lib/data/db/tables/player_match_table.dart b/lib/data/db/tables/player_match_table.dart index 30412ab..50dda0f 100644 --- a/lib/data/db/tables/player_match_table.dart +++ b/lib/data/db/tables/player_match_table.dart @@ -8,7 +8,9 @@ class PlayerMatchTable extends Table { 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()(); + TextColumn get teamId => text() + .references(TeamTable, #id, onDelete: KeyAction.setNull) + .nullable()(); @override Set> get primaryKey => {playerId, matchId}; diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 2ff02d6..b0b487c 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -4,6 +4,7 @@ import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/score_entry.dart'; +import 'package:tallee/data/models/team.dart'; import 'package:uuid/uuid.dart'; class Match { @@ -14,6 +15,7 @@ class Match { final Game game; final Group? group; final List players; + final List? teams; final String notes; Map scores; @@ -23,6 +25,7 @@ class Match { required this.players, this.endedAt, this.group, + this.teams, this.notes = '', String? id, DateTime? createdAt, @@ -55,6 +58,7 @@ class Match { ), group = null, players = [], + teams = [], scores = json['scores'] != null ? (json['scores'] as Map).map( (key, value) => MapEntry( @@ -78,6 +82,7 @@ class Match { 'gameId': game.id, 'groupId': group?.id, 'playerIds': players.map((player) => player.id).toList(), + 'teams': teams?.map((team) => team.toJson()).toList(), 'scores': scores.map((key, value) => MapEntry(key, value?.toJson())), 'notes': notes, }; diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index e47b220..29199f8 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -35,13 +35,11 @@ class DataTransferService { final groups = await db.groupDao.getAllGroups(); final players = await db.playerDao.getAllPlayers(); final games = await db.gameDao.getAllGames(); - final teams = await db.teamDao.getAllTeams(); final Map jsonMap = { 'players': players.map((player) => player.toJson()).toList(), 'games': games.map((game) => game.toJson()).toList(), 'groups': groups.map((group) => group.toJson()).toList(), - 'teams': teams.map((team) => team.toJson()).toList(), 'matches': matches.map((match) => match.toJson()).toList(), }; @@ -130,8 +128,6 @@ class DataTransferService { final importedGroups = parseGroupsFromJson(decodedJson, playerById); final groupById = {for (final g in importedGroups) g.id: g}; - final importedTeams = parseTeamsFromJson(decodedJson, playerById); - final importedMatches = parseMatchesFromJson( decodedJson, gameById, @@ -142,7 +138,6 @@ class DataTransferService { await db.playerDao.addPlayersAsList(players: importedPlayers); await db.gameDao.addGamesAsList(games: importedGames); await db.groupDao.addGroupsAsList(groups: importedGroups); - await db.teamDao.addTeamsAsList(teams: importedTeams); await db.matchDao.addMatchesAsList(matches: importedMatches); } @@ -190,13 +185,12 @@ class DataTransferService { }).toList(); } - /// Parses teams from JSON data. + /// Parses teams from a list of JSON objects. @visibleForTesting static List parseTeamsFromJson( - Map decodedJson, + List teamsJson, Map playerById, ) { - final teamsJson = (decodedJson['teams'] as List?) ?? []; return teamsJson.map((t) { final map = t as Map; final memberIds = (map['memberIds'] as List? ?? []) @@ -259,12 +253,16 @@ class DataTransferService { .whereType() .toList(); + final teamsJson = (map['teams'] as List?) ?? []; + final teams = parseTeamsFromJson(teamsJson, playersMap); + return Match( id: id, name: name, game: game, group: group, players: players, + teams: teams.isEmpty ? null : teams, createdAt: createdAt, endedAt: endedAt, notes: notes, diff --git a/test/db_tests/aggregates/team_test.dart b/test/db_tests/aggregates/team_test.dart index 39c5be5..592810d 100644 --- a/test/db_tests/aggregates/team_test.dart +++ b/test/db_tests/aggregates/team_test.dart @@ -1,3 +1,5 @@ +import 'dart:core' hide Match; + import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; @@ -18,8 +20,11 @@ void main() { late Team testTeam1; late Team testTeam2; late Team testTeam3; - late Game testGame1; - late Game testGame2; + late Team testTeam4; + late Game testGame; + late Match testMatch1; + late Match testMatch2; + late Match matchWithNoTeams; final fixedDate = DateTime(2025, 11, 19, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -40,27 +45,35 @@ void main() { testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]); testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]); testTeam3 = Team(name: 'Team Gamma', members: [testPlayer1, testPlayer3]); - testGame1 = Game( - name: 'Game 1', - ruleset: Ruleset.singleWinner, - description: 'Test game 1', + testTeam4 = Team(name: 'Team Omega', members: [testPlayer2, testPlayer4]); + testGame = Game( + name: 'Test Game', + ruleset: Ruleset.highestScore, color: GameColor.blue, icon: '', ); - testGame2 = Game( - name: 'Game 2', - ruleset: Ruleset.highestScore, - description: 'Test game 2', - color: GameColor.red, - icon: '', + testMatch1 = Match( + name: 'Match 1', + game: testGame, + players: [], + teams: [testTeam1, testTeam2], + ); + testMatch2 = Match( + name: 'Match 2', + game: testGame, + players: [], + teams: [testTeam3, testTeam4], + ); + matchWithNoTeams = Match( + name: 'Match with no teams', + game: testGame, + players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); }); - + await database.gameDao.addGame(game: testGame); await database.playerDao.addPlayersAsList( players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); - await database.gameDao.addGame(game: testGame1); - await database.gameDao.addGame(game: testGame2); }); tearDown(() async { @@ -68,460 +81,251 @@ void main() { }); group('Team Tests', () { - // Verifies that a single team can be added and retrieved with all fields intact. - test('Adding and fetching a single team works correctly', () async { - final added = await database.teamDao.addTeam(team: testTeam1); - expect(added, true); - - final fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(fetchedTeam.id, testTeam1.id); - expect(fetchedTeam.name, testTeam1.name); - expect(fetchedTeam.createdAt, testTeam1.createdAt); - }); - - // Verifies that multiple teams can be added at once and retrieved correctly. - test('Adding and fetching multiple teams works correctly', () async { - await database.teamDao.addTeamsAsList( - teams: [testTeam1, testTeam2, testTeam3], - ); - - final allTeams = await database.teamDao.getAllTeams(); - expect(allTeams.length, 3); - - final testTeams = { - testTeam1.id: testTeam1, - testTeam2.id: testTeam2, - testTeam3.id: testTeam3, - }; - - for (final team in allTeams) { - final testTeam = testTeams[team.id]!; - - expect(team.id, testTeam.id); - expect(team.name, testTeam.name); - expect(team.createdAt, testTeam.createdAt); - } - }); - - // Verifies that adding the same team twice does not create duplicates and returns false. - test('Adding the same team twice does not create duplicates', () async { - await database.teamDao.addTeam(team: testTeam1); - final addedAgain = await database.teamDao.addTeam(team: testTeam1); - - expect(addedAgain, false); - - final teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 1); - }); - - // Verifies that teamExists returns correct boolean based on team presence. - test('Team existence check works correctly', () async { - var teamExists = await database.teamDao.teamExists(teamId: testTeam1.id); - expect(teamExists, false); - - await database.teamDao.addTeam(team: testTeam1); - - teamExists = await database.teamDao.teamExists(teamId: testTeam1.id); - expect(teamExists, true); - }); - - // Verifies that deleteTeam removes the team and returns true. - test('Deleting a team works correctly', () async { - await database.teamDao.addTeam(team: testTeam1); - - final teamDeleted = await database.teamDao.deleteTeam( - teamId: testTeam1.id, - ); - expect(teamDeleted, true); - - final teamExists = await database.teamDao.teamExists( - teamId: testTeam1.id, - ); - expect(teamExists, false); - }); - - // Verifies that deleteTeam returns false for a non-existent team ID. - test('Deleting a non-existent team returns false', () async { - final teamDeleted = await database.teamDao.deleteTeam( - teamId: 'non-existent-id', - ); - expect(teamDeleted, false); - }); - - // Verifies that getTeamCount returns correct count through add/delete operations. - test('Getting the team count works correctly', () async { - var teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 0); - - await database.teamDao.addTeam(team: testTeam1); - - teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 1); - - await database.teamDao.addTeam(team: testTeam2); - - teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 2); - - await database.teamDao.deleteTeam(teamId: testTeam1.id); - - teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 1); - - await database.teamDao.deleteTeam(teamId: testTeam2.id); - - teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 0); - }); - - // Verifies that updateTeamName correctly updates only the name field. - test('Updating team name works correctly', () async { - await database.teamDao.addTeam(team: testTeam1); - - var fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - expect(fetchedTeam.name, testTeam1.name); - - const newName = 'Updated Team Name'; - await database.teamDao.updateTeamName( - teamId: testTeam1.id, - newName: newName, - ); - - fetchedTeam = await database.teamDao.getTeamById(teamId: testTeam1.id); - expect(fetchedTeam.name, newName); - }); - - // Verifies that deleteAllTeams removes all teams from the database. - test('Deleting all teams works correctly', () async { - await database.teamDao.addTeamsAsList( - teams: [testTeam1, testTeam2, testTeam3], - ); - - var teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 3); - - final deleted = await database.teamDao.deleteAllTeams(); - expect(deleted, true); - - teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 0); - }); - - // Verifies that deleteAllTeams returns false when no teams exist. - test('Deleting all teams when empty returns false', () async { - final deleted = await database.teamDao.deleteAllTeams(); - expect(deleted, false); - }); - - // Verifies that addTeamsAsList returns false when given an empty list. - test('Adding teams as list with empty list returns false', () async { - final added = await database.teamDao.addTeamsAsList(teams: []); - expect(added, false); - }); - - // Verifies that addTeamsAsList with duplicate IDs ignores duplicates and keeps the first. - test('Adding teams with duplicate IDs ignores duplicates', () async { - final duplicateTeam = Team( - id: testTeam1.id, - name: 'Duplicate Team', - members: [testPlayer4], - ); - - await database.teamDao.addTeamsAsList( - teams: [testTeam1, duplicateTeam, testTeam2], - ); - - final teamCount = await database.teamDao.getTeamCount(); - expect(teamCount, 2); - - // The first one should be kept (insertOrIgnore) - final fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - expect(fetchedTeam.name, testTeam1.name); - }); - - // Verifies that getAllTeams returns empty list when no teams exist. - test('Getting all teams when empty returns empty list', () async { - final allTeams = await database.teamDao.getAllTeams(); - expect(allTeams.isEmpty, true); - }); - - // Verifies that getTeamById throws exception for non-existent team. - test('Getting non-existent team throws exception', () async { - expect( - () => database.teamDao.getTeamById(teamId: 'non-existent-id'), - throwsA(isA()), - ); - }); - - // Verifies that updating team name preserves other fields. - test('Updating team name preserves other team fields', () async { - await database.teamDao.addTeam(team: testTeam1); - final originalTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - final originalCreatedAt = originalTeam.createdAt; - - const newName = 'Brand New Team Name'; - await database.teamDao.updateTeamName( - teamId: testTeam1.id, - newName: newName, - ); - - final updatedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(updatedTeam.name, newName); - expect(updatedTeam.id, testTeam1.id); - expect(updatedTeam.createdAt, originalCreatedAt); - }); - - // Verifies that team name can be updated to an empty string. - test('Updating team name to empty string works', () async { - await database.teamDao.addTeam(team: testTeam1); - - await database.teamDao.updateTeamName(teamId: testTeam1.id, newName: ''); - - final updatedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(updatedTeam.name, ''); - }); - - // Verifies that team name can be updated to a very long string. - test('Updating team name to long string works', () async { - await database.teamDao.addTeam(team: testTeam1); - final longName = 'A' * 500; // 500 character name - - await database.teamDao.updateTeamName( - teamId: testTeam1.id, - newName: longName, - ); - - final updatedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(updatedTeam.name, longName); - expect(updatedTeam.name.length, 500); - }); - - // Verifies that updating non-existent team name doesn't throw error. - test('Updating non-existent team name completes without error', () async { - expect( - () => database.teamDao.updateTeamName( - teamId: 'non-existent-id', - newName: 'New Name', - ), - returnsNormally, - ); - }); - - // Verifies that deleteTeam only affects the specified team. - test('Deleting one team does not affect other teams', () async { - await database.teamDao.addTeamsAsList( - teams: [testTeam1, testTeam2, testTeam3], - ); - - await database.teamDao.deleteTeam(teamId: testTeam2.id); - - final allTeams = await database.teamDao.getAllTeams(); - expect(allTeams.length, 2); - expect(allTeams.any((t) => t.id == testTeam1.id), true); - expect(allTeams.any((t) => t.id == testTeam2.id), false); - expect(allTeams.any((t) => t.id == testTeam3.id), true); - }); - - // Verifies that teams with overlapping members are independent. - test('Teams with overlapping members are independent', () async { - // Create two matches since player_match has primary key {playerId, matchId} - final match1 = Match( - name: 'Match 1', - game: testGame1, - players: [testPlayer1, testPlayer2], - ); - final match2 = Match( - name: 'Match 2', - game: testGame2, - players: [testPlayer1, testPlayer2], - ); - await database.matchDao.addMatch(match: match1); - await database.matchDao.addMatch(match: match2); - - // Add teams to database - await database.teamDao.addTeamsAsList(teams: [testTeam1, testTeam3]); - - // Associate players with teams through match1 - // testTeam1: player1, player2 - await database.playerMatchDao.addPlayerToMatch( - playerId: testPlayer1.id, - matchId: match1.id, - teamId: testTeam1.id, - ); - await database.playerMatchDao.addPlayerToMatch( - playerId: testPlayer2.id, - matchId: match1.id, - teamId: testTeam1.id, - ); - - // Associate players with teams through match2 - // testTeam3: player1, player3 (overlapping player1) - await database.playerMatchDao.addPlayerToMatch( - playerId: testPlayer1.id, - matchId: match2.id, - teamId: testTeam3.id, - ); - await database.playerMatchDao.addPlayerToMatch( - playerId: testPlayer3.id, - matchId: match2.id, - teamId: testTeam3.id, - ); - - final team1 = await database.teamDao.getTeamById(teamId: testTeam1.id); - final team3 = await database.teamDao.getTeamById(teamId: testTeam3.id); - - expect(team1.members.length, 2); - expect(team3.members.length, 2); - expect(team1.members.any((p) => p.id == testPlayer1.id), true); - expect(team3.members.any((p) => p.id == testPlayer1.id), true); - }); - - // Verifies that adding teams sequentially works correctly. - test('Adding teams sequentially maintains correct count', () async { - var count = await database.teamDao.getTeamCount(); - expect(count, 0); - - await database.teamDao.addTeam(team: testTeam1); - count = await database.teamDao.getTeamCount(); - expect(count, 1); - - await database.teamDao.addTeam(team: testTeam2); - count = await database.teamDao.getTeamCount(); - expect(count, 2); - - await database.teamDao.addTeam(team: testTeam3); - count = await database.teamDao.getTeamCount(); - expect(count, 3); - }); - - // Verifies that getAllTeams returns all teams with correct data. - test('Getting all teams returns all teams with correct data', () async { - await database.teamDao.addTeamsAsList( - teams: [testTeam1, testTeam2, testTeam3], - ); - - final allTeams = await database.teamDao.getAllTeams(); - - expect(allTeams.length, 3); - expect(allTeams.map((t) => t.id).toSet(), { - testTeam1.id, - testTeam2.id, - testTeam3.id, + group('CREATE', () { + test('Adding and fetching a single team works correctly', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); + final added = await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); + expect(added, isTrue); + + final fetchedTeam = await database.teamDao.getTeamById( + teamId: testTeam1.id, + ); + + expect(fetchedTeam.id, testTeam1.id); + expect(fetchedTeam.name, testTeam1.name); + expect(fetchedTeam.createdAt, testTeam1.createdAt); + expect(fetchedTeam.members.length, testTeam1.members.length); + for (int i = 0; i < fetchedTeam.members.length; i++) { + expect(fetchedTeam.members[i].id, testTeam1.members[i].id); + expect(fetchedTeam.members[i].name, testTeam1.members[i].name); + } + }); + + test('Adding and fetching multiple teams works correctly', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); + await database.teamDao.addTeamsAsList( + teams: [testTeam1, testTeam2, testTeam3], + matchId: matchWithNoTeams.id, + ); + + final allTeams = await database.teamDao.getAllTeams(); + expect(allTeams.length, 3); + + final testTeams = { + testTeam1.id: testTeam1, + testTeam2.id: testTeam2, + testTeam3.id: testTeam3, + }; + + for (final team in allTeams) { + final testTeam = testTeams[team.id]!; + + expect(team.id, testTeam.id); + expect(team.name, testTeam.name); + expect(team.createdAt, testTeam.createdAt); + } + }); + + test('addTeam() ignores duplicates', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); + var added = await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); + expect(added, isTrue); + + added = await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); + expect(added, isFalse); + + final teamCount = await database.teamDao.getTeamCount(); + expect(teamCount, 1); + }); + + test('addTeamsAsList() with empty list returns isFalse', () async { + final added = await database.teamDao.addTeamsAsList( + teams: [], + matchId: matchWithNoTeams.id, + ); + expect(added, isFalse); + }); + + test('addTeamsAsList() ignores duplicates', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); + final added = await database.teamDao.addTeamsAsList( + teams: [testTeam1, testTeam2, testTeam1], + matchId: matchWithNoTeams.id, + ); + expect(added, isTrue); + + final teamCount = await database.teamDao.getTeamCount(); + expect(teamCount, 2); }); }); - // Verifies that teamExists returns false for deleted teams. - test('Team existence returns false after deletion', () async { - await database.teamDao.addTeam(team: testTeam1); - expect(await database.teamDao.teamExists(teamId: testTeam1.id), true); + group('READ', () { + test('getTeamCount works correctly', () async { + var count = await database.teamDao.getTeamCount(); + expect(count, 0); - await database.teamDao.deleteTeam(teamId: testTeam1.id); - expect(await database.teamDao.teamExists(teamId: testTeam1.id), false); + await database.matchDao.addMatch(match: testMatch1); + + count = await database.teamDao.getTeamCount(); + expect(count, 2); + + await database.teamDao.addTeam( + team: testTeam2, + matchId: matchWithNoTeams.id, + ); + + count = await database.teamDao.getTeamCount(); + expect(count, 2); + + await database.teamDao.deleteTeam(teamId: testTeam1.id); + + count = await database.teamDao.getTeamCount(); + expect(count, 1); + + await database.teamDao.deleteTeam(teamId: testTeam2.id); + + count = await database.teamDao.getTeamCount(); + expect(count, 0); + }); + + test('teamExists() works correctly', () async { + var teamExists = await database.teamDao.teamExists( + teamId: testTeam1.id, + ); + expect(teamExists, isFalse); + + await database.matchDao.addMatch(match: matchWithNoTeams); + + await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); + + teamExists = await database.teamDao.teamExists(teamId: testTeam1.id); + expect(teamExists, isTrue); + }); + + test('getAllTeams() with no teams returns empty list', () async { + final allTeams = await database.teamDao.getAllTeams(); + expect(allTeams, isA>()); + expect(allTeams.isEmpty, isTrue); + }); + + test('getAllTeams() works correctly', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); + + await database.teamDao.addTeamsAsList( + teams: [testTeam1, testTeam2, testTeam3], + matchId: matchWithNoTeams.id, + ); + + final allTeams = await database.teamDao.getAllTeams(); + + expect(allTeams.length, 3); + expect(allTeams.map((t) => t.id).toSet(), { + testTeam1.id, + testTeam2.id, + testTeam3.id, + }); + }); + + test('Getting non-existent team throws exception', () async { + expect( + () => database.teamDao.getTeamById(teamId: 'non-existent-id'), + throwsA(isA()), + ); + }); }); - // Verifies that adding multiple teams in batch then deleting returns correct count. - test('Batch add then partial delete maintains correct count', () async { - await database.teamDao.addTeamsAsList( - teams: [testTeam1, testTeam2, testTeam3], - ); + group('UPDATED', () { + test('updateTeamName() works correctly', () async { + await database.matchDao.addMatch(match: matchWithNoTeams); - expect(await database.teamDao.getTeamCount(), 3); + await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); - await database.teamDao.deleteTeam(teamId: testTeam1.id); - expect(await database.teamDao.getTeamCount(), 2); + var fetchedTeam = await database.teamDao.getTeamById( + teamId: testTeam1.id, + ); + expect(fetchedTeam.name, testTeam1.name); - await database.teamDao.deleteTeam(teamId: testTeam3.id); - expect(await database.teamDao.getTeamCount(), 1); + const newName = 'New name'; + await database.teamDao.updateTeamName( + teamId: testTeam1.id, + newName: newName, + ); + + fetchedTeam = await database.teamDao.getTeamById(teamId: testTeam1.id); + expect(fetchedTeam.name, newName); + }); + + test('updateTeamName() does nothing for non-existent team', () async { + final updated = await database.teamDao.updateTeamName( + teamId: 'non-existing-id', + newName: 'New Name', + ); + expect(updated, isFalse); + + final allTeams = await database.teamDao.getAllTeams(); + expect(allTeams, isEmpty); + }); }); - // Verifies that deleteAllTeams with single team works. - test('Deleting all teams with single team returns true', () async { - await database.teamDao.addTeam(team: testTeam1); - expect(await database.teamDao.getTeamCount(), 1); + group('DELETE', () { + test('deleteTeam() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.matchDao.addMatch(match: matchWithNoTeams); - final deleted = await database.teamDao.deleteAllTeams(); - expect(deleted, true); - expect(await database.teamDao.getTeamCount(), 0); - }); + await database.teamDao.addTeam( + team: testTeam1, + matchId: matchWithNoTeams.id, + ); - // Verifies that addTeam after deleteAllTeams works correctly. - test('Adding team after deleteAllTeams works correctly', () async { - await database.teamDao.addTeamsAsList(teams: [testTeam1, testTeam2]); - expect(await database.teamDao.getTeamCount(), 2); + final deleted = await database.teamDao.deleteTeam(teamId: testTeam1.id); + expect(deleted, isTrue); - await database.teamDao.deleteAllTeams(); - expect(await database.teamDao.getTeamCount(), 0); + final teamExists = await database.teamDao.teamExists( + teamId: testTeam1.id, + ); + expect(teamExists, isFalse); + }); - final added = await database.teamDao.addTeam(team: testTeam3); - expect(added, true); - expect(await database.teamDao.getTeamCount(), 1); + test('Deleting a non-existent team returns isFalse', () async { + final deleted = await database.teamDao.deleteTeam( + teamId: 'non-existent-id', + ); + expect(deleted, isFalse); + }); - final fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam3.id, - ); - expect(fetchedTeam.name, testTeam3.name); - }); + test('deleteAllTeams() works correctly', () async { + await database.matchDao.addMatchesAsList( + matches: [testMatch1, testMatch2], + ); + var teamCount = await database.teamDao.getTeamCount(); + expect(teamCount, 4); - // Verifies that addTeamsAsList with partial duplicates ignores duplicates. - test('Adding teams with some duplicates ignores only duplicates', () async { - await database.teamDao.addTeam(team: testTeam1); + final deleted = await database.teamDao.deleteAllTeams(); + expect(deleted, isTrue); - final duplicateTeam1 = Team( - id: testTeam1.id, - name: 'Different Name', - members: [testPlayer3], - ); + teamCount = await database.teamDao.getTeamCount(); + expect(teamCount, 0); + }); - await database.teamDao.addTeamsAsList( - teams: [duplicateTeam1, testTeam2, testTeam3], - ); - - final allTeams = await database.teamDao.getAllTeams(); - expect(allTeams.length, 3); - - // Verify testTeam1 retained original name (was inserted first) - final team1 = await database.teamDao.getTeamById(teamId: testTeam1.id); - expect(team1.name, testTeam1.name); - }); - - // Verifies that team IDs are preserved correctly. - test('Team IDs are preserved through add and retrieve', () async { - await database.teamDao.addTeam(team: testTeam1); - - final fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(fetchedTeam.id, testTeam1.id); - }); - - // Verifies that createdAt timestamps are preserved. - test('Team createdAt timestamps are preserved', () async { - await database.teamDao.addTeam(team: testTeam1); - - final fetchedTeam = await database.teamDao.getTeamById( - teamId: testTeam1.id, - ); - - expect(fetchedTeam.createdAt, testTeam1.createdAt); + test('deleteAllTeams() with empty list returns false', () async { + final deleted = await database.teamDao.deleteAllTeams(); + expect(deleted, isFalse); + }); }); }); } diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index e863629..6aec390 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -99,7 +99,9 @@ void main() { await database.playerDao.addPlayer(player: testPlayer1); await database.gameDao.addGame(game: testGame); await database.groupDao.addGroup(group: testGroup); + /* await database.teamDao.addTeam(team: testTeam); +*/ await database.matchDao.addMatch(match: testMatch); var playerCount = await database.playerDao.getPlayerCount(); @@ -137,7 +139,9 @@ void main() { await database.playerDao.addPlayer(player: testPlayer2); await database.gameDao.addGame(game: testGame); await database.groupDao.addGroup(group: testGroup); + /* await database.teamDao.addTeam(team: testTeam); +*/ await database.matchDao.addMatch(match: testMatch); final ctx = await getContext(tester); @@ -244,7 +248,9 @@ void main() { }); testWidgets('Team data is correct', (tester) async { + /* await database.teamDao.addTeam(team: testTeam); +*/ final ctx = await getContext(tester); final jsonString = await DataTransferService.getAppDataAsJson(ctx); @@ -644,19 +650,17 @@ void main() { test('parseTeamsFromJson()', () { final playerById = {testPlayer1.id: testPlayer1}; - final jsonMap = { - 'teams': [ - { - 'id': testTeam.id, - 'name': testTeam.name, - 'memberIds': [testPlayer1.id], - 'createdAt': testTeam.createdAt.toIso8601String(), - }, - ], - }; + final teamsJson = [ + { + 'id': testTeam.id, + 'name': testTeam.name, + 'memberIds': [testPlayer1.id], + 'createdAt': testTeam.createdAt.toIso8601String(), + }, + ]; final teams = DataTransferService.parseTeamsFromJson( - jsonMap, + teamsJson, playerById, ); @@ -668,15 +672,21 @@ void main() { }); test('parseTeamsFromJson() empty list', () { - final jsonMap = {'teams': []}; - final teams = DataTransferService.parseTeamsFromJson(jsonMap, {}); + final teams = DataTransferService.parseTeamsFromJson([], {}); expect(teams, isEmpty); }); - test('parseTeamsFromJson() missing key', () { - final jsonMap = {}; - final teams = DataTransferService.parseTeamsFromJson(jsonMap, {}); - expect(teams, isEmpty); + test('parseTeamsFromJson() missing memberIds', () { + final teamsJson = [ + { + 'id': testTeam.id, + 'name': testTeam.name, + 'createdAt': testTeam.createdAt.toIso8601String(), + }, + ]; + final teams = DataTransferService.parseTeamsFromJson(teamsJson, {}); + expect(teams.length, 1); + expect(teams[0].members, isEmpty); }); test('parseMatchesFromJson()', () { From cbc8a1afcec9025b7b8c86886f4b578d13f86137 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 1 May 2026 16:49:33 +0200 Subject: [PATCH 010/127] Updated player-match dao and tests --- lib/data/dao/match_dao.dart | 47 +- lib/data/dao/player_match_dao.dart | 134 +-- .../relationships/player_match_test.dart | 928 +++++++----------- 3 files changed, 415 insertions(+), 694 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index faa0227..11cd5a2 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -270,8 +270,9 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { if (row.groupId != null) { group = await db.groupDao.getGroupById(groupId: row.groupId!); } - final players = - await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; + final players = await db.playerMatchDao.getPlayersOfMatch( + matchId: row.id, + ); final scores = await db.scoreEntryDao.getAllMatchScores( matchId: row.id, @@ -307,8 +308,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { group = await db.groupDao.getGroupById(groupId: result.groupId!); } - final players = - await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; + final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId); final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId); @@ -338,8 +338,9 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { rows.map((row) async { final game = await db.gameDao.getGameById(gameId: row.gameId); final group = await db.groupDao.getGroupById(groupId: groupId); - final players = - await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; + final players = await db.playerMatchDao.getPlayersOfMatch( + matchId: row.id, + ); final teams = await _getMatchTeams(matchId: row.id); return Match( id: row.id, @@ -447,40 +448,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return rowsAffected > 0; } - /// Replaces all players in a match with the provided list of players. - /// Removes all existing players from the match and adds the new players. - /// Also adds any new players to the player table if they don't exist. - Future updateMatchPlayers({ - required String matchId, - required List newPlayers, - }) async { - await db.transaction(() async { - // Remove all existing players from the match - final deleteQuery = delete(db.playerMatchTable) - ..where((p) => p.matchId.equals(matchId)); - await deleteQuery.go(); - - // Add new players to the player table if they don't exist - await Future.wait( - newPlayers.map((player) async { - if (!await db.playerDao.playerExists(playerId: player.id)) { - await db.playerDao.addPlayer(player: player); - } - }), - ); - - // Add the new players to the match - await Future.wait( - newPlayers.map( - (player) => db.playerMatchDao.addPlayerToMatch( - matchId: matchId, - playerId: player.id, - ), - ), - ); - }); - } - /* Delete */ /// Deletes the match with the given [matchId] from the database. diff --git a/lib/data/dao/player_match_dao.dart b/lib/data/dao/player_match_dao.dart index b467a1b..1c9d0dd 100644 --- a/lib/data/dao/player_match_dao.dart +++ b/lib/data/dao/player_match_dao.dart @@ -11,14 +11,16 @@ class PlayerMatchDao extends DatabaseAccessor with _$PlayerMatchDaoMixin { PlayerMatchDao(super.db); + /* Create */ + /// Associates a player with a match by inserting a record into the /// [PlayerMatchTable]. Optionally associates with a team and sets initial score. - Future addPlayerToMatch({ + Future addPlayerToMatch({ required String matchId, required String playerId, String? teamId, }) async { - await into(playerMatchTable).insert( + final rowsAffected = await into(playerMatchTable).insert( PlayerMatchTableCompanion.insert( playerId: playerId, matchId: matchId, @@ -26,42 +28,14 @@ class PlayerMatchDao extends DatabaseAccessor ), mode: InsertMode.insertOrReplace, ); - } - - /// Retrieves a list of [Player]s associated with the given [matchId]. - /// Returns null if no players are found. - Future?> getPlayersOfMatch({required String matchId}) async { - final result = await (select( - playerMatchTable, - )..where((p) => p.matchId.equals(matchId))).get(); - - if (result.isEmpty) return null; - - final futures = result.map( - (row) => db.playerDao.getPlayerById(playerId: row.playerId), - ); - final players = await Future.wait(futures); - return players; - } - - /// Updates the team for a player in a match. - /// Returns `true` if the update was successful, otherwise `false`. - Future updatePlayerTeam({ - required String matchId, - required String playerId, - required String? teamId, - }) async { - final rowsAffected = - await (update(playerMatchTable)..where( - (p) => p.matchId.equals(matchId) & p.playerId.equals(playerId), - )) - .write(PlayerMatchTableCompanion(teamId: Value(teamId))); return rowsAffected > 0; } + /* Read */ + /// Checks if there are any players associated with the given [matchId]. /// Returns `true` if there are players, otherwise `false`. - Future matchHasPlayers({required String matchId}) async { + Future hasMatchPlayers({required String matchId}) async { final count = await (selectOnly(playerMatchTable) ..where(playerMatchTable.matchId.equals(matchId)) @@ -87,32 +61,80 @@ class PlayerMatchDao extends DatabaseAccessor return (count ?? 0) > 0; } - /// Removes the association of a player with a match by deleting the record - /// from the [PlayerMatchTable]. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future removePlayerFromMatch({ + /// Retrieves a list of [Player]s associated with the given [matchId]. + /// Returns empty list if no players are found. + Future> getPlayersOfMatch({required String matchId}) async { + final result = await (select( + playerMatchTable, + )..where((p) => p.matchId.equals(matchId))).get(); + + if (result.isEmpty) return []; + + final futures = result.map( + (row) => db.playerDao.getPlayerById(playerId: row.playerId), + ); + final players = await Future.wait(futures); + return players; + } + + /// Retrieves a list of [Player]s associated with a specific team in a match. + /// Returns empty list if no players are found for the team in the match. + Future> getPlayersOfTeamInMatch({ + required String matchId, + required String teamId, + }) async { + final result = + await (select(playerMatchTable) + ..where((p) => p.matchId.equals(matchId)) + ..where((p) => p.teamId.equals(teamId))) + .get(); + + if (result.isEmpty) return []; + + final futures = result.map( + (row) => db.playerDao.getPlayerById(playerId: row.playerId), + ); + final players = await Future.wait(futures); + return players; + } + + /* Updated */ + + /// Updates the team for a player in a match. + /// Returns `true` if the update was successful, otherwise `false`. + Future updatePlayersTeam({ required String matchId, required String playerId, + required String? teamId, }) async { - final query = delete(playerMatchTable) - ..where((pg) => pg.matchId.equals(matchId)) - ..where((pg) => pg.playerId.equals(playerId)); - final rowsAffected = await query.go(); + final rowsAffected = + await (update(playerMatchTable)..where( + (p) => p.matchId.equals(matchId) & p.playerId.equals(playerId), + )) + .write(PlayerMatchTableCompanion(teamId: Value(teamId))); return rowsAffected > 0; } /// Updates the players associated with a match based on the provided /// [newPlayer] list. It adds new players and removes players that are no /// longer associated with the match. - Future updatePlayersFromMatch({ + Future updateMatchPlayers({ required String matchId, required List newPlayer, }) async { + if (newPlayer.isEmpty) return false; + final currentPlayers = await getPlayersOfMatch(matchId: matchId); // Create sets of player IDs for easy comparison - final currentPlayerIds = currentPlayers?.map((p) => p.id).toSet() ?? {}; + final currentPlayerIds = currentPlayers.map((p) => p.id).toSet(); final newPlayerIdsSet = newPlayer.map((p) => p.id).toSet(); + // Are the current and new player identical? + if (currentPlayerIds.containsAll(newPlayerIdsSet) && + newPlayerIdsSet.containsAll(currentPlayerIds)) { + return false; + } + // Determine players to add and remove final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds); final playersToRemove = currentPlayerIds.difference(newPlayerIdsSet); @@ -147,22 +169,22 @@ class PlayerMatchDao extends DatabaseAccessor ); } }); + return true; } - /// Retrieves all players in a specific team for a match. - Future> getPlayersInTeam({ + /* Delete */ + + /// Removes the association of a player with a match by deleting the record + /// from the [PlayerMatchTable]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future removePlayerFromMatch({ required String matchId, - required String teamId, + required String playerId, }) async { - final result = await (select( - playerMatchTable, - )..where((p) => p.matchId.equals(matchId) & p.teamId.equals(teamId))).get(); - - if (result.isEmpty) return []; - - final futures = result.map( - (row) => db.playerDao.getPlayerById(playerId: row.playerId), - ); - return Future.wait(futures); + final query = delete(playerMatchTable) + ..where((pg) => pg.matchId.equals(matchId)) + ..where((pg) => pg.playerId.equals(playerId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; } } diff --git a/test/db_tests/relationships/player_match_test.dart b/test/db_tests/relationships/player_match_test.dart index 92601f0..2cc5185 100644 --- a/test/db_tests/relationships/player_match_test.dart +++ b/test/db_tests/relationships/player_match_test.dart @@ -5,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/game.dart'; -import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/team.dart'; @@ -17,11 +16,8 @@ void main() { late Player testPlayer3; late Player testPlayer4; late Player testPlayer5; - late Player testPlayer6; - late Group testGroup; late Game testGame; - late Match testMatchOnlyGroup; - late Match testMatchOnlyPlayers; + late Match testMatch1; late Team testTeam1; late Team testTeam2; final fixedDate = DateTime(2025, 11, 19, 00, 11, 23); @@ -42,12 +38,6 @@ void main() { testPlayer3 = Player(name: 'Charlie'); testPlayer4 = Player(name: 'Diana'); testPlayer5 = Player(name: 'Eve'); - testPlayer6 = Player(name: 'Frank'); - testGroup = Group( - name: 'Test Group', - description: '', - members: [testPlayer1, testPlayer2, testPlayer3], - ); testGame = Game( name: 'Test Game', ruleset: Ruleset.singleWinner, @@ -55,31 +45,17 @@ void main() { color: GameColor.blue, icon: '', ); - testMatchOnlyGroup = Match( - name: 'Test Match with Group', - game: testGame, - players: testGroup.members, - group: testGroup, - ); - testMatchOnlyPlayers = Match( + testMatch1 = Match( name: 'Test Match with Players', game: testGame, - players: [testPlayer4, testPlayer5, testPlayer6], + players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]); testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]); }); await database.playerDao.addPlayersAsList( - players: [ - testPlayer1, - testPlayer2, - testPlayer3, - testPlayer4, - testPlayer5, - testPlayer6, - ], + players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); - await database.groupDao.addGroup(group: testGroup); await database.gameDao.addGame(game: testGame); }); tearDown(() async { @@ -87,603 +63,359 @@ void main() { }); group('Player-Match Tests', () { - test('Match has player works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.playerDao.addPlayer(player: testPlayer1); + group('CREATE', () { + test('addPlayerToMatch() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.playerDao.addPlayer(player: testPlayer1); - var matchHasPlayers = await database.playerMatchDao.matchHasPlayers( - matchId: testMatchOnlyGroup.id, - ); - - expect(matchHasPlayers, true); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - ); - - matchHasPlayers = await database.playerMatchDao.matchHasPlayers( - matchId: testMatchOnlyGroup.id, - ); - - expect(matchHasPlayers, true); - }); - - test('Adding a player to a match works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.playerDao.addPlayer(player: testPlayer5); - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer5.id, - ); - - var playerAdded = await database.playerMatchDao.isPlayerInMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer5.id, - ); - - expect(playerAdded, true); - - playerAdded = await database.playerMatchDao.isPlayerInMatch( - matchId: testMatchOnlyGroup.id, - playerId: '', - ); - - expect(playerAdded, false); - }); - - test('Removing player from match works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - - final playerToRemove = testMatchOnlyPlayers.players[0]; - - final removed = await database.playerMatchDao.removePlayerFromMatch( - playerId: playerToRemove.id, - matchId: testMatchOnlyPlayers.id, - ); - expect(removed, true); - - final result = await database.matchDao.getMatchById( - matchId: testMatchOnlyPlayers.id, - ); - expect(result.players.length, testMatchOnlyPlayers.players.length - 1); - - final playerExists = result.players.any((p) => p.id == playerToRemove.id); - expect(playerExists, false); - }); - - test('Retrieving players of a match works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - final players = - await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, - ) ?? - []; - - for (int i = 0; i < players.length; i++) { - expect(players[i].id, testMatchOnlyPlayers.players[i].id); - expect(players[i].name, testMatchOnlyPlayers.players[i].name); - expect(players[i].createdAt, testMatchOnlyPlayers.players[i].createdAt); - } - }); - - test('Updating the match players works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - - final newPlayers = [testPlayer1, testPlayer2, testPlayer4]; - await database.playerDao.addPlayersAsList(players: newPlayers); - - // First, remove all existing players - final existingPlayers = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, - ); - - if (existingPlayers == null || existingPlayers.isEmpty) { - fail('Existing players should not be null or empty'); - } - - await database.playerMatchDao.updatePlayersFromMatch( - matchId: testMatchOnlyPlayers.id, - newPlayer: newPlayers, - ); - - final updatedPlayers = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, - ); - - if (updatedPlayers == null) { - fail('Updated players should not be null'); - } - - expect(updatedPlayers.length, newPlayers.length); - - /// Create a map of new players for easy lookup - final testPlayers = {for (var p in newPlayers) p.id: p}; - - /// Verify each updated player matches the new players - for (final player in updatedPlayers) { - final testPlayer = testPlayers[player.id]!; - - expect(player.id, testPlayer.id); - expect(player.name, testPlayer.name); - expect(player.createdAt, testPlayer.createdAt); - } - }); - - test( - 'Adding the same player to separate matches works correctly', - () async { - final playersList = [testPlayer1, testPlayer2, testPlayer3]; - final match1 = Match( - name: 'Match 1', - game: testGame, - players: playersList, - notes: '', + var added = await database.playerMatchDao.addPlayerToMatch( + matchId: testMatch1.id, + playerId: testPlayer1.id, ); - final match2 = Match( - name: 'Match 2', - game: testGame, - players: playersList, - notes: '', + expect(added, isTrue); + + added = await database.playerMatchDao.isPlayerInMatch( + matchId: testMatch1.id, + playerId: testPlayer1.id, ); + expect(added, isTrue); + }); - await Future.wait([ - database.matchDao.addMatch(match: match1), - database.matchDao.addMatch(match: match2), - ]); + test('addPlayerToMatch() with team works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.teamDao.addTeam(team: testTeam1, matchId: testMatch1.id); - 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()), - ); - }, - ); - - // Verifies that getPlayersOfMatch returns null for a non-existent match. - test('getPlayersOfMatch returns null for non-existent match', () async { - final players = await database.playerMatchDao.getPlayersOfMatch( - matchId: 'non-existent-match-id', - ); - - expect(players, isNull); - }); - - test('Adding player with teamId works correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam1.id, - ); - - final playersInTeam = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, - ); - - expect(playersInTeam.length, 1); - expect(playersInTeam[0].id, testPlayer1.id); - }); - - test('updatePlayerTeam updates team correctly', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); - await database.teamDao.addTeam(team: testTeam2); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam1.id, - ); - - // Update player's team - final updated = await database.playerMatchDao.updatePlayerTeam( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam2.id, - ); - - expect(updated, true); - - // Verify player is now in testTeam2 - final playersInTeam2 = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam2.id, - ); - - expect(playersInTeam2.length, 1); - expect(playersInTeam2[0].id, testPlayer1.id); - - // Verify player is no longer in testTeam1 - final playersInTeam1 = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, - ); - - expect(playersInTeam1.isEmpty, true); - }); - - test('updatePlayerTeam can remove player from team', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam1.id, - ); - - // Remove player from team by setting teamId to null - final updated = await database.playerMatchDao.updatePlayerTeam( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: null, - ); - - expect(updated, true); - - final playersInTeam = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, - ); - - expect(playersInTeam.isEmpty, true); - }); - - test( - 'updatePlayerTeam returns false for non-existent player-match', - () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - - final updated = await database.playerMatchDao.updatePlayerTeam( - matchId: testMatchOnlyGroup.id, - playerId: 'non-existent-player-id', + await database.playerMatchDao.addPlayerToMatch( + matchId: testMatch1.id, + playerId: testPlayer3.id, teamId: testTeam1.id, ); - expect(updated, false); - }, - ); + final playersInTeam = await database.playerMatchDao + .getPlayersOfTeamInMatch( + matchId: testMatch1.id, + teamId: testTeam1.id, + ); - // Verifies that getPlayersInTeam returns empty list for non-existent team. - test('getPlayersInTeam returns empty list for non-existent team', () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); + expect(playersInTeam, isNotEmpty); + expect(playersInTeam.length, 3); + }); - final players = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyPlayers.id, - teamId: 'non-existent-team-id', - ); + test('addPlayerToMatch() ignores duplicates', () async { + await database.matchDao.addMatch(match: testMatch1); - expect(players.isEmpty, true); - }); - - test('getPlayersInTeam returns all players of a team', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam1.id, - ); - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer2.id, - teamId: testTeam1.id, - ); - - final playersInTeam = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, - ); - expect(playersInTeam.length, 2); - final playerIds = playersInTeam.map((p) => p.id).toSet(); - expect(playerIds.contains(testPlayer1.id), true); - expect(playerIds.contains(testPlayer2.id), true); - }); - - test( - 'removePlayerFromMatch returns false for non-existent player', - () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - - final removed = await database.playerMatchDao.removePlayerFromMatch( - playerId: 'non-existent-player-id', - matchId: testMatchOnlyPlayers.id, + final isInMatch = await database.playerMatchDao.isPlayerInMatch( + matchId: testMatch1.id, + playerId: testPlayer5.id, ); + expect(isInMatch, isFalse); - expect(removed, false); - }, - ); - - test('Adding same player twice to same match is ignored', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - ); - - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - ); - - final players = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyGroup.id, - ); - - expect(players?.length, 3); - }); - - test( - 'updatePlayersFromMatch with empty list removes all players', - () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - - // Verify players exist initially var players = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, + matchId: testMatch1.id, ); - expect(players?.length, 3); + expect(players.length, testMatch1.players.length); - // Update with empty list - await database.playerMatchDao.updatePlayersFromMatch( - matchId: testMatchOnlyPlayers.id, + await database.playerMatchDao.addPlayerToMatch( + matchId: testMatch1.id, + playerId: testPlayer1.id, + ); + await database.playerMatchDao.addPlayerToMatch( + matchId: testMatch1.id, + playerId: testPlayer1.id, + ); + + players = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatch1.id, + ); + + expect(players.length, testMatch1.players.length + 1); + }); + }); + + group('READ', () { + test('hasMatchPlayers() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + var matchHasPlayers = await database.playerMatchDao.hasMatchPlayers( + matchId: testMatch1.id, + ); + expect(matchHasPlayers, isTrue); + }); + + test('hasMatchPlayers() returns false for non-existent match', () async { + final hasPlayers = await database.playerMatchDao.hasMatchPlayers( + matchId: 'non-existent-match-id', + ); + expect(hasPlayers, isFalse); + }); + + test('isPlayerInMatch() works correctly', () async { + final isInMatch = await database.playerMatchDao.isPlayerInMatch( + matchId: testMatch1.id, + playerId: testPlayer1.id, + ); + expect(isInMatch, isTrue); + }); + + test('isPlayerInMatch() returns false for non-existent match', () async { + final isInMatch = await database.playerMatchDao.isPlayerInMatch( + matchId: 'non-existent-match-id', + playerId: testPlayer1.id, + ); + expect(isInMatch, isFalse); + }); + + test('getPlayersOfMatch() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final players = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatch1.id, + ); + expect(players, isNotEmpty); + + for (int i = 0; i < players.length; i++) { + expect(players[i].id, testMatch1.players[i].id); + expect(players[i].name, testMatch1.players[i].name); + expect(players[i].createdAt, testMatch1.players[i].createdAt); + } + }); + + test( + 'getPlayersOfMatch() returns empty list for non-existent match', + () async { + final players = await database.playerMatchDao.getPlayersOfMatch( + matchId: 'non-existent-match-id', + ); + expect(players, isEmpty); + }, + ); + + test('getPlayersInTeam() works correctly', () async { + // Create a match with teams + final matchWithTeams = Match( + name: 'Match with teams', + game: testGame, + players: [], + teams: [testTeam1, testTeam2], + ); + await database.matchDao.addMatch(match: matchWithTeams); + + var playersInTeam = await database.playerMatchDao + .getPlayersOfTeamInMatch( + matchId: matchWithTeams.id, + teamId: testTeam1.id, + ); + + expect(playersInTeam, isNotEmpty); + expect(playersInTeam.length, 2); + + var playerIds = playersInTeam.map((p) => p.id).toSet(); + expect(playerIds.contains(testPlayer1.id), isTrue); + expect(playerIds.contains(testPlayer2.id), isTrue); + + playersInTeam = await database.playerMatchDao.getPlayersOfTeamInMatch( + matchId: matchWithTeams.id, + teamId: testTeam2.id, + ); + + expect(playersInTeam, isNotEmpty); + expect(playersInTeam.length, 2); + + playerIds = playersInTeam.map((p) => p.id).toSet(); + expect(playerIds.contains(testPlayer3.id), isTrue); + expect(playerIds.contains(testPlayer4.id), isTrue); + }); + + test( + 'getPlayersInTeam() returns empty list for non-existent match', + () async { + final players = await database.playerMatchDao.getPlayersOfTeamInMatch( + matchId: 'non-existent-match-id', + teamId: testTeam1.id, + ); + expect(players, isEmpty); + }, + ); + }); + + group('UPDATE', () { + test('updateMatchPlayers() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final newPlayers = [testPlayer1, testPlayer2, testPlayer4]; + await database.playerDao.addPlayersAsList(players: newPlayers); + + final existingPlayers = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatch1.id, + ); + expect(existingPlayers, isNotEmpty); + + await database.playerMatchDao.updateMatchPlayers( + matchId: testMatch1.id, + newPlayer: newPlayers, + ); + + final updatedPlayers = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatch1.id, + ); + expect(updatedPlayers, isNotEmpty); + expect(updatedPlayers.length, newPlayers.length); + + /// Create a map of new players for easy lookup + final testPlayers = {for (var p in newPlayers) p.id: p}; + + for (final player in updatedPlayers) { + final testPlayer = testPlayers[player.id]!; + + expect(player.id, testPlayer.id); + expect(player.name, testPlayer.name); + expect(player.createdAt, testPlayer.createdAt); + } + }); + + test('updatePlayersTeam() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.teamDao.addTeam(team: testTeam1, matchId: testMatch1.id); + await database.teamDao.addTeam(team: testTeam2, matchId: testMatch1.id); + + await database.playerMatchDao.addPlayerToMatch( + matchId: testMatch1.id, + playerId: testPlayer1.id, + teamId: testTeam1.id, + ); + + final updated = await database.playerMatchDao.updatePlayersTeam( + matchId: testMatch1.id, + playerId: testPlayer1.id, + teamId: testTeam2.id, + ); + + expect(updated, isTrue); + + final playersInTeam2 = await database.playerMatchDao + .getPlayersOfTeamInMatch( + matchId: testMatch1.id, + teamId: testTeam2.id, + ); + expect(playersInTeam2, isNotEmpty); + expect(playersInTeam2.length, 3); + + final playersInTeam1 = await database.playerMatchDao + .getPlayersOfTeamInMatch( + matchId: testMatch1.id, + teamId: testTeam1.id, + ); + + expect(playersInTeam1, isNotEmpty); + expect(playersInTeam1.length, 1); + expect(playersInTeam1[0].id, testPlayer2.id); + }); + + test( + 'updatePlayersTeam() returns false for non-existent player-match', + () async { + await database.matchDao.addMatch(match: testMatch1); + + final updated = await database.playerMatchDao.updatePlayersTeam( + matchId: testMatch1.id, + playerId: 'non-existent-player-id', + teamId: testTeam1.id, + ); + expect(updated, isFalse); + }, + ); + + test('updateMatchPlayers() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + var matchPlayers = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(matchPlayers.players.length, testMatch1.players.length); + + final newPlayersList = [testPlayer1, testPlayer2]; + await database.playerMatchDao.updateMatchPlayers( + matchId: testMatch1.id, + newPlayer: newPlayersList, + ); + + matchPlayers = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + + expect(matchPlayers.players.length, 2); + expect(matchPlayers.players.any((p) => p.id == testPlayer1.id), isTrue); + expect(matchPlayers.players.any((p) => p.id == testPlayer2.id), isTrue); + }); + + test('updateMatchPlayers() with same players makes is ignored', () async { + await database.matchDao.addMatch(match: testMatch1); + + final originalPlayers = testMatch1.players; + + final updated = await database.playerMatchDao.updateMatchPlayers( + matchId: testMatch1.id, + newPlayer: originalPlayers, + ); + expect(updated, isFalse); + + final players = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatch1.id, + ); + + expect(players.length, originalPlayers.length); + final playerIds = players.map((p) => p.id).toSet(); + for (final originalPlayer in originalPlayers) { + expect(playerIds.contains(originalPlayer.id), isTrue); + } + }); + + test('updateMatchPlayers() with empty list returns false', () async { + await database.matchDao.addMatch(match: testMatch1); + final updated = await database.playerMatchDao.updateMatchPlayers( + matchId: testMatch1.id, newPlayer: [], ); - // Verify all players are removed - players = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, + expect(updated, isFalse); + }); + }); + + group('DELETE', () { + test('removePlayerFromMatch() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final playerToRemove = testMatch1.players[0]; + + final removed = await database.playerMatchDao.removePlayerFromMatch( + playerId: playerToRemove.id, + matchId: testMatch1.id, ); - expect(players, isNull); - }, - ); + expect(removed, isTrue); - test('updatePlayersFromMatch with same players makes no changes', () async { - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - - final originalPlayers = [testPlayer4, testPlayer5, testPlayer6]; - - await database.playerMatchDao.updatePlayersFromMatch( - matchId: testMatchOnlyPlayers.id, - newPlayer: originalPlayers, - ); - - final players = await database.playerMatchDao.getPlayersOfMatch( - matchId: testMatchOnlyPlayers.id, - ); - - expect(players?.length, originalPlayers.length); - final playerIds = players!.map((p) => p.id).toSet(); - for (final originalPlayer in originalPlayers) { - expect(playerIds.contains(originalPlayer.id), true); - } - }); - - test('matchHasPlayers returns false for non-existent match', () async { - final hasPlayers = await database.playerMatchDao.matchHasPlayers( - matchId: 'non-existent-match-id', - ); - - expect(hasPlayers, false); - }); - - test('isPlayerInMatch returns false for non-existent match', () async { - final isInMatch = await database.playerMatchDao.isPlayerInMatch( - matchId: 'non-existent-match-id', - playerId: testPlayer1.id, - ); - - expect(isInMatch, false); - }); - - // Verifies that getPlayersInTeam returns empty list for non-existent match. - test( - 'getPlayersInTeam returns empty list for non-existent match', - () async { - await database.teamDao.addTeam(team: testTeam1); - - final players = await database.playerMatchDao.getPlayersInTeam( - matchId: 'non-existent-match-id', - teamId: testTeam1.id, + final result = await database.matchDao.getMatchById( + matchId: testMatch1.id, ); + expect(result.players.length, testMatch1.players.length - 1); - expect(players.isEmpty, true); - }, - ); - - // Verifies that players in different teams within the same match are returned correctly. - test('Players in different teams within same match are separate', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); - await database.teamDao.addTeam(team: testTeam2); - - // Add players to different teams - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - teamId: testTeam1.id, - ); - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer2.id, - teamId: testTeam1.id, - ); - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer3.id, - teamId: testTeam2.id, - ); - - // Verify team 1 players - final playersInTeam1 = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, - ); - - expect(playersInTeam1.length, 2); - final team1Ids = playersInTeam1.map((p) => p.id).toSet(); - expect(team1Ids.contains(testPlayer1.id), true); - expect(team1Ids.contains(testPlayer2.id), true); - expect(team1Ids.contains(testPlayer3.id), false); - - // Verify team 2 players - final playersInTeam2 = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam2.id, - ); - expect(playersInTeam2.length, 1); - expect(playersInTeam2[0].id, testPlayer3.id); - }); - - // Verifies that removePlayerFromMatch does not affect other matches. - test('removePlayerFromMatch does not affect other matches', () async { - final playersList = [testPlayer1, testPlayer2]; - final match1 = Match( - name: 'Match 1', - game: testGame, - players: playersList, - ); - final match2 = Match( - name: 'Match 2', - game: testGame, - players: playersList, - ); - - await Future.wait([ - database.matchDao.addMatch(match: match1), - database.matchDao.addMatch(match: match2), - ]); - - // Remove player from match1 - final removed = await database.playerMatchDao.removePlayerFromMatch( - playerId: testPlayer1.id, - matchId: match1.id, - ); - expect(removed, true); - - // Verify player is removed from match1 - final isInMatch1 = await database.playerMatchDao.isPlayerInMatch( - matchId: match1.id, - playerId: testPlayer1.id, - ); - expect(isInMatch1, false); - - // Verify player still exists in match2 - final isInMatch2 = await database.playerMatchDao.isPlayerInMatch( - matchId: match2.id, - playerId: testPlayer1.id, - ); - expect(isInMatch2, true); - }); - - // Verifies that updatePlayersFromMatch on non-existent match fails with constraint error. - test( - 'updatePlayersFromMatch on non-existent match fails with foreign key constraint', - () async { - // Should throw due to foreign key constraint - match doesn't exist - await expectLater( - database.playerMatchDao.updatePlayersFromMatch( - matchId: 'non-existent-match-id', - newPlayer: [testPlayer1, testPlayer2], - ), - throwsA(anything), + final playerExists = result.players.any( + (p) => p.id == playerToRemove.id, ); - }, - ); + expect(playerExists, isFalse); + }); - // Verifies that a player can be in a match without being assigned to a team. - test('Player can exist in match without team assignment', () async { - await database.matchDao.addMatch(match: testMatchOnlyGroup); - await database.teamDao.addTeam(team: testTeam1); + test( + 'removePlayerFromMatch() returns false for non-existent player', + () async { + await database.matchDao.addMatch(match: testMatch1); - // Add player to match without team - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, + final removed = await database.playerMatchDao.removePlayerFromMatch( + playerId: 'non-existent-player-id', + matchId: testMatch1.id, + ); + + expect(removed, isFalse); + }, ); - - // Add another player to match with team - await database.playerMatchDao.addPlayerToMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer2.id, - teamId: testTeam1.id, - ); - - // Verify both players are in the match - final isPlayer1InMatch = await database.playerMatchDao.isPlayerInMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer1.id, - ); - final isPlayer2InMatch = await database.playerMatchDao.isPlayerInMatch( - matchId: testMatchOnlyGroup.id, - playerId: testPlayer2.id, - ); - - expect(isPlayer1InMatch, true); - expect(isPlayer2InMatch, true); - - // Verify only player2 is in the team - final playersInTeam = await database.playerMatchDao.getPlayersInTeam( - matchId: testMatchOnlyGroup.id, - teamId: testTeam1.id, - ); - - expect(playersInTeam.length, 1); - expect(playersInTeam[0].id, testPlayer2.id); - }); - - // Verifies that replaceMatchPlayers removes all existing players and replaces with new list. - test('replaceMatchPlayers replaces all match players correctly', () async { - // Create initial match with 3 players - await database.matchDao.addMatch(match: testMatchOnlyPlayers); - - // Verify initial players - var matchPlayers = await database.matchDao.getMatchById( - matchId: testMatchOnlyPlayers.id, - ); - expect(matchPlayers.players.length, 3); - - // Replace with new list containing 2 different players - final newPlayersList = [testPlayer1, testPlayer2]; - await database.matchDao.replaceMatchPlayers( - matchId: testMatchOnlyPlayers.id, - newPlayers: newPlayersList, - ); - - // Get updated match and verify players - matchPlayers = await database.matchDao.getMatchById( - matchId: testMatchOnlyPlayers.id, - ); - - expect(matchPlayers.players.length, 2); - expect(matchPlayers.players.any((p) => p.id == testPlayer1.id), true); - expect(matchPlayers.players.any((p) => p.id == testPlayer2.id), true); - expect(matchPlayers.players.any((p) => p.id == testPlayer4.id), false); - expect(matchPlayers.players.any((p) => p.id == testPlayer5.id), false); - expect(matchPlayers.players.any((p) => p.id == testPlayer6.id), false); }); }); } From ae3a8b496e8436800b7a3b81e0ddf2eae927a10a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 1 May 2026 17:22:20 +0200 Subject: [PATCH 011/127] Added copyWith(), == and hashCode overwrites for model classes --- lib/data/models/game.dart | 39 +++++++++++++++++++- lib/data/models/group.dart | 37 +++++++++++++++++++ lib/data/models/match.dart | 63 +++++++++++++++++++++++++++++--- lib/data/models/player.dart | 32 +++++++++++++++- lib/data/models/score_entry.dart | 20 ++++++++++ lib/data/models/team.dart | 37 +++++++++++++++++-- pubspec.yaml | 1 + 7 files changed, 215 insertions(+), 14 deletions(-) diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index 607db0a..c02f455 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -28,7 +28,43 @@ class Game { return 'Game{id: $id, name: $name, ruleset: $ruleset, description: $description, color: $color, icon: $icon}'; } - /// Creates a Game instance from a JSON object. + Game copyWith({ + String? id, + DateTime? createdAt, + String? name, + Ruleset? ruleset, + String? description, + GameColor? color, + String? icon, + }) { + return Game( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + name: name ?? this.name, + ruleset: ruleset ?? this.ruleset, + description: description ?? this.description, + color: color ?? this.color, + icon: icon ?? this.icon, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Game && + runtimeType == other.runtimeType && + id == other.id && + createdAt == other.createdAt && + name == other.name && + ruleset == other.ruleset && + description == other.description && + color == other.color && + icon == other.icon; + + @override + int get hashCode => + Object.hash(id, createdAt, name, ruleset, description, color, icon); + Game.fromJson(Map json) : id = json['id'], createdAt = DateTime.parse(json['createdAt']), @@ -41,7 +77,6 @@ class Game { color = GameColor.values.firstWhere((e) => e.name == json['color']), icon = json['icon']; - /// Converts the Game instance to a JSON object. Map toJson() => { 'id': id, 'createdAt': createdAt.toIso8601String(), diff --git a/lib/data/models/group.dart b/lib/data/models/group.dart index c684541..5c1515c 100644 --- a/lib/data/models/group.dart +++ b/lib/data/models/group.dart @@ -1,4 +1,5 @@ import 'package:clock/clock.dart'; +import 'package:collection/collection.dart'; import 'package:tallee/data/models/player.dart'; import 'package:uuid/uuid.dart'; @@ -24,6 +25,42 @@ class Group { return 'Group{id: $id, name: $name, description: $description, members: $members}'; } + Group copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + List? members, + }) { + return Group( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + members: members ?? this.members, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Group && + runtimeType == other.runtimeType && + id == other.id && + name == other.name && + description == other.description && + createdAt == other.createdAt && + const DeepCollectionEquality().equals(members, other.members); + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + const DeepCollectionEquality().hash(members), + ); + /// Creates a Group instance from a JSON object where the related [Player] /// objects are represented by their IDs. Group.fromJson(Map json) diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index b0b487c..9d14bb3 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -1,4 +1,5 @@ import 'package:clock/clock.dart'; +import 'package:collection/collection.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; @@ -39,9 +40,62 @@ class Match { return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, mvp: $mvp}'; } - /// Creates a Match instance from a JSON object where related objects are - /// represented by their IDs. Therefore, the game, group, and players are not - /// fully constructed here. + Match copyWith({ + String? id, + DateTime? createdAt, + DateTime? endedAt, + String? name, + Game? game, + Group? group, + List? players, + List? teams, + String? notes, + Map? scores, + }) { + return Match( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + endedAt: endedAt ?? this.endedAt, + name: name ?? this.name, + game: game ?? this.game, + group: group ?? this.group, + players: players ?? this.players, + teams: teams ?? this.teams, + notes: notes ?? this.notes, + scores: scores ?? this.scores, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Match && + runtimeType == other.runtimeType && + id == other.id && + createdAt == other.createdAt && + endedAt == other.endedAt && + name == other.name && + game == other.game && + group == other.group && + const DeepCollectionEquality().equals(players, other.players) && + const DeepCollectionEquality().equals(teams, other.teams) && + notes == other.notes && + const DeepCollectionEquality().equals(scores, other.scores); + + @override + int get hashCode => Object.hash( + id, + createdAt, + endedAt, + name, + game, + group, + const DeepCollectionEquality().hash(players), + const DeepCollectionEquality().hash(teams), + notes, + const DeepCollectionEquality().hash(scores), + ); + Match.fromJson(Map json) : id = json['id'], createdAt = DateTime.parse(json['createdAt']), @@ -71,9 +125,6 @@ class Match { : {}, notes = json['notes'] ?? ''; - /// Converts the Match instance to a JSON object. Related objects are - /// represented by their IDs, so the game, group, and players are not fully - /// serialized here. Map toJson() => { 'id': id, 'createdAt': createdAt.toIso8601String(), diff --git a/lib/data/models/player.dart b/lib/data/models/player.dart index 12d17f0..3e42fb9 100644 --- a/lib/data/models/player.dart +++ b/lib/data/models/player.dart @@ -23,7 +23,36 @@ class Player { return 'Player{id: $id, createdAt: $createdAt, name: $name, nameCount: $nameCount, description: $description}'; } - /// Creates a Player instance from a JSON object. + Player copyWith({ + String? id, + DateTime? createdAt, + String? name, + int? nameCount, + String? description, + }) { + return Player( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + name: name ?? this.name, + nameCount: nameCount ?? this.nameCount, + description: description ?? this.description, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Player && + runtimeType == other.runtimeType && + id == other.id && + createdAt == other.createdAt && + name == other.name && + nameCount == other.nameCount && + description == other.description; + + @override + int get hashCode => Object.hash(id, createdAt, name, nameCount, description); + Player.fromJson(Map json) : id = json['id'], createdAt = DateTime.parse(json['createdAt']), @@ -31,7 +60,6 @@ class Player { nameCount = 0, description = json['description']; - /// Converts the Player instance to a JSON object. Map toJson() => { 'id': id, 'createdAt': createdAt.toIso8601String(), diff --git a/lib/data/models/score_entry.dart b/lib/data/models/score_entry.dart index f9c5ff0..a88f304 100644 --- a/lib/data/models/score_entry.dart +++ b/lib/data/models/score_entry.dart @@ -10,6 +10,26 @@ class ScoreEntry { return 'ScoreEntry{roundNumber: $roundNumber, score: $score, change: $change}'; } + ScoreEntry copyWith({int? roundNumber, int? score, int? change}) { + return ScoreEntry( + roundNumber: roundNumber ?? this.roundNumber, + score: score ?? this.score, + change: change ?? this.change, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ScoreEntry && + runtimeType == other.runtimeType && + roundNumber == other.roundNumber && + score == other.score && + change == other.change; + + @override + int get hashCode => Object.hash(roundNumber, score, change); + ScoreEntry.fromJson(Map json) : roundNumber = json['roundNumber'], score = json['score'], diff --git a/lib/data/models/team.dart b/lib/data/models/team.dart index f5941c4..b16e2ec 100644 --- a/lib/data/models/team.dart +++ b/lib/data/models/team.dart @@ -1,4 +1,5 @@ import 'package:clock/clock.dart'; +import 'package:collection/collection.dart'; import 'package:tallee/data/models/player.dart'; import 'package:uuid/uuid.dart'; @@ -21,16 +22,44 @@ class Team { return 'Team{id: $id, name: $name, members: $members}'; } - /// Creates a Team instance from a JSON object (memberIds format). - /// Player objects are reconstructed from memberIds by the DataTransferService. + Team copyWith({ + String? id, + String? name, + DateTime? createdAt, + List? members, + }) { + return Team( + id: id ?? this.id, + name: name ?? this.name, + createdAt: createdAt ?? this.createdAt, + members: members ?? this.members, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Team && + runtimeType == other.runtimeType && + id == other.id && + name == other.name && + createdAt == other.createdAt && + const DeepCollectionEquality().equals(members, other.members); + + @override + int get hashCode => Object.hash( + id, + name, + createdAt, + const DeepCollectionEquality().hash(members), + ); + Team.fromJson(Map json) : id = json['id'], name = json['name'], createdAt = DateTime.parse(json['createdAt']), members = []; // Populated during import via DataTransferService - /// Converts the Team instance to a JSON object. Related objects are - /// represented by their IDs. Map toJson() => { 'id': id, 'name': name, diff --git a/pubspec.yaml b/pubspec.yaml index 363ea7f..d5ed039 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: dependencies: clock: ^1.1.2 + collection: ^1.19.1 cupertino_icons: ^1.0.6 drift: ^2.27.0 drift_flutter: ^0.2.4 From 0f2e3493c4b0297787ba1ade2eb6f36b4b01e9de Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 1 May 2026 17:46:47 +0200 Subject: [PATCH 012/127] Updated player-group dao + tests --- lib/data/dao/group_dao.dart | 44 -- lib/data/dao/player_group_dao.dart | 79 +++- .../group_view/create_group_view.dart | 2 +- test/db_tests/aggregates/group_test.dart | 6 +- .../relationships/player_group_test.dart | 431 +++++++----------- 5 files changed, 228 insertions(+), 334 deletions(-) diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 552b138..d1029d0 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -238,48 +238,4 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { ); return rowsAffected > 0; } - - /// Replaces all players in a group with the provided list of players. - /// Removes all existing players from the group and adds the new players. - /// Also adds any new players to the player table if they don't exist. - /// Returns `true` if the group exists and players were replaced, `false` otherwise. - Future replaceGroupPlayers({ - required String groupId, - required List newPlayers, - }) async { - if (!await groupExists(groupId: groupId)) return false; - - await db.transaction(() async { - // Remove all existing players from the group - final deleteQuery = delete(db.playerGroupTable) - ..where((p) => p.groupId.equals(groupId)); - await deleteQuery.go(); - - // Add new players to the player table if they don't exist - await Future.wait( - newPlayers.map((player) async { - if (!await db.playerDao.playerExists(playerId: player.id)) { - await db.playerDao.addPlayer(player: player); - } - }), - ); - - // Add the new players to the group - await db.batch( - (b) => b.insertAll( - db.playerGroupTable, - newPlayers - .map( - (player) => PlayerGroupTableCompanion.insert( - playerId: player.id, - groupId: groupId, - ), - ) - .toList(), - mode: InsertMode.insertOrReplace, - ), - ); - }); - return true; - } } diff --git a/lib/data/dao/player_group_dao.dart b/lib/data/dao/player_group_dao.dart index 9411486..48c5653 100644 --- a/lib/data/dao/player_group_dao.dart +++ b/lib/data/dao/player_group_dao.dart @@ -11,8 +11,7 @@ class PlayerGroupDao extends DatabaseAccessor with _$PlayerGroupDaoMixin { PlayerGroupDao(super.db); - /// No need for a groupHasPlayers method since the members attribute is - /// not nullable + /* Create */ /// Adds a [player] to a group with the given [groupId]. /// If the player is already in the group, no action is taken. @@ -33,10 +32,11 @@ class PlayerGroupDao extends DatabaseAccessor await into(playerGroupTable).insert( PlayerGroupTableCompanion.insert(playerId: player.id, groupId: groupId), ); - return true; } + /* Read */ + /// Retrieves all players belonging to a specific group by [groupId]. Future> getPlayersOfGroup({required String groupId}) async { final query = select(playerGroupTable) @@ -53,18 +53,6 @@ class PlayerGroupDao extends DatabaseAccessor return groupMembers; } - /// Removes a player from a group based on [playerId] and [groupId]. - /// Returns `true` if more than 0 rows were affected, otherwise `false`. - Future removePlayerFromGroup({ - required String playerId, - required String groupId, - }) async { - final query = delete(playerGroupTable) - ..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId)); - final rowsAffected = await query.go(); - return rowsAffected > 0; - } - /// Checks if a player with [playerId] is in the group with [groupId]. /// Returns `true` if the player is in the group, otherwise `false`. Future isPlayerInGroup({ @@ -76,4 +64,65 @@ class PlayerGroupDao extends DatabaseAccessor final result = await query.getSingleOrNull(); return result != null; } + + /* Update */ + + /// Replaces all players in a group with the provided list of players. + /// Removes all existing players from the group and adds the new players. + /// Also adds any new players to the player table if they don't exist. + /// Returns `true` if the group exists and players were replaced, `false` otherwise. + Future replaceGroupPlayers({ + required String groupId, + required List newPlayers, + }) async { + if (!await db.groupDao.groupExists(groupId: groupId)) return false; + if (newPlayers.isEmpty) return false; + + await db.transaction(() async { + // Remove all existing players from the group + final deleteQuery = delete(db.playerGroupTable) + ..where((p) => p.groupId.equals(groupId)); + await deleteQuery.go(); + + // Add new players to the player table if they don't exist + await Future.wait( + newPlayers.map((player) async { + if (!await db.playerDao.playerExists(playerId: player.id)) { + await db.playerDao.addPlayer(player: player); + } + }), + ); + + // Add the new players to the group + await db.batch( + (b) => b.insertAll( + db.playerGroupTable, + newPlayers + .map( + (player) => PlayerGroupTableCompanion.insert( + playerId: player.id, + groupId: groupId, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + }); + return true; + } + + /* Delete */ + + /// Removes a player from a group based on [playerId] and [groupId]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future removePlayerFromGroup({ + required String playerId, + required String groupId, + }) async { + final query = delete(playerGroupTable) + ..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } } diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index f88e2db..593499e 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -177,7 +177,7 @@ class _CreateGroupViewState extends State { } if (widget.groupToEdit!.members != selectedPlayers) { - successfullMemberChange = await db.groupDao.replaceGroupPlayers( + successfullMemberChange = await db.playerGroupDao.replaceGroupPlayers( groupId: widget.groupToEdit!.id, newPlayers: selectedPlayers, ); diff --git a/test/db_tests/aggregates/group_test.dart b/test/db_tests/aggregates/group_test.dart index 9040daf..786a260 100644 --- a/test/db_tests/aggregates/group_test.dart +++ b/test/db_tests/aggregates/group_test.dart @@ -306,7 +306,7 @@ void main() { ); final newPlayers = [testPlayer2, testPlayer4]; - final replaced = await database.groupDao.replaceGroupPlayers( + final replaced = await database.playerGroupDao.replaceGroupPlayers( groupId: testGroup1.id, newPlayers: newPlayers, ); @@ -332,7 +332,7 @@ void main() { ); expect(initialGroup.members.length, 3); - final replaced = await database.groupDao.replaceGroupPlayers( + final replaced = await database.playerGroupDao.replaceGroupPlayers( groupId: testGroup1.id, newPlayers: [], ); @@ -348,7 +348,7 @@ void main() { test( 'replaceGroupPlayers() returns false for non-existent group', () async { - final replaced = await database.groupDao.replaceGroupPlayers( + final replaced = await database.playerGroupDao.replaceGroupPlayers( groupId: 'non-existent-id', newPlayers: [testPlayer1], ); diff --git a/test/db_tests/relationships/player_group_test.dart b/test/db_tests/relationships/player_group_test.dart index f687b1c..42d083b 100644 --- a/test/db_tests/relationships/player_group_test.dart +++ b/test/db_tests/relationships/player_group_test.dart @@ -42,189 +42,162 @@ void main() { }); group('Player-Group Tests', () { - // Verifies that a player can be added to an existing group and isPlayerInGroup returns true. - test('Adding a player to a group works correctly', () async { - await database.groupDao.addGroup(group: testGroup); - await database.playerDao.addPlayer(player: testPlayer4); - await database.playerGroupDao.addPlayerToGroup( - groupId: testGroup.id, - player: testPlayer4, - ); - - var playerAdded = await database.playerGroupDao.isPlayerInGroup( - groupId: testGroup.id, - playerId: testPlayer4.id, - ); - - expect(playerAdded, true); - - playerAdded = await database.playerGroupDao.isPlayerInGroup( - groupId: testGroup.id, - playerId: '', - ); - - expect(playerAdded, false); - }); - - // Verifies that a player can be removed from a group and the group's member count decreases. - test('Removing player from group works correctly', () async { - await database.groupDao.addGroup(group: testGroup); - - final playerToRemove = testGroup.members[0]; - - final removed = await database.playerGroupDao.removePlayerFromGroup( - playerId: playerToRemove.id, - groupId: testGroup.id, - ); - expect(removed, true); - - final result = await database.groupDao.getGroupById( - groupId: testGroup.id, - ); - expect(result.members.length, testGroup.members.length - 1); - - final playerExists = result.members.any((p) => p.id == playerToRemove.id); - expect(playerExists, false); - }); - - // Verifies that getPlayersOfGroup returns all members of a group with correct data. - test('Retrieving players of a group works correctly', () async { - await database.groupDao.addGroup(group: testGroup); - final players = await database.playerGroupDao.getPlayersOfGroup( - groupId: testGroup.id, - ); - - for (int i = 0; i < players.length; i++) { - expect(players[i].id, testGroup.members[i].id); - expect(players[i].name, testGroup.members[i].name); - expect(players[i].createdAt, testGroup.members[i].createdAt); - } - }); - - // Verifies that isPlayerInGroup returns false for non-existent player. - test('isPlayerInGroup returns false for non-existent player', () async { - await database.groupDao.addGroup(group: testGroup); - - final result = await database.playerGroupDao.isPlayerInGroup( - playerId: 'non-existent-player-id', - groupId: testGroup.id, - ); - - expect(result, false); - }); - - // Verifies that isPlayerInGroup returns false for non-existent group. - test('isPlayerInGroup returns false for non-existent group', () async { - await database.playerDao.addPlayer(player: testPlayer1); - - final result = await database.playerGroupDao.isPlayerInGroup( - playerId: testPlayer1.id, - groupId: 'non-existent-group-id', - ); - - expect(result, false); - }); - - // Verifies that addPlayerToGroup returns false when player already in group. - test( - 'addPlayerToGroup returns false when player already in group', - () async { + group('CREATE', () { + test('addPlayerToGroup() works correctly', () async { await database.groupDao.addGroup(group: testGroup); - - // testPlayer1 is already in testGroup via group creation - final result = await database.playerGroupDao.addPlayerToGroup( - player: testPlayer1, - groupId: testGroup.id, - ); - - expect(result, false); - }, - ); - - // Verifies that addPlayerToGroup adds player to player table if not exists. - test( - 'addPlayerToGroup adds player to player table if not exists', - () async { - await database.groupDao.addGroup(group: testGroup); - - // testPlayer4 is not in the database yet - var playerExists = await database.playerDao.playerExists( - playerId: testPlayer4.id, - ); - expect(playerExists, false); - + await database.playerDao.addPlayer(player: testPlayer4); await database.playerGroupDao.addPlayerToGroup( - player: testPlayer4, groupId: testGroup.id, + player: testPlayer4, ); - // Now player should exist in player table - playerExists = await database.playerDao.playerExists( + var playerAdded = await database.playerGroupDao.isPlayerInGroup( + groupId: testGroup.id, playerId: testPlayer4.id, ); - expect(playerExists, true); - }, - ); - // Verifies that removePlayerFromGroup returns false for non-existent player. - test( - 'removePlayerFromGroup returns false for non-existent player', - () async { + expect(playerAdded, true); + }); + + test( + 'addPlayerToGroup() returns false when player already in group', + () async { + await database.groupDao.addGroup(group: testGroup); + + final added = await database.playerGroupDao.addPlayerToGroup( + player: testPlayer1, + groupId: testGroup.id, + ); + expect(added, isFalse); + }, + ); + + test( + 'addPlayerToGroup() adds player to player table if not exists', + () async { + await database.groupDao.addGroup(group: testGroup); + + var playerExists = await database.playerDao.playerExists( + playerId: testPlayer4.id, + ); + expect(playerExists, isFalse); + + await database.playerGroupDao.addPlayerToGroup( + player: testPlayer4, + groupId: testGroup.id, + ); + + playerExists = await database.playerDao.playerExists( + playerId: testPlayer4.id, + ); + expect(playerExists, isTrue); + }, + ); + }); + group('READ', () { + test( + 'isPlayerInGroup() returns false for non-existent player or group', + () async { + await database.groupDao.addGroup(group: testGroup); + + var isInGroup = await database.playerGroupDao.isPlayerInGroup( + playerId: 'non-existent-player-id', + groupId: testGroup.id, + ); + expect(isInGroup, false); + + isInGroup = await database.playerGroupDao.isPlayerInGroup( + playerId: testPlayer1.id, + groupId: 'non-existent-group-id', + ); + expect(isInGroup, false); + + isInGroup = await database.playerGroupDao.isPlayerInGroup( + playerId: 'non-existent-player-id', + groupId: 'non-existent-group-id', + ); + expect(isInGroup, false); + }, + ); + + test('getPlayersOfGroup() works correctly', () async { await database.groupDao.addGroup(group: testGroup); - - final result = await database.playerGroupDao.removePlayerFromGroup( - playerId: 'non-existent-player-id', + final players = await database.playerGroupDao.getPlayersOfGroup( groupId: testGroup.id, ); - expect(result, false); - }, - ); + for (int i = 0; i < players.length; i++) { + expect(players[i].id, testGroup.members[i].id); + expect(players[i].name, testGroup.members[i].name); + expect(players[i].createdAt, testGroup.members[i].createdAt); + } + }); - // Verifies that removePlayerFromGroup returns false for non-existent group. - test( - 'removePlayerFromGroup returns false for non-existent group', - () async { - await database.playerDao.addPlayer(player: testPlayer1); + test('getPlayersOfGroup() returns empty list for empty group', () async { + final emptyGroup = Group(name: 'Empty Group', members: []); + await database.groupDao.addGroup(group: emptyGroup); - final result = await database.playerGroupDao.removePlayerFromGroup( - playerId: testPlayer1.id, - groupId: 'non-existent-group-id', + final players = await database.playerGroupDao.getPlayersOfGroup( + groupId: emptyGroup.id, ); + expect(players, isEmpty); + }); - expect(result, false); - }, - ); - - // Verifies that getPlayersOfGroup returns empty list for group with no members. - test('getPlayersOfGroup returns empty list for empty group', () async { - final emptyGroup = Group( - name: 'Empty Group', - description: '', - members: [], + test( + 'getPlayersOfGroup() returns empty list for non-existent group', + () async { + final players = await database.playerGroupDao.getPlayersOfGroup( + groupId: 'non-existent-group-id', + ); + expect(players, isEmpty); + }, ); - await database.groupDao.addGroup(group: emptyGroup); + }); + group('UPDATE', () { + test('replaceGroupPlayers() works correctly ', () async { + await database.groupDao.addGroup(group: testGroup); - final players = await database.playerGroupDao.getPlayersOfGroup( - groupId: emptyGroup.id, - ); + var groupMembers = await database.groupDao.getGroupById( + groupId: testGroup.id, + ); + expect(groupMembers.members.length, testGroup.members.length); - expect(players, isEmpty); + final newPlayersList = [testPlayer3, testPlayer4]; + + final replaced = await database.playerGroupDao.replaceGroupPlayers( + groupId: testGroup.id, + newPlayers: newPlayersList, + ); + expect(replaced, isTrue); + + groupMembers = await database.groupDao.getGroupById( + groupId: testGroup.id, + ); + expect(groupMembers.members.length, 2); + expect(groupMembers.members.any((p) => p.id == testPlayer3.id), true); + expect(groupMembers.members.any((p) => p.id == testPlayer4.id), true); + }); + }); + group('DELETE', () { + test('removePlayerFromGroup() works correctly', () async { + await database.groupDao.addGroup(group: testGroup); + + final removed = await database.playerGroupDao.removePlayerFromGroup( + playerId: testPlayer1.id, + groupId: testGroup.id, + ); + expect(removed, true); + + final result = await database.groupDao.getGroupById( + groupId: testGroup.id, + ); + expect(result.members.length, testGroup.members.length - 1); + + final playerExists = result.members.any((p) => p.id == testPlayer1.id); + expect(playerExists, false); + }); }); - // Verifies that getPlayersOfGroup returns empty list for non-existent group. - test( - 'getPlayersOfGroup returns empty list for non-existent group', - () async { - final players = await database.playerGroupDao.getPlayersOfGroup( - groupId: 'non-existent-group-id', - ); - - expect(players, isEmpty); - }, - ); - - // Verifies that removing all players from a group leaves the group empty. test('Removing all players from a group leaves group empty', () async { await database.groupDao.addGroup(group: testGroup); @@ -240,137 +213,53 @@ void main() { ); expect(players, isEmpty); - // Group should still exist final groupExists = await database.groupDao.groupExists( groupId: testGroup.id, ); expect(groupExists, true); }); - // Verifies that a player can be in multiple groups. - test('Player can be in multiple groups', () async { - final secondGroup = Group( - name: 'Second Group', - description: '', - members: [], - ); + test('removePlayerFromGroup() works correctly', () async { await database.groupDao.addGroup(group: testGroup); - await database.groupDao.addGroup(group: secondGroup); - // Add testPlayer1 to second group (already in testGroup) - await database.playerGroupDao.addPlayerToGroup( - player: testPlayer1, - groupId: secondGroup.id, - ); - - final inFirstGroup = await database.playerGroupDao.isPlayerInGroup( + var removed = await database.playerGroupDao.removePlayerFromGroup( playerId: testPlayer1.id, groupId: testGroup.id, ); - final inSecondGroup = await database.playerGroupDao.isPlayerInGroup( + expect(removed, true); + + removed = await database.playerGroupDao.removePlayerFromGroup( playerId: testPlayer1.id, - groupId: secondGroup.id, - ); - - expect(inFirstGroup, true); - expect(inSecondGroup, true); - }); - - // Verifies that removing player from one group doesn't affect other groups. - test( - 'Removing player from one group does not affect other groups', - () async { - final secondGroup = Group( - name: 'Second Group', - description: '', - members: [testPlayer1], - ); - await database.groupDao.addGroup(group: testGroup); - await database.groupDao.addGroup(group: secondGroup); - - // Remove testPlayer1 from testGroup - await database.playerGroupDao.removePlayerFromGroup( - playerId: testPlayer1.id, - groupId: testGroup.id, - ); - - final inFirstGroup = await database.playerGroupDao.isPlayerInGroup( - playerId: testPlayer1.id, - groupId: testGroup.id, - ); - final inSecondGroup = await database.playerGroupDao.isPlayerInGroup( - playerId: testPlayer1.id, - groupId: secondGroup.id, - ); - - expect(inFirstGroup, false); - expect(inSecondGroup, true); - }, - ); - - // Verifies that addPlayerToGroup returns true on successful addition. - test('addPlayerToGroup returns true on successful addition', () async { - await database.groupDao.addGroup(group: testGroup); - await database.playerDao.addPlayer(player: testPlayer4); - - final result = await database.playerGroupDao.addPlayerToGroup( - player: testPlayer4, groupId: testGroup.id, ); - - expect(result, true); + expect(removed, false); }); - // Verifies that removing the same player twice returns false on second attempt. test( - 'Removing same player twice returns false on second attempt', + 'removePlayerFromGroup() returns false for non-existent player or group', () async { await database.groupDao.addGroup(group: testGroup); - final firstRemoval = await database.playerGroupDao - .removePlayerFromGroup( - playerId: testPlayer1.id, - groupId: testGroup.id, - ); - expect(firstRemoval, true); + await database.groupDao.addGroup(group: testGroup); - final secondRemoval = await database.playerGroupDao - .removePlayerFromGroup( - playerId: testPlayer1.id, - groupId: testGroup.id, - ); - expect(secondRemoval, false); + var removed = await database.playerGroupDao.removePlayerFromGroup( + playerId: 'non-existent-player-id', + groupId: testGroup.id, + ); + expect(removed, false); + + removed = await database.playerGroupDao.removePlayerFromGroup( + playerId: testPlayer1.id, + groupId: 'non-existent-group-id', + ); + expect(removed, false); + + removed = await database.playerGroupDao.removePlayerFromGroup( + playerId: 'non-existent-player-id', + groupId: 'non-existent-group-id', + ); + expect(removed, false); }, ); - - // Verifies that replaceGroupPlayers removes all existing players and replaces with new list. - test('replaceGroupPlayers replaces all group members correctly', () async { - // Create initial group with 3 players - await database.groupDao.addGroup(group: testGroup); - - // Verify initial members - var groupMembers = await database.groupDao.getGroupById( - groupId: testGroup.id, - ); - expect(groupMembers.members.length, 3); - - // Replace with new list containing 2 different players - final newPlayersList = [testPlayer3, testPlayer4]; - await database.groupDao.replaceGroupPlayers( - groupId: testGroup.id, - newPlayers: newPlayersList, - ); - - // Get updated group and verify members - groupMembers = await database.groupDao.getGroupById( - groupId: testGroup.id, - ); - - expect(groupMembers.members.length, 2); - expect(groupMembers.members.any((p) => p.id == testPlayer3.id), true); - expect(groupMembers.members.any((p) => p.id == testPlayer4.id), true); - expect(groupMembers.members.any((p) => p.id == testPlayer1.id), false); - expect(groupMembers.members.any((p) => p.id == testPlayer2.id), false); - }); }); } From ab9a8d01939875dffe44f4740b91a6eeaafb898d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 1 May 2026 18:01:07 +0200 Subject: [PATCH 013/127] Fixed test issues --- lib/data/dao/match_dao.dart | 21 ++++- .../relationships/player_match_test.dart | 6 +- test/services/data_transfer_service_test.dart | 79 +++++++++++-------- 3 files changed, 67 insertions(+), 39 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 11cd5a2..f43909c 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -42,11 +42,24 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { await db.teamDao.addTeamsAsList(teams: match.teams!, matchId: match.id); } + // Collect all player IDs that are already in teams + final playersInTeams = {}; + if (match.teams != null) { + for (final team in match.teams!) { + for (final member in team.members) { + playersInTeams.add(member.id); + } + } + } + + // Add players that are not in teams for (final p in match.players) { - await db.playerMatchDao.addPlayerToMatch( - matchId: match.id, - playerId: p.id, - ); + if (!playersInTeams.contains(p.id)) { + await db.playerMatchDao.addPlayerToMatch( + matchId: match.id, + playerId: p.id, + ); + } } for (final pid in match.scores.keys) { diff --git a/test/db_tests/relationships/player_match_test.dart b/test/db_tests/relationships/player_match_test.dart index 2cc5185..85ccab9 100644 --- a/test/db_tests/relationships/player_match_test.dart +++ b/test/db_tests/relationships/player_match_test.dart @@ -103,6 +103,7 @@ void main() { test('addPlayerToMatch() ignores duplicates', () async { await database.matchDao.addMatch(match: testMatch1); + await database.playerDao.addPlayer(player: testPlayer5); final isInMatch = await database.playerMatchDao.isPlayerInMatch( matchId: testMatch1.id, @@ -117,11 +118,11 @@ void main() { await database.playerMatchDao.addPlayerToMatch( matchId: testMatch1.id, - playerId: testPlayer1.id, + playerId: testPlayer5.id, ); await database.playerMatchDao.addPlayerToMatch( matchId: testMatch1.id, - playerId: testPlayer1.id, + playerId: testPlayer5.id, ); players = await database.playerMatchDao.getPlayersOfMatch( @@ -149,6 +150,7 @@ void main() { }); test('isPlayerInMatch() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); final isInMatch = await database.playerMatchDao.isPlayerInMatch( matchId: testMatch1.id, playerId: testPlayer1.id, diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index 6aec390..781ee67 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -99,10 +99,8 @@ void main() { await database.playerDao.addPlayer(player: testPlayer1); await database.gameDao.addGame(game: testGame); await database.groupDao.addGroup(group: testGroup); - /* - await database.teamDao.addTeam(team: testTeam); -*/ await database.matchDao.addMatch(match: testMatch); + await database.teamDao.addTeam(team: testTeam, matchId: testMatch.id); var playerCount = await database.playerDao.getPlayerCount(); var gameCount = await database.gameDao.getGameCount(); @@ -154,19 +152,16 @@ void main() { expect(decoded.containsKey('players'), true); expect(decoded.containsKey('games'), true); expect(decoded.containsKey('groups'), true); - expect(decoded.containsKey('teams'), true); expect(decoded.containsKey('matches'), true); final players = decoded['players'] as List; final games = decoded['games'] as List; final groups = decoded['groups'] as List; - final teams = decoded['teams'] as List; final matches = decoded['matches'] as List; expect(players.length, 2); expect(games.length, 1); expect(groups.length, 1); - expect(teams.length, 1); expect(matches.length, 1); }); @@ -179,13 +174,11 @@ void main() { final players = decoded['players'] as List; final games = decoded['games'] as List; final groups = decoded['groups'] as List; - final teams = decoded['teams'] as List; final matches = decoded['matches'] as List; expect(players, isEmpty); expect(games, isEmpty); expect(groups, isEmpty); - expect(teams, isEmpty); expect(matches, isEmpty); }); }); @@ -247,31 +240,6 @@ void main() { expect(memberIds, containsAll([testPlayer1.id, testPlayer2.id])); }); - testWidgets('Team data is correct', (tester) async { - /* - await database.teamDao.addTeam(team: testTeam); -*/ - - final ctx = await getContext(tester); - final jsonString = await DataTransferService.getAppDataAsJson(ctx); - final decoded = json.decode(jsonString) as Map; - final teams = decoded['teams'] as List; - - expect(teams.length, 1); - - final teamData = teams[0] as Map; - - expect(teamData['id'], testTeam.id); - expect(teamData['name'], testTeam.name); - expect(teamData['memberIds'], isA()); - - // Note: In this system, teams don't have independent members. - // Team members are only tracked through matches via PlayerMatchTable. - // Therefore, memberIds will be empty for standalone teams. - final memberIds = teamData['memberIds'] as List; - expect(memberIds, isEmpty); - }); - testWidgets('Match data is correct', (tester) async { await database.playerDao.addPlayersAsList( players: [testPlayer1, testPlayer2], @@ -323,6 +291,51 @@ void main() { expect(player2Score.change, 15); }); + testWidgets('Match with teams is handled correctly', (tester) async { + final matchWithTeams = Match( + name: 'Match with Teams', + game: testGame, + players: [testPlayer1, testPlayer2], + teams: [testTeam], + notes: 'Team match', + ); + + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2], + ); + await database.gameDao.addGame(game: testGame); + await database.matchDao.addMatch(match: matchWithTeams); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final matches = decoded['matches'] as List; + + expect(matches.length, 1); + + final matchData = matches[0] as Map; + expect(matchData['id'], matchWithTeams.id); + expect(matchData['name'], matchWithTeams.name); + expect( + matchData['teams'], + isNotNull, + reason: 'teams should not be null', + ); + expect(matchData['teams'], isA()); + + final teamsInMatch = matchData['teams'] as List; + expect(teamsInMatch.length, 1); + + final teamData = teamsInMatch[0] as Map; + expect(teamData['id'], testTeam.id); + expect(teamData['name'], testTeam.name); + expect(teamData['memberIds'], isA()); + + final memberIds = teamData['memberIds'] as List; + expect(memberIds.length, 2); + expect(memberIds, containsAll([testPlayer1.id, testPlayer2.id])); + }); + testWidgets('Match without group is handled correctly', (tester) async { final matchWithoutGroup = Match( name: 'No Group Match', From 078daeffc9da7c6e7ac104b38e4d82d1a05d5ea4 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 1 May 2026 18:06:09 +0200 Subject: [PATCH 014/127] Fixed test issues --- test/db_tests/aggregates/group_test.dart | 60 +++++++++---------- test/db_tests/entities/game_test.dart | 38 ++++++------ test/db_tests/entities/player_test.dart | 28 ++++----- .../relationships/player_group_test.dart | 28 ++++----- test/db_tests/values/score_entry_test.dart | 32 +++++----- test/services/data_transfer_service_test.dart | 8 +-- 6 files changed, 94 insertions(+), 100 deletions(-) diff --git a/test/db_tests/aggregates/group_test.dart b/test/db_tests/aggregates/group_test.dart index 786a260..2ae1a3f 100644 --- a/test/db_tests/aggregates/group_test.dart +++ b/test/db_tests/aggregates/group_test.dart @@ -99,10 +99,10 @@ void main() { test('addGroup() returns false when group already exists', () async { final firstAdd = await database.groupDao.addGroup(group: testGroup1); - expect(firstAdd, true); + expect(firstAdd, isTrue); final secondAdd = await database.groupDao.addGroup(group: testGroup1); - expect(secondAdd, false); + expect(secondAdd, isFalse); final allGroups = await database.groupDao.getAllGroups(); expect(allGroups.length, 1); @@ -150,14 +150,14 @@ void main() { var groupExists = await database.groupDao.groupExists( groupId: testGroup1.id, ); - expect(groupExists, false); + expect(groupExists, isFalse); await database.groupDao.addGroup(group: testGroup1); groupExists = await database.groupDao.groupExists( groupId: testGroup1.id, ); - expect(groupExists, true); + expect(groupExists, isTrue); }); test('getGroupCount() works correctly', () async { @@ -165,19 +165,19 @@ void main() { expect(count, 0); var added = await database.groupDao.addGroup(group: testGroup1); - expect(added, true); + expect(added, isTrue); count = await database.groupDao.getGroupCount(); expect(count, 1); added = await database.groupDao.addGroup(group: testGroup2); - expect(added, true); + expect(added, isTrue); count = await database.groupDao.getGroupCount(); expect(count, 2); final removed = await database.groupDao.deleteGroup( groupId: testGroup1.id, ); - expect(removed, true); + expect(removed, isTrue); count = await database.groupDao.getGroupCount(); expect(count, 1); }); @@ -225,7 +225,7 @@ void main() { groupId: 'non-existent-id', newName: 'New name', ); - expect(updated, false); + expect(updated, isFalse); }); test('updateGroupDescription() works correctly', () async { @@ -236,7 +236,7 @@ void main() { groupId: testGroup1.id, newDescription: newDescription, ); - expect(updated, true); + expect(updated, isTrue); final group = await database.groupDao.getGroupById( groupId: testGroup1.id, @@ -251,7 +251,7 @@ void main() { groupId: 'non-existent-id', newDescription: 'New description', ); - expect(updated, false); + expect(updated, isFalse); }, ); @@ -288,21 +288,21 @@ void main() { .map((p) => p.id) .toList() .contains(testPlayer1.id), - true, + isTrue, ); expect( initialGroup.members .map((p) => p.id) .toList() .contains(testPlayer2.id), - true, + isTrue, ); expect( initialGroup.members .map((p) => p.id) .toList() .contains(testPlayer3.id), - true, + isTrue, ); final newPlayers = [testPlayer2, testPlayer4]; @@ -310,7 +310,7 @@ void main() { groupId: testGroup1.id, newPlayers: newPlayers, ); - expect(replaced, true); + expect(replaced, isTrue); final updatedGroup = await database.groupDao.getGroupById( groupId: testGroup1.id, @@ -318,31 +318,25 @@ void main() { expect(updatedGroup.members.length, 2); final memberIds = updatedGroup.members.map((p) => p.id).toList(); - expect(memberIds.contains(testPlayer2.id), true); - expect(memberIds.contains(testPlayer4.id), true); - expect(memberIds.contains(testPlayer1.id), false); - expect(memberIds.contains(testPlayer3.id), false); + expect(memberIds.contains(testPlayer2.id), isTrue); + expect(memberIds.contains(testPlayer4.id), isTrue); + expect(memberIds.contains(testPlayer1.id), isFalse); + expect(memberIds.contains(testPlayer3.id), isFalse); }); - test('replaceGroupPlayers() with empty list works correctly', () async { + test('replaceGroupPlayers() ignores empty list ', () async { await database.groupDao.addGroup(group: testGroup1); - final initialGroup = await database.groupDao.getGroupById( - groupId: testGroup1.id, - ); - expect(initialGroup.members.length, 3); - final replaced = await database.playerGroupDao.replaceGroupPlayers( groupId: testGroup1.id, newPlayers: [], ); - expect(replaced, true); + expect(replaced, isFalse); final updatedGroup = await database.groupDao.getGroupById( groupId: testGroup1.id, ); - expect(updatedGroup.members.length, 0); - expect(updatedGroup.members, isEmpty); + expect(updatedGroup.members.length, testGroup1.members.length); }); test( @@ -352,7 +346,7 @@ void main() { groupId: 'non-existent-id', newPlayers: [testPlayer1], ); - expect(replaced, false); + expect(replaced, isFalse); }, ); }); @@ -364,19 +358,19 @@ void main() { final groupDeleted = await database.groupDao.deleteGroup( groupId: testGroup1.id, ); - expect(groupDeleted, true); + expect(groupDeleted, isTrue); final groupExists = await database.groupDao.groupExists( groupId: testGroup1.id, ); - expect(groupExists, false); + expect(groupExists, isFalse); }); test('deleteGroup() returns false for non-existent group', () async { final deleted = await database.groupDao.deleteGroup( groupId: 'non-existent-id', ); - expect(deleted, false); + expect(deleted, isFalse); }); test('deleteAllGroups() works correctly', () async { @@ -388,7 +382,7 @@ void main() { expect(count, 2); final deleted = await database.groupDao.deleteAllGroups(); - expect(deleted, true); + expect(deleted, isTrue); count = await database.groupDao.getGroupCount(); expect(count, 0); @@ -396,7 +390,7 @@ void main() { test('deleteAllGroups() returns false when no groups exist', () async { final deleted = await database.groupDao.deleteAllGroups(); - expect(deleted, false); + expect(deleted, isFalse); }); }); diff --git a/test/db_tests/entities/game_test.dart b/test/db_tests/entities/game_test.dart index f3e99b6..a5af11a 100644 --- a/test/db_tests/entities/game_test.dart +++ b/test/db_tests/entities/game_test.dart @@ -58,7 +58,7 @@ void main() { group('CREATE', () { test('Adding and fetching a single game works correctly', () async { final added = await database.gameDao.addGame(game: testGame1); - expect(added, true); + expect(added, isTrue); final game = await database.gameDao.getGameById(gameId: testGame1.id); expect(game.id, testGame1.id); @@ -74,7 +74,7 @@ void main() { final added = await database.gameDao.addGamesAsList( games: [testGame1, testGame2, testGame3], ); - expect(added, true); + expect(added, isTrue); final allGames = await database.gameDao.getAllGames(); expect(allGames.length, 3); @@ -101,7 +101,7 @@ void main() { test('addGamesAsList() returns false for empty list', () async { final result = await database.gameDao.addGamesAsList(games: []); - expect(result, false); + expect(result, isFalse); final allGames = await database.gameDao.getAllGames(); expect(allGames.length, 0); @@ -111,7 +111,7 @@ void main() { final added = await database.gameDao.addGamesAsList( games: [testGame1, testGame2, testGame1], ); - expect(added, true); + expect(added, isTrue); final allGames = await database.gameDao.getAllGames(); expect(allGames.length, 2); @@ -160,11 +160,11 @@ void main() { test('gameExists() works correctly', () async { var exists = await database.gameDao.gameExists(gameId: testGame1.id); - expect(exists, false); + expect(exists, isFalse); await database.gameDao.addGame(game: testGame1); exists = await database.gameDao.gameExists(gameId: testGame1.id); - expect(exists, true); + expect(exists, isTrue); }); test('getAllGames() returns empty list when no games exist', () async { @@ -199,7 +199,7 @@ void main() { gameId: testGame1.id, newName: newName, ); - expect(updated, true); + expect(updated, isTrue); final updatedGame = await database.gameDao.getGameById( gameId: testGame1.id, @@ -212,7 +212,7 @@ void main() { gameId: 'non-existent-id', newName: 'New name', ); - expect(updated, false); + expect(updated, isFalse); final allGames = await database.gameDao.getAllGames(); expect(allGames, isEmpty); @@ -226,7 +226,7 @@ void main() { gameId: testGame1.id, newRuleset: ruleset, ); - expect(updated, true); + expect(updated, isTrue); final updatedGame = await database.gameDao.getGameById( gameId: testGame1.id, @@ -239,7 +239,7 @@ void main() { gameId: 'non-existent-id', newRuleset: Ruleset.lowestScore, ); - expect(updated, false); + expect(updated, isFalse); final allGames = await database.gameDao.getAllGames(); expect(allGames, isEmpty); @@ -253,7 +253,7 @@ void main() { gameId: testGame1.id, newDescription: newDescription, ); - expect(updated, true); + expect(updated, isTrue); final updatedGame = await database.gameDao.getGameById( gameId: testGame1.id, @@ -268,7 +268,7 @@ void main() { gameId: 'non-existent-id', newDescription: 'New description', ); - expect(updated, false); + expect(updated, isFalse); final allGames = await database.gameDao.getAllGames(); expect(allGames, isEmpty); @@ -294,7 +294,7 @@ void main() { gameId: 'non-existent-id', newColor: GameColor.green, ); - expect(updated, false); + expect(updated, isFalse); final allGames = await database.gameDao.getAllGames(); expect(allGames, isEmpty); @@ -308,7 +308,7 @@ void main() { gameId: testGame1.id, newIcon: newIcon, ); - expect(updated, true); + expect(updated, isTrue); final updatedGame = await database.gameDao.getGameById( gameId: testGame1.id, @@ -321,7 +321,7 @@ void main() { gameId: 'non-existent-id', newIcon: 'New icon', ); - expect(updated, false); + expect(updated, isFalse); final allGames = await database.gameDao.getAllGames(); expect(allGames, isEmpty); @@ -367,7 +367,7 @@ void main() { await database.gameDao.addGame(game: testGame1); final deleted = await database.gameDao.deleteGame(gameId: testGame1.id); - expect(deleted, true); + expect(deleted, isTrue); final allGames = await database.gameDao.getAllGames(); expect(allGames, isEmpty); @@ -377,7 +377,7 @@ void main() { final deleted = await database.gameDao.deleteGame( gameId: 'non-existent-id', ); - expect(deleted, false); + expect(deleted, isFalse); }); test('deleteAllGames() removes all games', () async { @@ -389,7 +389,7 @@ void main() { expect(count, 3); final deleted = await database.gameDao.deleteAllGames(); - expect(deleted, true); + expect(deleted, isTrue); count = await database.gameDao.getGameCount(); expect(count, 0); @@ -397,7 +397,7 @@ void main() { test('deleteAllGames() returns false when no games exist', () async { final deleted = await database.gameDao.deleteAllGames(); - expect(deleted, false); + expect(deleted, isFalse); }); }); }); diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index 963d10e..1e224bc 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -94,15 +94,15 @@ void main() { test('addPlayer() returns false when player already exists', () async { var added = await database.playerDao.addPlayer(player: testPlayer1); - expect(added, true); + expect(added, isTrue); added = await database.playerDao.addPlayer(player: testPlayer1); - expect(added, false); + expect(added, isFalse); }); test('addPlayersAsList() handles empty list correctly', () async { final added = await database.playerDao.addPlayersAsList(players: []); - expect(added, false); + expect(added, isFalse); final allPlayers = await database.playerDao.getAllPlayers(); expect(allPlayers, isEmpty); @@ -163,13 +163,13 @@ void main() { var playerExists = await database.playerDao.playerExists( playerId: testPlayer1.id, ); - expect(playerExists, false); + expect(playerExists, isFalse); await database.playerDao.addPlayer(player: testPlayer1); playerExists = await database.playerDao.playerExists( playerId: testPlayer1.id, ); - expect(playerExists, true); + expect(playerExists, isTrue); }); test( @@ -227,7 +227,7 @@ void main() { playerId: 'non-existent-id', newName: 'New name', ); - expect(updated, false); + expect(updated, isFalse); final allPlayers = await database.playerDao.getAllPlayers(); expect(allPlayers, isEmpty); @@ -242,7 +242,7 @@ void main() { playerId: testPlayer1.id, newDescription: newDescription, ); - expect(updated, true); + expect(updated, isTrue); final player = await database.playerDao.getPlayerById( playerId: testPlayer1.id, @@ -257,7 +257,7 @@ void main() { playerId: 'non-existent-id', newDescription: 'New description', ); - expect(updated, false); + expect(updated, isFalse); final allPlayers = await database.playerDao.getAllPlayers(); expect(allPlayers, isEmpty); @@ -305,19 +305,19 @@ void main() { final playerDeleted = await database.playerDao.deletePlayer( playerId: testPlayer1.id, ); - expect(playerDeleted, true); + expect(playerDeleted, isTrue); final playerExists = await database.playerDao.playerExists( playerId: testPlayer1.id, ); - expect(playerExists, false); + expect(playerExists, isFalse); }); test('deletePlayer() returns false for non-existent player', () async { final deleted = await database.playerDao.deletePlayer( playerId: 'non-existent-id', ); - expect(deleted, false); + expect(deleted, isFalse); }); test('deleteAllPlayers() removes all players', () async { @@ -329,7 +329,7 @@ void main() { expect(playerCount, 3); final deleted = await database.playerDao.deleteAllPlayers(); - expect(deleted, true); + expect(deleted, isTrue); playerCount = await database.playerDao.getPlayerCount(); expect(playerCount, 0); @@ -337,7 +337,7 @@ void main() { test('deleteAllPlayers() returns false when no players exist', () async { final deleted = await database.playerDao.deleteAllPlayers(); - expect(deleted, false); + expect(deleted, isFalse); }); }); @@ -429,7 +429,7 @@ void main() { playerId: testPlayer1.id, nameCount: 2, ); - expect(success, true); + expect(success, isTrue); final player = await database.playerDao.getPlayerById( playerId: testPlayer1.id, diff --git a/test/db_tests/relationships/player_group_test.dart b/test/db_tests/relationships/player_group_test.dart index 42d083b..70e3d30 100644 --- a/test/db_tests/relationships/player_group_test.dart +++ b/test/db_tests/relationships/player_group_test.dart @@ -56,7 +56,7 @@ void main() { playerId: testPlayer4.id, ); - expect(playerAdded, true); + expect(playerAdded, isTrue); }); test( @@ -104,19 +104,19 @@ void main() { playerId: 'non-existent-player-id', groupId: testGroup.id, ); - expect(isInGroup, false); + expect(isInGroup, isFalse); isInGroup = await database.playerGroupDao.isPlayerInGroup( playerId: testPlayer1.id, groupId: 'non-existent-group-id', ); - expect(isInGroup, false); + expect(isInGroup, isFalse); isInGroup = await database.playerGroupDao.isPlayerInGroup( playerId: 'non-existent-player-id', groupId: 'non-existent-group-id', ); - expect(isInGroup, false); + expect(isInGroup, isFalse); }, ); @@ -174,8 +174,8 @@ void main() { groupId: testGroup.id, ); expect(groupMembers.members.length, 2); - expect(groupMembers.members.any((p) => p.id == testPlayer3.id), true); - expect(groupMembers.members.any((p) => p.id == testPlayer4.id), true); + expect(groupMembers.members.any((p) => p.id == testPlayer3.id), isTrue); + expect(groupMembers.members.any((p) => p.id == testPlayer4.id), isTrue); }); }); group('DELETE', () { @@ -186,7 +186,7 @@ void main() { playerId: testPlayer1.id, groupId: testGroup.id, ); - expect(removed, true); + expect(removed, isTrue); final result = await database.groupDao.getGroupById( groupId: testGroup.id, @@ -194,7 +194,7 @@ void main() { expect(result.members.length, testGroup.members.length - 1); final playerExists = result.members.any((p) => p.id == testPlayer1.id); - expect(playerExists, false); + expect(playerExists, isFalse); }); }); @@ -216,7 +216,7 @@ void main() { final groupExists = await database.groupDao.groupExists( groupId: testGroup.id, ); - expect(groupExists, true); + expect(groupExists, isTrue); }); test('removePlayerFromGroup() works correctly', () async { @@ -226,13 +226,13 @@ void main() { playerId: testPlayer1.id, groupId: testGroup.id, ); - expect(removed, true); + expect(removed, isTrue); removed = await database.playerGroupDao.removePlayerFromGroup( playerId: testPlayer1.id, groupId: testGroup.id, ); - expect(removed, false); + expect(removed, isFalse); }); test( @@ -246,19 +246,19 @@ void main() { playerId: 'non-existent-player-id', groupId: testGroup.id, ); - expect(removed, false); + expect(removed, isFalse); removed = await database.playerGroupDao.removePlayerFromGroup( playerId: testPlayer1.id, groupId: 'non-existent-group-id', ); - expect(removed, false); + expect(removed, isFalse); removed = await database.playerGroupDao.removePlayerFromGroup( playerId: 'non-existent-player-id', groupId: 'non-existent-group-id', ); - expect(removed, false); + expect(removed, isFalse); }, ); }); diff --git a/test/db_tests/values/score_entry_test.dart b/test/db_tests/values/score_entry_test.dart index bb41a9a..a4b5df4 100644 --- a/test/db_tests/values/score_entry_test.dart +++ b/test/db_tests/values/score_entry_test.dart @@ -259,7 +259,7 @@ void main() { matchId: testMatch1.id, ); - expect(scores.isEmpty, true); + expect(scores.isEmpty, isTrue); }); test('getAllPlayerScoresInMatch() works correctly', () async { @@ -299,7 +299,7 @@ void main() { matchId: testMatch1.id, ); - expect(playerScores.isEmpty, true); + expect(playerScores.isEmpty, isTrue); }); test('Scores are isolated across different matches', () async { @@ -438,7 +438,7 @@ void main() { newEntry: newEntry, ); - expect(updated, true); + expect(updated, isTrue); final score = await database.scoreEntryDao.getScore( playerId: testPlayer1.id, @@ -458,7 +458,7 @@ void main() { newEntry: entryRound1, ); - expect(updated, false); + expect(updated, isFalse); }); }); @@ -476,7 +476,7 @@ void main() { roundNumber: 1, ); - expect(deleted, true); + expect(deleted, isTrue); final score = await database.scoreEntryDao.getScore( playerId: testPlayer1.id, @@ -494,7 +494,7 @@ void main() { roundNumber: 1, ); - expect(deleted, false); + expect(deleted, isFalse); }); test('deleteAllScoresForMatch() works correctly', () async { @@ -523,7 +523,7 @@ void main() { matchId: testMatch1.id, ); - expect(deleted, true); + expect(deleted, isTrue); final match1Scores = await database.scoreEntryDao.getAllMatchScores( matchId: testMatch1.id, @@ -555,7 +555,7 @@ void main() { matchId: testMatch1.id, ); - expect(deleted, true); + expect(deleted, isTrue); final player1Scores = await database.scoreEntryDao .getAllPlayerScoresInMatch( @@ -578,7 +578,7 @@ void main() { var hasWinner = await database.scoreEntryDao.hasWinner( matchId: testMatch1.id, ); - expect(hasWinner, false); + expect(hasWinner, isFalse); await database.scoreEntryDao.setWinner( playerId: testPlayer1.id, @@ -588,7 +588,7 @@ void main() { hasWinner = await database.scoreEntryDao.hasWinner( matchId: testMatch1.id, ); - expect(hasWinner, true); + expect(hasWinner, isTrue); }); test('getWinnersForMatch() returns correct winner', () async { @@ -612,7 +612,7 @@ void main() { var removed = await database.scoreEntryDao.removeWinner( matchId: testMatch1.id, ); - expect(removed, false); + expect(removed, isFalse); await database.scoreEntryDao.setWinner( playerId: testPlayer1.id, @@ -622,7 +622,7 @@ void main() { removed = await database.scoreEntryDao.removeWinner( matchId: testMatch1.id, ); - expect(removed, true); + expect(removed, isTrue); var winner = await database.scoreEntryDao.getWinner( matchId: testMatch1.id, @@ -636,7 +636,7 @@ void main() { var hasLooser = await database.scoreEntryDao.hasLoser( matchId: testMatch1.id, ); - expect(hasLooser, false); + expect(hasLooser, isFalse); await database.scoreEntryDao.setLoser( playerId: testPlayer1.id, @@ -646,7 +646,7 @@ void main() { hasLooser = await database.scoreEntryDao.hasLoser( matchId: testMatch1.id, ); - expect(hasLooser, true); + expect(hasLooser, isTrue); }); test('getLoser() returns correct winner', () async { @@ -670,7 +670,7 @@ void main() { var removed = await database.scoreEntryDao.removeLoser( matchId: testMatch1.id, ); - expect(removed, false); + expect(removed, isFalse); await database.scoreEntryDao.setLoser( playerId: testPlayer1.id, @@ -680,7 +680,7 @@ void main() { removed = await database.scoreEntryDao.removeLoser( matchId: testMatch1.id, ); - expect(removed, true); + expect(removed, isTrue); var looser = await database.scoreEntryDao.getLoser( matchId: testMatch1.id, diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index 781ee67..fec70b7 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -149,10 +149,10 @@ void main() { final decoded = json.decode(jsonString) as Map; - expect(decoded.containsKey('players'), true); - expect(decoded.containsKey('games'), true); - expect(decoded.containsKey('groups'), true); - expect(decoded.containsKey('matches'), true); + expect(decoded.containsKey('players'), isTrue); + expect(decoded.containsKey('games'), isTrue); + expect(decoded.containsKey('groups'), isTrue); + expect(decoded.containsKey('matches'), isTrue); final players = decoded['players'] as List; final games = decoded['games'] as List; From 9e4f44491cea776cb9557be5f857b6e5a6a5c3f1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 01:28:00 +0200 Subject: [PATCH 015/127] Made match notes non nullable --- lib/data/dao/match_dao.dart | 10 +++---- lib/data/db/database.g.dart | 43 ++++++++++++++--------------- lib/data/db/tables/match_table.dart | 2 +- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index f43909c..6959b1b 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -30,7 +30,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { gameId: match.game.id, groupId: Value(match.group?.id), name: match.name, - notes: Value(match.notes), + notes: match.notes, createdAt: match.createdAt, endedAt: Value(match.endedAt), ), @@ -142,7 +142,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { gameId: match.game.id, groupId: Value(match.group?.id), name: match.name, - notes: Value(match.notes), + notes: match.notes, createdAt: match.createdAt, endedAt: Value(match.endedAt), ), @@ -300,7 +300,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { group: group, players: players, teams: teams.isEmpty ? null : teams, - notes: row.notes ?? '', + notes: row.notes, createdAt: row.createdAt, endedAt: row.endedAt, scores: scores, @@ -334,7 +334,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { group: group, players: players, teams: teams.isEmpty ? null : teams, - notes: result.notes ?? '', + notes: result.notes, createdAt: result.createdAt, endedAt: result.endedAt, scores: scores, @@ -362,7 +362,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { group: group, players: players, teams: teams.isEmpty ? null : teams, - notes: row.notes ?? '', + notes: row.notes, createdAt: row.createdAt, endedAt: row.endedAt, ); diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index b4f3702..c8d0faa 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -1190,9 +1190,9 @@ class $MatchTableTable extends MatchTable late final GeneratedColumn notes = GeneratedColumn( 'notes', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, ); static const VerificationMeta _createdAtMeta = const VerificationMeta( 'createdAt', @@ -1270,6 +1270,8 @@ class $MatchTableTable extends MatchTable _notesMeta, notes.isAcceptableOrUnknown(data['notes']!, _notesMeta), ); + } else if (isInserting) { + context.missing(_notesMeta); } if (data.containsKey('created_at')) { context.handle( @@ -1313,7 +1315,7 @@ class $MatchTableTable extends MatchTable notes: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}notes'], - ), + )!, createdAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}created_at'], @@ -1336,7 +1338,7 @@ class MatchTableData extends DataClass implements Insertable { final String gameId; final String? groupId; final String name; - final String? notes; + final String notes; final DateTime createdAt; final DateTime? endedAt; const MatchTableData({ @@ -1344,7 +1346,7 @@ class MatchTableData extends DataClass implements Insertable { required this.gameId, this.groupId, required this.name, - this.notes, + required this.notes, required this.createdAt, this.endedAt, }); @@ -1357,9 +1359,7 @@ class MatchTableData extends DataClass implements Insertable { map['group_id'] = Variable(groupId); } map['name'] = Variable(name); - if (!nullToAbsent || notes != null) { - map['notes'] = Variable(notes); - } + map['notes'] = Variable(notes); map['created_at'] = Variable(createdAt); if (!nullToAbsent || endedAt != null) { map['ended_at'] = Variable(endedAt); @@ -1375,9 +1375,7 @@ class MatchTableData extends DataClass implements Insertable { ? const Value.absent() : Value(groupId), name: Value(name), - notes: notes == null && nullToAbsent - ? const Value.absent() - : Value(notes), + notes: Value(notes), createdAt: Value(createdAt), endedAt: endedAt == null && nullToAbsent ? const Value.absent() @@ -1395,7 +1393,7 @@ class MatchTableData extends DataClass implements Insertable { gameId: serializer.fromJson(json['gameId']), groupId: serializer.fromJson(json['groupId']), name: serializer.fromJson(json['name']), - notes: serializer.fromJson(json['notes']), + notes: serializer.fromJson(json['notes']), createdAt: serializer.fromJson(json['createdAt']), endedAt: serializer.fromJson(json['endedAt']), ); @@ -1408,7 +1406,7 @@ class MatchTableData extends DataClass implements Insertable { 'gameId': serializer.toJson(gameId), 'groupId': serializer.toJson(groupId), 'name': serializer.toJson(name), - 'notes': serializer.toJson(notes), + 'notes': serializer.toJson(notes), 'createdAt': serializer.toJson(createdAt), 'endedAt': serializer.toJson(endedAt), }; @@ -1419,7 +1417,7 @@ class MatchTableData extends DataClass implements Insertable { String? gameId, Value groupId = const Value.absent(), String? name, - Value notes = const Value.absent(), + String? notes, DateTime? createdAt, Value endedAt = const Value.absent(), }) => MatchTableData( @@ -1427,7 +1425,7 @@ class MatchTableData extends DataClass implements Insertable { gameId: gameId ?? this.gameId, groupId: groupId.present ? groupId.value : this.groupId, name: name ?? this.name, - notes: notes.present ? notes.value : this.notes, + notes: notes ?? this.notes, createdAt: createdAt ?? this.createdAt, endedAt: endedAt.present ? endedAt.value : this.endedAt, ); @@ -1478,7 +1476,7 @@ class MatchTableCompanion extends UpdateCompanion { final Value gameId; final Value groupId; final Value name; - final Value notes; + final Value notes; final Value createdAt; final Value endedAt; final Value rowid; @@ -1497,13 +1495,14 @@ class MatchTableCompanion extends UpdateCompanion { required String gameId, this.groupId = const Value.absent(), required String name, - this.notes = const Value.absent(), + required String notes, required DateTime createdAt, this.endedAt = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), gameId = Value(gameId), name = Value(name), + notes = Value(notes), createdAt = Value(createdAt); static Insertable custom({ Expression? id, @@ -1532,7 +1531,7 @@ class MatchTableCompanion extends UpdateCompanion { Value? gameId, Value? groupId, Value? name, - Value? notes, + Value? notes, Value? createdAt, Value? endedAt, Value? rowid, @@ -4093,7 +4092,7 @@ typedef $$MatchTableTableCreateCompanionBuilder = required String gameId, Value groupId, required String name, - Value notes, + required String notes, required DateTime createdAt, Value endedAt, Value rowid, @@ -4104,7 +4103,7 @@ typedef $$MatchTableTableUpdateCompanionBuilder = Value gameId, Value groupId, Value name, - Value notes, + Value notes, Value createdAt, Value endedAt, Value rowid, @@ -4567,7 +4566,7 @@ class $$MatchTableTableTableManager Value gameId = const Value.absent(), Value groupId = const Value.absent(), Value name = const Value.absent(), - Value notes = const Value.absent(), + Value notes = const Value.absent(), Value createdAt = const Value.absent(), Value endedAt = const Value.absent(), Value rowid = const Value.absent(), @@ -4587,7 +4586,7 @@ class $$MatchTableTableTableManager required String gameId, Value groupId = const Value.absent(), required String name, - Value notes = const Value.absent(), + required String notes, required DateTime createdAt, Value endedAt = const Value.absent(), Value rowid = const Value.absent(), diff --git a/lib/data/db/tables/match_table.dart b/lib/data/db/tables/match_table.dart index 25b0a73..c565547 100644 --- a/lib/data/db/tables/match_table.dart +++ b/lib/data/db/tables/match_table.dart @@ -12,7 +12,7 @@ class MatchTable extends Table { .references(GroupTable, #id, onDelete: KeyAction.setNull) .nullable()(); TextColumn get name => text()(); - TextColumn get notes => text().nullable()(); + TextColumn get notes => text()(); DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get endedAt => dateTime().nullable()(); From 5789650c973e56ebf47213a1e78b592dcad7cd2b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 01:31:49 +0200 Subject: [PATCH 016/127] Refactoring --- lib/data/dao/game_dao.dart | 22 ++++++++-------- lib/data/dao/group_dao.dart | 12 ++++----- lib/data/dao/match_dao.dart | 12 ++++----- lib/data/dao/player_dao.dart | 16 ++++++------ lib/data/dao/player_match_dao.dart | 8 +++--- lib/data/dao/score_entry_dao.dart | 8 +++--- lib/data/dao/team_dao.dart | 4 +-- .../group_view/create_group_view.dart | 2 +- .../create_match/create_match_view.dart | 4 +-- test/db_tests/aggregates/group_test.dart | 12 ++++----- test/db_tests/aggregates/match_test.dart | 10 +++---- test/db_tests/aggregates/team_test.dart | 4 +-- test/db_tests/entities/game_test.dart | 26 +++++++++---------- test/db_tests/entities/player_test.dart | 14 +++++----- .../relationships/player_match_test.dart | 8 +++--- test/db_tests/values/score_entry_test.dart | 4 +-- 16 files changed, 83 insertions(+), 83 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index 099c501..400f04a 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -117,14 +117,14 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /* Update */ - /// Updates the name of the game with the given [gameId] to [newName]. + /// Updates the name of the game with the given [gameId] to [name]. Future updateGameName({ required String gameId, - required String newName, + required String name, }) async { final rowsAffected = await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(name: Value(newName)), + GameTableCompanion(name: Value(name)), ); return rowsAffected > 0; } @@ -132,11 +132,11 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /// Updates the ruleset of the game with the given [gameId]. Future updateGameRuleset({ required String gameId, - required Ruleset newRuleset, + required Ruleset ruleset, }) async { final rowsAffected = await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(ruleset: Value(newRuleset.name)), + GameTableCompanion(ruleset: Value(ruleset.name)), ); return rowsAffected > 0; } @@ -144,11 +144,11 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /// Updates the description of the game with the given [gameId]. Future updateGameDescription({ required String gameId, - required String newDescription, + required String description, }) async { final rowsAffected = await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(description: Value(newDescription)), + GameTableCompanion(description: Value(description)), ); return rowsAffected > 0; } @@ -156,11 +156,11 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /// Updates the color of the game with the given [gameId]. Future updateGameColor({ required String gameId, - required GameColor newColor, + required GameColor color, }) async { final rowsAffected = await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(color: Value(newColor.name)), + GameTableCompanion(color: Value(color.name)), ); return rowsAffected > 0; } @@ -168,11 +168,11 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /// Updates the icon of the game with the given [gameId]. Future updateGameIcon({ required String gameId, - required String newIcon, + required String icon, }) async { final rowsAffected = await (update(gameTable)..where((g) => g.id.equals(gameId))).write( - GameTableCompanion(icon: Value(newIcon)), + GameTableCompanion(icon: Value(icon)), ); return rowsAffected > 0; } diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index d1029d0..bffe5a4 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -213,28 +213,28 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { /* Update */ - /// Updates the name of the group with the given [id] to [newName]. + /// Updates the name of the group with the given [id] to [name]. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateGroupName({ required String groupId, - required String newName, + required String name, }) async { final rowsAffected = await (update(groupTable)..where((g) => g.id.equals(groupId))).write( - GroupTableCompanion(name: Value(newName)), + GroupTableCompanion(name: Value(name)), ); return rowsAffected > 0; } - /// Updates the description of the group with the given [groupId] to [newDescription]. + /// Updates the description of the group with the given [groupId] to [description]. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateGroupDescription({ required String groupId, - required String newDescription, + required String description, }) async { final rowsAffected = await (update(groupTable)..where((g) => g.id.equals(groupId))).write( - GroupTableCompanion(description: Value(newDescription)), + GroupTableCompanion(description: Value(description)), ); return rowsAffected > 0; } diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 6959b1b..69aaeef 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -395,30 +395,30 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /* Update */ - /// Changes the name of the match with the given [matchId] to [newName]. + /// Changes the name of the match with the given [matchId] to [name]. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateMatchName({ required String matchId, - required String newName, + required String name, }) async { final query = update(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.write( - MatchTableCompanion(name: Value(newName)), + MatchTableCompanion(name: Value(name)), ); return rowsAffected > 0; } /// Updates the group of the match with the given [matchId]. - /// Replaces the existing group association with the new group specified by [newGroupId]. + /// Replaces the existing group association with the new group specified by [groupId]. /// Pass null to remove the group association. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateMatchGroup({ required String matchId, - required String? newGroupId, + required String? groupId, }) async { final query = update(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.write( - MatchTableCompanion(groupId: Value(newGroupId)), + MatchTableCompanion(groupId: Value(groupId)), ); return rowsAffected > 0; } diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index dd46e17..51e5845 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -158,10 +158,10 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /* Update */ - /// Updates the name of the player with the given [playerId] to [newName]. + /// Updates the name of the player with the given [playerId] to [name]. Future updatePlayerName({ required String playerId, - required String newName, + required String name, }) async { // Get previous name and name count for the player before updating final previousPlayerName = @@ -173,13 +173,13 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { final rowsAffected = await (update(playerTable)..where((p) => p.id.equals(playerId))).write( - PlayerTableCompanion(name: Value(newName)), + PlayerTableCompanion(name: Value(name)), ); // Update name count for the new name - final count = await calculateNameCount(name: newName); + final count = await calculateNameCount(name: name); if (count > 0) { - await (update(playerTable)..where((p) => p.name.equals(newName))).write( + await (update(playerTable)..where((p) => p.name.equals(name))).write( PlayerTableCompanion(nameCount: Value(count)), ); } @@ -200,15 +200,15 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { } /// Updates the description of the player with the given [playerId] to - /// [newDescription]. + /// [description]. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updatePlayerDescription({ required String playerId, - required String newDescription, + required String description, }) async { final rowsAffected = await (update(playerTable)..where((g) => g.id.equals(playerId))).write( - PlayerTableCompanion(description: Value(newDescription)), + PlayerTableCompanion(description: Value(description)), ); return rowsAffected > 0; } diff --git a/lib/data/dao/player_match_dao.dart b/lib/data/dao/player_match_dao.dart index 1c9d0dd..d119468 100644 --- a/lib/data/dao/player_match_dao.dart +++ b/lib/data/dao/player_match_dao.dart @@ -116,18 +116,18 @@ class PlayerMatchDao extends DatabaseAccessor } /// Updates the players associated with a match based on the provided - /// [newPlayer] list. It adds new players and removes players that are no + /// [player] list. It adds new players and removes players that are no /// longer associated with the match. Future updateMatchPlayers({ required String matchId, - required List newPlayer, + required List player, }) async { - if (newPlayer.isEmpty) return false; + if (player.isEmpty) return false; final currentPlayers = await getPlayersOfMatch(matchId: matchId); // Create sets of player IDs for easy comparison final currentPlayerIds = currentPlayers.map((p) => p.id).toSet(); - final newPlayerIdsSet = newPlayer.map((p) => p.id).toSet(); + final newPlayerIdsSet = player.map((p) => p.id).toSet(); // Are the current and new player identical? if (currentPlayerIds.containsAll(newPlayerIdsSet) && diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index 0d04e33..9c4e01d 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -162,19 +162,19 @@ class ScoreEntryDao extends DatabaseAccessor Future updateScore({ required String playerId, required String matchId, - required ScoreEntry newEntry, + required ScoreEntry entry, }) async { final rowsAffected = await (update(scoreEntryTable)..where( (s) => s.playerId.equals(playerId) & s.matchId.equals(matchId) & - s.roundNumber.equals(newEntry.roundNumber), + s.roundNumber.equals(entry.roundNumber), )) .write( ScoreEntryTableCompanion( - score: Value(newEntry.score), - change: Value(newEntry.change), + score: Value(entry.score), + change: Value(entry.change), ), ); return rowsAffected > 0; diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index 708b475..cba68fb 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -153,11 +153,11 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { /// Updates the name of the team with the given [teamId]. Future updateTeamName({ required String teamId, - required String newName, + required String name, }) async { final rowsAffected = await (update(teamTable)..where((t) => t.id.equals(teamId))).write( - TeamTableCompanion(name: Value(newName)), + TeamTableCompanion(name: Value(name)), ); return rowsAffected > 0; } diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 593499e..3a2ee60 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -172,7 +172,7 @@ class _CreateGroupViewState extends State { if (widget.groupToEdit!.name != groupName) { successfullNameChange = await db.groupDao.updateGroupName( groupId: widget.groupToEdit!.id, - newName: groupName, + name: groupName, ); } 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 1a04c78..cb26de8 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 @@ -271,14 +271,14 @@ class _CreateMatchViewState extends State { if (widget.matchToEdit!.name != updatedMatch.name) { await db.matchDao.updateMatchName( matchId: widget.matchToEdit!.id, - newName: updatedMatch.name, + name: updatedMatch.name, ); } if (widget.matchToEdit!.group?.id != updatedMatch.group?.id) { await db.matchDao.updateMatchGroup( matchId: widget.matchToEdit!.id, - newGroupId: updatedMatch.group?.id, + groupId: updatedMatch.group?.id, ); } diff --git a/test/db_tests/aggregates/group_test.dart b/test/db_tests/aggregates/group_test.dart index 2ae1a3f..1498523 100644 --- a/test/db_tests/aggregates/group_test.dart +++ b/test/db_tests/aggregates/group_test.dart @@ -211,7 +211,7 @@ void main() { const newName = 'New name'; await database.groupDao.updateGroupName( groupId: testGroup1.id, - newName: newName, + name: newName, ); final result = await database.groupDao.getGroupById( @@ -223,7 +223,7 @@ void main() { test('updateGroupName() returns false for non-existent group', () async { final updated = await database.groupDao.updateGroupName( groupId: 'non-existent-id', - newName: 'New name', + name: 'New name', ); expect(updated, isFalse); }); @@ -234,7 +234,7 @@ void main() { const newDescription = 'New description'; final updated = await database.groupDao.updateGroupDescription( groupId: testGroup1.id, - newDescription: newDescription, + description: newDescription, ); expect(updated, isTrue); @@ -249,7 +249,7 @@ void main() { () async { final updated = await database.groupDao.updateGroupDescription( groupId: 'non-existent-id', - newDescription: 'New description', + description: 'New description', ); expect(updated, isFalse); }, @@ -262,11 +262,11 @@ void main() { await database.groupDao.updateGroupName( groupId: testGroup1.id, - newName: newName, + name: newName, ); await database.groupDao.updateGroupDescription( groupId: testGroup1.id, - newDescription: newDescription, + description: newDescription, ); final updatedGroup = await database.groupDao.getGroupById( diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index a056b2c..fccecfe 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -268,7 +268,7 @@ void main() { const newName = 'New name'; await database.matchDao.updateMatchName( matchId: testMatch1.id, - newName: newName, + name: newName, ); final fetchedMatch = await database.matchDao.getMatchById( @@ -280,7 +280,7 @@ void main() { test('updateMatchName() does nothing for non-existent match', () async { final updated = await database.matchDao.updateMatchName( matchId: 'non-existing-id', - newName: 'New Name', + name: 'New Name', ); expect(updated, isFalse); @@ -294,7 +294,7 @@ void main() { await database.matchDao.updateMatchGroup( matchId: testMatch1.id, - newGroupId: testGroup2.id, + groupId: testGroup2.id, ); final fetchedMatch = await database.matchDao.getMatchById( @@ -306,7 +306,7 @@ void main() { test('updateMatchGroup() does nothing for non-existent match', () async { final updated = await database.matchDao.updateMatchGroup( matchId: 'non-existing-id', - newGroupId: 'group-id', + groupId: 'group-id', ); expect(updated, isFalse); @@ -362,7 +362,7 @@ void main() { const newName = 'New name'; await database.matchDao.updateMatchName( matchId: testMatch1.id, - newName: newName, + name: newName, ); final fetchedMatch = await database.matchDao.getMatchById( diff --git a/test/db_tests/aggregates/team_test.dart b/test/db_tests/aggregates/team_test.dart index 592810d..fefdcc5 100644 --- a/test/db_tests/aggregates/team_test.dart +++ b/test/db_tests/aggregates/team_test.dart @@ -263,7 +263,7 @@ void main() { const newName = 'New name'; await database.teamDao.updateTeamName( teamId: testTeam1.id, - newName: newName, + name: newName, ); fetchedTeam = await database.teamDao.getTeamById(teamId: testTeam1.id); @@ -273,7 +273,7 @@ void main() { test('updateTeamName() does nothing for non-existent team', () async { final updated = await database.teamDao.updateTeamName( teamId: 'non-existing-id', - newName: 'New Name', + name: 'New Name', ); expect(updated, isFalse); diff --git a/test/db_tests/entities/game_test.dart b/test/db_tests/entities/game_test.dart index a5af11a..778d43b 100644 --- a/test/db_tests/entities/game_test.dart +++ b/test/db_tests/entities/game_test.dart @@ -197,7 +197,7 @@ void main() { final updated = await database.gameDao.updateGameName( gameId: testGame1.id, - newName: newName, + name: newName, ); expect(updated, isTrue); @@ -210,7 +210,7 @@ void main() { test('updateGameName() does nothing for non-existent game', () async { final updated = await database.gameDao.updateGameName( gameId: 'non-existent-id', - newName: 'New name', + name: 'New name', ); expect(updated, isFalse); @@ -224,7 +224,7 @@ void main() { final updated = await database.gameDao.updateGameRuleset( gameId: testGame1.id, - newRuleset: ruleset, + ruleset: ruleset, ); expect(updated, isTrue); @@ -237,7 +237,7 @@ void main() { test('updateGameRuleset() does nothing for non-existent game', () async { final updated = await database.gameDao.updateGameRuleset( gameId: 'non-existent-id', - newRuleset: Ruleset.lowestScore, + ruleset: Ruleset.lowestScore, ); expect(updated, isFalse); @@ -251,7 +251,7 @@ void main() { final updated = await database.gameDao.updateGameDescription( gameId: testGame1.id, - newDescription: newDescription, + description: newDescription, ); expect(updated, isTrue); @@ -266,7 +266,7 @@ void main() { () async { final updated = await database.gameDao.updateGameDescription( gameId: 'non-existent-id', - newDescription: 'New description', + description: 'New description', ); expect(updated, isFalse); @@ -280,7 +280,7 @@ void main() { await database.gameDao.updateGameColor( gameId: testGame1.id, - newColor: GameColor.green, + color: GameColor.green, ); final updatedGame = await database.gameDao.getGameById( @@ -292,7 +292,7 @@ void main() { test('updateGameColor() does nothing for non-existent game', () async { final updated = await database.gameDao.updateGameColor( gameId: 'non-existent-id', - newColor: GameColor.green, + color: GameColor.green, ); expect(updated, isFalse); @@ -306,7 +306,7 @@ void main() { final updated = await database.gameDao.updateGameIcon( gameId: testGame1.id, - newIcon: newIcon, + icon: newIcon, ); expect(updated, isTrue); @@ -319,7 +319,7 @@ void main() { test('updateGameIcon() does nothing for non-existent game', () async { final updated = await database.gameDao.updateGameIcon( gameId: 'non-existent-id', - newIcon: 'New icon', + icon: 'New icon', ); expect(updated, isFalse); @@ -333,19 +333,19 @@ void main() { const newName = 'New name'; await database.gameDao.updateGameName( gameId: testGame1.id, - newName: newName, + name: newName, ); const newGameColor = GameColor.teal; await database.gameDao.updateGameColor( gameId: testGame1.id, - newColor: newGameColor, + color: newGameColor, ); const newDescription = 'New description'; await database.gameDao.updateGameDescription( gameId: testGame1.id, - newDescription: newDescription, + description: newDescription, ); final updatedGame = await database.gameDao.getGameById( diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index 1e224bc..bfcced4 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -213,7 +213,7 @@ void main() { await database.playerDao.updatePlayerName( playerId: testPlayer1.id, - newName: newName, + name: newName, ); final player = await database.playerDao.getPlayerById( @@ -225,7 +225,7 @@ void main() { test('updatePlayerName() does nothing for non-existent player', () async { final updated = await database.playerDao.updatePlayerName( playerId: 'non-existent-id', - newName: 'New name', + name: 'New name', ); expect(updated, isFalse); @@ -240,7 +240,7 @@ void main() { final updated = await database.playerDao.updatePlayerDescription( playerId: testPlayer1.id, - newDescription: newDescription, + description: newDescription, ); expect(updated, isTrue); @@ -255,7 +255,7 @@ void main() { () async { final updated = await database.playerDao.updatePlayerDescription( playerId: 'non-existent-id', - newDescription: 'New description', + description: 'New description', ); expect(updated, isFalse); @@ -269,7 +269,7 @@ void main() { await database.playerDao.updatePlayerName( playerId: testPlayer1.id, - newName: 'First Update', + name: 'First Update', ); var fetchedPlayer = await database.playerDao.getPlayerById( @@ -279,7 +279,7 @@ void main() { await database.playerDao.updatePlayerName( playerId: testPlayer1.id, - newName: 'Second Update', + name: 'Second Update', ); fetchedPlayer = await database.playerDao.getPlayerById( @@ -289,7 +289,7 @@ void main() { await database.playerDao.updatePlayerDescription( playerId: testPlayer1.id, - newDescription: 'Third Update', + description: 'Third Update', ); fetchedPlayer = await database.playerDao.getPlayerById( diff --git a/test/db_tests/relationships/player_match_test.dart b/test/db_tests/relationships/player_match_test.dart index 85ccab9..6d879c3 100644 --- a/test/db_tests/relationships/player_match_test.dart +++ b/test/db_tests/relationships/player_match_test.dart @@ -253,7 +253,7 @@ void main() { await database.playerMatchDao.updateMatchPlayers( matchId: testMatch1.id, - newPlayer: newPlayers, + player: newPlayers, ); final updatedPlayers = await database.playerMatchDao.getPlayersOfMatch( @@ -337,7 +337,7 @@ void main() { final newPlayersList = [testPlayer1, testPlayer2]; await database.playerMatchDao.updateMatchPlayers( matchId: testMatch1.id, - newPlayer: newPlayersList, + player: newPlayersList, ); matchPlayers = await database.matchDao.getMatchById( @@ -356,7 +356,7 @@ void main() { final updated = await database.playerMatchDao.updateMatchPlayers( matchId: testMatch1.id, - newPlayer: originalPlayers, + player: originalPlayers, ); expect(updated, isFalse); @@ -375,7 +375,7 @@ void main() { await database.matchDao.addMatch(match: testMatch1); final updated = await database.playerMatchDao.updateMatchPlayers( matchId: testMatch1.id, - newPlayer: [], + player: [], ); expect(updated, isFalse); diff --git a/test/db_tests/values/score_entry_test.dart b/test/db_tests/values/score_entry_test.dart index a4b5df4..f6cc292 100644 --- a/test/db_tests/values/score_entry_test.dart +++ b/test/db_tests/values/score_entry_test.dart @@ -435,7 +435,7 @@ void main() { final updated = await database.scoreEntryDao.updateScore( playerId: testPlayer1.id, matchId: testMatch1.id, - newEntry: newEntry, + entry: newEntry, ); expect(updated, isTrue); @@ -455,7 +455,7 @@ void main() { final updated = await database.scoreEntryDao.updateScore( playerId: testPlayer1.id, matchId: testMatch1.id, - newEntry: entryRound1, + entry: entryRound1, ); expect(updated, isFalse); From c64fd0c9b4d70fcce74f5ff46b805d392c949b3d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 15:33:31 +0200 Subject: [PATCH 017/127] Added game label --- lib/core/common.dart | 2 +- .../match_view/match_detail_view.dart | 35 +++++++++ lib/presentation/widgets/game_label.dart | 71 +++++++++++++++++++ .../widgets/tiles/match_tile.dart | 56 ++------------- 4 files changed, 114 insertions(+), 50 deletions(-) create mode 100644 lib/presentation/widgets/game_label.dart diff --git a/lib/core/common.dart b/lib/core/common.dart index 4c02350..2db8d19 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -54,7 +54,7 @@ Color getColorFromGameColor(GameColor color) { case GameColor.green: return Colors.green; case GameColor.yellow: - return Colors.yellow; + return const Color(0xFFF7CA28); case GameColor.purple: return Colors.purple; case GameColor.orange: diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 2117b77..a0f8760 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.dart'; @@ -14,6 +15,7 @@ import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; +import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -102,6 +104,7 @@ class _MatchDetailViewState extends State { bottom: 100, ), children: [ + // Controller Icon const Center( child: ColoredIconContainer( icon: Icons.sports_esports, @@ -110,6 +113,8 @@ class _MatchDetailViewState extends State { ), ), const SizedBox(height: 10), + + // Match Name Text( match.name, style: const TextStyle( @@ -120,6 +125,8 @@ class _MatchDetailViewState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 5), + + // Creation Date Text( '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}', style: const TextStyle( @@ -129,6 +136,8 @@ class _MatchDetailViewState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 10), + + // Group Name if (match.group != null) ...[ Row( mainAxisAlignment: MainAxisAlignment.center, @@ -143,6 +152,8 @@ class _MatchDetailViewState extends State { ), const SizedBox(height: 20), ], + + // Players InfoTile( title: loc.players, icon: Icons.people, @@ -162,6 +173,30 @@ class _MatchDetailViewState extends State { ), ), const SizedBox(height: 15), + + // Game + InfoTile( + title: loc.game, + icon: RpgAwesome.clovers_card, + horizontalAlignment: CrossAxisAlignment.start, + content: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ), + child: GameLabel( + title: match.game.name, + description: translateRulesetToString( + match.game.ruleset, + context, + ), + color: match.game.color, + ), + ), + ), + const SizedBox(height: 15), + + // Results InfoTile( title: loc.results, icon: Icons.emoji_events, diff --git a/lib/presentation/widgets/game_label.dart b/lib/presentation/widgets/game_label.dart new file mode 100644 index 0000000..553e637 --- /dev/null +++ b/lib/presentation/widgets/game_label.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/enums.dart'; + +class GameLabel extends StatelessWidget { + const GameLabel({ + super.key, + required this.title, + required this.description, + required this.color, + }); + + final String title; + final String description; + final GameColor color; + + @override + Widget build(BuildContext context) { + final backgroundColor = getColorFromGameColor(color); + final fontColor = backgroundColor.computeLuminance() > 0.5 + ? Colors.black + : Colors.white; + + return IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Title + Container( + decoration: BoxDecoration( + color: backgroundColor.withAlpha(230), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Text( + title, + style: TextStyle( + fontSize: 12, + color: fontColor, + fontWeight: FontWeight.bold, + ), + ), + ), + + // Description + Container( + decoration: BoxDecoration( + color: backgroundColor.withAlpha(140), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Text( + description, + style: TextStyle( + fontSize: 12, + color: fontColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index f7585d6..f939601 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -7,6 +7,7 @@ import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; class MatchTile extends StatefulWidget { @@ -116,56 +117,13 @@ class _MatchTileState extends State { // Game + Ruleset Badge if (!widget.compact) - IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // Game - Container( - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(230), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - bottomLeft: Radius.circular(8), - ), - ), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ), - child: Text( - match.game.name, - style: const TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - // Ruleset - Container( - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(140), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(8), - bottomRight: Radius.circular(8), - ), - ), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ), - child: Text( - translateRulesetToString(match.game.ruleset, context), - style: const TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ], + GameLabel( + title: match.game.name, + description: translateRulesetToString( + match.game.ruleset, + context, ), + color: match.game.color, ), const SizedBox(height: 12), From b664bcacda19ab81deaf813561d8ac5ba4aade86 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 16:02:49 +0200 Subject: [PATCH 018/127] Created seperate game tile and simplified title description list tile --- lib/presentation/widgets/tiles/game_tile.dart | 151 ++++++++++++++++++ .../tiles/title_description_list_tile.dart | 77 ++------- 2 files changed, 168 insertions(+), 60 deletions(-) create mode 100644 lib/presentation/widgets/tiles/game_tile.dart diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart new file mode 100644 index 0000000..1d494b9 --- /dev/null +++ b/lib/presentation/widgets/tiles/game_tile.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; + +class GameTile extends StatelessWidget { + /// A list tile widget that displays a title and description, with optional highlighting and badge. + /// - [title]: The title text displayed on the tile. + /// - [description]: The description text displayed below the title. + /// - [onTap]: The callback invoked when the tile is tapped. + /// - [onLongPress]: The callback invoked when the tile is tapped. + /// - [isHighlighted]: A boolean to determine if the tile should be highlighted. + /// - [badgeText]: Optional text to display in a badge on the right side of the title. + /// - [badgeColor]: Optional color for the badge background. + const GameTile({ + super.key, + required this.title, + required this.description, + this.onTap, + this.onLongPress, + this.isHighlighted = false, + this.badgeText, + this.badgeColor, + }); + + /// The title text displayed on the tile. + final String title; + + /// The description text displayed below the title. + final String description; + + /// The callback invoked when the tile is tapped. + final VoidCallback? onTap; + + /// The callback invoked when the tile is long-pressed. + final VoidCallback? onLongPress; + + /// A boolean to determine if the tile should be highlighted. + final bool isHighlighted; + + /// Optional text to display in a badge on the right side of the title. + final String? badgeText; + + /// Optional color for the badge background. + final Color? badgeColor; + + @override + Widget build(BuildContext context) { + final badgeTextColor = badgeColor != null + ? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white) + : Colors.white; + + final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange); + + return GestureDetector( + onTap: onTap, + onLongPress: onLongPress, + child: AnimatedContainer( + margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + decoration: !isHighlighted + ? CustomTheme.standardBoxDecoration + : CustomTheme.highlightedBoxDecoration.copyWith( + border: Border.all( + color: gameColor.withValues(alpha: 0.9), + width: 2, + ), + ), + duration: const Duration(milliseconds: 200), + child: Stack( + children: [ + // Gradient overlay + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + gameColor.withValues(alpha: 0.08), + gameColor.withValues(alpha: 0.02), + Colors.transparent, + ], + stops: const [0.0, 0.5, 1.0], + ), + ), + ), + ), + + // Content + Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // Title + Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + + // Badge + if (badgeText != null) ...[ + const SizedBox(height: 5), + Container( + constraints: const BoxConstraints(maxWidth: 250), + padding: const EdgeInsets.symmetric( + vertical: 2, + horizontal: 6, + ), + decoration: BoxDecoration( + color: gameColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + badgeText!, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: TextStyle( + color: badgeTextColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + + // Description + if (description.isNotEmpty) ...[ + const SizedBox(height: 10), + Text(description, style: const TextStyle(fontSize: 14)), + const SizedBox(height: 2.5), + ], + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart index 95163f2..bf45c1e 100644 --- a/lib/presentation/widgets/tiles/title_description_list_tile.dart +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -2,23 +2,17 @@ import 'package:flutter/material.dart'; import 'package:tallee/core/custom_theme.dart'; class TitleDescriptionListTile extends StatelessWidget { - /// A list tile widget that displays a title and description, with optional highlighting and badge. + /// A list tile widget that displays a title and description /// - [title]: The title text displayed on the tile. /// - [description]: The description text displayed below the title. /// - [onTap]: The callback invoked when the tile is tapped. - /// - [onLongPress]: The callback invoked when the tile is tapped. /// - [isHighlighted]: A boolean to determine if the tile should be highlighted. - /// - [badgeText]: Optional text to display in a badge on the right side of the title. - /// - [badgeColor]: Optional color for the badge background. const TitleDescriptionListTile({ super.key, required this.title, required this.description, this.onTap, - this.onLongPress, this.isHighlighted = false, - this.badgeText, - this.badgeColor, }); /// The title text displayed on the tile. @@ -30,23 +24,13 @@ class TitleDescriptionListTile extends StatelessWidget { /// The callback invoked when the tile is tapped. final VoidCallback? onTap; - /// The callback invoked when the tile is long-pressed. - final VoidCallback? onLongPress; - /// A boolean to determine if the tile should be highlighted. final bool isHighlighted; - /// Optional text to display in a badge on the right side of the title. - final String? badgeText; - - /// Optional color for the badge background. - final Color? badgeColor; - @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, - onLongPress: onLongPress, child: AnimatedContainer( margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), @@ -57,53 +41,26 @@ class TitleDescriptionListTile extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 230, - child: Text( - title, - overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), + // Title + SizedBox( + width: 230, + child: Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, ), - if (badgeText != null) ...[ - const Spacer(), - Container( - constraints: const BoxConstraints(maxWidth: 115), - padding: const EdgeInsets.symmetric( - vertical: 2, - horizontal: 6, - ), - decoration: BoxDecoration( - color: badgeColor ?? CustomTheme.primaryColor, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - badgeText!, - overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ], + ), ), + + // Description if (description.isNotEmpty) ...[ - const SizedBox(height: 5), + const SizedBox(height: 10), Text(description, style: const TextStyle(fontSize: 14)), const SizedBox(height: 2.5), ], From e895359dac3c3441ef3f82222edd06b90f0833a1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 16:02:59 +0200 Subject: [PATCH 019/127] Updated highlighting --- lib/core/custom_theme.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index 3274db9..b32ce63 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -63,9 +63,8 @@ class CustomTheme { static BoxDecoration highlightedBoxDecoration = BoxDecoration( color: boxColor, - border: Border.all(color: primaryColor), + border: Border.all(color: textColor, width: 2), borderRadius: standardBorderRadiusAll, - boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)], ); // ==================== Component Themes ==================== From 633a0599eba257c5e010a70908954084c9f44668 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 16:04:20 +0200 Subject: [PATCH 020/127] Implemented game tile in choose game view --- lib/core/common.dart | 8 +++++--- .../match_view/create_match/choose_game_view.dart | 8 ++++++-- .../create_match/create_game/choose_color_view.dart | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 2db8d19..14d90aa 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -58,7 +58,7 @@ Color getColorFromGameColor(GameColor color) { case GameColor.purple: return Colors.purple; case GameColor.orange: - return Colors.orange; + return const Color(0xFFef681f); case GameColor.pink: return Colors.pink; case GameColor.teal: @@ -66,8 +66,10 @@ Color getColorFromGameColor(GameColor color) { } } -/// Counts how many players in the match are not part of the group -/// Returns the count as a string, or an empty string if there is no group +/// Counts how many players in the [match] are not part of the group +/// +/// Returns the text you append after the group name, e.g. " + 5" or an empty +/// string if there are no extra players String getExtraPlayerCount(Match match) { int count = 0; diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 8f3e06e..42d5253 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -6,7 +6,7 @@ import 'package:tallee/data/models/game.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; -import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; class ChooseGameView extends StatefulWidget { /// A view that allows the user to choose a game from a list of available games @@ -110,6 +110,7 @@ class _ChooseGameViewState extends State { }, child: Column( children: [ + // Search Bar Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: CustomSearchBar( @@ -121,15 +122,18 @@ class _ChooseGameViewState extends State { ), ), const SizedBox(height: 5), + + // Game list Expanded( child: ListView.builder( itemCount: filteredGames.length, itemBuilder: (BuildContext context, int index) { final game = filteredGames[index]; - return TitleDescriptionListTile( + return GameTile( title: game.name, description: game.description, badgeText: translateRulesetToString(game.ruleset, context), + badgeColor: getColorFromGameColor(game.color), isHighlighted: selectedGameId == game.id, onTap: () async { setState(() { diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart index bf764ad..e6d0b7e 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart @@ -3,7 +3,7 @@ import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; class ChooseColorView extends StatefulWidget { /// A view that allows the user to choose a color from a list of available game colors @@ -54,7 +54,7 @@ class _ChooseColorViewState extends State { itemCount: colors.length, itemBuilder: (BuildContext context, int index) { final color = colors[index]; - return TitleDescriptionListTile( + return GameTile( onTap: () { setState(() { if (selectedColor == color) { From 2e1314ccd40b081fc17ed66e5f836240c75de9a9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 16:11:15 +0200 Subject: [PATCH 021/127] Implemented consistency changes --- .../create_game/create_game_view.dart | 61 ++++++++++++------- .../create_match/create_match_view.dart | 45 +++++++------- 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart index 2907720..c27ecac 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -183,6 +183,7 @@ class _CreateGameViewState extends State { body: SafeArea( child: Column( children: [ + // Game name input field Container( margin: CustomTheme.tileMargin, child: TextInputField( @@ -191,30 +192,35 @@ class _CreateGameViewState extends State { hintText: loc.game_name, ), ), - ChooseTile( - title: loc.ruleset, - trailingText: selectedRuleset == null - ? loc.none - : translateRulesetToString(selectedRuleset!, context), - onPressed: () async { - final result = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseRulesetView( - rulesets: _rulesets, - initialRulesetIndex: selectedRulesetIndex, + + // Choose ruleset tile + if (!isEditMode()) + ChooseTile( + title: loc.ruleset, + trailingText: selectedRuleset == null + ? loc.none + : translateRulesetToString(selectedRuleset!, context), + onPressed: () async { + final result = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => ChooseRulesetView( + rulesets: _rulesets, + initialRulesetIndex: selectedRulesetIndex, + ), ), - ), - ); - if (mounted) { - setState(() { - selectedRuleset = result; - selectedRulesetIndex = result == null - ? -1 - : _rulesets.indexWhere((r) => r.$1 == result); - }); - } - }, - ), + ); + if (mounted) { + setState(() { + selectedRuleset = result; + selectedRulesetIndex = result == null + ? -1 + : _rulesets.indexWhere((r) => r.$1 == result); + }); + } + }, + ), + + // Choose color tile ChooseTile( title: loc.color, trailingText: selectedColor == null @@ -234,6 +240,8 @@ class _CreateGameViewState extends State { } }, ), + + // Description input field Container( margin: CustomTheme.tileMargin, child: TextInputField( @@ -245,7 +253,10 @@ class _CreateGameViewState extends State { showCounterText: true, ), ), + const Spacer(), + + // Create/Edit game button Padding( padding: const EdgeInsets.all(12.0), child: CustomWidthButton( @@ -349,4 +360,8 @@ class _CreateGameViewState extends State { ); } } + + bool isEditMode() { + return widget.gameToEdit != null; + } } 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 b042ebb..945e68a 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 @@ -126,29 +126,30 @@ class _CreateMatchViewState extends State { ), // Game selection tile. - ChooseTile( - title: loc.game, - trailingText: selectedGame == null - ? loc.none_group - : selectedGame!.name, - onPressed: () async { - selectedGame = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseGameView( - games: gamesList, - initialGameId: selectedGame?.id ?? '', + if (!isEditMode()) + ChooseTile( + title: loc.game, + trailingText: selectedGame == null + ? loc.none_group + : selectedGame!.name, + onPressed: () async { + selectedGame = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => ChooseGameView( + games: gamesList, + initialGameId: selectedGame?.id ?? '', + ), ), - ), - ); - setState(() { - if (selectedGame != null) { - hintText = selectedGame!.name; - } else { - hintText = loc.match_name; - } - }); - }, - ), + ); + setState(() { + if (selectedGame != null) { + hintText = selectedGame!.name; + } else { + hintText = loc.match_name; + } + }); + }, + ), // Group selection tile. ChooseTile( From 92bf74683f4cdeaafcc514f8ad94959b3ca3e479 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 2 May 2026 16:32:25 +0200 Subject: [PATCH 022/127] feat: games with match associations cant be deleted --- lib/data/dao/game_dao.dart | 21 ++++++++++++++ .../create_match/choose_game_view.dart | 22 ++++++++++++++ .../create_game/create_game_view.dart | 29 ++++++++++--------- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index f07e2c7..98ac6c3 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -176,4 +176,25 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { final rowsAffected = await query.go(); return rowsAffected > 0; } + + /// Retrieves all games with their respective match counts. + /// Returns a list of tuples (Game, matchCount). + Future> getGameUsage() async { + final games = await getAllGames(); + + final results = <(Game, int)>[]; + + for (final game in games) { + final matchCount = + await (selectOnly(db.matchTable) + ..where(db.matchTable.gameId.equals(game.id)) + ..addColumns([db.matchTable.id.count()])) + .map((row) => row.read(db.matchTable.id.count())) + .getSingle(); + + results.add((game, matchCount ?? 0)); + } + + return results; + } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 42d5253..fca65bb 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart'; @@ -34,6 +36,10 @@ class ChooseGameView extends StatefulWidget { } class _ChooseGameViewState extends State { + late final AppDatabase db; + + late List<(Game, int)> gameCounts = []; + /// Controller for the search bar final TextEditingController searchBarController = TextEditingController(); @@ -45,6 +51,9 @@ class _ChooseGameViewState extends State { @override void initState() { + db = Provider.of(context, listen: false); + fetchGameCounts(); + selectedGameId = widget.initialGameId; // Start with all games visible @@ -150,6 +159,7 @@ class _ChooseGameViewState extends State { adaptivePageRoute( builder: (context) => CreateGameView( gameToEdit: game, + canDelete: canDeleteGame(game), onGameChanged: () { widget.onGamesUpdated?.call(); }, @@ -209,4 +219,16 @@ class _ChooseGameViewState extends State { void _refreshFromSource() { _applySearchFilter(searchBarController.text); } + + Future fetchGameCounts() async { + gameCounts = await db.gameDao.getGameUsage(); + } + + // A game can only be deleted if there are no matches using it + bool canDeleteGame(Game game) { + final count = gameCounts + .firstWhere((gc) => gc.$1.id == game.id, orElse: () => (game, 0)) + .$2; + return count == 0; + } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart index c27ecac..ba4d101 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -23,15 +23,18 @@ import 'package:tallee/presentation/widgets/tiles/choose_tile.dart'; class CreateGameView extends StatefulWidget { const CreateGameView({ super.key, - this.gameToEdit, required this.onGameChanged, + this.gameToEdit, + this.canDelete = false, }); + /// Callback to invoke when the game is created or edited + final VoidCallback onGameChanged; + /// An optional game to prefill the fields final Game? gameToEdit; - /// Callback to invoke when the game is created or edited - final VoidCallback onGameChanged; + final bool canDelete; @override State createState() => _CreateGameViewState(); @@ -41,7 +44,6 @@ class _CreateGameViewState extends State { /// GlobalKey for ScaffoldMessenger to show snackbars final _scaffoldMessengerKey = GlobalKey(); - /// The database instance for accessing game data. late final AppDatabase db; /// The currently selected ruleset for the game. @@ -133,13 +135,12 @@ class _CreateGameViewState extends State { backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( title: Text(isEditing ? loc.edit_game : loc.create_game), - actions: widget.gameToEdit == null - ? [] - : [ - IconButton( - icon: const Icon(Icons.delete), - onPressed: () async { - if (widget.gameToEdit != null) { + actions: [ + if (isEditMode()) + IconButton( + icon: const Icon(Icons.delete), + onPressed: widget.canDelete + ? () async { showDialog( context: context, builder: (context) => CustomAlertDialog( @@ -176,9 +177,9 @@ class _CreateGameViewState extends State { } }); } - }, - ), - ], + : null, + ), + ], ), body: SafeArea( child: Column( From e3aef81ab67dbb17df95a43064e0776c0704d81c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 3 May 2026 01:00:44 +0200 Subject: [PATCH 023/127] feat: Deleting games associated with matches deletes them --- lib/data/dao/match_dao.dart | 19 ++++ lib/l10n/arb/app_de.arb | 8 ++ lib/l10n/arb/app_en.arb | 8 ++ lib/l10n/generated/app_localizations.dart | 6 ++ lib/l10n/generated/app_localizations_de.dart | 11 +++ lib/l10n/generated/app_localizations_en.dart | 11 +++ .../create_match/choose_game_view.dart | 9 +- .../create_game/create_game_view.dart | 94 +++++++++++-------- .../buttons/animated_dialog_button.dart | 65 ++++++++----- .../widgets/dialog/custom_dialog_action.dart | 4 + test/db_tests/aggregates/match_test.dart | 50 ++++++++++ 11 files changed, 218 insertions(+), 67 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 93df7d7..48098ee 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -299,6 +299,25 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return count ?? 0; } + /// Retrieves the number of matches associated with a specific game. + Future getMatchCountByGame({required String gameId}) async { + final count = + await (selectOnly(matchTable) + ..where(matchTable.gameId.equals(gameId)) + ..addColumns([matchTable.id.count()])) + .map((row) => row.read(matchTable.id.count())) + .getSingle(); + return count ?? 0; + } + + /// Deletes all matches associated with a specific game. + /// Returns the number of matches deleted. + Future deleteMatchesByGame({required String gameId}) async { + final query = delete(matchTable)..where((m) => m.gameId.equals(gameId)); + final rowsAffected = await query.go(); + return rowsAffected; + } + /// Retrieves all matches associated with the given [groupId]. /// Queries the database directly, filtering by [groupId]. Future> getGroupMatches({required String groupId}) async { diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index ba4fe38..e518525 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -34,6 +34,14 @@ "delete": "Löschen", "delete_all_data": "Alle Daten löschen", "delete_game": "Spielvorlage löschen", + "delete_game_with_matches_warning": "Wenn du diese Spielvorlage löschst, werden {count, plural, =1{1 Spiel} other{{count} Spiele}} mit dieser Spielvorlage ebenfalls gelöscht.", + "@delete_game_with_matches_warning": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "delete_group": "Gruppe löschen", "delete_match": "Spiel löschen", "description": "Beschreibung", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 98d1c38..c01f0b2 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -389,6 +389,14 @@ "delete": "Delete", "delete_all_data": "Delete all data", "delete_game": "Delete Game", + "delete_game_with_matches_warning": "If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.", + "@delete_game_with_matches_warning": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "delete_group": "Delete Group", "delete_match": "Delete Match", "description": "Description", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 8e44e7b..790597f 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -302,6 +302,12 @@ abstract class AppLocalizations { /// **'Delete Game'** String get delete_game; + /// No description provided for @delete_game_with_matches_warning. + /// + /// In en, this message translates to: + /// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'** + String delete_game_with_matches_warning(int count); + /// Confirmation dialog for deleting a group /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 3c2b4e3..2b20848 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -114,6 +114,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String get delete_game => 'Spielvorlage löschen'; + @override + String delete_game_with_matches_warning(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Spiele', + one: '1 Spiel', + ); + return 'Wenn du diese Spielvorlage löschst, werden $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; + } + @override String get delete_group => 'Gruppe löschen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index e14b7a0..323d8c8 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -114,6 +114,17 @@ class AppLocalizationsEn extends AppLocalizations { @override String get delete_game => 'Delete Game'; + @override + String delete_game_with_matches_warning(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count matches', + one: '1 match', + ); + return 'If you delete this game template, $_temp0 using this game template will also be deleted.'; + } + @override String get delete_group => 'Delete Group'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index fca65bb..ef92638 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -159,7 +159,7 @@ class _ChooseGameViewState extends State { adaptivePageRoute( builder: (context) => CreateGameView( gameToEdit: game, - canDelete: canDeleteGame(game), + matchCount: getMatchCount(game), onGameChanged: () { widget.onGamesUpdated?.call(); }, @@ -224,11 +224,10 @@ class _ChooseGameViewState extends State { gameCounts = await db.gameDao.getGameUsage(); } - // A game can only be deleted if there are no matches using it - bool canDeleteGame(Game game) { - final count = gameCounts + // Returns the number of matches that use the given [game]. + int getMatchCount(Game game) { + return gameCounts .firstWhere((gc) => gc.$1.id == game.id, orElse: () => (game, 0)) .$2; - return count == 0; } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart index ba4d101..52e6c14 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -25,7 +25,7 @@ class CreateGameView extends StatefulWidget { super.key, required this.onGameChanged, this.gameToEdit, - this.canDelete = false, + this.matchCount = 0, }); /// Callback to invoke when the game is created or edited @@ -34,7 +34,7 @@ class CreateGameView extends StatefulWidget { /// An optional game to prefill the fields final Game? gameToEdit; - final bool canDelete; + final int matchCount; @override State createState() => _CreateGameViewState(); @@ -139,45 +139,59 @@ class _CreateGameViewState extends State { if (isEditMode()) IconButton( icon: const Icon(Icons.delete), - onPressed: widget.canDelete - ? () async { - showDialog( - context: context, - builder: (context) => CustomAlertDialog( - title: loc.delete_game, - content: Text(loc.this_cannot_be_undone), - actions: [ - CustomDialogAction( - onPressed: () => - Navigator.of(context).pop(false), - text: loc.cancel, - ), - CustomDialogAction( - onPressed: () => - Navigator.of(context).pop(true), - text: loc.delete, - ), - ], - ), - ).then((confirmed) async { - if (confirmed == true && context.mounted) { - bool success = await db.gameDao.deleteGame( - gameId: widget.gameToEdit!.id, - ); - if (!context.mounted) return; - if (success) { - widget.onGameChanged.call(); - Navigator.of( - context, - ).pop((game: widget.gameToEdit, delete: true)); - } else { - if (!mounted) return; - showSnackbar(message: loc.error_deleting_game); - } - } - }); + onPressed: () async { + if (!context.mounted) return; + + // Build the dialog content based on match count + final String dialogContent = widget.matchCount > 0 + ? loc.delete_game_with_matches_warning(widget.matchCount) + : loc.this_cannot_be_undone; + + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: loc.delete_game, + content: Text(dialogContent), + actions: [ + CustomDialogAction( + isDestructive: true, + onPressed: () => Navigator.of(context).pop(true), + text: loc.delete, + ), + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(false), + buttonType: ButtonType.secondary, + text: loc.cancel, + ), + ], + ), + ).then((confirmed) async { + if (confirmed == true && context.mounted) { + // Delete assocaited matches + if (widget.matchCount > 0) { + await db.matchDao.deleteMatchesByGame( + gameId: widget.gameToEdit!.id, + ); } - : null, + + // Delete the targetted game + bool success = await db.gameDao.deleteGame( + gameId: widget.gameToEdit!.id, + ); + + if (!context.mounted) return; + if (success) { + widget.onGameChanged.call(); + Navigator.of( + context, + ).pop((game: widget.gameToEdit, delete: true)); + } else { + if (!mounted) return; + showSnackbar(message: loc.error_deleting_game); + } + } + }); + }, ), ], ), diff --git a/lib/presentation/widgets/buttons/animated_dialog_button.dart b/lib/presentation/widgets/buttons/animated_dialog_button.dart index 70deea6..8c8765e 100644 --- a/lib/presentation/widgets/buttons/animated_dialog_button.dart +++ b/lib/presentation/widgets/buttons/animated_dialog_button.dart @@ -14,6 +14,7 @@ class AnimatedDialogButton extends StatefulWidget { required this.onPressed, this.buttonConstraints, this.buttonType = ButtonType.primary, + this.isDescructive = false, }); final String buttonText; @@ -24,6 +25,8 @@ class AnimatedDialogButton extends StatefulWidget { final ButtonType buttonType; + final bool isDescructive; + @override State createState() => _AnimatedDialogButtonState(); } @@ -33,28 +36,8 @@ class _AnimatedDialogButtonState extends State { @override Widget build(BuildContext context) { - final textStyling = TextStyle( - color: widget.buttonType == ButtonType.primary - ? Colors.black - : Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ); - - final buttonDecoration = widget.buttonType == ButtonType.primary - // Primary - ? BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ) - : widget.buttonType == ButtonType.secondary - // Secondary - ? BoxDecoration( - border: BoxBorder.all(color: Colors.white, width: 2), - borderRadius: BorderRadius.circular(12), - ) - // Tertiary - : const BoxDecoration(); + final textStyling = _getTextStyling(); + final buttonDecoration = _getButtonDecoration(); return GestureDetector( onTapDown: (_) => setState(() => _isPressed = true), @@ -84,4 +67,42 @@ class _AnimatedDialogButtonState extends State { ), ); } + + TextStyle _getTextStyling() { + late Color textColor; + if (widget.buttonType == ButtonType.primary) { + textColor = widget.isDescructive ? Colors.white : Colors.black; + } else if (widget.buttonType == ButtonType.secondary) { + textColor = widget.isDescructive ? Colors.red : Colors.white; + } else { + textColor = widget.isDescructive ? Colors.red : Colors.white; + } + + return TextStyle( + color: textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ); + } + + BoxDecoration _getButtonDecoration() { + if (widget.buttonType == ButtonType.primary) { + // Primary + return BoxDecoration( + color: widget.isDescructive ? Colors.red : Colors.white, + borderRadius: BorderRadius.circular(12), + ); + } else if (widget.buttonType == ButtonType.secondary) { + // Secondary + return BoxDecoration( + border: BoxBorder.all( + color: widget.isDescructive ? Colors.red : Colors.white, + width: 2, + ), + borderRadius: BorderRadius.circular(12), + ); + } + // Tertiary + return const BoxDecoration(); + } } diff --git a/lib/presentation/widgets/dialog/custom_dialog_action.dart b/lib/presentation/widgets/dialog/custom_dialog_action.dart index aec0dfa..47024dc 100644 --- a/lib/presentation/widgets/dialog/custom_dialog_action.dart +++ b/lib/presentation/widgets/dialog/custom_dialog_action.dart @@ -12,6 +12,7 @@ class CustomDialogAction extends StatelessWidget { required this.onPressed, required this.text, this.buttonType = ButtonType.primary, + this.isDestructive = false, }); final String text; @@ -20,12 +21,15 @@ class CustomDialogAction extends StatelessWidget { final VoidCallback onPressed; + final bool isDestructive; + @override Widget build(BuildContext context) { return AnimatedDialogButton( onPressed: onPressed, buttonText: text, buttonType: buttonType, + isDescructive: isDestructive, buttonConstraints: const BoxConstraints(minWidth: 300), ); } diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 3305b9a..9ba33ac 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -366,5 +366,55 @@ void main() { expect(match.group, isNotNull); expect(match.group!.id, testGroup1.id); }); + + test('getMatchCountByGame() works correctly', () async { + var count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 0); + + await database.matchDao.addMatch(match: testMatch1); + count = await database.matchDao.getMatchCountByGame(gameId: testGame.id); + expect(count, 1); + + await database.matchDao.addMatch(match: testMatch2); + count = await database.matchDao.getMatchCountByGame(gameId: testGame.id); + expect(count, 2); + }); + + test('getMatchCountByGame() returns 0 for non-existent game', () async { + final count = await database.matchDao.getMatchCountByGame( + gameId: 'non-existent-game-id', + ); + expect(count, 0); + }); + + test('deleteMatchesByGame() deletes all matches for a game', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.matchDao.addMatch(match: testMatch2); + + var count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 2); + + final deletedCount = await database.matchDao.deleteMatchesByGame( + gameId: testGame.id, + ); + expect(deletedCount, 2); + + count = await database.matchDao.getMatchCountByGame(gameId: testGame.id); + expect(count, 0); + + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); + + test('deleteMatchesByGame() returns 0 for non-existent game', () async { + final deletedCount = await database.matchDao.deleteMatchesByGame( + gameId: 'non-existent-game-id', + ); + expect(deletedCount, 0); + }); }); } From 5d832c98a79a5cf19cac72ac68d305c45bf2d807 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 3 May 2026 10:45:18 +0200 Subject: [PATCH 024/127] fix: callbacks when game deletes matches --- .../main_menu/match_view/create_match/choose_game_view.dart | 1 + .../match_view/create_match/create_match_view.dart | 4 ++++ lib/presentation/views/main_menu/match_view/match_view.dart | 6 ++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index ef92638..fc9e76c 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -177,6 +177,7 @@ class _ChooseGameViewState extends State { if (result.delete) { setState(() { widget.games.removeAt(originalIndex); + widget.onGamesUpdated?.call(); }); } else { setState(() { 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 945e68a..6f1bf95 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,10 +28,13 @@ class CreateMatchView extends StatefulWidget { this.onWinnerChanged, this.matchToEdit, this.onMatchUpdated, + this.onMatchesUpdated, }); final VoidCallback? onWinnerChanged; + final VoidCallback? onMatchesUpdated; + final void Function(Match)? onMatchUpdated; /// An optional match to prefill the fields for editing. @@ -138,6 +141,7 @@ class _CreateMatchViewState extends State { builder: (context) => ChooseGameView( games: gamesList, initialGameId: selectedGame?.id ?? '', + onGamesUpdated: widget.onMatchesUpdated, ), ), ); 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 2fb36e7..b7b9147 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -118,8 +118,10 @@ class _MatchViewState extends State { Navigator.push( context, adaptivePageRoute( - builder: (context) => - CreateMatchView(onWinnerChanged: loadMatches), + builder: (context) => CreateMatchView( + onWinnerChanged: loadMatches, + onMatchesUpdated: loadMatches, + ), ), ); }, From 6272794cc5347f961c108c7973324cb390dad13c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 3 May 2026 10:59:46 +0200 Subject: [PATCH 025/127] feat: showing game color in choose tile --- lib/data/models/game.dart | 10 +++--- .../create_game/create_game_view.dart | 31 ++++++++++++++----- .../create_match/create_match_view.dart | 12 +++---- .../widgets/tiles/choose_tile.dart | 8 ++--- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index 4888df4..1699fc0 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -14,15 +14,13 @@ class Game { Game({ required this.name, required this.ruleset, - required this.color, + this.color = GameColor.orange, + this.description = '', + this.icon = '', String? id, DateTime? createdAt, - String? description, - String? icon, }) : id = id ?? const Uuid().v4(), - createdAt = createdAt ?? clock.now(), - description = description ?? '', - icon = icon ?? ''; + createdAt = createdAt ?? clock.now(); @override String toString() { diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart index 52e6c14..7351b4d 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -56,7 +56,7 @@ class _CreateGameViewState extends State { late List<(Ruleset, String)> _rulesets; /// The currently selected color for the game. - GameColor? selectedColor; + GameColor? selectedColor = GameColor.orange; /// Controller for the game name input field. final _gameNameController = TextEditingController(); @@ -212,9 +212,11 @@ class _CreateGameViewState extends State { if (!isEditMode()) ChooseTile( title: loc.ruleset, - trailingText: selectedRuleset == null - ? loc.none - : translateRulesetToString(selectedRuleset!, context), + trailing: selectedRuleset == null + ? Text(loc.none) + : Text( + translateRulesetToString(selectedRuleset!, context), + ), onPressed: () async { final result = await Navigator.of(context).push( adaptivePageRoute( @@ -238,9 +240,24 @@ class _CreateGameViewState extends State { // Choose color tile ChooseTile( title: loc.color, - trailingText: selectedColor == null - ? loc.none - : translateGameColorToString(selectedColor!, context), + trailing: selectedColor == null + ? Text(loc.none) + : Row( + children: [ + Text( + translateGameColorToString(selectedColor!, context), + ), + Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(left: 12), + decoration: BoxDecoration( + color: getColorFromGameColor(selectedColor!), + shape: BoxShape.circle, + ), + ), + ], + ), onPressed: () async { final result = await Navigator.of(context).push( adaptivePageRoute( 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 6f1bf95..f0c0bf2 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 @@ -132,9 +132,9 @@ class _CreateMatchViewState extends State { if (!isEditMode()) ChooseTile( title: loc.game, - trailingText: selectedGame == null - ? loc.none_group - : selectedGame!.name, + trailing: selectedGame == null + ? Text(loc.none_group) + : Text(selectedGame!.name), onPressed: () async { selectedGame = await Navigator.of(context).push( adaptivePageRoute( @@ -158,9 +158,9 @@ class _CreateMatchViewState extends State { // Group selection tile. ChooseTile( title: loc.group, - trailingText: selectedGroup == null - ? loc.none_group - : selectedGroup!.name, + trailing: selectedGroup == null + ? Text(loc.none_group) + : Text(selectedGroup!.name), onPressed: () async { // Remove all players from the previously selected group from // the selected players list, in case the user deselects the diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index 10ded6b..234c663 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -4,12 +4,12 @@ import 'package:tallee/core/custom_theme.dart'; class ChooseTile extends StatefulWidget { /// A tile widget that allows users to choose an option by tapping on it. /// - [title]: The title text displayed on the tile. - /// - [trailingText]: Optional trailing text displayed on the tile. + /// - [trailing]: Optional trailing text displayed on the tile. /// - [onPressed]: The callback invoked when the tile is tapped. const ChooseTile({ super.key, required this.title, - this.trailingText, + this.trailing, this.onPressed, }); @@ -20,7 +20,7 @@ class ChooseTile extends StatefulWidget { final VoidCallback? onPressed; /// Optional trailing text displayed on the tile. - final String? trailingText; + final Widget? trailing; @override State createState() => _ChooseTileState(); @@ -42,7 +42,7 @@ class _ChooseTileState extends State { style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const Spacer(), - if (widget.trailingText != null) Text(widget.trailingText!), + if (widget.trailing != null) widget.trailing!, const SizedBox(width: 10), const Icon(Icons.arrow_forward_ios, size: 16), ], From 8c52db9981e066018d062c55360b18a9f8c3eeb3 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 10:43:43 +0200 Subject: [PATCH 026/127] Added popups to create game view --- .../create_game/create_game_view.dart | 240 ++++++++++++++---- .../widgets/tiles/choose_tile.dart | 9 +- pubspec.yaml | 1 + 3 files changed, 198 insertions(+), 52 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart index 7351b4d..0c3fde6 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_popup/flutter_popup.dart'; import 'package:provider/provider.dart'; -import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/constants.dart'; import 'package:tallee/core/custom_theme.dart'; @@ -9,8 +9,6 @@ import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; @@ -55,6 +53,9 @@ class _CreateGameViewState extends State { /// A list of available rulesets and their localized names. late List<(Ruleset, String)> _rulesets; + /// A list of available game colors and their localized names. + late List<(GameColor, String)> _colors; + /// The currently selected color for the game. GameColor? selectedColor = GameColor.orange; @@ -105,6 +106,16 @@ class _CreateGameViewState extends State { translateRulesetToString(Ruleset.multipleWinners, context), ), ]; + _colors = [ + (GameColor.green, translateGameColorToString(GameColor.green, context)), + (GameColor.teal, translateGameColorToString(GameColor.teal, context)), + (GameColor.blue, translateGameColorToString(GameColor.blue, context)), + (GameColor.purple, translateGameColorToString(GameColor.purple, context)), + (GameColor.pink, translateGameColorToString(GameColor.pink, context)), + (GameColor.red, translateGameColorToString(GameColor.red, context)), + (GameColor.orange, translateGameColorToString(GameColor.orange, context)), + (GameColor.yellow, translateGameColorToString(GameColor.yellow, context)), + ]; if (widget.gameToEdit != null) { _gameNameController.text = widget.gameToEdit!.name; @@ -212,65 +223,192 @@ class _CreateGameViewState extends State { if (!isEditMode()) ChooseTile( title: loc.ruleset, - trailing: selectedRuleset == null - ? Text(loc.none) - : Text( - translateRulesetToString(selectedRuleset!, context), - ), - onPressed: () async { - final result = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseRulesetView( - rulesets: _rulesets, - initialRulesetIndex: selectedRulesetIndex, + trailing: CustomPopup( + showArrow: true, + arrowColor: CustomTheme.boxBorderColor, + contentPadding: const EdgeInsets.symmetric( + horizontal: 0, + vertical: 10, + ), + barrierColor: Colors.transparent, + contentDecoration: CustomTheme.standardBoxDecoration, + content: StatefulBuilder( + builder: (context, setPopupState) => SizedBox( + width: 250, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + _rulesets.length, + (index) => GestureDetector( + onTap: () { + setState(() { + selectedRuleset = _rulesets[index].$1; + }); + setPopupState(() {}); + }, + child: Column( + children: [ + Container( + margin: const EdgeInsets.symmetric( + horizontal: 12, + ), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + color: + selectedRuleset == _rulesets[index].$1 + ? CustomTheme.textColor.withAlpha(20) + : Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: Row( + spacing: 8, + children: [ + Text( + _rulesets[index].$2, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 15, + ), + ), + ], + ), + ), + ), + if (index < _rulesets.length - 1) + const Divider(indent: 15, endIndent: 15), + ], + ), + ), + ), ), ), - ); - if (mounted) { - setState(() { - selectedRuleset = result; - selectedRulesetIndex = result == null - ? -1 - : _rulesets.indexWhere((r) => r.$1 == result); - }); - } - }, + ), + child: selectedRuleset == null + ? Text(loc.none) + : Text( + translateRulesetToString(selectedRuleset!, context), + ), + ), ), // Choose color tile ChooseTile( title: loc.color, - trailing: selectedColor == null - ? Text(loc.none) - : Row( - children: [ - Text( - translateGameColorToString(selectedColor!, context), - ), - Container( - width: 16, - height: 16, - margin: const EdgeInsets.only(left: 12), - decoration: BoxDecoration( - color: getColorFromGameColor(selectedColor!), - shape: BoxShape.circle, + trailing: Row( + spacing: 8, + children: [ + // Selected Color + Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(left: 12), + decoration: BoxDecoration( + color: getColorFromGameColor(selectedColor!), + shape: BoxShape.circle, + ), + ), + + //Popup + CustomPopup( + showArrow: true, + arrowColor: CustomTheme.boxBorderColor, + contentPadding: const EdgeInsets.symmetric( + horizontal: 0, + vertical: 10, + ), + barrierColor: Colors.transparent, + contentDecoration: CustomTheme.standardBoxDecoration, + content: StatefulBuilder( + builder: (context, setPopupState) => SizedBox( + width: 150, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + _colors.length, + (index) => GestureDetector( + onTap: () { + setState(() { + selectedColor = _colors[index].$1; + }); + setPopupState(() {}); + }, + child: Column( + children: [ + // Selected Highlighting + Container( + margin: const EdgeInsets.symmetric( + horizontal: 12, + ), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + color: + selectedColor == _colors[index].$1 + ? CustomTheme.textColor.withAlpha( + 20, + ) + : Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 6, + ), + child: Row( + spacing: 8, + children: selectedColor == null + ? [Text(loc.none)] + : [ + Container( + width: 16, + height: 16, + margin: + const EdgeInsets.only( + left: 12, + ), + decoration: BoxDecoration( + color: + getColorFromGameColor( + _colors[index].$1, + ), + shape: BoxShape.circle, + ), + ), + Text( + _colors[index].$2, + style: const TextStyle( + color: + CustomTheme.textColor, + fontSize: 15, + ), + ), + ], + ), + ), + ), + if (index < _colors.length - 1) + const Divider(indent: 15, endIndent: 15), + ], + ), + ), ), ), - ], + ), + ), + child: Text( + translateGameColorToString(selectedColor!, context), ), - onPressed: () async { - final result = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => - ChooseColorView(initialColor: selectedColor), ), - ); - if (mounted) { - setState(() { - selectedColor = result; - }); - } - }, + ], + ), ), // Description input field diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index 234c663..1f72328 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:tallee/core/custom_theme.dart'; @@ -44,7 +46,12 @@ class _ChooseTileState extends State { const Spacer(), if (widget.trailing != null) widget.trailing!, const SizedBox(width: 10), - const Icon(Icons.arrow_forward_ios, size: 16), + widget.onPressed == null + ? Transform.rotate( + angle: pi / 2, + child: const Icon(Icons.arrow_forward_ios, size: 16), + ) + : const Icon(Icons.arrow_forward_ios, size: 16), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 363ea7f..d9964fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + flutter_popup: ^3.3.9 fluttericon: ^2.0.0 font_awesome_flutter: ^11.0.0 intl: any From 94bb477cd9141da5ed6c3c626953492546b480b1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 10:44:40 +0200 Subject: [PATCH 027/127] Added popups to create game view replacing two screens --- .../create_match/choose_game_view.dart | 2 +- .../create_game/choose_color_view.dart | 78 --------------- .../create_game/choose_ruleset_view.dart | 99 ------------------- .../{create_game => }/create_game_view.dart | 0 4 files changed, 1 insertion(+), 178 deletions(-) delete mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart delete mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart rename lib/presentation/views/main_menu/match_view/create_match/{create_game => }/create_game_view.dart (100%) diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index fc9e76c..cdd73c2 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -6,7 +6,7 @@ import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game_view.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart deleted file mode 100644 index e6d0b7e..0000000 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_color_view.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tallee/core/common.dart'; -import 'package:tallee/core/custom_theme.dart'; -import 'package:tallee/core/enums.dart'; -import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; - -class ChooseColorView extends StatefulWidget { - /// A view that allows the user to choose a color from a list of available game colors - /// - [initialColor]: The initially selected color - const ChooseColorView({super.key, this.initialColor}); - - /// The initially selected color - final GameColor? initialColor; - - @override - State createState() => _ChooseColorViewState(); -} - -class _ChooseColorViewState extends State { - /// Currently selected color - GameColor? selectedColor; - - @override - void initState() { - selectedColor = widget.initialColor; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - const colors = GameColor.values; - - return Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: () { - Navigator.of(context).pop(selectedColor); - }, - ), - title: Text(loc.choose_color), - ), - body: PopScope( - canPop: false, - onPopInvokedWithResult: (bool didPop, Object? result) { - if (didPop) return; - Navigator.of(context).pop(selectedColor); - }, - child: ListView.builder( - padding: const EdgeInsets.only(bottom: 85), - itemCount: colors.length, - itemBuilder: (BuildContext context, int index) { - final color = colors[index]; - return GameTile( - onTap: () { - setState(() { - if (selectedColor == color) { - selectedColor = null; - } else { - selectedColor = color; - } - }); - }, - title: translateGameColorToString(color, context), - description: '', - isHighlighted: selectedColor == color, - badgeText: ' ', //Breite für Color Badge - badgeColor: getColorFromGameColor(color), - ); - }, - ), - ), - ); - } -} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart deleted file mode 100644 index 6b69b22..0000000 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game/choose_ruleset_view.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tallee/core/common.dart'; -import 'package:tallee/core/custom_theme.dart'; -import 'package:tallee/core/enums.dart'; -import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; - -class ChooseRulesetView extends StatefulWidget { - /// A view that allows the user to choose a ruleset from a list of available rulesets - /// - [rulesets]: A list of tuples containing the ruleset and its description - /// - [initialRulesetIndex]: The index of the initially selected ruleset - const ChooseRulesetView({ - super.key, - required this.rulesets, - required this.initialRulesetIndex, - }); - - /// A list of tuples containing the ruleset and its description - final List<(Ruleset, String)> rulesets; - - /// The index of the initially selected ruleset - final int initialRulesetIndex; - @override - State createState() => _ChooseRulesetViewState(); -} - -class _ChooseRulesetViewState extends State { - /// Currently selected ruleset index - late int selectedRulesetIndex; - - @override - void initState() { - selectedRulesetIndex = widget.initialRulesetIndex; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return DefaultTabController( - length: 2, - initialIndex: 0, - child: Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: () { - Navigator.of(context).pop( - selectedRulesetIndex == -1 - ? null - : widget.rulesets[selectedRulesetIndex].$1, - ); - }, - ), - title: Text(loc.choose_ruleset), - ), - body: PopScope( - // This fixes that the Android Back Gesture didn't return the - // selectedRulesetIndex and therefore the selected Ruleset wasn't saved - canPop: false, - onPopInvokedWithResult: (bool didPop, Object? result) { - if (didPop) { - return; - } - Navigator.of(context).pop( - selectedRulesetIndex == -1 - ? null - : widget.rulesets[selectedRulesetIndex].$1, - ); - }, - child: ListView.builder( - padding: const EdgeInsets.only(bottom: 85), - itemCount: widget.rulesets.length, - itemBuilder: (BuildContext context, int index) { - return TitleDescriptionListTile( - onTap: () async { - setState(() { - if (selectedRulesetIndex == index) { - selectedRulesetIndex = -1; - } else { - selectedRulesetIndex = index; - } - }); - }, - title: translateRulesetToString( - widget.rulesets[index].$1, - context, - ), - description: widget.rulesets[index].$2, - isHighlighted: selectedRulesetIndex == index, - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart similarity index 100% rename from lib/presentation/views/main_menu/match_view/create_match/create_game/create_game_view.dart rename to lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart From 8194fb2f282d7be2f5230e77784ea784a0a3da9e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 11:03:34 +0200 Subject: [PATCH 028/127] Removed selectedRulesetIndex --- .../create_match/create_game_view.dart | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 0c3fde6..415794a 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -44,19 +44,10 @@ class _CreateGameViewState extends State { late final AppDatabase db; - /// The currently selected ruleset for the game. + late List<(Ruleset, String)> _rulesets; Ruleset? selectedRuleset; - /// The index of the currently selected ruleset. - int selectedRulesetIndex = -1; - - /// A list of available rulesets and their localized names. - late List<(Ruleset, String)> _rulesets; - - /// A list of available game colors and their localized names. late List<(GameColor, String)> _colors; - - /// The currently selected color for the game. GameColor? selectedColor = GameColor.orange; /// Controller for the game name input field. @@ -122,10 +113,7 @@ class _CreateGameViewState extends State { _descriptionController.text = widget.gameToEdit!.description; selectedRuleset = widget.gameToEdit!.ruleset; selectedColor = widget.gameToEdit!.color; - - selectedRulesetIndex = _rulesets.indexWhere( - (r) => r.$1 == selectedRuleset, - ); + selectedRuleset = widget.gameToEdit!.ruleset; } } @@ -435,7 +423,7 @@ class _CreateGameViewState extends State { buttonType: ButtonType.primary, onPressed: _gameNameController.text.trim().isNotEmpty && - selectedRulesetIndex != -1 && + selectedRuleset != null && selectedColor != null ? () async { Game newGame = Game( From b53facc16c35f6eafac41d350635b7396f43743f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 11:03:59 +0200 Subject: [PATCH 029/127] Added singleWinner as default ruleset --- .../main_menu/match_view/create_match/create_game_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 415794a..6928c78 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -45,7 +45,7 @@ class _CreateGameViewState extends State { late final AppDatabase db; late List<(Ruleset, String)> _rulesets; - Ruleset? selectedRuleset; + Ruleset? selectedRuleset = Ruleset.singleWinner; late List<(GameColor, String)> _colors; GameColor? selectedColor = GameColor.orange; From a9b86fe7ffeb9ad3ee494d7797ae6e98c79c6167 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 11:09:05 +0200 Subject: [PATCH 030/127] Added icons to rulesets --- lib/core/common.dart | 16 +++++++++++++++ .../create_match/create_game_view.dart | 20 +++++++++++++------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 14d90aa..fc61a94 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -66,6 +66,22 @@ Color getColorFromGameColor(GameColor color) { } } +/// Returns [IconData] corresponding to a [Ruleset] enum value. +IconData getRulesetIcon(Ruleset ruleset) { + switch (ruleset) { + case Ruleset.highestScore: + return Icons.arrow_upward; + case Ruleset.lowestScore: + return Icons.arrow_downward; + case Ruleset.singleWinner: + return Icons.emoji_events; + case Ruleset.singleLoser: + return Icons.sentiment_dissatisfied; + case Ruleset.multipleWinners: + return Icons.group; + } +} + /// Counts how many players in the [match] are not part of the group /// /// Returns the text you append after the group name, e.g. " + 5" or an empty diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 6928c78..aeb2099 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -222,7 +222,7 @@ class _CreateGameViewState extends State { contentDecoration: CustomTheme.standardBoxDecoration, content: StatefulBuilder( builder: (context, setPopupState) => SizedBox( - width: 250, + width: 280, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -258,6 +258,10 @@ class _CreateGameViewState extends State { child: Row( spacing: 8, children: [ + Icon( + getRulesetIcon(_rulesets[index].$1), + size: 16, + ), Text( _rulesets[index].$2, style: const TextStyle( @@ -278,11 +282,15 @@ class _CreateGameViewState extends State { ), ), ), - child: selectedRuleset == null - ? Text(loc.none) - : Text( - translateRulesetToString(selectedRuleset!, context), - ), + child: Row( + children: [ + Icon(getRulesetIcon(selectedRuleset!), size: 16), + SizedBox(width: 5), + Text( + translateRulesetToString(selectedRuleset!, context), + ), + ], + ), ), ), From ea5577c288d8280263a146e5d2c5d07dcb2e6acd Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:00:00 +0200 Subject: [PATCH 031/127] Implemented new live edit mode --- .../match_view/match_result_view.dart | 239 +++++++++++------- .../live_edit_list_tile.dart | 97 +++++++ pubspec.yaml | 1 + 3 files changed, 241 insertions(+), 96 deletions(-) create mode 100644 lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart 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 8b41920..146b984 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 @@ -8,8 +8,9 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart'; -import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart'; class MatchResultView extends StatefulWidget { /// A view that allows selecting and saving the winner of a match @@ -30,6 +31,8 @@ class MatchResultView extends StatefulWidget { class _MatchResultViewState extends State { late final AppDatabase db; + bool isLiveEditMode = false; + late final Ruleset ruleset; /// List of all players who participated in the match @@ -88,115 +91,159 @@ class _MatchResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () { - widget.onWinnerChanged?.call(); - Navigator.of(context).pop(_selectedPlayer); - }, - ), + leading: isLiveEditMode + ? IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () { + setState(() { + isLiveEditMode = false; + }); + }, + ) + : IconButton( + icon: const Icon(Icons.close), + onPressed: () { + widget.onWinnerChanged?.call(); + Navigator.of(context).pop(_selectedPlayer); + }, + ), title: Text(widget.match.name), ), body: SafeArea( child: Column( children: [ Expanded( - child: Container( - margin: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 10, - ), - decoration: BoxDecoration( - color: CustomTheme.boxColor, - border: Border.all(color: CustomTheme.boxBorderColor), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${getTitleForRuleset(loc)}:', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - if (rulesetSupportsWinnerSelection()) - Expanded( - child: RadioGroup( - groupValue: _selectedPlayer, - onChanged: (Player? value) async { + child: isLiveEditMode && rulesetSupportsScoreEntry() + ? ListView.builder( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return LiveEditListTile( + title: allPlayers[index].name, + onChanged: (value) { setState(() { - _selectedPlayer = value; + controller[index].text = value.toString(); }); }, - child: ListView.builder( - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return CustomRadioListTile( - text: allPlayers[index].name, - value: allPlayers[index], - onContainerTap: (value) async { + value: int.tryParse(controller[index].text) ?? 0, + ); + }, + ) + : Container( + margin: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 10, + ), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorderColor), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${getTitleForRuleset(loc)}:', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + if (rulesetSupportsWinnerSelection()) + Expanded( + child: RadioGroup( + groupValue: _selectedPlayer, + onChanged: (Player? value) async { setState(() { - // Check if the already selected player is the same as the newly tapped player. - if (_selectedPlayer == value) { - // If yes deselected the player by setting it to null. - _selectedPlayer = null; - } else { - // If no assign the newly tapped player to the selected player. - (_selectedPlayer = value); - } + _selectedPlayer = value; }); }, - ); - }, - ), - ), + child: ListView.builder( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomRadioListTile( + text: allPlayers[index].name, + value: allPlayers[index], + onContainerTap: (value) async { + setState(() { + // Check if the already selected player is the same as the newly tapped player. + if (_selectedPlayer == value) { + // If yes deselected the player by setting it to null. + _selectedPlayer = null; + } else { + // If no assign the newly tapped player to the selected player. + (_selectedPlayer = value); + } + }); + }, + ); + }, + ), + ), + ), + if (rulesetSupportsScoreEntry()) + Expanded( + child: ListView.separated( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return ScoreListTile( + text: allPlayers[index].name, + controller: controller[index], + ); + }, + separatorBuilder: + (BuildContext context, int index) { + return const Padding( + padding: EdgeInsets.symmetric( + vertical: 8.0, + ), + child: Divider(indent: 20), + ); + }, + ), + ), + ], ), - if (rulesetSupportsScoreEntry()) - Expanded( - child: ListView.separated( - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return ScoreListTile( - text: allPlayers[index].name, - controller: controller[index], - ); - }, - separatorBuilder: (BuildContext context, int index) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Divider(indent: 20), - ); - }, - ), - ), - ], + ), + ), + if (!isLiveEditMode) ...[ + if (rulesetSupportsScoreEntry()) + // Button to switch to live edit mode + ...[ + CustomWidthButton( + text: 'Live-Edit Modus', + sizeRelativeToWidth: 0.95, + buttonType: ButtonType.secondary, + onPressed: () => setState(() { + isLiveEditMode = true; + }), ), + const SizedBox(height: 10), + ], + + // Save Changes Button + CustomWidthButton( + text: loc.save_changes, + sizeRelativeToWidth: 0.95, + onPressed: canSave + ? () async { + final ending = DateTime.now(); + await db.matchDao.updateMatchEndedAt( + matchId: widget.match.id, + endedAt: ending, + ); + await _handleSaving(); + if (!context.mounted) return; + Navigator.of(context).pop(_selectedPlayer); + } + : null, ), - ), - CustomWidthButton( - text: loc.save_changes, - sizeRelativeToWidth: 0.95, - onPressed: canSave - ? () async { - final ending = DateTime.now(); - await db.matchDao.updateMatchEndedAt( - matchId: widget.match.id, - endedAt: ending, - ); - await _handleSaving(); - if (!context.mounted) return; - Navigator.of(context).pop(_selectedPlayer); - } - : null, - ), + ], ], ), ), diff --git a/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart new file mode 100644 index 0000000..4021755 --- /dev/null +++ b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_numeric_text/flutter_numeric_text.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; + +class LiveEditListTile extends StatefulWidget { + const LiveEditListTile({ + super.key, + required this.title, + required this.value, + this.onChanged, + }); + + final String title; + + final int value; + + final void Function(int newValue)? onChanged; + + @override + State createState() => _LiveEditListTileState(); +} + +class _LiveEditListTileState extends State { + int _score = 0; + final int maxScore = 9999; + final int minScore = -9999; + + @override + void initState() { + _score = widget.value; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 10), + margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: CustomTheme.standardBoxDecoration, + child: Column( + children: [ + Text( + widget.title, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + Padding( + padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MainMenuButton( + onPressed: () => _score > minScore + ? { + setState(() { + _score--; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.remove_rounded, + ), + SizedBox( + width: 150, + child: NumericText( + _score.toString(), + maxLines: 1, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 48, + fontWeight: FontWeight.w600, + ), + ), + ), + MainMenuButton( + onPressed: () => _score < maxScore + ? { + setState(() { + _score++; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.add_rounded, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 363ea7f..2b66b83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + flutter_numeric_text: ^1.3.3 fluttericon: ^2.0.0 font_awesome_flutter: ^11.0.0 intl: any From 5877793b99e9315c4a9ad4f98aaad207030634f7 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:00:13 +0200 Subject: [PATCH 032/127] Updated secondary button style --- lib/presentation/widgets/buttons/custom_width_button.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/widgets/buttons/custom_width_button.dart b/lib/presentation/widgets/buttons/custom_width_button.dart index 489ceae..4fde6f8 100644 --- a/lib/presentation/widgets/buttons/custom_width_button.dart +++ b/lib/presentation/widgets/buttons/custom_width_button.dart @@ -89,7 +89,7 @@ class CustomWidthButton extends StatelessWidget { MediaQuery.sizeOf(context).width * sizeRelativeToWidth, 60, ), - side: BorderSide(color: borderSideColor, width: 2), + side: BorderSide(color: borderSideColor, width: 3), shape: RoundedRectangleBorder( borderRadius: CustomTheme.standardBorderRadiusAll, ), From 1e95f1997dba1dee5837e7c21840fa07cb3c4518 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:00:26 +0200 Subject: [PATCH 033/127] Updated border color --- .../widgets/tiles/{ => match_result_view}/score_list_tile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename lib/presentation/widgets/tiles/{ => match_result_view}/score_list_tile.dart (97%) diff --git a/lib/presentation/widgets/tiles/score_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart similarity index 97% rename from lib/presentation/widgets/tiles/score_list_tile.dart rename to lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart index 52103fa..64492ae 100644 --- a/lib/presentation/widgets/tiles/score_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart @@ -62,7 +62,7 @@ class ScoreListTile extends StatelessWidget { enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( - color: CustomTheme.textColor.withAlpha(100), + color: CustomTheme.textColor.withAlpha(250), width: 2, ), ), From 3dfd2c7c087745697552b452aa61dba9c66284d9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:00:33 +0200 Subject: [PATCH 034/127] Moved file --- .../tiles/{ => match_result_view}/custom_radio_list_tile.dart | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/presentation/widgets/tiles/{ => match_result_view}/custom_radio_list_tile.dart (100%) diff --git a/lib/presentation/widgets/tiles/custom_radio_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart similarity index 100% rename from lib/presentation/widgets/tiles/custom_radio_list_tile.dart rename to lib/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart From e25a2bde690dc4374d74a43b6da9c036cc16aff6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:01:22 +0200 Subject: [PATCH 035/127] Added delay before reversing animation --- lib/presentation/widgets/buttons/main_menu_button.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index c583456..5eb76f1 100644 --- a/lib/presentation/widgets/buttons/main_menu_button.dart +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -53,10 +53,11 @@ class _MainMenuButtonState extends State _animationController.forward(); }, onTapUp: (_) async { - await _animationController.reverse(); if (mounted) { widget.onPressed(); } + await Future.delayed(const Duration(milliseconds: 100)); + await _animationController.reverse(); }, onTapCancel: () { _animationController.reverse(); From 3be7dac22751dc5ddd189849394eda8d8bb7d43f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:29:28 +0200 Subject: [PATCH 036/127] Added button to leave live edit mode --- .../match_view/match_result_view.dart | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) 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 146b984..549d86c 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 @@ -8,6 +8,7 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart'; @@ -91,22 +92,15 @@ class _MatchResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( - leading: isLiveEditMode + leading: !isLiveEditMode ? IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: () { - setState(() { - isLiveEditMode = false; - }); - }, - ) - : IconButton( icon: const Icon(Icons.close), onPressed: () { widget.onWinnerChanged?.call(); Navigator.of(context).pop(_selectedPlayer); }, - ), + ) + : null, title: Text(widget.match.name), ), body: SafeArea( @@ -115,17 +109,34 @@ class _MatchResultViewState extends State { Expanded( child: isLiveEditMode && rulesetSupportsScoreEntry() ? ListView.builder( - itemCount: allPlayers.length, + itemCount: allPlayers.length + 1, itemBuilder: (context, index) { - return LiveEditListTile( - title: allPlayers[index].name, - onChanged: (value) { - setState(() { - controller[index].text = value.toString(); - }); - }, - value: int.tryParse(controller[index].text) ?? 0, - ); + if (index < allPlayers.length) { + return LiveEditListTile( + title: allPlayers[index].name, + onChanged: (value) { + setState(() { + controller[index].text = value.toString(); + }); + }, + value: int.tryParse(controller[index].text) ?? 0, + ); + } else { + return Center( + child: Padding( + padding: const EdgeInsets.only(top: 30), + child: MainMenuButton( + text: 'Ansicht verlassen', + onPressed: () => { + setState(() { + isLiveEditMode = false; + }), + }, + icon: Icons.close, + ), + ), + ); + } }, ) : Container( @@ -147,7 +158,7 @@ class _MatchResultViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${getTitleForRuleset(loc)}:', + getTitleForRuleset(loc), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, From fef83808600cbfa27a2c3f3edbb3517a1eccf82b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:41:01 +0200 Subject: [PATCH 037/127] added const --- .../main_menu/match_view/create_match/create_game_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index aeb2099..8594bd9 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -285,7 +285,7 @@ class _CreateGameViewState extends State { child: Row( children: [ Icon(getRulesetIcon(selectedRuleset!), size: 16), - SizedBox(width: 5), + const SizedBox(width: 5), Text( translateRulesetToString(selectedRuleset!, context), ), From ee7bff906241154f4456a566a50771f0a3d566c2 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Tue, 5 May 2026 09:30:20 +0000 Subject: [PATCH 038/127] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d5ed039..6241a64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.23+257 +version: 0.0.24+258 environment: sdk: ^3.8.1 From f0c1ce9881f426376e6995d4bec640e70b784c03 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Tue, 5 May 2026 09:30:59 +0000 Subject: [PATCH 039/127] Updated licenses [skip ci] --- .../settings_view/licenses/oss_licenses.dart | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 8811411..4a585bb 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -160,6 +160,7 @@ const allDependencies = [ /// Direct `dependencies`. const dependencies = [ _clock, + _collection, _cupertino_icons, _drift, _drift_flutter, @@ -444,13 +445,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// build 4.0.5 +/// build 4.0.6 const _build = Package( name: 'build', description: 'A package for authoring build_runner compatible code generators.', repository: 'https://github.com/dart-lang/build/tree/master/build', authors: [], - version: '4.0.5', + version: '4.0.6', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -567,13 +568,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// build_runner 2.14.0 +/// build_runner 2.15.0 const _build_runner = Package( name: 'build_runner', description: 'A build system for Dart code generation and modular compilation.', repository: 'https://github.com/dart-lang/build/tree/master/build_runner', authors: [], - version: '2.14.0', + version: '2.15.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -651,14 +652,14 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// built_value 8.12.5 +/// built_value 8.12.6 const _built_value = Package( name: 'built_value', description: '''Value types with builders, Dart classes as enums, and serialization. This library is the runtime dependency. ''', repository: 'https://github.com/google/built_value.dart/tree/master/built_value', authors: [], - version: '8.12.5', + version: '8.12.6', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -36204,13 +36205,13 @@ Copyright (C) 2009-2017, International Business Machines Corporation, Google, and others. All Rights Reserved.''', ); -/// source_gen 4.2.2 +/// source_gen 4.2.3 const _source_gen = Package( name: 'source_gen', description: 'Source code generation builders and utilities for the Dart build system', repository: 'https://github.com/dart-lang/source_gen/tree/master/source_gen', authors: [], - version: '4.2.2', + version: '4.2.3', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -37481,13 +37482,13 @@ freely, subject to the following restrictions: 3. This notice may not be removed or altered from any source distribution.''', ); -/// vm_service 15.1.0 +/// vm_service 15.2.0 const _vm_service = Package( name: 'vm_service', description: 'A library to communicate with a service implementing the Dart VM service protocol.', repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/vm_service', authors: [], - version: '15.1.0', + version: '15.2.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -37881,16 +37882,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.23+257 +/// tallee 0.0.24+258 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.23+257', + version: '0.0.24+258', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('clock'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], + dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], devDependencies: [PackageRef('flutter_test'), PackageRef('build_runner'), PackageRef('dart_pubspec_licenses'), PackageRef('drift_dev'), PackageRef('flutter_lints')], license: '''GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 From 98960083350ef50d6cff3527261d9cb252a3076e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 11:42:34 +0200 Subject: [PATCH 040/127] Refactoring --- lib/data/dao/match_dao.dart | 2 +- .../views/main_menu/group_view/create_group_view.dart | 2 +- .../views/main_menu/group_view/group_detail_view.dart | 4 +++- test/db_tests/aggregates/match_test.dart | 6 +++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 340273d..88cca35 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -354,7 +354,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Retrieves all matches associated with the given [groupId]. /// Queries the database directly, filtering by [groupId]. - Future> getGroupMatches({required String groupId}) async { + Future> getMatchesByGroup({required String groupId}) async { final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); final rows = await query.get(); diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 3a2ee60..b4a5b97 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -197,7 +197,7 @@ class _CreateGroupViewState extends State { /// obsolete. For each such match, the group association is removed by setting /// its [groupId] to null. Future deleteObsoleteMatchGroupRelations() async { - final groupMatches = await db.matchDao.getGroupMatches( + final groupMatches = await db.matchDao.getMatchesByGroup( groupId: widget.groupToEdit!.id, ); diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 92c3bba..3d5e805 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -244,7 +244,9 @@ class _GroupDetailViewState extends State { /// Loads statistics for this group Future _loadStatistics() async { isLoading = true; - final groupMatches = await db.matchDao.getGroupMatches(groupId: _group.id); + final groupMatches = await db.matchDao.getMatchesByGroup( + groupId: _group.id, + ); setState(() { totalMatches = groupMatches.length; diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 367f38f..2c9b768 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -241,15 +241,15 @@ void main() { expect(matchExists, isTrue); }); - test('getGroupMatches() works correctly', () async { - var matches = await database.matchDao.getGroupMatches( + test('getMatchesByGroup() works correctly', () async { + var matches = await database.matchDao.getMatchesByGroup( groupId: 'non-existing-id', ); expect(matches, isEmpty); await database.matchDao.addMatch(match: testMatch1); - matches = await database.matchDao.getGroupMatches( + matches = await database.matchDao.getMatchesByGroup( groupId: testGroup1.id, ); expect(matches, isNotEmpty); From eadf05e116583847ecd97ebe2b216b5ebe82c0e6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 11:43:35 +0200 Subject: [PATCH 041/127] Fixed incoming changes --- .../create_match/create_game_view.dart | 16 +++++----------- .../create_match/create_match_view.dart | 7 ------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 8594bd9..2094554 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -469,38 +469,32 @@ class _CreateGameViewState extends State { final oldGame = widget.gameToEdit!; if (oldGame.name != newGame.name) { - await db.gameDao.updateGameName( - gameId: oldGame.id, - newName: newGame.name, - ); + await db.gameDao.updateGameName(gameId: oldGame.id, name: newGame.name); } if (oldGame.description != newGame.description) { await db.gameDao.updateGameDescription( gameId: oldGame.id, - newDescription: newGame.description, + description: newGame.description, ); } if (oldGame.ruleset != newGame.ruleset) { await db.gameDao.updateGameRuleset( gameId: oldGame.id, - newRuleset: newGame.ruleset, + ruleset: newGame.ruleset, ); } if (oldGame.color != newGame.color) { await db.gameDao.updateGameColor( gameId: oldGame.id, - newColor: newGame.color, + color: newGame.color, ); } if (oldGame.icon != newGame.icon) { - await db.gameDao.updateGameIcon( - gameId: oldGame.id, - newIcon: newGame.icon, - ); + await db.gameDao.updateGameIcon(gameId: oldGame.id, icon: newGame.icon); } } 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 c09ff49..14908b6 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 @@ -295,13 +295,6 @@ class _CreateMatchViewState extends State { ); } - if (widget.matchToEdit!.game.id != updatedMatch.game.id) { - await db.matchDao.updateMatchGame( - matchId: widget.matchToEdit!.id, - gameId: updatedMatch.game.id, - ); - } - // Add players who are in updatedMatch but not in the original match for (var player in updatedMatch.players) { if (!widget.matchToEdit!.players.any((p) => p.id == player.id)) { From f3380e6c08d6976424d31636e5c77bc6cc5c3409 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 11:45:22 +0200 Subject: [PATCH 042/127] Reordered tests --- test/db_tests/aggregates/match_test.dart | 98 ++++++++++++------------ 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 2c9b768..7f627f7 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -259,6 +259,57 @@ void main() { expect(match.group, isNotNull); expect(match.group!.id, testGroup1.id); }); + + test('getMatchCount() works correctly', () async { + var matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 0); + + await database.matchDao.addMatch(match: testMatch1); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 1); + + await database.matchDao.addMatch(match: testMatch2); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 2); + + await database.matchDao.deleteMatch(matchId: testMatch1.id); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 1); + + await database.matchDao.deleteMatch(matchId: testMatch2.id); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 0); + }); + + test('getMatchCountByGame() works correctly', () async { + var count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 0); + + await database.matchDao.addMatch(match: testMatch1); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 1); + + await database.matchDao.addMatch(match: testMatch2); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 2); + }); + + test('getMatchCountByGame() returns 0 for non-existent game', () async { + final count = await database.matchDao.getMatchCountByGame( + gameId: 'non-existent-game-id', + ); + expect(count, 0); + }); }); group('UPDATE', () { @@ -408,31 +459,6 @@ void main() { final allMatches = await database.matchDao.getAllMatches(); expect(allMatches, isEmpty); }); - - test('Getting the match count works correctly', () async { - var matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); - - await database.matchDao.addMatch(match: testMatch1); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); - - await database.matchDao.addMatch(match: testMatch2); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 2); - - await database.matchDao.deleteMatch(matchId: testMatch1.id); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); - - await database.matchDao.deleteMatch(matchId: testMatch2.id); - - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); - }); }); group('DELETE', () { @@ -472,28 +498,6 @@ void main() { }); }); - test('getMatchCountByGame() works correctly', () async { - var count = await database.matchDao.getMatchCountByGame( - gameId: testGame.id, - ); - expect(count, 0); - - await database.matchDao.addMatch(match: testMatch1); - count = await database.matchDao.getMatchCountByGame(gameId: testGame.id); - expect(count, 1); - - await database.matchDao.addMatch(match: testMatch2); - count = await database.matchDao.getMatchCountByGame(gameId: testGame.id); - expect(count, 2); - }); - - test('getMatchCountByGame() returns 0 for non-existent game', () async { - final count = await database.matchDao.getMatchCountByGame( - gameId: 'non-existent-game-id', - ); - expect(count, 0); - }); - test('deleteMatchesByGame() deletes all matches for a game', () async { await database.matchDao.addMatch(match: testMatch1); await database.matchDao.addMatch(match: testMatch2); From f2626bd5af97315cfc1591c3b2aae45e000d708e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 13:42:39 +0200 Subject: [PATCH 043/127] Updated statistics view --- .../views/main_menu/statistics_view.dart | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 98a8e1d..fc6de83 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -6,6 +6,7 @@ import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; import 'package:tallee/presentation/widgets/top_centered_message.dart'; @@ -18,6 +19,9 @@ class StatisticsView extends StatefulWidget { } class _StatisticsViewState extends State { + int matchCount = 0; + int groupCount = 0; + List<(Player, int)> winCounts = List.filled(6, ( Player(name: 'Skeleton Player'), 1, @@ -53,7 +57,27 @@ class _StatisticsViewState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox(height: constraints.maxHeight * 0.01), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.15, + title: loc.matches, + icon: Icons.groups_rounded, + value: matchCount, + ), + SizedBox(width: constraints.maxWidth * 0.05), + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.15, + title: loc.groups, + icon: Icons.groups_rounded, + value: groupCount, + ), + ], + ), + SizedBox(height: constraints.maxHeight * 0.02), Visibility( visible: winCounts.isEmpty && @@ -115,11 +139,17 @@ class _StatisticsViewState extends State { Future.wait([ db.matchDao.getAllMatches(), db.playerDao.getAllPlayers(), + db.matchDao.getMatchCount(), + db.groupDao.getGroupCount(), Future.delayed(Constants.MINIMUM_SKELETON_DURATION), ]).then((results) async { if (!mounted) return; + final matches = results[0] as List; final players = results[1] as List; + matchCount = results[2] as int; + groupCount = results[3] as int; + winCounts = _calculateWinsForAllPlayers( matches: matches, players: players, @@ -134,6 +164,7 @@ class _StatisticsViewState extends State { winCounts: winCounts, matchCounts: matchCounts, ); + setState(() { isLoading = false; }); From f8c6d3d0892aa0fb92a646015a9af96a10296206 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 13:42:43 +0200 Subject: [PATCH 044/127] Deleted home view --- .../main_menu/custom_navigation_bar.dart | 17 +- .../views/main_menu/home_view.dart | 259 ------------------ 2 files changed, 4 insertions(+), 272 deletions(-) delete mode 100644 lib/presentation/views/main_menu/home_view.dart diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 16316ad..5e23077 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -3,7 +3,6 @@ import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart'; -import 'package:tallee/presentation/views/main_menu/home_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view.dart'; @@ -31,7 +30,6 @@ class _CustomNavigationBarState extends State final loc = AppLocalizations.of(context); // Pretty ugly but works final List tabs = [ - KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()), KeyedSubtree( key: ValueKey('matches_$tabKeyCount'), child: const MatchView(), @@ -101,27 +99,20 @@ class _CustomNavigationBarState extends State NavbarItem( index: 0, isSelected: currentIndex == 0, - icon: Icons.home_rounded, - label: loc.home, - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 1, - isSelected: currentIndex == 1, icon: Icons.gamepad_rounded, label: loc.matches, onTabTapped: onTabTapped, ), NavbarItem( - index: 2, - isSelected: currentIndex == 2, + index: 1, + isSelected: currentIndex == 1, icon: Icons.group_rounded, label: loc.groups, onTabTapped: onTabTapped, ), NavbarItem( - index: 3, - isSelected: currentIndex == 3, + index: 2, + isSelected: currentIndex == 2, icon: Icons.bar_chart_rounded, label: loc.statistics, onTabTapped: onTabTapped, diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart deleted file mode 100644 index 321f12b..0000000 --- a/lib/presentation/views/main_menu/home_view.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:tallee/core/adaptive_page_route.dart'; -import 'package:tallee/core/constants.dart'; -import 'package:tallee/core/enums.dart'; -import 'package:tallee/data/db/database.dart'; -import 'package:tallee/data/models/game.dart'; -import 'package:tallee/data/models/group.dart'; -import 'package:tallee/data/models/match.dart'; -import 'package:tallee/data/models/player.dart'; -import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; -import 'package:tallee/presentation/widgets/app_skeleton.dart'; -import 'package:tallee/presentation/widgets/buttons/quick_create_button.dart'; -import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; -import 'package:tallee/presentation/widgets/tiles/match_tile.dart'; -import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart'; - -class HomeView extends StatefulWidget { - /// The main home view of the application, displaying quick info, - /// recent matches, and quick create options. - const HomeView({super.key}); - - @override - State createState() => _HomeViewState(); -} - -class _HomeViewState extends State { - bool isLoading = true; - - /// Amount of matches in the database - int matchCount = 0; - - /// Amount of groups in the database - int groupCount = 0; - - /// Loaded recent matches from the database - List loadedRecentMatches = []; - - /// Recent matches to display, initially filled with skeleton matches - List recentMatches = List.filled( - 2, - Match( - name: 'Skeleton Match', - game: Game( - name: 'Skeleton Game', - ruleset: Ruleset.singleWinner, - description: 'This is a skeleton game description.', - color: GameColor.blue, - icon: '', - ), - group: Group( - name: 'Skeleton Group', - description: 'This is a skeleton group description.', - members: [ - Player( - name: - 'Skeleton Player 1' - '', - ), - Player( - name: - 'Skeleton Player 2' - '', - ), - ], - ), - notes: 'These are skeleton notes.', - players: [ - Player( - name: - 'Skeleton Player 1' - '', - ), - Player( - name: - 'Skeleton Player 2' - '', - ), - ], - ), - ); - - @override - void initState() { - super.initState(); - loadHomeViewData(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return AppSkeleton( - fixLayoutBuilder: true, - enabled: isLoading, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, - title: loc.matches, - icon: Icons.groups_rounded, - value: matchCount, - ), - SizedBox(width: constraints.maxWidth * 0.05), - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, - title: loc.groups, - icon: Icons.groups_rounded, - value: groupCount, - ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: InfoTile( - width: constraints.maxWidth * 0.95, - title: loc.recent_matches, - icon: Icons.history_rounded, - content: Column( - children: [ - if (recentMatches.isNotEmpty) - for (Match match in recentMatches) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 6.0, - ), - 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), - ), - ); - await loadRecentMatches(); - - setState(() { - print('loaded'); - }); - }, - ), - ) - else - Center( - heightFactor: 5, - child: Text(loc.no_recent_matches_available), - ), - ], - ), - ), - ), - Padding( - padding: EdgeInsets.zero, - child: InfoTile( - width: constraints.maxWidth * 0.95, - title: loc.quick_create, - icon: Icons.add_box_rounded, - content: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - QuickCreateButton( - text: 'Category 1', - onPressed: () {}, - ), - QuickCreateButton( - text: 'Category 2', - onPressed: () {}, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - QuickCreateButton( - text: 'Category 3', - onPressed: () {}, - ), - QuickCreateButton( - text: 'Category 4', - onPressed: () {}, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - QuickCreateButton( - text: 'Category 5', - onPressed: () {}, - ), - QuickCreateButton( - text: 'Category 6', - onPressed: () {}, - ), - ], - ), - ], - ), - ), - ), - SizedBox(height: MediaQuery.paddingOf(context).bottom), - ], - ), - ), - ); - }, - ); - } - - /// Loads the data for the HomeView from the database. - /// This includes the match count, group count, and recent matches. - Future loadHomeViewData() async { - final db = Provider.of(context, listen: false); - Future.wait([ - db.matchDao.getMatchCount(), - db.groupDao.getGroupCount(), - db.matchDao.getAllMatches(), - Future.delayed(Constants.MINIMUM_SKELETON_DURATION), - ]).then((results) { - matchCount = results[0] as int; - groupCount = results[1] as int; - loadedRecentMatches = results[2] as List; - recentMatches = - (loadedRecentMatches - ..sort((a, b) => b.createdAt.compareTo(a.createdAt))) - .take(2) - .toList(); - if (mounted) { - setState(() { - isLoading = false; - }); - } - }); - } - - Future loadRecentMatches() async { - final db = Provider.of(context, listen: false); - final matches = await db.matchDao.getAllMatches(); - recentMatches = - (matches..sort((a, b) => b.createdAt.compareTo(a.createdAt))) - .take(2) - .toList(); - } -} From dc3807356e8fc2037c6bc8ae795e04528a16343e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 22:28:55 +0200 Subject: [PATCH 045/127] Changed button style --- .../match_view/match_result_view.dart | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) 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 549d86c..ffa9ba3 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 @@ -8,7 +8,6 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart'; @@ -109,34 +108,17 @@ class _MatchResultViewState extends State { Expanded( child: isLiveEditMode && rulesetSupportsScoreEntry() ? ListView.builder( - itemCount: allPlayers.length + 1, + itemCount: allPlayers.length, itemBuilder: (context, index) { - if (index < allPlayers.length) { - return LiveEditListTile( - title: allPlayers[index].name, - onChanged: (value) { - setState(() { - controller[index].text = value.toString(); - }); - }, - value: int.tryParse(controller[index].text) ?? 0, - ); - } else { - return Center( - child: Padding( - padding: const EdgeInsets.only(top: 30), - child: MainMenuButton( - text: 'Ansicht verlassen', - onPressed: () => { - setState(() { - isLiveEditMode = false; - }), - }, - icon: Icons.close, - ), - ), - ); - } + return LiveEditListTile( + title: allPlayers[index].name, + onChanged: (value) { + setState(() { + controller[index].text = value.toString(); + }); + }, + value: int.tryParse(controller[index].text) ?? 0, + ); }, ) : Container( @@ -254,6 +236,14 @@ class _MatchResultViewState extends State { } : null, ), + ] else ...[ + CustomWidthButton( + text: 'Ansicht verlassen', + sizeRelativeToWidth: 0.95, + onPressed: () => setState(() { + isLiveEditMode = false; + }), + ), ], ], ), From bb46cace031ed6b7b71d0b33f70ac507d355097b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 22:36:01 +0200 Subject: [PATCH 046/127] Added localizations --- lib/l10n/arb/app_de.arb | 2 ++ lib/l10n/arb/app_en.arb | 2 ++ lib/l10n/generated/app_localizations.dart | 12 ++++++++++++ lib/l10n/generated/app_localizations_de.dart | 6 ++++++ lib/l10n/generated/app_localizations_en.dart | 6 ++++++ 5 files changed, 28 insertions(+) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 46c780a..65f1813 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -32,6 +32,7 @@ "error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen", "error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen", "error_reading_file": "Fehler beim Lesen der Datei", + "exit_view": "Ansicht verlassen", "export_canceled": "Export abgebrochen", "export_data": "Daten exportieren", "format_exception": "Formatfehler (siehe Konsole)", @@ -50,6 +51,7 @@ "legal": "Rechtliches", "legal_notice": "Impressum", "licenses": "Lizenzen", + "live_edit_mode": "Live-Bearbeitungsmodus", "match_in_progress": "Spiel läuft...", "match_name": "Spieltitel", "match_profile": "Spielprofil", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a85e1b0..7d54e92 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -366,6 +366,7 @@ "error_deleting_group": "Error while deleting group, please try again", "error_editing_group": "Error while editing group, please try again", "error_reading_file": "Error reading file", + "exit_view": "Exit View", "export_canceled": "Export canceled", "export_data": "Export data", "format_exception": "Format Exception (see console)", @@ -384,6 +385,7 @@ "legal": "Legal", "legal_notice": "Legal Notice", "licenses": "Licenses", + "live_edit_mode": "Live Edit Mode", "match_in_progress": "Match in progress...", "match_name": "Match name", "match_profile": "Match Profile", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 99c9317..ea52dfc 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -290,6 +290,12 @@ abstract class AppLocalizations { /// **'Error reading file'** String get error_reading_file; + /// No description provided for @exit_view. + /// + /// In en, this message translates to: + /// **'Exit View'** + String get exit_view; + /// Message when export is canceled /// /// In en, this message translates to: @@ -398,6 +404,12 @@ abstract class AppLocalizations { /// **'Licenses'** String get licenses; + /// No description provided for @live_edit_mode. + /// + /// In en, this message translates to: + /// **'Live Edit Mode'** + String get live_edit_mode; + /// Message when match is in progress /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 51b4c62..45859c0 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -111,6 +111,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get error_reading_file => 'Fehler beim Lesen der Datei'; + @override + String get exit_view => 'Ansicht verlassen'; + @override String get export_canceled => 'Export abgebrochen'; @@ -165,6 +168,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get licenses => 'Lizenzen'; + @override + String get live_edit_mode => 'Live-Bearbeitungsmodus'; + @override String get match_in_progress => 'Spiel läuft...'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 2b42e47..d8b0261 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -111,6 +111,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get error_reading_file => 'Error reading file'; + @override + String get exit_view => 'Exit View'; + @override String get export_canceled => 'Export canceled'; @@ -165,6 +168,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get licenses => 'Licenses'; + @override + String get live_edit_mode => 'Live Edit Mode'; + @override String get match_in_progress => 'Match in progress...'; From 883a32e0caddc7fbba3ce9959142257333566ff8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 22:38:11 +0200 Subject: [PATCH 047/127] Fixed button --- .../views/main_menu/match_view/match_result_view.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 ffa9ba3..6a85945 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 @@ -91,6 +91,7 @@ class _MatchResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( + automaticallyImplyLeading: !isLiveEditMode, leading: !isLiveEditMode ? IconButton( icon: const Icon(Icons.close), @@ -209,7 +210,7 @@ class _MatchResultViewState extends State { // Button to switch to live edit mode ...[ CustomWidthButton( - text: 'Live-Edit Modus', + text: loc.live_edit_mode, sizeRelativeToWidth: 0.95, buttonType: ButtonType.secondary, onPressed: () => setState(() { @@ -238,7 +239,7 @@ class _MatchResultViewState extends State { ), ] else ...[ CustomWidthButton( - text: 'Ansicht verlassen', + text: loc.exit_view, sizeRelativeToWidth: 0.95, onPressed: () => setState(() { isLiveEditMode = false; From ec4d6ce5ec951234c240363073ebb8af6b2668ec Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 22:45:35 +0200 Subject: [PATCH 048/127] Docs --- .../views/main_menu/match_view/match_result_view.dart | 2 ++ 1 file changed, 2 insertions(+) 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 6a85945..0de569a 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 @@ -41,6 +41,7 @@ class _MatchResultViewState extends State { /// List of text controllers for score entry, one for each player late final List controller; + /// Flag to indicate if the save button should be enabled late bool canSave; /// Currently selected winner player @@ -60,6 +61,7 @@ class _MatchResultViewState extends State { (index) => TextEditingController()..addListener(() => onTextEnter()), ); + // Prefill fields if (widget.match.mvp.isNotEmpty) { if (rulesetSupportsWinnerSelection()) { _selectedPlayer = allPlayers.firstWhere( From 5bac5f1c38649c750bfb51d171054701ecaff9c8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 22:45:57 +0200 Subject: [PATCH 049/127] fix: score update --- .../views/main_menu/match_view/match_detail_view.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 2117b77..9b53b15 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -205,7 +205,9 @@ class _MatchDetailViewState extends State { match: match, onWinnerChanged: () { widget.onMatchUpdate.call(); - setState(() {}); + setState(() { + updateScoresForCurrentMatch(); + }); }, ), ), @@ -333,4 +335,10 @@ class _MatchDetailViewState extends State { return match.game.ruleset == Ruleset.singleWinner || match.game.ruleset == Ruleset.singleLoser; } + + void updateScoresForCurrentMatch() { + db.scoreEntryDao + .getAllMatchScores(matchId: match.id) + .then((scores) => match.scores = scores); + } } From 045d2afa3958db3d12818209264731dc448b7e8d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 6 May 2026 19:54:34 +0200 Subject: [PATCH 050/127] removed descriptions --- lib/l10n/arb/app_de.arb | 1 - lib/l10n/arb/app_en.arb | 366 ++-------------------------------------- 2 files changed, 10 insertions(+), 357 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index e518525..ab8166a 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -97,7 +97,6 @@ "played_matches": "Gespielte Spiele", "player_name": "Spieler:innenname", "players": "Spieler:innen", - "players_count": "{count} Spieler", "point": "Punkt", "points": "Punkte", "privacy_policy": "Datenschutzerklärung", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index c01f0b2..e41bb83 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,360 +1,6 @@ { "@@locale": "en", - "@all_players": { - "description": "Label for all players list" - }, - "@all_players_selected": { - "description": "Message when all players are added to selection" - }, - "@amount_of_matches": { - "description": "Label for amount of matches statistic" - }, - "@app_name": { - "description": "The name of the App" - }, - "@best_player": { - "description": "Label for best player statistic" - }, - "@cancel": { - "description": "Cancel button text" - }, - "@choose_color": { - "description": "Label for choosing a color" - }, - "@choose_game": { - "description": "Label for choosing a game" - }, - "@choose_group": { - "description": "Label for choosing a group" - }, - "@choose_ruleset": { - "description": "Label for choosing a ruleset" - }, - "@color": { - "description": "Color label" - }, - "@could_not_add_player": { - "description": "Error message when adding a player fails" - }, - "@create_game": { - "description": "Button text to create a game" - }, - "@create_group": { - "description": "Button text to create a group" - }, - "@create_match": { - "description": "Button text to create a match" - }, - "@create_new_group": { - "description": "Appbar text to create a new group" - }, - "@create_new_match": { - "description": "Appbar text to create a new match" - }, - "@created_on": { - "description": "Label for creation date" - }, - "@data": { - "description": "Data label" - }, - "@data_successfully_deleted": { - "description": "Success message after deleting data" - }, - "@data_successfully_exported": { - "description": "Success message after exporting data" - }, - "@data_successfully_imported": { - "description": "Success message after importing data" - }, - "@days_ago": { - "description": "Date format for days ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@delete": { - "description": "Delete button text" - }, - "@delete_all_data": { - "description": "Confirmation dialog for deleting all data" - }, - "@delete_game": { - "description": "Button text to delete a game" - }, - "@delete_group": { - "description": "Confirmation dialog for deleting a group" - }, - "@delete_match": { - "description": "Button text to delete a match" - }, - "@description": { - "description": "Description label" - }, - "@edit_game": { - "description": "Button text to edit a game" - }, - "@edit_group": { - "description": "Button & Appbar label for editing a group" - }, - "@edit_match": { - "description": "Button & Appbar label for editing a match" - }, - "@enter_points": { - "description": "Label to enter players points" - }, - "@enter_results": { - "description": "Button text to enter match results" - }, - "@error_creating_group": { - "description": "Error message when group creation fails" - }, - "@error_deleting_game": { - "description": "Error message when game deletion fails" - }, - "@error_deleting_group": { - "description": "Error message when group deletion fails" - }, - "@error_editing_group": { - "description": "Error message when group editing fails" - }, - "@error_reading_file": { - "description": "Error message when file cannot be read" - }, - "@export_canceled": { - "description": "Message when export is canceled" - }, - "@export_data": { - "description": "Export data menu item" - }, - "@format_exception": { - "description": "Error message for format exceptions" - }, - "@game": { - "description": "Game label" - }, - "@game_name": { - "description": "Placeholder for game name search" - }, - "@group": { - "description": "Group label" - }, - "@group_name": { - "description": "Placeholder for group name input" - }, - "@group_profile": { - "description": "Title for group profile view" - }, - "@groups": { - "description": "Label for groups" - }, - "@home": { - "description": "Home tab label" - }, - "@import_canceled": { - "description": "Message when import is canceled" - }, - "@import_data": { - "description": "Import data menu item" - }, - "@info": { - "description": "Info label" - }, - "@invalid_schema": { - "description": "Error message for invalid schema" - }, - "@least_points": { - "description": "Title for least points ruleset" - }, - "@legal": { - "description": "Legal section header" - }, - "@legal_notice": { - "description": "Legal notice menu item" - }, - "@licenses": { - "description": "Licenses menu item" - }, - "@match_in_progress": { - "description": "Message when match is in progress" - }, - "@match_name": { - "description": "Placeholder for match name input" - }, - "@match_profile": { - "description": "Title for match profile view" - }, - "@matches": { - "description": "Label for matches" - }, - "@members": { - "description": "Label for group members" - }, - "@most_points": { - "description": "Title for most points ruleset" - }, - "@no_data_available": { - "description": "Message when no data in the statistic tiles is given" - }, - "@no_groups_created_yet": { - "description": "Message when no groups exist" - }, - "@no_licenses_found": { - "description": "Message when no licenses are found" - }, - "@no_license_text_available": { - "description": "Message when no license text is available" - }, - "@no_matches_created_yet": { - "description": "Message when no matches exist" - }, - "@no_players_created_yet": { - "description": "Message when no players exist" - }, - "@no_players_found_with_that_name": { - "description": "Message when search returns no results" - }, - "@no_players_selected": { - "description": "Message when no players are selected" - }, - "@no_recent_matches_available": { - "description": "Message when no recent matches exist" - }, - "@no_results_entered_yet": { - "description": "Message when no results have been entered yet" - }, - "@no_second_match_available": { - "description": "Message when no second match exists" - }, - "@no_statistics_available": { - "description": "Message when no statistics are available, because no matches were played yet" - }, - "@none": { - "description": "None option label" - }, - "@none_group": { - "description": "None group option label" - }, - "@not_available": { - "description": "Abbreviation for not available" - }, - "@played_matches": { - "description": "Label for played matches statistic" - }, - "@player_name": { - "description": "Placeholder for player name input" - }, - "@players": { - "description": "Players label" - }, - "@players_count": { - "description": "Shows the number of players", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@points": { - "description": "Points label" - }, - "@privacy_policy": { - "description": "Privacy policy menu item" - }, - "@quick_create": { - "description": "Title for quick create section" - }, - "@recent_matches": { - "description": "Title for recent matches section" - }, - "@results": { - "description": "Label for match results" - }, - "@ruleset": { - "description": "Ruleset label" - }, - "@ruleset_least_points": { - "description": "Description for least points ruleset" - }, - "@ruleset_most_points": { - "description": "Description for most points ruleset" - }, - "@ruleset_single_loser": { - "description": "Description for single loser ruleset" - }, - "@ruleset_single_winner": { - "description": "Description for single winner ruleset" - }, - "@save_changes": { - "description": "Save changes button text" - }, - "@search_for_groups": { - "description": "Hint text for group search input field" - }, - "@search_for_players": { - "description": "Hint text for player search input field" - }, - "@select_winner": { - "description": "Label to select the winner" - }, - "@select_loser": { - "description": "Label to select the loser" - }, - "@selected_players": { - "description": "Shows the number of selected players" - }, - "@settings": { - "description": "Label for the App Settings" - }, - "@single_loser": { - "description": "Title for single loser ruleset" - }, - "@single_winner": { - "description": "Title for single winner ruleset" - }, - "@statistics": { - "description": "Statistics tab label" - }, - "@stats": { - "description": "Stats tab label (short)" - }, - "@successfully_added_player": { - "description": "Success message when adding a player", - "placeholders": { - "playerName": { - "type": "String", - "example": "John" - } - } - }, - "@there_is_no_group_matching_your_search": { - "description": "Message when search returns no groups" - }, - "@this_cannot_be_undone": { - "description": "Warning message for irreversible actions" - }, - "@today_at": { - "description": "Date format for today" - }, - "@undo": { - "description": "Undo button text" - }, - "@unknown_exception": { - "description": "Error message for unknown exceptions" - }, - "@winner": { - "description": "Winner label" - }, - "@winrate": { - "description": "Label for winrate statistic" - }, - "@wins": { - "description": "Label for wins statistic" - }, - "@yesterday_at": { - "description": "Date format for yesterday" - }, + "all_players": "All players", "all_players_selected": "All players selected", "amount_of_matches": "Amount of Matches", @@ -452,7 +98,6 @@ "played_matches": "Played Matches", "player_name": "Player name", "players": "Players", - "players_count": "{count} Players", "point": "Point", "points": "Points", "privacy_policy": "Privacy Policy", @@ -480,6 +125,15 @@ "statistics": "Statistics", "stats": "Stats", "successfully_added_player": "Successfully added player {playerName}", + "@successfully_added_player": { + "description": "Success message when adding a player", + "placeholders": { + "playerName": { + "type": "String", + "example": "John" + } + } + }, "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", "tie": "Tie", From 46041be837a31160ab19434dc5f0bbdf803f4bfc Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 6 May 2026 19:55:56 +0200 Subject: [PATCH 051/127] compiling localizations --- lib/l10n/generated/app_localizations.dart | 230 +++++++++---------- lib/l10n/generated/app_localizations_de.dart | 7 +- lib/l10n/generated/app_localizations_en.dart | 7 +- 3 files changed, 114 insertions(+), 130 deletions(-) diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 790597f..dd8f9cb 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -98,67 +98,67 @@ abstract class AppLocalizations { Locale('en'), ]; - /// Label for all players list + /// No description provided for @all_players. /// /// In en, this message translates to: /// **'All players'** String get all_players; - /// Message when all players are added to selection + /// No description provided for @all_players_selected. /// /// In en, this message translates to: /// **'All players selected'** String get all_players_selected; - /// Label for amount of matches statistic + /// No description provided for @amount_of_matches. /// /// In en, this message translates to: /// **'Amount of Matches'** String get amount_of_matches; - /// The name of the App + /// No description provided for @app_name. /// /// In en, this message translates to: /// **'Tallee'** String get app_name; - /// Label for best player statistic + /// No description provided for @best_player. /// /// In en, this message translates to: /// **'Best Player'** String get best_player; - /// Cancel button text + /// No description provided for @cancel. /// /// In en, this message translates to: /// **'Cancel'** String get cancel; - /// Label for choosing a color + /// No description provided for @choose_color. /// /// In en, this message translates to: /// **'Choose Color'** String get choose_color; - /// Label for choosing a game + /// No description provided for @choose_game. /// /// In en, this message translates to: /// **'Choose Game'** String get choose_game; - /// Label for choosing a group + /// No description provided for @choose_group. /// /// In en, this message translates to: /// **'Choose Group'** String get choose_group; - /// Label for choosing a ruleset + /// No description provided for @choose_ruleset. /// /// In en, this message translates to: /// **'Choose Ruleset'** String get choose_ruleset; - /// Color label + /// No description provided for @color. /// /// In en, this message translates to: /// **'Color'** @@ -212,91 +212,91 @@ abstract class AppLocalizations { /// **'Yellow'** String get color_yellow; - /// Error message when adding a player fails + /// No description provided for @could_not_add_player. /// /// In en, this message translates to: /// **'Could not add player'** String could_not_add_player(Object playerName); - /// Button text to create a game + /// No description provided for @create_game. /// /// In en, this message translates to: /// **'Create Game'** String get create_game; - /// Button text to create a group + /// No description provided for @create_group. /// /// In en, this message translates to: /// **'Create Group'** String get create_group; - /// Button text to create a match + /// No description provided for @create_match. /// /// In en, this message translates to: /// **'Create match'** String get create_match; - /// Appbar text to create a new group + /// No description provided for @create_new_group. /// /// In en, this message translates to: /// **'Create new group'** String get create_new_group; - /// Label for creation date + /// No description provided for @created_on. /// /// In en, this message translates to: /// **'Created on'** String get created_on; - /// Appbar text to create a new match + /// No description provided for @create_new_match. /// /// In en, this message translates to: /// **'Create new match'** String get create_new_match; - /// Data label + /// No description provided for @data. /// /// In en, this message translates to: /// **'Data'** String get data; - /// Success message after deleting data + /// No description provided for @data_successfully_deleted. /// /// In en, this message translates to: /// **'Data successfully deleted'** String get data_successfully_deleted; - /// Success message after exporting data + /// No description provided for @data_successfully_exported. /// /// In en, this message translates to: /// **'Data successfully exported'** String get data_successfully_exported; - /// Success message after importing data + /// No description provided for @data_successfully_imported. /// /// In en, this message translates to: /// **'Data successfully imported'** String get data_successfully_imported; - /// Date format for days ago + /// No description provided for @days_ago. /// /// In en, this message translates to: /// **'{count} days ago'** - String days_ago(int count); + String days_ago(Object count); - /// Delete button text + /// No description provided for @delete. /// /// In en, this message translates to: /// **'Delete'** String get delete; - /// Confirmation dialog for deleting all data + /// No description provided for @delete_all_data. /// /// In en, this message translates to: /// **'Delete all data'** String get delete_all_data; - /// Button text to delete a game + /// No description provided for @delete_game. /// /// In en, this message translates to: /// **'Delete Game'** @@ -308,457 +308,451 @@ abstract class AppLocalizations { /// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'** String delete_game_with_matches_warning(int count); - /// Confirmation dialog for deleting a group + /// No description provided for @delete_group. /// /// In en, this message translates to: /// **'Delete Group'** String get delete_group; - /// Button text to delete a match + /// No description provided for @delete_match. /// /// In en, this message translates to: /// **'Delete Match'** String get delete_match; - /// Description label + /// No description provided for @description. /// /// In en, this message translates to: /// **'Description'** String get description; - /// Button text to edit a game + /// No description provided for @edit_game. /// /// In en, this message translates to: /// **'Edit Game'** String get edit_game; - /// Button & Appbar label for editing a group + /// No description provided for @edit_group. /// /// In en, this message translates to: /// **'Edit Group'** String get edit_group; - /// Button & Appbar label for editing a match + /// No description provided for @edit_match. /// /// In en, this message translates to: /// **'Edit Match'** String get edit_match; - /// Label to enter players points + /// No description provided for @enter_points. /// /// In en, this message translates to: /// **'Enter points'** String get enter_points; - /// Button text to enter match results + /// No description provided for @enter_results. /// /// In en, this message translates to: /// **'Enter Results'** String get enter_results; - /// Error message when group creation fails + /// No description provided for @error_creating_group. /// /// In en, this message translates to: /// **'Error while creating group, please try again'** String get error_creating_group; - /// Error message when game deletion fails + /// No description provided for @error_deleting_game. /// /// In en, this message translates to: /// **'Error while deleting game, please try again'** String get error_deleting_game; - /// Error message when group deletion fails + /// No description provided for @error_deleting_group. /// /// In en, this message translates to: /// **'Error while deleting group, please try again'** String get error_deleting_group; - /// Error message when group editing fails + /// No description provided for @error_editing_group. /// /// In en, this message translates to: /// **'Error while editing group, please try again'** String get error_editing_group; - /// Error message when file cannot be read + /// No description provided for @error_reading_file. /// /// In en, this message translates to: /// **'Error reading file'** String get error_reading_file; - /// Message when export is canceled + /// No description provided for @export_canceled. /// /// In en, this message translates to: /// **'Export canceled'** String get export_canceled; - /// Export data menu item + /// No description provided for @export_data. /// /// In en, this message translates to: /// **'Export data'** String get export_data; - /// Error message for format exceptions + /// No description provided for @format_exception. /// /// In en, this message translates to: /// **'Format Exception (see console)'** String get format_exception; - /// Game label + /// No description provided for @game. /// /// In en, this message translates to: /// **'Game'** String get game; - /// Placeholder for game name search + /// No description provided for @game_name. /// /// In en, this message translates to: /// **'Game Name'** String get game_name; - /// Group label + /// No description provided for @group. /// /// In en, this message translates to: /// **'Group'** String get group; - /// Placeholder for group name input + /// No description provided for @group_name. /// /// In en, this message translates to: /// **'Group name'** String get group_name; - /// Title for group profile view + /// No description provided for @group_profile. /// /// In en, this message translates to: /// **'Group Profile'** String get group_profile; - /// Label for groups + /// No description provided for @groups. /// /// In en, this message translates to: /// **'Groups'** String get groups; - /// Home tab label + /// No description provided for @home. /// /// In en, this message translates to: /// **'Home'** String get home; - /// Message when import is canceled + /// No description provided for @import_canceled. /// /// In en, this message translates to: /// **'Import canceled'** String get import_canceled; - /// Import data menu item + /// No description provided for @import_data. /// /// In en, this message translates to: /// **'Import data'** String get import_data; - /// Info label + /// No description provided for @info. /// /// In en, this message translates to: /// **'Info'** String get info; - /// Error message for invalid schema + /// No description provided for @invalid_schema. /// /// In en, this message translates to: /// **'Invalid Schema'** String get invalid_schema; - /// Title for least points ruleset + /// No description provided for @least_points. /// /// In en, this message translates to: /// **'Least Points'** String get least_points; - /// Legal section header + /// No description provided for @legal. /// /// In en, this message translates to: /// **'Legal'** String get legal; - /// Legal notice menu item + /// No description provided for @legal_notice. /// /// In en, this message translates to: /// **'Legal Notice'** String get legal_notice; - /// Licenses menu item + /// No description provided for @licenses. /// /// In en, this message translates to: /// **'Licenses'** String get licenses; - /// Message when match is in progress + /// No description provided for @match_in_progress. /// /// In en, this message translates to: /// **'Match in progress...'** String get match_in_progress; - /// Placeholder for match name input + /// No description provided for @match_name. /// /// In en, this message translates to: /// **'Match name'** String get match_name; - /// Title for match profile view + /// No description provided for @match_profile. /// /// In en, this message translates to: /// **'Match Profile'** String get match_profile; - /// Label for matches + /// No description provided for @matches. /// /// In en, this message translates to: /// **'Matches'** String get matches; - /// Label for group members + /// No description provided for @members. /// /// In en, this message translates to: /// **'Members'** String get members; - /// Title for most points ruleset + /// No description provided for @most_points. /// /// In en, this message translates to: /// **'Most Points'** String get most_points; - /// Message when no data in the statistic tiles is given + /// No description provided for @no_data_available. /// /// In en, this message translates to: /// **'No data available'** String get no_data_available; - /// Message when no groups exist + /// No description provided for @no_groups_created_yet. /// /// In en, this message translates to: /// **'No groups created yet'** String get no_groups_created_yet; - /// Message when no licenses are found + /// No description provided for @no_licenses_found. /// /// In en, this message translates to: /// **'No licenses found'** String get no_licenses_found; - /// Message when no license text is available + /// No description provided for @no_license_text_available. /// /// In en, this message translates to: /// **'No license text available'** String get no_license_text_available; - /// Message when no matches exist + /// No description provided for @no_matches_created_yet. /// /// In en, this message translates to: /// **'No matches created yet'** String get no_matches_created_yet; - /// Message when no players exist + /// No description provided for @no_players_created_yet. /// /// In en, this message translates to: /// **'No players created yet'** String get no_players_created_yet; - /// Message when search returns no results + /// No description provided for @no_players_found_with_that_name. /// /// In en, this message translates to: /// **'No players found with that name'** String get no_players_found_with_that_name; - /// Message when no players are selected + /// No description provided for @no_players_selected. /// /// In en, this message translates to: /// **'No players selected'** String get no_players_selected; - /// Message when no recent matches exist + /// No description provided for @no_recent_matches_available. /// /// In en, this message translates to: /// **'No recent matches available'** String get no_recent_matches_available; - /// Message when no results have been entered yet + /// No description provided for @no_results_entered_yet. /// /// In en, this message translates to: /// **'No results entered yet'** String get no_results_entered_yet; - /// Message when no second match exists + /// No description provided for @no_second_match_available. /// /// In en, this message translates to: /// **'No second match available'** String get no_second_match_available; - /// Message when no statistics are available, because no matches were played yet + /// No description provided for @no_statistics_available. /// /// In en, this message translates to: /// **'No statistics available'** String get no_statistics_available; - /// None option label + /// No description provided for @none. /// /// In en, this message translates to: /// **'None'** String get none; - /// None group option label + /// No description provided for @none_group. /// /// In en, this message translates to: /// **'None'** String get none_group; - /// Abbreviation for not available + /// No description provided for @not_available. /// /// In en, this message translates to: /// **'Not available'** String get not_available; - /// Label for played matches statistic + /// No description provided for @played_matches. /// /// In en, this message translates to: /// **'Played Matches'** String get played_matches; - /// Placeholder for player name input + /// No description provided for @player_name. /// /// In en, this message translates to: /// **'Player name'** String get player_name; - /// Players label + /// No description provided for @players. /// /// In en, this message translates to: /// **'Players'** String get players; - /// Shows the number of players - /// - /// In en, this message translates to: - /// **'{count} Players'** - String players_count(int count); - /// No description provided for @point. /// /// In en, this message translates to: /// **'Point'** String get point; - /// Points label + /// No description provided for @points. /// /// In en, this message translates to: /// **'Points'** String get points; - /// Privacy policy menu item + /// No description provided for @privacy_policy. /// /// In en, this message translates to: /// **'Privacy Policy'** String get privacy_policy; - /// Title for quick create section + /// No description provided for @quick_create. /// /// In en, this message translates to: /// **'Quick Create'** String get quick_create; - /// Title for recent matches section + /// No description provided for @recent_matches. /// /// In en, this message translates to: /// **'Recent Matches'** String get recent_matches; - /// Label for match results + /// No description provided for @results. /// /// In en, this message translates to: /// **'Results'** String get results; - /// Ruleset label + /// No description provided for @ruleset. /// /// In en, this message translates to: /// **'Ruleset'** String get ruleset; - /// Description for least points ruleset + /// No description provided for @ruleset_least_points. /// /// In en, this message translates to: /// **'Inverse scoring: the player with the fewest points wins.'** String get ruleset_least_points; - /// Description for most points ruleset + /// No description provided for @ruleset_most_points. /// /// In en, this message translates to: /// **'Traditional ruleset: the player with the most points wins.'** String get ruleset_most_points; - /// Description for single loser ruleset + /// No description provided for @ruleset_single_loser. /// /// In en, this message translates to: /// **'Exactly one loser is determined; last place receives the penalty or consequence.'** String get ruleset_single_loser; - /// Description for single winner ruleset + /// No description provided for @ruleset_single_winner. /// /// In en, this message translates to: /// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'** String get ruleset_single_winner; - /// Save changes button text + /// No description provided for @save_changes. /// /// In en, this message translates to: /// **'Save Changes'** String get save_changes; - /// Hint text for group search input field + /// No description provided for @search_for_groups. /// /// In en, this message translates to: /// **'Search for groups'** String get search_for_groups; - /// Hint text for player search input field + /// No description provided for @search_for_players. /// /// In en, this message translates to: /// **'Search for players'** String get search_for_players; - /// Label to select the winner + /// No description provided for @select_winner. /// /// In en, this message translates to: /// **'Select Winner'** String get select_winner; - /// Label to select the loser + /// No description provided for @select_loser. /// /// In en, this message translates to: /// **'Select Loser'** String get select_loser; - /// Shows the number of selected players + /// No description provided for @selected_players. /// /// In en, this message translates to: /// **'Selected players'** String get selected_players; - /// Label for the App Settings + /// No description provided for @settings. /// /// In en, this message translates to: /// **'Settings'** String get settings; - /// Title for single loser ruleset + /// No description provided for @single_loser. /// /// In en, this message translates to: /// **'Single Loser'** String get single_loser; - /// Title for single winner ruleset + /// No description provided for @single_winner. /// /// In en, this message translates to: /// **'Single Winner'** @@ -788,13 +782,13 @@ abstract class AppLocalizations { /// **'Multiple Winners'** String get multiple_winners; - /// Statistics tab label + /// No description provided for @statistics. /// /// In en, this message translates to: /// **'Statistics'** String get statistics; - /// Stats tab label (short) + /// No description provided for @stats. /// /// In en, this message translates to: /// **'Stats'** @@ -806,13 +800,13 @@ abstract class AppLocalizations { /// **'Successfully added player {playerName}'** String successfully_added_player(String playerName); - /// Message when search returns no groups + /// No description provided for @there_is_no_group_matching_your_search. /// /// In en, this message translates to: /// **'There is no group matching your search'** String get there_is_no_group_matching_your_search; - /// Warning message for irreversible actions + /// No description provided for @this_cannot_be_undone. /// /// In en, this message translates to: /// **'This can\'t be undone.'** @@ -824,43 +818,43 @@ abstract class AppLocalizations { /// **'Tie'** String get tie; - /// Date format for today + /// No description provided for @today_at. /// /// In en, this message translates to: /// **'Today at'** String get today_at; - /// Undo button text + /// No description provided for @undo. /// /// In en, this message translates to: /// **'Undo'** String get undo; - /// Error message for unknown exceptions + /// No description provided for @unknown_exception. /// /// In en, this message translates to: /// **'Unknown Exception (see console)'** String get unknown_exception; - /// Winner label + /// No description provided for @winner. /// /// In en, this message translates to: /// **'Winner'** String get winner; - /// Label for winrate statistic + /// No description provided for @winrate. /// /// In en, this message translates to: /// **'Winrate'** String get winrate; - /// Label for wins statistic + /// No description provided for @wins. /// /// In en, this message translates to: /// **'Wins'** String get wins; - /// Date format for yesterday + /// No description provided for @yesterday_at. /// /// In en, this message translates to: /// **'Yesterday at'** diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 2b20848..fe846c3 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -101,7 +101,7 @@ class AppLocalizationsDe extends AppLocalizations { String get data_successfully_imported => 'Daten erfolgreich importiert'; @override - String days_ago(int count) { + String days_ago(Object count) { return 'vor $count Tagen'; } @@ -295,11 +295,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get players => 'Spieler:innen'; - @override - String players_count(int count) { - return '$count Spieler'; - } - @override String get point => 'Punkt'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 323d8c8..899e3a5 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -101,7 +101,7 @@ class AppLocalizationsEn extends AppLocalizations { String get data_successfully_imported => 'Data successfully imported'; @override - String days_ago(int count) { + String days_ago(Object count) { return '$count days ago'; } @@ -295,11 +295,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get players => 'Players'; - @override - String players_count(int count) { - return '$count Players'; - } - @override String get point => 'Point'; From 013fd2918254fd03bec503d00e3dd54fdcfdccc2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 6 May 2026 20:18:46 +0200 Subject: [PATCH 052/127] Updated LiveEditListTile --- .../match_view/match_result_view.dart | 5 ++ .../live_edit_list_tile.dart | 76 ++++++++++--------- 2 files changed, 47 insertions(+), 34 deletions(-) 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 0c991f1..357012c 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 @@ -110,6 +110,7 @@ class _MatchResultViewState extends State { children: [ Expanded( child: isLiveEditMode && rulesetSupportsScoreEntry() + // Live Edit Mode ? ListView.builder( itemCount: allPlayers.length, itemBuilder: (context, index) { @@ -124,6 +125,7 @@ class _MatchResultViewState extends State { ); }, ) + // Normal Mode : Container( margin: const EdgeInsets.symmetric( horizontal: 12, @@ -150,6 +152,8 @@ class _MatchResultViewState extends State { ), ), const SizedBox(height: 10), + + // Show player selection if (rulesetSupportsWinnerSelection()) Expanded( child: RadioGroup( @@ -182,6 +186,7 @@ class _MatchResultViewState extends State { ), ), ), + // Show score entry if (rulesetSupportsScoreEntry()) Expanded( child: ListView.separated( diff --git a/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart index 4021755..80243b8 100644 --- a/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart @@ -35,32 +35,35 @@ class _LiveEditListTileState extends State { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(vertical: 10), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), decoration: CustomTheme.standardBoxDecoration, - child: Column( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - widget.title, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + MainMenuButton( + onPressed: () => _score > minScore + ? { + setState(() { + _score--; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.remove_rounded, ), - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Expanded( + child: Column( children: [ - MainMenuButton( - onPressed: () => _score > minScore - ? { - setState(() { - _score--; - if (widget.onChanged != null) { - widget.onChanged!(_score); - } - }), - } - : null, - icon: Icons.remove_rounded, + Text( + widget.title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), ), SizedBox( width: 150, @@ -68,28 +71,33 @@ class _LiveEditListTileState extends State { _score.toString(), maxLines: 1, textAlign: TextAlign.center, + textWidthBasis: TextWidthBasis.longestLine, + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), style: const TextStyle( fontSize: 48, fontWeight: FontWeight.w600, ), ), ), - MainMenuButton( - onPressed: () => _score < maxScore - ? { - setState(() { - _score++; - if (widget.onChanged != null) { - widget.onChanged!(_score); - } - }), - } - : null, - icon: Icons.add_rounded, - ), ], ), ), + MainMenuButton( + onPressed: () => _score < maxScore + ? { + setState(() { + _score++; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.add_rounded, + ), ], ), ); From 87ea5b47eedb1b757dae16c41f95959fcad97470 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 6 May 2026 20:34:55 +0200 Subject: [PATCH 053/127] Improved skeleton data --- .../views/main_menu/match_view/match_view.dart | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 2fb36e7..4f70347 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -10,6 +10,7 @@ import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_detail_view.dart'; @@ -30,8 +31,7 @@ class _MatchViewState extends State { late final AppDatabase db; bool isLoading = true; - /// Loaded matches from the database, - /// initially filled with skeleton matches + /// Loaded matches from the database, initially filled with skeleton matches List matches = List.filled( 4, Match( @@ -46,7 +46,15 @@ class _MatchViewState extends State { name: 'Group name', members: List.filled(5, Player(name: 'Player')), ), - players: [Player(name: 'Player')], + players: [ + Player(name: 'Player'), + Player(name: 'Player'), + Player(name: 'Player'), + Player(name: 'Player'), + Player(id: 'mvp_id', name: 'Player'), + ], + scores: {'mvp_id': ScoreEntry(score: 1)}, + endedAt: DateTime.now(), ), ); From 34a24c9dec9c5eb640d9276fd2cf9dd1e518e3ac Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Thu, 7 May 2026 20:39:01 +0200 Subject: [PATCH 054/127] Enhance navbar item animations --- lib/presentation/widgets/navbar_item.dart | 117 +++++++++++++--------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/lib/presentation/widgets/navbar_item.dart b/lib/presentation/widgets/navbar_item.dart index 17c055c..160c000 100644 --- a/lib/presentation/widgets/navbar_item.dart +++ b/lib/presentation/widgets/navbar_item.dart @@ -44,24 +44,49 @@ class _NavbarItemState extends State /// Scale animation for the icon when selected late Animation _scaleAnimation; + /// Color animation for the icon + late Animation _iconColorAnimation; + + /// Background color animation for the icon container + late Animation _bgColorAnimation; + + /// Font size animation for the label + late Animation _fontSizeAnimation; + + /// A simple double tween used to lerp between two font weights + late Animation _fontWeightT; + @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, + // Set initial value directly so the visual state matches widget.isSelected + value: widget.isSelected ? 1.0 : 0.0, ); - _scaleAnimation = Tween(begin: 1.0, end: 1.2).animate( - CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOutBack, - ), + final curved = CurvedAnimation( + parent: _animationController, + curve: Curves.easeOut, ); - if (widget.isSelected) { - _animationController.forward(); - } + _scaleAnimation = Tween(begin: 1.0, end: 1.2).animate(curved); + + _iconColorAnimation = ColorTween( + begin: CustomTheme.navBarItemUnselectedColor, + end: CustomTheme.navBarItemSelectedColor, + ).animate(curved); + + _bgColorAnimation = ColorTween( + begin: Colors.transparent, + end: CustomTheme.primaryColor.withAlpha(50), + ).animate(curved); + + _fontSizeAnimation = Tween(begin: 11.0, end: 12.0).animate(curved); + + // drives font weight interpolation + _fontWeightT = Tween(begin: 0.0, end: 1.0).animate(curved); } // Retrigger animation on selection change @@ -83,46 +108,44 @@ class _NavbarItemState extends State behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.symmetric(vertical: 5.0), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedContainer( - width: 50, - height: 50, - decoration: BoxDecoration( - color: widget.isSelected - ? CustomTheme.primaryColor.withAlpha(50) - : Colors.transparent, - borderRadius: const BorderRadius.all(Radius.circular(15)), - ), - duration: const Duration(milliseconds: 200), - child: ScaleTransition( - scale: widget.isSelected - ? _scaleAnimation - : const AlwaysStoppedAnimation(1.0), - child: Icon( - widget.icon, - color: widget.isSelected - ? CustomTheme.navBarItemSelectedColor - : CustomTheme.navBarItemUnselectedColor, - size: 32, + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final iconColor = _iconColorAnimation.value!; + final bgColor = _bgColorAnimation.value!; + final fontSize = _fontSizeAnimation.value; + final fontWeight = FontWeight.lerp( + FontWeight.w500, + FontWeight.bold, + _fontWeightT.value, + ); + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: bgColor, + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + child: ScaleTransition( + scale: _scaleAnimation, + child: Icon(widget.icon, color: iconColor, size: 32), + ), ), - ), - ), - Text( - widget.label, - style: TextStyle( - color: widget.isSelected - ? CustomTheme.navBarItemSelectedColor - : CustomTheme.navBarItemUnselectedColor, - fontSize: widget.isSelected ? 12 : 11, - fontWeight: widget.isSelected - ? FontWeight.bold - : FontWeight.w500, - ), - ), - ], + Text( + widget.label, + style: TextStyle( + color: iconColor, + fontSize: fontSize, + fontWeight: fontWeight, + ), + ), + ], + ); + }, ), ), ), From b6a252287dfee7c58341b487b0825e2fe0feabfd Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Fri, 8 May 2026 12:17:40 +0200 Subject: [PATCH 055/127] refactor: adjust heights and alignment in QuickInfoTile for better layout --- lib/presentation/views/main_menu/home_view.dart | 4 ++-- lib/presentation/widgets/tiles/quick_info_tile.dart | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index 321f12b..627db82 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -104,7 +104,7 @@ class _HomeViewState extends State { children: [ QuickInfoTile( width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, + height: constraints.maxHeight * 0.13, title: loc.matches, icon: Icons.groups_rounded, value: matchCount, @@ -112,7 +112,7 @@ class _HomeViewState extends State { SizedBox(width: constraints.maxWidth * 0.05), QuickInfoTile( width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, + height: constraints.maxHeight * 0.13, title: loc.groups, icon: Icons.groups_rounded, value: groupCount, diff --git a/lib/presentation/widgets/tiles/quick_info_tile.dart b/lib/presentation/widgets/tiles/quick_info_tile.dart index 5646fa5..c36aa92 100644 --- a/lib/presentation/widgets/tiles/quick_info_tile.dart +++ b/lib/presentation/widgets/tiles/quick_info_tile.dart @@ -50,7 +50,7 @@ class _QuickInfoTileState extends State { width: widget.width ?? 180, decoration: CustomTheme.standardBoxDecoration, child: Column( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ @@ -65,7 +65,6 @@ class _QuickInfoTileState extends State { ), ], ), - const Spacer(), Text( widget.value.toString(), style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold), From 4dd794c08ffd2ef0f10618269d51fc367cae881c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 8 May 2026 18:02:00 +0200 Subject: [PATCH 056/127] Applied #212 fix --- lib/presentation/views/main_menu/statistics_view.dart | 4 ++-- lib/presentation/widgets/tiles/quick_info_tile.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index fc6de83..8659a2e 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -62,7 +62,7 @@ class _StatisticsViewState extends State { children: [ QuickInfoTile( width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, + height: constraints.maxHeight * 0.13, title: loc.matches, icon: Icons.groups_rounded, value: matchCount, @@ -70,7 +70,7 @@ class _StatisticsViewState extends State { SizedBox(width: constraints.maxWidth * 0.05), QuickInfoTile( width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, + height: constraints.maxHeight * 0.13, title: loc.groups, icon: Icons.groups_rounded, value: groupCount, diff --git a/lib/presentation/widgets/tiles/quick_info_tile.dart b/lib/presentation/widgets/tiles/quick_info_tile.dart index 5646fa5..79fa513 100644 --- a/lib/presentation/widgets/tiles/quick_info_tile.dart +++ b/lib/presentation/widgets/tiles/quick_info_tile.dart @@ -50,7 +50,7 @@ class _QuickInfoTileState extends State { width: widget.width ?? 180, decoration: CustomTheme.standardBoxDecoration, child: Column( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ From 610a842b8af8c3ce5588e1b8b60bcb8c0de1cb6a Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 8 May 2026 16:03:46 +0000 Subject: [PATCH 057/127] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6241a64..7afeda4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.24+258 +version: 0.0.25+259 environment: sdk: ^3.8.1 From 23f0c9c23e9a9f398d93c3532744608f66a24b5d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 13:42:39 +0200 Subject: [PATCH 058/127] Updated statistics view --- .../views/main_menu/statistics_view.dart | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 98a8e1d..fc6de83 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -6,6 +6,7 @@ import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; import 'package:tallee/presentation/widgets/top_centered_message.dart'; @@ -18,6 +19,9 @@ class StatisticsView extends StatefulWidget { } class _StatisticsViewState extends State { + int matchCount = 0; + int groupCount = 0; + List<(Player, int)> winCounts = List.filled(6, ( Player(name: 'Skeleton Player'), 1, @@ -53,7 +57,27 @@ class _StatisticsViewState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox(height: constraints.maxHeight * 0.01), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.15, + title: loc.matches, + icon: Icons.groups_rounded, + value: matchCount, + ), + SizedBox(width: constraints.maxWidth * 0.05), + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.15, + title: loc.groups, + icon: Icons.groups_rounded, + value: groupCount, + ), + ], + ), + SizedBox(height: constraints.maxHeight * 0.02), Visibility( visible: winCounts.isEmpty && @@ -115,11 +139,17 @@ class _StatisticsViewState extends State { Future.wait([ db.matchDao.getAllMatches(), db.playerDao.getAllPlayers(), + db.matchDao.getMatchCount(), + db.groupDao.getGroupCount(), Future.delayed(Constants.MINIMUM_SKELETON_DURATION), ]).then((results) async { if (!mounted) return; + final matches = results[0] as List; final players = results[1] as List; + matchCount = results[2] as int; + groupCount = results[3] as int; + winCounts = _calculateWinsForAllPlayers( matches: matches, players: players, @@ -134,6 +164,7 @@ class _StatisticsViewState extends State { winCounts: winCounts, matchCounts: matchCounts, ); + setState(() { isLoading = false; }); From aeba2e93e0b471a447d876cc8d9305a9bddeedd0 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 8 May 2026 16:04:25 +0000 Subject: [PATCH 059/127] Updated licenses [skip ci] --- .../settings_view/licenses/oss_licenses.dart | 220 +----------------- 1 file changed, 7 insertions(+), 213 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 4a585bb..0876c07 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -37116,21 +37116,19 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// url_launcher_web 2.4.2 +/// url_launcher_web 2.4.3 const _url_launcher_web = Package( name: 'url_launcher_web', description: 'Web platform implementation of url_launcher', repository: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_web', authors: [], - version: '2.4.2', - spdxIdentifiers: ['Apache-2.0', 'BSD-3-Clause'], + version: '2.4.3', + spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, dependencies: [PackageRef('flutter'), PackageRef('flutter_web_plugins'), PackageRef('url_launcher_platform_interface'), PackageRef('web')], devDependencies: [PackageRef('flutter_test')], - license: '''url_launcher_web - -Copyright 2013 The Flutter Authors + license: '''Copyright 2013 The Flutter Authors Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -37154,211 +37152,7 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --------------------------------------------------------------------------------- -platform_detect - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2017 Workiva Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License.''', +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); /// url_launcher_windows 3.1.5 @@ -37882,12 +37676,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.24+258 +/// tallee 0.0.25+259 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.24+258', + version: '0.0.25+259', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, From 284395bb77953e2e7a5678df77cdd8d1b4a0f66f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 13:42:43 +0200 Subject: [PATCH 060/127] Deleted home view --- .../main_menu/custom_navigation_bar.dart | 17 +- .../views/main_menu/home_view.dart | 259 ------------------ 2 files changed, 4 insertions(+), 272 deletions(-) delete mode 100644 lib/presentation/views/main_menu/home_view.dart diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 16316ad..5e23077 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -3,7 +3,6 @@ import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart'; -import 'package:tallee/presentation/views/main_menu/home_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view.dart'; @@ -31,7 +30,6 @@ class _CustomNavigationBarState extends State final loc = AppLocalizations.of(context); // Pretty ugly but works final List tabs = [ - KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()), KeyedSubtree( key: ValueKey('matches_$tabKeyCount'), child: const MatchView(), @@ -101,27 +99,20 @@ class _CustomNavigationBarState extends State NavbarItem( index: 0, isSelected: currentIndex == 0, - icon: Icons.home_rounded, - label: loc.home, - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 1, - isSelected: currentIndex == 1, icon: Icons.gamepad_rounded, label: loc.matches, onTabTapped: onTabTapped, ), NavbarItem( - index: 2, - isSelected: currentIndex == 2, + index: 1, + isSelected: currentIndex == 1, icon: Icons.group_rounded, label: loc.groups, onTabTapped: onTabTapped, ), NavbarItem( - index: 3, - isSelected: currentIndex == 3, + index: 2, + isSelected: currentIndex == 2, icon: Icons.bar_chart_rounded, label: loc.statistics, onTabTapped: onTabTapped, diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart deleted file mode 100644 index 627db82..0000000 --- a/lib/presentation/views/main_menu/home_view.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:tallee/core/adaptive_page_route.dart'; -import 'package:tallee/core/constants.dart'; -import 'package:tallee/core/enums.dart'; -import 'package:tallee/data/db/database.dart'; -import 'package:tallee/data/models/game.dart'; -import 'package:tallee/data/models/group.dart'; -import 'package:tallee/data/models/match.dart'; -import 'package:tallee/data/models/player.dart'; -import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; -import 'package:tallee/presentation/widgets/app_skeleton.dart'; -import 'package:tallee/presentation/widgets/buttons/quick_create_button.dart'; -import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; -import 'package:tallee/presentation/widgets/tiles/match_tile.dart'; -import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart'; - -class HomeView extends StatefulWidget { - /// The main home view of the application, displaying quick info, - /// recent matches, and quick create options. - const HomeView({super.key}); - - @override - State createState() => _HomeViewState(); -} - -class _HomeViewState extends State { - bool isLoading = true; - - /// Amount of matches in the database - int matchCount = 0; - - /// Amount of groups in the database - int groupCount = 0; - - /// Loaded recent matches from the database - List loadedRecentMatches = []; - - /// Recent matches to display, initially filled with skeleton matches - List recentMatches = List.filled( - 2, - Match( - name: 'Skeleton Match', - game: Game( - name: 'Skeleton Game', - ruleset: Ruleset.singleWinner, - description: 'This is a skeleton game description.', - color: GameColor.blue, - icon: '', - ), - group: Group( - name: 'Skeleton Group', - description: 'This is a skeleton group description.', - members: [ - Player( - name: - 'Skeleton Player 1' - '', - ), - Player( - name: - 'Skeleton Player 2' - '', - ), - ], - ), - notes: 'These are skeleton notes.', - players: [ - Player( - name: - 'Skeleton Player 1' - '', - ), - Player( - name: - 'Skeleton Player 2' - '', - ), - ], - ), - ); - - @override - void initState() { - super.initState(); - loadHomeViewData(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return AppSkeleton( - fixLayoutBuilder: true, - enabled: isLoading, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.13, - title: loc.matches, - icon: Icons.groups_rounded, - value: matchCount, - ), - SizedBox(width: constraints.maxWidth * 0.05), - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.13, - title: loc.groups, - icon: Icons.groups_rounded, - value: groupCount, - ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: InfoTile( - width: constraints.maxWidth * 0.95, - title: loc.recent_matches, - icon: Icons.history_rounded, - content: Column( - children: [ - if (recentMatches.isNotEmpty) - for (Match match in recentMatches) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 6.0, - ), - 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), - ), - ); - await loadRecentMatches(); - - setState(() { - print('loaded'); - }); - }, - ), - ) - else - Center( - heightFactor: 5, - child: Text(loc.no_recent_matches_available), - ), - ], - ), - ), - ), - Padding( - padding: EdgeInsets.zero, - child: InfoTile( - width: constraints.maxWidth * 0.95, - title: loc.quick_create, - icon: Icons.add_box_rounded, - content: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - QuickCreateButton( - text: 'Category 1', - onPressed: () {}, - ), - QuickCreateButton( - text: 'Category 2', - onPressed: () {}, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - QuickCreateButton( - text: 'Category 3', - onPressed: () {}, - ), - QuickCreateButton( - text: 'Category 4', - onPressed: () {}, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - QuickCreateButton( - text: 'Category 5', - onPressed: () {}, - ), - QuickCreateButton( - text: 'Category 6', - onPressed: () {}, - ), - ], - ), - ], - ), - ), - ), - SizedBox(height: MediaQuery.paddingOf(context).bottom), - ], - ), - ), - ); - }, - ); - } - - /// Loads the data for the HomeView from the database. - /// This includes the match count, group count, and recent matches. - Future loadHomeViewData() async { - final db = Provider.of(context, listen: false); - Future.wait([ - db.matchDao.getMatchCount(), - db.groupDao.getGroupCount(), - db.matchDao.getAllMatches(), - Future.delayed(Constants.MINIMUM_SKELETON_DURATION), - ]).then((results) { - matchCount = results[0] as int; - groupCount = results[1] as int; - loadedRecentMatches = results[2] as List; - recentMatches = - (loadedRecentMatches - ..sort((a, b) => b.createdAt.compareTo(a.createdAt))) - .take(2) - .toList(); - if (mounted) { - setState(() { - isLoading = false; - }); - } - }); - } - - Future loadRecentMatches() async { - final db = Provider.of(context, listen: false); - final matches = await db.matchDao.getAllMatches(); - recentMatches = - (matches..sort((a, b) => b.createdAt.compareTo(a.createdAt))) - .take(2) - .toList(); - } -} From f6dccbfc76076793c2cd5facab7a17837011cd95 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 6 May 2026 20:34:55 +0200 Subject: [PATCH 061/127] Improved skeleton data --- .../views/main_menu/match_view/match_view.dart | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 2fb36e7..4f70347 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -10,6 +10,7 @@ import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_detail_view.dart'; @@ -30,8 +31,7 @@ class _MatchViewState extends State { late final AppDatabase db; bool isLoading = true; - /// Loaded matches from the database, - /// initially filled with skeleton matches + /// Loaded matches from the database, initially filled with skeleton matches List matches = List.filled( 4, Match( @@ -46,7 +46,15 @@ class _MatchViewState extends State { name: 'Group name', members: List.filled(5, Player(name: 'Player')), ), - players: [Player(name: 'Player')], + players: [ + Player(name: 'Player'), + Player(name: 'Player'), + Player(name: 'Player'), + Player(name: 'Player'), + Player(id: 'mvp_id', name: 'Player'), + ], + scores: {'mvp_id': ScoreEntry(score: 1)}, + endedAt: DateTime.now(), ), ); From cd7e61e2c73d6db315e2ba6f68058525ab26a4f6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 8 May 2026 18:02:00 +0200 Subject: [PATCH 062/127] Applied #212 fix --- lib/presentation/views/main_menu/statistics_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index fc6de83..8659a2e 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -62,7 +62,7 @@ class _StatisticsViewState extends State { children: [ QuickInfoTile( width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, + height: constraints.maxHeight * 0.13, title: loc.matches, icon: Icons.groups_rounded, value: matchCount, @@ -70,7 +70,7 @@ class _StatisticsViewState extends State { SizedBox(width: constraints.maxWidth * 0.05), QuickInfoTile( width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, + height: constraints.maxHeight * 0.13, title: loc.groups, icon: Icons.groups_rounded, value: groupCount, From 514b1c32e7a44ca8b5d6ecce0a652aab1c705958 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 8 May 2026 16:52:14 +0000 Subject: [PATCH 063/127] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7afeda4..283cbc2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.25+259 +version: 0.0.26+260 environment: sdk: ^3.8.1 From 3494d397ed192f37e903b82abe153166d712afa1 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 8 May 2026 16:52:55 +0000 Subject: [PATCH 064/127] Updated licenses [skip ci] --- .../views/main_menu/settings_view/licenses/oss_licenses.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 0876c07..a837a65 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -37676,12 +37676,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.25+259 +/// tallee 0.0.26+260 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.25+259', + version: '0.0.26+260', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, From 4520281cb65f284e5c60982137acb4de4390323e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 8 May 2026 19:33:22 +0200 Subject: [PATCH 065/127] fix: popup area --- .../create_match/create_game_view.dart | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 2094554..3baa5b3 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -297,20 +297,7 @@ class _CreateGameViewState extends State { // Choose color tile ChooseTile( title: loc.color, - trailing: Row( - spacing: 8, - children: [ - // Selected Color - Container( - width: 16, - height: 16, - margin: const EdgeInsets.only(left: 12), - decoration: BoxDecoration( - color: getColorFromGameColor(selectedColor!), - shape: BoxShape.circle, - ), - ), - + trailing: //Popup CustomPopup( showArrow: true, @@ -399,12 +386,25 @@ class _CreateGameViewState extends State { ), ), ), - child: Text( - translateGameColorToString(selectedColor!, context), + child: Row( + spacing: 8, + children: [ + // Selected Color + Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(left: 12), + decoration: BoxDecoration( + color: getColorFromGameColor(selectedColor!), + shape: BoxShape.circle, + ), + ), + Text( + translateGameColorToString(selectedColor!, context), + ), + ], ), ), - ], - ), ), // Description input field From 0d1ed3e666e56d5eba53633e1d825d3ee47f4afd Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 8 May 2026 19:36:52 +0200 Subject: [PATCH 066/127] fix: no element error --- .../main_menu/match_view/create_match/choose_game_view.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index cdd73c2..78a10f1 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -176,6 +176,10 @@ class _ChooseGameViewState extends State { } if (result.delete) { setState(() { + // deselect the game + if (selectedGameId == game.id) { + selectedGameId = ''; + } widget.games.removeAt(originalIndex); widget.onGamesUpdated?.call(); }); From 28fb608b3023be797586b46b82bd9cae10cd8603 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 8 May 2026 19:45:46 +0200 Subject: [PATCH 067/127] fix: increased font size --- .../main_menu/match_view/create_match/create_game_view.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 3baa5b3..f20817a 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -150,7 +150,10 @@ class _CreateGameViewState extends State { context: context, builder: (context) => CustomAlertDialog( title: loc.delete_game, - content: Text(dialogContent), + content: Text( + dialogContent, + style: const TextStyle(fontSize: 15), + ), actions: [ CustomDialogAction( isDestructive: true, From 86a920e9340479858a0610eb519a2a4e6c509dc2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 8 May 2026 23:33:15 +0200 Subject: [PATCH 068/127] fix: tab title --- lib/presentation/views/main_menu/custom_navigation_bar.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 5e23077..bf6ded3 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -136,12 +136,10 @@ class _CustomNavigationBarState extends State final loc = AppLocalizations.of(context); switch (currentIndex) { case 0: - return loc.home; - case 1: return loc.matches; - case 2: + case 1: return loc.groups; - case 3: + case 2: return loc.statistics; default: return ''; From 5b3b706c28b65dfa1ffea9420feae28251eda0b3 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 8 May 2026 22:00:28 +0000 Subject: [PATCH 069/127] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 283cbc2..3025651 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.26+260 +version: 0.0.27+261 environment: sdk: ^3.8.1 From 044a6acbbe38583f83152c63725d8b9d4f3a3b8b Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 8 May 2026 22:02:41 +0000 Subject: [PATCH 070/127] Updated licenses [skip ci] --- .../views/main_menu/settings_view/licenses/oss_licenses.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index a837a65..396d7b7 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -37676,12 +37676,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.26+260 +/// tallee 0.0.27+261 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.26+260', + version: '0.0.27+261', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, From bc997633eba24305d9b6059857c5de507df999e1 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 02:08:40 +0200 Subject: [PATCH 071/127] feat: add placement ruleset and related localization --- lib/core/common.dart | 2 + lib/core/enums.dart | 2 + lib/data/models/match.dart | 3 + lib/l10n/arb/app_de.arb | 2 + lib/l10n/arb/app_en.arb | 12 +++ lib/l10n/generated/app_localizations.dart | 18 ++++ lib/l10n/generated/app_localizations_de.dart | 10 ++ lib/l10n/generated/app_localizations_en.dart | 10 ++ .../match_view/match_detail_view.dart | 17 ++-- .../match_view/match_result_view.dart | 94 +++++++++++++++++++ .../widgets/tiles/text_icon_list_tile.dart | 12 +++ 11 files changed, 176 insertions(+), 6 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 8027180..187e3c1 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -18,6 +18,8 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) { return loc.single_loser; case Ruleset.multipleWinners: return loc.multiple_winners; + case Ruleset.placement: + return loc.placement; } } diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 6b33124..605d3aa 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -32,12 +32,14 @@ enum ExportResult { success, canceled, unknownException } /// - [Ruleset.singleWinner]: The match is won by a single player. /// - [Ruleset.singleLoser]: The match has a single loser. /// - [Ruleset.multipleWinners]: Multiple players can be winners. +/// - [Ruleset.placement]: The player with the highest placement wins. enum Ruleset { highestScore, lowestScore, singleWinner, singleLoser, multipleWinners, + placement, } /// Different colors available for games diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 9d14bb3..679f8a4 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -156,6 +156,9 @@ class Match { case Ruleset.multipleWinners: return []; + + case Ruleset.placement: + return _getPlayersWithHighestScore().take(1).toList(); } } diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 46c780a..fedd7dc 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -71,6 +71,7 @@ "none": "Kein", "none_group": "Keine", "not_available": "Nicht verfügbar", + "placement": "Platzierung", "played_matches": "Gespielte Spiele", "player_name": "Spieler:innenname", "players": "Spieler:innen", @@ -85,6 +86,7 @@ "ruleset": "Regelwerk", "ruleset_least_points": "Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.", "ruleset_most_points": "Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.", + "ruleset_placement": "Spieler:innen können in einer Reihenfolge angeordnet werden, die ihre Platzierung reflektiert.", "ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.", "ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.", "save_changes": "Änderungen speichern", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a85e1b0..82f7404 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -77,6 +77,9 @@ "@delete_match": { "description": "Button text to delete a match" }, + "@drag_to_set_placement": { + "description": "Label for dragging to set placement" + }, "@edit_group": { "description": "Button & Appbar label for editing a group" }, @@ -218,6 +221,9 @@ "@not_available": { "description": "Abbreviation for not available" }, + "@placement": { + "description": "Title for placement ruleset" + }, "@played_matches": { "description": "Label for played matches statistic" }, @@ -259,6 +265,9 @@ "@ruleset_most_points": { "description": "Description for most points ruleset" }, + "@ruleset_placement": { + "description": "Description for placement ruleset" + }, "@ruleset_single_loser": { "description": "Description for single loser ruleset" }, @@ -358,6 +367,7 @@ "delete_all_data": "Delete all data", "delete_group": "Delete Group", "delete_match": "Delete Match", + "drag_to_set_placement": "Drag to set placement", "edit_group": "Edit Group", "edit_match": "Edit Match", "enter_points": "Enter points", @@ -405,6 +415,7 @@ "none": "None", "none_group": "None", "not_available": "Not available", + "placement": "Placement", "played_matches": "Played Matches", "player_name": "Player name", "players": "Players", @@ -418,6 +429,7 @@ "ruleset": "Ruleset", "ruleset_least_points": "Inverse scoring: the player with the fewest points wins.", "ruleset_most_points": "Traditional ruleset: the player with the most points wins.", + "ruleset_placement": "Players can be arranged in an order, which reflects their placement.", "ruleset_single_loser": "Exactly one loser is determined; last place receives the penalty or consequence.", "ruleset_single_winner": "Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.", "save_changes": "Save Changes", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 99c9317..f4cd87c 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -242,6 +242,12 @@ abstract class AppLocalizations { /// **'Delete Match'** String get delete_match; + /// Label for dragging to set placement + /// + /// In en, this message translates to: + /// **'Drag to set placement'** + String get drag_to_set_placement; + /// Button & Appbar label for editing a group /// /// In en, this message translates to: @@ -524,6 +530,12 @@ abstract class AppLocalizations { /// **'Not available'** String get not_available; + /// Title for placement ruleset + /// + /// In en, this message translates to: + /// **'Placement'** + String get placement; + /// Label for played matches statistic /// /// In en, this message translates to: @@ -602,6 +614,12 @@ abstract class AppLocalizations { /// **'Traditional ruleset: the player with the most points wins.'** String get ruleset_most_points; + /// Description for placement ruleset + /// + /// In en, this message translates to: + /// **'Players can be arranged in an order, which reflects their placement.'** + String get ruleset_placement; + /// Description for single loser ruleset /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 51b4c62..f53261d 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -84,6 +84,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get delete_match => 'Spiel löschen'; + @override + String get drag_to_set_placement => 'Ziehen, um die Platzierung zu setzen'; + @override String get edit_group => 'Gruppe bearbeiten'; @@ -229,6 +232,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get not_available => 'Nicht verfügbar'; + @override + String get placement => 'Platzierung'; + @override String get played_matches => 'Gespielte Spiele'; @@ -272,6 +278,10 @@ class AppLocalizationsDe extends AppLocalizations { String get ruleset_most_points => 'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.'; + @override + String get ruleset_placement => + 'Spieler:innen können in einer Reihenfolge angeordnet werden, die ihre Platzierung reflektiert.'; + @override String get ruleset_single_loser => 'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 2b42e47..6dcfda1 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -84,6 +84,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get delete_match => 'Delete Match'; + @override + String get drag_to_set_placement => 'Drag to set placement'; + @override String get edit_group => 'Edit Group'; @@ -229,6 +232,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get not_available => 'Not available'; + @override + String get placement => 'Placement'; + @override String get played_matches => 'Played Matches'; @@ -272,6 +278,10 @@ class AppLocalizationsEn extends AppLocalizations { String get ruleset_most_points => 'Traditional ruleset: the player with the most points wins.'; + @override + String get ruleset_placement => + 'Players can be arranged in an order, which reflects their placement.'; + @override String get ruleset_single_loser => 'Exactly one loser is determined; last place receives the penalty or consequence.'; diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 2117b77..6f09301 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -288,34 +288,39 @@ class _MatchDetailViewState extends State { } } - /// Returns the result widget for scores + /// Returns the result widget for scores or placement Widget getScoreResultWidget(AppLocalizations loc) { List<(String, int)> playerScores = []; for (var player in match.players) { int score = match.scores[player.id]?.score ?? 0; playerScores.add((player.name, score)); } - if (widget.match.game.ruleset == Ruleset.highestScore) { + + final ruleset = match.game.ruleset; + + if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) { playerScores.sort((a, b) => b.$2.compareTo(a.$2)); - } else if (widget.match.game.ruleset == Ruleset.lowestScore) { + } else if (ruleset == Ruleset.lowestScore) { playerScores.sort((a, b) => a.$2.compareTo(b.$2)); } return Column( children: [ - for (var score in playerScores) + for (var i = 0; i < playerScores.length; i++) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - score.$1, + playerScores[i].$1, style: const TextStyle( fontSize: 16, color: CustomTheme.textColor, ), ), Text( - getPointLabel(loc, score.$2), + ruleset == Ruleset.placement + ? '#${i + 1}' + : getPointLabel(loc, playerScores[i].$2), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, 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 1fd6780..bf85e03 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 @@ -10,6 +10,7 @@ import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_list_tile.dart'; class MatchResultView extends StatefulWidget { /// A view that allows selecting and saving the winner of a match @@ -68,6 +69,12 @@ class _MatchResultViewState extends State { final score = scoreList?.score ?? 0; controller[i].text = score.toString(); } + } else if (rulesetSupportsPlacement()) { + allPlayers.sort((a, b) { + final scoreA = widget.match.scores[a.id]?.score ?? 0; + final scoreB = widget.match.scores[b.id]?.score ?? 0; + return scoreB.compareTo(scoreA); + }); } super.initState(); } @@ -177,6 +184,70 @@ class _MatchResultViewState extends State { }, ), ), + if (rulesetSupportsPlacement()) + Expanded( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Column( + children: [ + for (int i = 0; i < allPlayers.length; i++) + Container( + alignment: Alignment.center, + height: 60, + child: Container( + decoration: + CustomTheme.standardBoxDecoration, + alignment: Alignment.center, + height: 50, + width: 40, + child: Text( + " #${i + 1} ", + style: const TextStyle( + color: CustomTheme.primaryColor, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ), + ], + ), + ), + Expanded( + child: ReorderableListView.builder( + padding: EdgeInsets.zero, + proxyDecorator: (child, index, animation) { + return Material( + color: Colors.transparent, + child: child, + ); + }, + onReorder: (int oldIndex, int newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final Player item = allPlayers.removeAt( + oldIndex, + ); + allPlayers.insert(newIndex, item); + }); + }, + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return TextIconListTile( + key: ValueKey(allPlayers[index].id), + text: allPlayers[index].name, + iconEnabled: false, + ); + }, + ), + ), + ], + ), + ), ], ), ), @@ -222,6 +293,8 @@ class _MatchResultViewState extends State { } else if (ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore) { await _handleScores(); + } else if (ruleset == Ruleset.placement) { + await _handlePlacement(); } widget.onWinnerChanged?.call(); @@ -267,12 +340,29 @@ class _MatchResultViewState extends State { } } + /// Handles saving the placement for each player in the database. + Future _handlePlacement() async { + for (int i = 0; i < allPlayers.length; i++) { + await db.scoreEntryDao.addScore( + matchId: widget.match.id, + playerId: allPlayers[i].id, + entry: ScoreEntry( + roundNumber: 0, + score: allPlayers.length - i, + change: 0, + ), + ); + } + } + String getTitleForRuleset(AppLocalizations loc) { switch (ruleset) { case Ruleset.singleWinner: return loc.select_winner; case Ruleset.singleLoser: return loc.select_loser; + case Ruleset.placement: + return loc.drag_to_set_placement; default: return loc.enter_points; } @@ -285,4 +375,8 @@ class _MatchResultViewState extends State { bool rulesetSupportsScoreEntry() { return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore; } + + bool rulesetSupportsPlacement() { + return ruleset == Ruleset.placement; + } } diff --git a/lib/presentation/widgets/tiles/text_icon_list_tile.dart b/lib/presentation/widgets/tiles/text_icon_list_tile.dart index a31f2ae..f77b5c3 100644 --- a/lib/presentation/widgets/tiles/text_icon_list_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_list_tile.dart @@ -10,6 +10,7 @@ class TextIconListTile extends StatelessWidget { super.key, required this.text, this.suffixText = '', + this.prefixText = '', this.iconEnabled = true, this.onPressed, }); @@ -20,6 +21,9 @@ class TextIconListTile extends StatelessWidget { /// An optional suffix text to display after the main text. final String suffixText; + /// An optional prefix text to display before the main text. + final String prefixText; + /// A boolean to determine if the icon should be displayed. final bool iconEnabled; @@ -44,6 +48,14 @@ class TextIconListTile extends StatelessWidget { text: TextSpan( style: DefaultTextStyle.of(context).style, children: [ + TextSpan( + text: prefixText, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), + ), TextSpan( text: text, style: const TextStyle( From 868460b0236af71f7c254a42b6a875d4f4e4b9e9 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 11:50:49 +0200 Subject: [PATCH 072/127] feat: update TextIconListTile to support custom icons --- .../main_menu/match_view/match_result_view.dart | 2 +- lib/presentation/widgets/player_selection.dart | 1 + .../widgets/tiles/text_icon_list_tile.dart | 13 +++++-------- 3 files changed, 7 insertions(+), 9 deletions(-) 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 bf85e03..122f4ec 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 @@ -240,7 +240,7 @@ class _MatchResultViewState extends State { return TextIconListTile( key: ValueKey(allPlayers[index].id), text: allPlayers[index].name, - iconEnabled: false, + icon: Icons.drag_handle, ); }, ), diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 0fc8ea0..cdcc2ed 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -196,6 +196,7 @@ class _PlayerSelectionState extends State { return TextIconListTile( text: suggestedPlayers[index].name, suffixText: getNameCountText(suggestedPlayers[index]), + icon: Icons.add, onPressed: () { setState(() { // If the player is not already selected diff --git a/lib/presentation/widgets/tiles/text_icon_list_tile.dart b/lib/presentation/widgets/tiles/text_icon_list_tile.dart index f77b5c3..4c0d648 100644 --- a/lib/presentation/widgets/tiles/text_icon_list_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_list_tile.dart @@ -11,7 +11,7 @@ class TextIconListTile extends StatelessWidget { required this.text, this.suffixText = '', this.prefixText = '', - this.iconEnabled = true, + this.icon, this.onPressed, }); @@ -24,8 +24,8 @@ class TextIconListTile extends StatelessWidget { /// An optional prefix text to display before the main text. final String prefixText; - /// A boolean to determine if the icon should be displayed. - final bool iconEnabled; + /// The icon to display in the tile. + final IconData? icon; /// The callback to be invoked when the icon is pressed. final VoidCallback? onPressed; @@ -76,11 +76,8 @@ class TextIconListTile extends StatelessWidget { ), ), ), - if (iconEnabled) - GestureDetector( - onTap: onPressed, - child: const Icon(Icons.add, size: 20), - ), + if (icon != null) + GestureDetector(onTap: onPressed, child: Icon(icon, size: 20)), ], ), ); From 8b7a519e645f2672108a31c3afdd62e7ea411212 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 11:51:13 +0200 Subject: [PATCH 073/127] fix: update string interpolation and use const TextStyle for consistency --- .../views/main_menu/match_view/match_result_view.dart | 2 +- lib/presentation/widgets/tiles/text_icon_list_tile.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 122f4ec..bc825c0 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 @@ -203,7 +203,7 @@ class _MatchResultViewState extends State { height: 50, width: 40, child: Text( - " #${i + 1} ", + ' #${i + 1} ', style: const TextStyle( color: CustomTheme.primaryColor, fontWeight: FontWeight.bold, diff --git a/lib/presentation/widgets/tiles/text_icon_list_tile.dart b/lib/presentation/widgets/tiles/text_icon_list_tile.dart index 4c0d648..924a164 100644 --- a/lib/presentation/widgets/tiles/text_icon_list_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_list_tile.dart @@ -50,7 +50,7 @@ class TextIconListTile extends StatelessWidget { children: [ TextSpan( text: prefixText, - style: TextStyle( + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: CustomTheme.primaryColor, From 6f155182b518f8591c2bff0b51c2267296f5df2b Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 12:33:54 +0200 Subject: [PATCH 074/127] remove scroll-physics from single winner & placement ruleset listview --- lib/l10n/generated/app_localizations_de.dart | 2 +- .../views/main_menu/match_view/match_result_view.dart | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index f53261d..383c3f4 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -85,7 +85,7 @@ class AppLocalizationsDe extends AppLocalizations { String get delete_match => 'Spiel löschen'; @override - String get drag_to_set_placement => 'Ziehen, um die Platzierung zu setzen'; + String get drag_to_set_placement => 'Drag to set placement'; @override String get edit_group => 'Gruppe bearbeiten'; 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 bc825c0..c0e250a 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 @@ -144,6 +144,7 @@ class _MatchResultViewState extends State { }); }, child: ListView.builder( + physics: NeverScrollableScrollPhysics(), itemCount: allPlayers.length, itemBuilder: (context, index) { return CustomRadioListTile( @@ -217,6 +218,7 @@ class _MatchResultViewState extends State { ), Expanded( child: ReorderableListView.builder( + physics: const NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, proxyDecorator: (child, index, animation) { return Material( From f0062dd9d9b9a57bcb4f10c04dcd00d1290107e4 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 12:34:07 +0200 Subject: [PATCH 075/127] add const --- .../views/main_menu/match_view/match_result_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c0e250a..7d5ed92 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 @@ -144,7 +144,7 @@ class _MatchResultViewState extends State { }); }, child: ListView.builder( - physics: NeverScrollableScrollPhysics(), + physics: const NeverScrollableScrollPhysics(), itemCount: allPlayers.length, itemBuilder: (context, index) { return CustomRadioListTile( From 350c5430a4bb607009c694748871ea6ffe4f01a4 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 12:57:42 +0200 Subject: [PATCH 076/127] implement updateMatchStateAfterSave to refresh match scores --- .../main_menu/match_view/match_detail_view.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 6f09301..3b52505 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -205,7 +205,7 @@ class _MatchDetailViewState extends State { match: match, onWinnerChanged: () { widget.onMatchUpdate.call(); - setState(() {}); + updateMatchStateAfterSave(); }, ), ), @@ -230,6 +230,17 @@ class _MatchDetailViewState extends State { widget.onMatchUpdate.call(); } + /// Updates the match scores after saving in MatchResultView + Future updateMatchStateAfterSave() async { + final scores = await db.scoreEntryDao.getAllMatchScores(matchId: match.id); + + if (!mounted) return; + + setState(() { + match.scores = scores; + }); + } + /// Returns the widget to be displayed in the result [InfoTile] Widget getResultWidget(AppLocalizations loc) { if (isSingleRowResult()) { From 2fdcc3e8aa218ecbeba96f4c6adef9b3a55cfc20 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 13:01:42 +0200 Subject: [PATCH 077/127] remove unecessessary prefix text --- .../widgets/tiles/text_icon_list_tile.dart | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/presentation/widgets/tiles/text_icon_list_tile.dart b/lib/presentation/widgets/tiles/text_icon_list_tile.dart index 924a164..04a0803 100644 --- a/lib/presentation/widgets/tiles/text_icon_list_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_list_tile.dart @@ -10,7 +10,6 @@ class TextIconListTile extends StatelessWidget { super.key, required this.text, this.suffixText = '', - this.prefixText = '', this.icon, this.onPressed, }); @@ -21,9 +20,6 @@ class TextIconListTile extends StatelessWidget { /// An optional suffix text to display after the main text. final String suffixText; - /// An optional prefix text to display before the main text. - final String prefixText; - /// The icon to display in the tile. final IconData? icon; @@ -48,14 +44,6 @@ class TextIconListTile extends StatelessWidget { text: TextSpan( style: DefaultTextStyle.of(context).style, children: [ - TextSpan( - text: prefixText, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, - ), - ), TextSpan( text: text, style: const TextStyle( From 5b668d28b79e4ee7d0dc20f8de833d24cccfc4c3 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 13:02:42 +0200 Subject: [PATCH 078/127] add translation for drag to set placement --- lib/l10n/arb/app_de.arb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index fedd7dc..a09ef91 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -24,6 +24,7 @@ "delete_all_data": "Alle Daten löschen", "delete_group": "Gruppe löschen", "delete_match": "Spiel löschen", + "drag_to_set_placement": "Ziehen um Platzierung zu setzen", "edit_group": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten", "enter_points": "Punkte eingeben", From fd553e1d245c3980178a823ce2179c304935ae55 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 13:27:24 +0200 Subject: [PATCH 079/127] fix: chevron tap --- .../match_view/create_match/create_game_view.dart | 15 +++++++++++++++ lib/presentation/widgets/tiles/choose_tile.dart | 13 ++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index f20817a..274c960 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_popup/flutter_popup.dart'; import 'package:provider/provider.dart'; @@ -292,6 +294,11 @@ class _CreateGameViewState extends State { Text( translateRulesetToString(selectedRuleset!, context), ), + const SizedBox(width: 5), + Transform.rotate( + angle: pi / 2, + child: const Icon(Icons.arrow_forward_ios, size: 16), + ), ], ), ), @@ -405,6 +412,14 @@ class _CreateGameViewState extends State { Text( translateGameColorToString(selectedColor!, context), ), + const SizedBox(width: 5), + Transform.rotate( + angle: pi / 2, + child: const Icon( + Icons.arrow_forward_ios, + size: 16, + ), + ), ], ), ), diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index 1f72328..41cc7f0 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:tallee/core/custom_theme.dart'; @@ -45,13 +43,10 @@ class _ChooseTileState extends State { ), const Spacer(), if (widget.trailing != null) widget.trailing!, - const SizedBox(width: 10), - widget.onPressed == null - ? Transform.rotate( - angle: pi / 2, - child: const Icon(Icons.arrow_forward_ios, size: 16), - ) - : const Icon(Icons.arrow_forward_ios, size: 16), + if (widget.onPressed != null) ...[ + const SizedBox(width: 10), + const Icon(Icons.arrow_forward_ios, size: 16), + ], ], ), ), From df757af7ec5e1df101e1550507380a8a61ce8f3e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 13:33:50 +0200 Subject: [PATCH 080/127] fix: choose tile alignment --- .../create_match/create_game_view.dart | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 274c960..3f4169e 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -288,13 +288,16 @@ class _CreateGameViewState extends State { ), ), child: Row( + spacing: 8, children: [ Icon(getRulesetIcon(selectedRuleset!), size: 16), - const SizedBox(width: 5), - Text( - translateRulesetToString(selectedRuleset!, context), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Text( + translateRulesetToString(selectedRuleset!, context), + textAlign: TextAlign.right, + ), ), - const SizedBox(width: 5), Transform.rotate( angle: pi / 2, child: const Icon(Icons.arrow_forward_ios, size: 16), @@ -403,16 +406,20 @@ class _CreateGameViewState extends State { Container( width: 16, height: 16, - margin: const EdgeInsets.only(left: 12), decoration: BoxDecoration( color: getColorFromGameColor(selectedColor!), shape: BoxShape.circle, ), ), - Text( - translateGameColorToString(selectedColor!, context), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Text( + translateGameColorToString( + selectedColor!, + context, + ), + ), ), - const SizedBox(width: 5), Transform.rotate( angle: pi / 2, child: const Icon( From 90331bfc0779a9fc9234567ecd863f71d776d085 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 14:29:01 +0200 Subject: [PATCH 081/127] fix: button view logic --- .../match_view/match_result_view.dart | 78 ++++++++----------- 1 file changed, 33 insertions(+), 45 deletions(-) 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 357012c..2c6976e 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 @@ -50,7 +50,7 @@ class _MatchResultViewState extends State { @override void initState() { db = Provider.of(context, listen: false); - ruleset = widget.match.game.ruleset; + ruleset = Ruleset.highestScore; //widget.match.game.ruleset; canSave = !rulesetSupportsScoreEntry(); allPlayers = widget.match.players; @@ -93,16 +93,13 @@ class _MatchResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( - automaticallyImplyLeading: !isLiveEditMode, - leading: !isLiveEditMode - ? IconButton( - icon: const Icon(Icons.close), - onPressed: () { - widget.onWinnerChanged?.call(); - Navigator.of(context).pop(_selectedPlayer); - }, - ) - : null, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + widget.onWinnerChanged?.call(); + Navigator.of(context).pop(_selectedPlayer); + }, + ), title: Text(widget.match.name), ), body: SafeArea( @@ -212,47 +209,38 @@ class _MatchResultViewState extends State { ), ), ), - if (!isLiveEditMode) ...[ - if (rulesetSupportsScoreEntry()) - // Button to switch to live edit mode - ...[ - CustomWidthButton( - text: loc.live_edit_mode, - sizeRelativeToWidth: 0.95, - buttonType: ButtonType.secondary, - onPressed: () => setState(() { - isLiveEditMode = true; - }), - ), - const SizedBox(height: 10), - ], - // Save Changes Button + if (rulesetSupportsScoreEntry()) + // Button to switch to live edit mode + ...[ CustomWidthButton( - text: loc.save_changes, - sizeRelativeToWidth: 0.95, - onPressed: canSave - ? () async { - final ending = DateTime.now(); - await db.matchDao.updateMatchEndedAt( - matchId: widget.match.id, - endedAt: ending, - ); - await _handleSaving(); - if (!context.mounted) return; - Navigator.of(context).pop(_selectedPlayer); - } - : null, - ), - ] else ...[ - CustomWidthButton( - text: loc.exit_view, + text: isLiveEditMode ? loc.exit_view : loc.live_edit_mode, sizeRelativeToWidth: 0.95, + buttonType: ButtonType.secondary, onPressed: () => setState(() { - isLiveEditMode = false; + isLiveEditMode = !isLiveEditMode; }), ), + const SizedBox(height: 10), ], + + // Save Changes Button + CustomWidthButton( + text: loc.save_changes, + sizeRelativeToWidth: 0.95, + onPressed: canSave + ? () async { + final ending = DateTime.now(); + await db.matchDao.updateMatchEndedAt( + matchId: widget.match.id, + endedAt: ending, + ); + await _handleSaving(); + if (!context.mounted) return; + Navigator.of(context).pop(_selectedPlayer); + } + : null, + ), ], ), ), From 8dbf2a573ebb35b866a4c5299a021815c9064d70 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 14:39:28 +0200 Subject: [PATCH 082/127] feat: negative numbers in score list tile --- .../match_result_view/score_list_tile.dart | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart index 64492ae..e4cfff9 100644 --- a/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart @@ -40,9 +40,13 @@ class ScoreListTile extends StatelessWidget { height: 40, child: TextField( controller: controller, - keyboardType: TextInputType.number, - maxLength: 4, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], + keyboardType: const TextInputType.numberWithOptions(signed: true), + maxLength: 5, + inputFormatters: [ + TextInputFormatter.withFunction((oldValue, newValue) { + return isValidScoreInput(newValue.text) ? newValue : oldValue; + }), + ], textAlign: TextAlign.center, style: const TextStyle( fontSize: 16, @@ -80,4 +84,21 @@ class ScoreListTile extends StatelessWidget { ), ); } + + /// Validates the input for the score text field. + bool isValidScoreInput(String text) { + if (text.isEmpty || text == '-') { + return true; + } + + final isNegative = text.startsWith('-'); + final digits = isNegative ? text.substring(1) : text; + + if (digits.isEmpty || digits.length > 4) { + return false; + } + + // CHeck if all characters are digits 0 <= x <= 9 + return digits.codeUnits.every((unit) => unit >= 48 && unit <= 57); + } } From 40e2229aa58d8c71160943093e23324ced7a970e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 14:52:12 +0200 Subject: [PATCH 083/127] feat: long press adds 10 points repeatedly --- .../widgets/buttons/main_menu_button.dart | 37 ++++++++++++++++++- .../live_edit_list_tile.dart | 20 ++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index 5eb76f1..c5c7a34 100644 --- a/lib/presentation/widgets/buttons/main_menu_button.dart +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; class MainMenuButton extends StatefulWidget { @@ -10,6 +12,7 @@ class MainMenuButton extends StatefulWidget { required this.onPressed, required this.icon, this.text, + this.onLongPressed, }); /// The callback to be invoked when the button is pressed. @@ -21,6 +24,8 @@ class MainMenuButton extends StatefulWidget { /// The text of the button. final String? text; + final void Function()? onLongPressed; + @override State createState() => _MainMenuButtonState(); } @@ -30,6 +35,14 @@ class _MainMenuButtonState extends State late AnimationController _animationController; late Animation _scaleAnimation; + /// How long the button needs to be pressed to register it as long press + Timer? _longPressTimer; + + /// How much time between two onLongPressed calls + Timer? _repeatTimer; + + bool _isLongPressing = false; + @override void initState() { super.initState(); @@ -51,15 +64,29 @@ class _MainMenuButtonState extends State child: GestureDetector( onTapDown: (_) { _animationController.forward(); + if (widget.onLongPressed != null) { + _longPressTimer = Timer(const Duration(milliseconds: 400), () { + _isLongPressing = true; + widget.onLongPressed?.call(); + _repeatTimer = Timer.periodic( + const Duration(milliseconds: 250), + (_) => widget.onLongPressed?.call(), + ); + }); + } }, onTapUp: (_) async { - if (mounted) { + _cancelTimers(); + if (mounted && !_isLongPressing) { widget.onPressed(); } + _isLongPressing = false; await Future.delayed(const Duration(milliseconds: 100)); await _animationController.reverse(); }, onTapCancel: () { + _isLongPressing = false; + _cancelTimers(); _animationController.reverse(); }, child: Container( @@ -93,7 +120,15 @@ class _MainMenuButtonState extends State @override void dispose() { + _cancelTimers(); _animationController.dispose(); super.dispose(); } + + void _cancelTimers() { + _longPressTimer?.cancel(); + _longPressTimer = null; + _repeatTimer?.cancel(); + _repeatTimer = null; + } } diff --git a/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart index 80243b8..d663efc 100644 --- a/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart @@ -53,6 +53,16 @@ class _LiveEditListTileState extends State { }), } : null, + onLongPressed: () => _score > minScore + ? { + setState(() { + _score -= 10; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, icon: Icons.remove_rounded, ), Expanded( @@ -96,6 +106,16 @@ class _LiveEditListTileState extends State { }), } : null, + onLongPressed: () => _score > minScore + ? { + setState(() { + _score += 10; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, icon: Icons.add_rounded, ), ], From f9eafa5b3d06d389b4f2eb7b33d938b568971686 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 15:01:21 +0200 Subject: [PATCH 084/127] add: empty game list messages --- lib/l10n/arb/app_de.arb | 2 + lib/l10n/arb/app_en.arb | 2 + lib/l10n/generated/app_localizations.dart | 12 ++ lib/l10n/generated/app_localizations_de.dart | 7 + lib/l10n/generated/app_localizations_en.dart | 7 + .../create_match/choose_game_view.dart | 132 ++++++++++-------- 6 files changed, 107 insertions(+), 55 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index ab8166a..dd8c744 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -80,6 +80,7 @@ "members": "Mitglieder", "most_points": "Höchste Punkte", "no_data_available": "Keine Daten verfügbar", + "no_games_created_yet": "Noch keine Spielvorlagen erstellt", "no_groups_created_yet": "Noch keine Gruppen erstellt", "no_licenses_found": "Keine Lizenzen gefunden", "no_license_text_available": "Kein Lizenztext verfügbar", @@ -125,6 +126,7 @@ "statistics": "Statistiken", "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", + "there_are_no_games_matching_your_search": "Es gibt keine Spielvorlagen, die deiner Suche entspricht", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "tie": "Unentschieden", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e41bb83..11a908e 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -81,6 +81,7 @@ "members": "Members", "most_points": "Most Points", "no_data_available": "No data available", + "no_games_created_yet": "No games created yet", "no_groups_created_yet": "No groups created yet", "no_licenses_found": "No licenses found", "no_license_text_available": "No license text available", @@ -134,6 +135,7 @@ } } }, + "there_are_no_games_matching_your_search": "There are no games matching your search", "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", "tie": "Tie", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index dd8f9cb..1f6abff 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -536,6 +536,12 @@ abstract class AppLocalizations { /// **'No data available'** String get no_data_available; + /// No description provided for @no_games_created_yet. + /// + /// In en, this message translates to: + /// **'No games created yet'** + String get no_games_created_yet; + /// No description provided for @no_groups_created_yet. /// /// In en, this message translates to: @@ -800,6 +806,12 @@ abstract class AppLocalizations { /// **'Successfully added player {playerName}'** String successfully_added_player(String playerName); + /// No description provided for @there_are_no_games_matching_your_search. + /// + /// In en, this message translates to: + /// **'There are no games matching your search'** + String get there_are_no_games_matching_your_search; + /// No description provided for @there_is_no_group_matching_your_search. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index fe846c3..bff60d0 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -243,6 +243,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get no_data_available => 'Keine Daten verfügbar'; + @override + String get no_games_created_yet => 'Noch keine Spielvorlagen erstellt'; + @override String get no_groups_created_yet => 'Noch keine Gruppen erstellt'; @@ -382,6 +385,10 @@ class AppLocalizationsDe extends AppLocalizations { return 'Spieler:in $playerName erfolgreich hinzugefügt'; } + @override + String get there_are_no_games_matching_your_search => + 'Es gibt keine Spielvorlagen, die deiner Suche entspricht'; + @override String get there_is_no_group_matching_your_search => 'Es gibt keine Gruppe, die deiner Suche entspricht'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 899e3a5..ae7d813 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -243,6 +243,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no_data_available => 'No data available'; + @override + String get no_games_created_yet => 'No games created yet'; + @override String get no_groups_created_yet => 'No groups created yet'; @@ -382,6 +385,10 @@ class AppLocalizationsEn extends AppLocalizations { return 'Successfully added player $playerName'; } + @override + String get there_are_no_games_matching_your_search => + 'There are no games matching your search'; + @override String get there_is_no_group_matching_your_search => 'There is no group matching your search'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 78a10f1..c019213 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -9,6 +9,7 @@ import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game_view.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; +import 'package:tallee/presentation/widgets/top_centered_message.dart'; class ChooseGameView extends StatefulWidget { /// A view that allows the user to choose a game from a list of available games @@ -134,65 +135,86 @@ class _ChooseGameViewState extends State { // Game list Expanded( - child: ListView.builder( - itemCount: filteredGames.length, - itemBuilder: (BuildContext context, int index) { - final game = filteredGames[index]; - return GameTile( - title: game.name, - description: game.description, - badgeText: translateRulesetToString(game.ruleset, context), - badgeColor: getColorFromGameColor(game.color), - isHighlighted: selectedGameId == game.id, - onTap: () async { - setState(() { - if (selectedGameId == game.id) { - selectedGameId = ''; - } else { - selectedGameId = game.id; - } - }); - }, - onLongPress: () async { - final result = await Navigator.push( + child: Visibility( + visible: filteredGames.isNotEmpty, + replacement: Visibility( + visible: widget.games.isNotEmpty, + replacement: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: loc.no_games_created_yet, + ), + child: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: AppLocalizations.of( + context, + ).there_are_no_games_matching_your_search, + ), + ), + child: ListView.builder( + itemCount: filteredGames.length, + itemBuilder: (BuildContext context, int index) { + final game = filteredGames[index]; + return GameTile( + title: game.name, + description: game.description, + badgeText: translateRulesetToString( + game.ruleset, context, - adaptivePageRoute( - builder: (context) => CreateGameView( - gameToEdit: game, - matchCount: getMatchCount(game), - onGameChanged: () { - widget.onGamesUpdated?.call(); - }, + ), + badgeColor: getColorFromGameColor(game.color), + isHighlighted: selectedGameId == game.id, + onTap: () async { + setState(() { + if (selectedGameId == game.id) { + selectedGameId = ''; + } else { + selectedGameId = game.id; + } + }); + }, + onLongPress: () async { + final result = await Navigator.push( + context, + adaptivePageRoute( + builder: (context) => CreateGameView( + gameToEdit: game, + matchCount: getMatchCount(game), + onGameChanged: () { + widget.onGamesUpdated?.call(); + }, + ), ), - ), - ); - if (result != null && result.game != null) { - // Find the index in the original list to mutate - final originalIndex = widget.games.indexWhere( - (g) => g.id == game.id, ); - if (originalIndex == -1) { - return; + if (result != null && result.game != null) { + // Find the index in the original list to mutate + final originalIndex = widget.games.indexWhere( + (g) => g.id == game.id, + ); + if (originalIndex == -1) { + return; + } + if (result.delete) { + setState(() { + // deselect the game + if (selectedGameId == game.id) { + selectedGameId = ''; + } + widget.games.removeAt(originalIndex); + widget.onGamesUpdated?.call(); + }); + } else { + setState(() { + widget.games[originalIndex] = result.game; + }); + } + _refreshFromSource(); } - if (result.delete) { - setState(() { - // deselect the game - if (selectedGameId == game.id) { - selectedGameId = ''; - } - widget.games.removeAt(originalIndex); - widget.onGamesUpdated?.call(); - }); - } else { - setState(() { - widget.games[originalIndex] = result.game; - }); - } - _refreshFromSource(); - } - }, - ); - }, + }, + ); + }, + ), ), ), ], From 2d2a83ea4cb2e6420acdd6d6c48364db1bce652f Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 17:14:38 +0200 Subject: [PATCH 085/127] remove matchStateUpdate bcs alr implement in another pr --- .../main_menu/match_view/match_detail_view.dart | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 3b52505..ed02538 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -205,7 +205,6 @@ class _MatchDetailViewState extends State { match: match, onWinnerChanged: () { widget.onMatchUpdate.call(); - updateMatchStateAfterSave(); }, ), ), @@ -230,17 +229,6 @@ class _MatchDetailViewState extends State { widget.onMatchUpdate.call(); } - /// Updates the match scores after saving in MatchResultView - Future updateMatchStateAfterSave() async { - final scores = await db.scoreEntryDao.getAllMatchScores(matchId: match.id); - - if (!mounted) return; - - setState(() { - match.scores = scores; - }); - } - /// Returns the widget to be displayed in the result [InfoTile] Widget getResultWidget(AppLocalizations loc) { if (isSingleRowResult()) { From fbb83aaf7b4981d01a409c2f22c86904f7500314 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 17:17:33 +0200 Subject: [PATCH 086/127] fix: localization plural --- lib/l10n/arb/app_de.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index dd8c744..9107bff 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -34,7 +34,7 @@ "delete": "Löschen", "delete_all_data": "Alle Daten löschen", "delete_game": "Spielvorlage löschen", - "delete_game_with_matches_warning": "Wenn du diese Spielvorlage löschst, werden {count, plural, =1{1 Spiel} other{{count} Spiele}} mit dieser Spielvorlage ebenfalls gelöscht.", + "delete_game_with_matches_warning": "Wenn du diese Spielvorlage löschst, {count, plural, =1{wird 1 Spiel} other{werden {count} Spiele}} mit dieser Spielvorlage ebenfalls gelöscht.", "@delete_game_with_matches_warning": { "placeholders": { "count": { From 5c9db7244a5f2d5e7270fa1bb3de6f76865e6f5b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 17:22:16 +0200 Subject: [PATCH 087/127] fix: adjusted test --- test/db_tests/aggregates/match_test.dart | 32 ++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 7f627f7..0c5b1df 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -261,28 +261,28 @@ void main() { }); test('getMatchCount() works correctly', () async { - var matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); + var count = await database.matchDao.getMatchCount(); + expect(count, 0); await database.matchDao.addMatch(match: testMatch1); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); + count = await database.matchDao.getMatchCount(); + expect(count, 1); await database.matchDao.addMatch(match: testMatch2); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 2); + count = await database.matchDao.getMatchCount(); + expect(count, 2); await database.matchDao.deleteMatch(matchId: testMatch1.id); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 1); + count = await database.matchDao.getMatchCount(); + expect(count, 1); await database.matchDao.deleteMatch(matchId: testMatch2.id); - matchCount = await database.matchDao.getMatchCount(); - expect(matchCount, 0); + count = await database.matchDao.getMatchCount(); + expect(count, 0); }); test('getMatchCountByGame() works correctly', () async { @@ -302,6 +302,18 @@ void main() { gameId: testGame.id, ); expect(count, 2); + + await database.matchDao.deleteMatch(matchId: testMatch1.id); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 1); + + await database.matchDao.deleteMatch(matchId: testMatch2.id); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 0); }); test('getMatchCountByGame() returns 0 for non-existent game', () async { From f7c8160c581202569fdcd7bacd4c313d7022a5fb Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 17:28:57 +0200 Subject: [PATCH 088/127] fix: removed print in test --- test/db_tests/aggregates/match_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 0c5b1df..37c1cd0 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -449,7 +449,6 @@ void main() { await database.matchDao.addMatch(match: testMatch1); DateTime newEndedAt = DateTime(2030, 1, 1, 12, 0, 0); - print(newEndedAt); await database.matchDao.updateMatchEndedAt( matchId: testMatch1.id, endedAt: newEndedAt, From 385bd39aa1ba6cf2e41533dc4c3aa42ad0ee3854 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 17:30:44 +0200 Subject: [PATCH 089/127] change placement tile decoration --- .../main_menu/match_view/match_result_view.dart | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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 7d5ed92..33c319d 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 @@ -198,11 +198,17 @@ class _MatchResultViewState extends State { alignment: Alignment.center, height: 60, child: Container( - decoration: - CustomTheme.standardBoxDecoration, + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all( + color: CustomTheme.primaryColor, + ), + borderRadius: CustomTheme + .standardBorderRadiusAll, + ), alignment: Alignment.center, height: 50, - width: 40, + width: 41, child: Text( ' #${i + 1} ', style: const TextStyle( @@ -223,7 +229,8 @@ class _MatchResultViewState extends State { proxyDecorator: (child, index, animation) { return Material( color: Colors.transparent, - child: child, + elevation: 8, + child: Opacity(opacity: 0.9, child: child), ); }, onReorder: (int oldIndex, int newIndex) { From 9781a20b385a944cf7b79bf4cc57c59e80221acd Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 17:36:18 +0200 Subject: [PATCH 090/127] implement dragging animation --- .../match_view/match_result_view.dart | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) 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 33c319d..f0c6fb4 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 @@ -227,10 +227,42 @@ class _MatchResultViewState extends State { physics: const NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, proxyDecorator: (child, index, animation) { - return Material( - color: Colors.transparent, - elevation: 8, - child: Opacity(opacity: 0.9, child: child), + return AnimatedBuilder( + animation: animation, + builder: (context, child) { + final t = Curves.easeInOut.transform( + animation.value, + ); + return Opacity( + opacity: t, + child: Material( + color: Colors.transparent, + child: Container( + decoration: BoxDecoration( + borderRadius: CustomTheme + .standardBorderRadiusAll, + boxShadow: [ + BoxShadow( + color: CustomTheme + .primaryColor + .withAlpha(30), + blurRadius: 4, + ), + BoxShadow( + color: CustomTheme + .primaryColor + .withAlpha(18), + blurRadius: 12, + spreadRadius: 2, + ), + ], + ), + child: child, + ), + ), + ); + }, + child: child, ); }, onReorder: (int oldIndex, int newIndex) { From 616c23937551d54ad0c270abc10fff3aceaa143f Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 17:47:23 +0200 Subject: [PATCH 091/127] change placement to text in match detail view --- lib/l10n/arb/app_de.arb | 1 + lib/l10n/arb/app_en.arb | 4 +++ lib/l10n/generated/app_localizations.dart | 6 ++++ lib/l10n/generated/app_localizations_de.dart | 5 ++- lib/l10n/generated/app_localizations_en.dart | 3 ++ .../match_view/match_detail_view.dart | 33 ++++++++++++++++++- 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index a09ef91..bdb1da2 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -73,6 +73,7 @@ "none_group": "Keine", "not_available": "Nicht verfügbar", "placement": "Platzierung", + "place": "Platz", "played_matches": "Gespielte Spiele", "player_name": "Spieler:innenname", "players": "Spieler:innen", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 82f7404..a987a38 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -224,6 +224,9 @@ "@placement": { "description": "Title for placement ruleset" }, + "@place": { + "description": "Label for placement text in match detail view" + }, "@played_matches": { "description": "Label for played matches statistic" }, @@ -416,6 +419,7 @@ "none_group": "None", "not_available": "Not available", "placement": "Placement", + "place": "place", "played_matches": "Played Matches", "player_name": "Player name", "players": "Players", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index f4cd87c..481d5ea 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -536,6 +536,12 @@ abstract class AppLocalizations { /// **'Placement'** String get placement; + /// Label for placement text in match detail view + /// + /// In en, this message translates to: + /// **'place'** + String get place; + /// Label for played matches statistic /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 383c3f4..aa67755 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -85,7 +85,7 @@ class AppLocalizationsDe extends AppLocalizations { String get delete_match => 'Spiel löschen'; @override - String get drag_to_set_placement => 'Drag to set placement'; + String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen'; @override String get edit_group => 'Gruppe bearbeiten'; @@ -235,6 +235,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get placement => 'Platzierung'; + @override + String get place => 'Platz'; + @override String get played_matches => 'Gespielte Spiele'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 6dcfda1..180600c 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -235,6 +235,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get placement => 'Placement'; + @override + String get place => 'place'; + @override String get played_matches => 'Played Matches'; diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index ed02538..2dc9756 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -318,7 +318,7 @@ class _MatchDetailViewState extends State { ), Text( ruleset == Ruleset.placement - ? '#${i + 1}' + ? getPlacementText(i + 1, context, loc) : getPointLabel(loc, playerScores[i].$2), style: const TextStyle( fontSize: 16, @@ -337,4 +337,35 @@ class _MatchDetailViewState extends State { return match.game.ruleset == Ruleset.singleWinner || match.game.ruleset == Ruleset.singleLoser; } + + String getPlacementText( + int rank, + BuildContext context, + AppLocalizations loc, + ) { + final locale = Localizations.localeOf(context).languageCode; + + if (locale == 'de') { + return '$rank. ${loc.place}'; + } + + return '${_ordinalEn(rank)} ${loc.place}'; + } + + String _ordinalEn(int number) { + if (number % 100 >= 11 && number % 100 <= 13) { + return '${number}th'; + } + + switch (number % 10) { + case 1: + return '${number}st'; + case 2: + return '${number}nd'; + case 3: + return '${number}rd'; + default: + return '${number}th'; + } + } } From 79ce3efd0ab8d87bf7eac64f9c86862e65fa3a30 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 18:19:30 +0200 Subject: [PATCH 092/127] implement setPlacement in Score Dao & add placement game type to match tile --- lib/data/dao/score_entry_dao.dart | 15 +++++++++++++++ .../match_view/match_result_view.dart | 19 ++++++------------- .../widgets/tiles/match_tile.dart | 3 ++- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index 9c4e01d..cf6a449 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -353,4 +353,19 @@ class ScoreEntryDao extends DatabaseAccessor return await deleteAllScoresForMatch(matchId: matchId); } } + + /// Sets the placement for each player in a match. + /// The highest score is assigned to the first player, the second highest to the second player, and so on. + Future setPlacements({ + required String matchId, + required List players, + }) async { + for (int i = 0; i < players.length; i++) { + await db.scoreEntryDao.addScore( + matchId: matchId, + playerId: players[i].id, + entry: ScoreEntry(roundNumber: 0, score: players.length - i, change: 0), + ); + } + } } 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 f0c6fb4..839d109 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 @@ -199,7 +199,7 @@ class _MatchResultViewState extends State { height: 60, child: Container( decoration: BoxDecoration( - color: CustomTheme.boxColor, + color: CustomTheme.primaryColor, border: Border.all( color: CustomTheme.primaryColor, ), @@ -212,7 +212,7 @@ class _MatchResultViewState extends State { child: Text( ' #${i + 1} ', style: const TextStyle( - color: CustomTheme.primaryColor, + color: CustomTheme.textColor, fontWeight: FontWeight.bold, fontSize: 16, ), @@ -383,17 +383,10 @@ class _MatchResultViewState extends State { /// Handles saving the placement for each player in the database. Future _handlePlacement() async { - for (int i = 0; i < allPlayers.length; i++) { - await db.scoreEntryDao.addScore( - matchId: widget.match.id, - playerId: allPlayers[i].id, - entry: ScoreEntry( - roundNumber: 0, - score: allPlayers.length - i, - change: 0, - ), - ); - } + await db.scoreEntryDao.setPlacements( + matchId: widget.match.id, + players: allPlayers, + ); } String getTitleForRuleset(AppLocalizations loc) { diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index f7585d6..1149a67 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -303,8 +303,9 @@ class _MatchTileState extends State { final mvp = widget.match.mvp; final mvpScore = widget.match.scores[mvp.first.id]?.score ?? 0; final mvpNames = mvp.map((player) => player.name).join(', '); - return '${loc.winner}: $mvpNames (${getPointLabel(loc, mvpScore)})'; + } else if (ruleset == Ruleset.placement) { + return '${loc.winner}: ${widget.match.mvp.first.name}'; } return '${loc.winner}: n.A.'; } From 496d411af625340cdda0691adddaf2fa4a9ae7bb Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 18:37:02 +0200 Subject: [PATCH 093/127] feat: updated drag effect --- .../match_view/match_result_view.dart | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) 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 839d109..1c907a4 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 @@ -229,40 +229,35 @@ class _MatchResultViewState extends State { proxyDecorator: (child, index, animation) { return AnimatedBuilder( animation: animation, + child: child, builder: (context, child) { - final t = Curves.easeInOut.transform( - animation.value, - ); - return Opacity( - opacity: t, - child: Material( - color: Colors.transparent, - child: Container( - decoration: BoxDecoration( - borderRadius: CustomTheme - .standardBorderRadiusAll, - boxShadow: [ - BoxShadow( - color: CustomTheme - .primaryColor - .withAlpha(30), - blurRadius: 4, + final alpha = + (Curves.easeInOut.transform( + animation.value, + ) * + 40) + .toInt(); + return Stack( + children: [ + child!, + Positioned.fill( + left: 4, + top: 4, + right: 4, + bottom: 4, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white.withAlpha( + alpha, ), - BoxShadow( - color: CustomTheme - .primaryColor - .withAlpha(18), - blurRadius: 12, - spreadRadius: 2, - ), - ], + borderRadius: CustomTheme + .standardBorderRadiusAll, + ), ), - child: child, ), - ), + ], ); }, - child: child, ); }, onReorder: (int oldIndex, int newIndex) { From 1de0ef52ad75d0051be4d2fe20c63e3c4138ad5d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 18:41:03 +0200 Subject: [PATCH 094/127] feat: updated placement num styling --- .../views/main_menu/match_view/match_result_view.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 1c907a4..06e0da5 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 @@ -189,6 +189,7 @@ class _MatchResultViewState extends State { Expanded( child: Row( children: [ + // Placement indicators Padding( padding: const EdgeInsets.only(right: 8.0), child: Column( @@ -199,16 +200,13 @@ class _MatchResultViewState extends State { height: 60, child: Container( decoration: BoxDecoration( - color: CustomTheme.primaryColor, - border: Border.all( - color: CustomTheme.primaryColor, - ), + color: CustomTheme.boxBorderColor, borderRadius: CustomTheme .standardBorderRadiusAll, ), alignment: Alignment.center, height: 50, - width: 41, + width: 50, child: Text( ' #${i + 1} ', style: const TextStyle( @@ -222,6 +220,8 @@ class _MatchResultViewState extends State { ], ), ), + + // Drag list Expanded( child: ReorderableListView.builder( physics: const NeverScrollableScrollPhysics(), From ae572a5dbd4ed07aa253d801fa1fc2b4445cad17 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 18:55:00 +0200 Subject: [PATCH 095/127] Refactored trailing widgets --- lib/l10n/generated/app_localizations_de.dart | 6 +- .../create_match/create_game_view.dart | 398 ++++++++---------- 2 files changed, 186 insertions(+), 218 deletions(-) diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index bff60d0..3666d11 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -119,10 +119,10 @@ class AppLocalizationsDe extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count Spiele', - one: '1 Spiel', + other: 'werden $count Spiele', + one: 'wird 1 Spiel', ); - return 'Wenn du diese Spielvorlage löschst, werden $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; + return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; } @override diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 3f4169e..e0f9d85 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -214,223 +214,10 @@ class _CreateGameViewState extends State { // Choose ruleset tile if (!isEditMode()) - ChooseTile( - title: loc.ruleset, - trailing: CustomPopup( - showArrow: true, - arrowColor: CustomTheme.boxBorderColor, - contentPadding: const EdgeInsets.symmetric( - horizontal: 0, - vertical: 10, - ), - barrierColor: Colors.transparent, - contentDecoration: CustomTheme.standardBoxDecoration, - content: StatefulBuilder( - builder: (context, setPopupState) => SizedBox( - width: 280, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: List.generate( - _rulesets.length, - (index) => GestureDetector( - onTap: () { - setState(() { - selectedRuleset = _rulesets[index].$1; - }); - setPopupState(() {}); - }, - child: Column( - children: [ - Container( - margin: const EdgeInsets.symmetric( - horizontal: 12, - ), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - color: - selectedRuleset == _rulesets[index].$1 - ? CustomTheme.textColor.withAlpha(20) - : Colors.transparent, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, - ), - child: Row( - spacing: 8, - children: [ - Icon( - getRulesetIcon(_rulesets[index].$1), - size: 16, - ), - Text( - _rulesets[index].$2, - style: const TextStyle( - color: CustomTheme.textColor, - fontSize: 15, - ), - ), - ], - ), - ), - ), - if (index < _rulesets.length - 1) - const Divider(indent: 15, endIndent: 15), - ], - ), - ), - ), - ), - ), - ), - child: Row( - spacing: 8, - children: [ - Icon(getRulesetIcon(selectedRuleset!), size: 16), - Padding( - padding: const EdgeInsets.only(right: 5), - child: Text( - translateRulesetToString(selectedRuleset!, context), - textAlign: TextAlign.right, - ), - ), - Transform.rotate( - angle: pi / 2, - child: const Icon(Icons.arrow_forward_ios, size: 16), - ), - ], - ), - ), - ), + ChooseTile(title: loc.ruleset, trailing: getColorDropdown(loc)), // Choose color tile - ChooseTile( - title: loc.color, - trailing: - //Popup - CustomPopup( - showArrow: true, - arrowColor: CustomTheme.boxBorderColor, - contentPadding: const EdgeInsets.symmetric( - horizontal: 0, - vertical: 10, - ), - barrierColor: Colors.transparent, - contentDecoration: CustomTheme.standardBoxDecoration, - content: StatefulBuilder( - builder: (context, setPopupState) => SizedBox( - width: 150, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: List.generate( - _colors.length, - (index) => GestureDetector( - onTap: () { - setState(() { - selectedColor = _colors[index].$1; - }); - setPopupState(() {}); - }, - child: Column( - children: [ - // Selected Highlighting - Container( - margin: const EdgeInsets.symmetric( - horizontal: 12, - ), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - color: - selectedColor == _colors[index].$1 - ? CustomTheme.textColor.withAlpha( - 20, - ) - : Colors.transparent, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 6, - ), - child: Row( - spacing: 8, - children: selectedColor == null - ? [Text(loc.none)] - : [ - Container( - width: 16, - height: 16, - margin: - const EdgeInsets.only( - left: 12, - ), - decoration: BoxDecoration( - color: - getColorFromGameColor( - _colors[index].$1, - ), - shape: BoxShape.circle, - ), - ), - Text( - _colors[index].$2, - style: const TextStyle( - color: - CustomTheme.textColor, - fontSize: 15, - ), - ), - ], - ), - ), - ), - if (index < _colors.length - 1) - const Divider(indent: 15, endIndent: 15), - ], - ), - ), - ), - ), - ), - ), - child: Row( - spacing: 8, - children: [ - // Selected Color - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: getColorFromGameColor(selectedColor!), - shape: BoxShape.circle, - ), - ), - Padding( - padding: const EdgeInsets.only(right: 5), - child: Text( - translateGameColorToString( - selectedColor!, - context, - ), - ), - ), - Transform.rotate( - angle: pi / 2, - child: const Icon( - Icons.arrow_forward_ios, - size: 16, - ), - ), - ], - ), - ), - ), + ChooseTile(title: loc.color, trailing: getRulesetDropdown(loc)), // Description input field Container( @@ -549,4 +336,185 @@ class _CreateGameViewState extends State { bool isEditMode() { return widget.gameToEdit != null; } + + Widget getRulesetDropdown(AppLocalizations loc) { + return CustomPopup( + showArrow: true, + arrowColor: CustomTheme.boxBorderColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10), + barrierColor: Colors.transparent, + contentDecoration: CustomTheme.standardBoxDecoration, + content: StatefulBuilder( + builder: (context, setPopupState) => SizedBox( + width: 280, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + _rulesets.length, + (index) => GestureDetector( + onTap: () { + setState(() { + selectedRuleset = _rulesets[index].$1; + }); + setPopupState(() {}); + }, + child: Column( + children: [ + Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + color: selectedRuleset == _rulesets[index].$1 + ? CustomTheme.textColor.withAlpha(20) + : Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: Row( + spacing: 8, + children: [ + Icon(getRulesetIcon(_rulesets[index].$1), size: 16), + Text( + _rulesets[index].$2, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 15, + ), + ), + ], + ), + ), + ), + if (index < _rulesets.length - 1) + const Divider(indent: 15, endIndent: 15), + ], + ), + ), + ), + ), + ), + ), + child: Row( + spacing: 8, + children: [ + Icon(getRulesetIcon(selectedRuleset!), size: 16), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Text( + translateRulesetToString(selectedRuleset!, context), + textAlign: TextAlign.right, + ), + ), + Transform.rotate( + angle: pi / 2, + child: const Icon(Icons.arrow_forward_ios, size: 16), + ), + ], + ), + ); + } + + Widget getColorDropdown(AppLocalizations loc) { + return CustomPopup( + showArrow: true, + arrowColor: CustomTheme.boxBorderColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10), + barrierColor: Colors.transparent, + contentDecoration: CustomTheme.standardBoxDecoration, + content: StatefulBuilder( + builder: (context, setPopupState) => SizedBox( + width: 150, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + _colors.length, + (index) => GestureDetector( + onTap: () { + setState(() { + selectedColor = _colors[index].$1; + }); + setPopupState(() {}); + }, + child: Column( + children: [ + // Selected Highlighting + Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + color: selectedColor == _colors[index].$1 + ? CustomTheme.textColor.withAlpha(20) + : Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + spacing: 8, + children: selectedColor == null + ? [Text(loc.none)] + : [ + Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(left: 12), + decoration: BoxDecoration( + color: getColorFromGameColor( + _colors[index].$1, + ), + shape: BoxShape.circle, + ), + ), + Text( + _colors[index].$2, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 15, + ), + ), + ], + ), + ), + ), + if (index < _colors.length - 1) + const Divider(indent: 15, endIndent: 15), + ], + ), + ), + ), + ), + ), + ), + child: Row( + spacing: 8, + children: [ + // Selected Color + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: getColorFromGameColor(selectedColor!), + shape: BoxShape.circle, + ), + ), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Text(translateGameColorToString(selectedColor!, context)), + ), + Transform.rotate( + angle: pi / 2, + child: const Icon(Icons.arrow_forward_ios, size: 16), + ), + ], + ), + ); + } } From 84f8a77c7283843ab7329748c2b67cf2860abf2d Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Sat, 9 May 2026 17:14:43 +0000 Subject: [PATCH 096/127] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 28a008c..5e3ec4c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.27+261 +version: 0.0.28+262 environment: sdk: ^3.8.1 From 9e76acce29c2f5819a3df2eed96c14e3ef1f116e Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Sat, 9 May 2026 17:15:22 +0000 Subject: [PATCH 097/127] Updated licenses [skip ci] --- .../settings_view/licenses/oss_licenses.dart | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 396d7b7..92f3080 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -55,6 +55,7 @@ const allDependencies = [ _flutter_lints, _flutter_localizations, _flutter_plugin_android_lifecycle, + _flutter_popup, _flutter_test, _flutter_web_plugins, _fluttericon, @@ -168,6 +169,7 @@ const dependencies = [ _file_saver, _flutter, _flutter_localizations, + _flutter_popup, _fluttericon, _font_awesome_flutter, _intl, @@ -2628,6 +2630,41 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); +/// flutter_popup 3.3.9 +const _flutter_popup = Package( + name: 'flutter_popup', + description: 'The flutter_popup package is a versatile tool for creating customizable popups in Flutter apps. Its highlight feature effectively guides user attention to specific areas', + homepage: 'https://github.com/herowws/flutter_popup', + authors: [], + version: '3.3.9', + spdxIdentifiers: ['MIT'], + isMarkdown: false, + isSdk: false, + dependencies: [PackageRef('flutter')], + devDependencies: [PackageRef('flutter_lints'), PackageRef('flutter_test')], + license: '''MIT License + +Copyright (c) 2023 mopriestt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''', + ); + /// flutter_test null const _flutter_test = Package( name: 'flutter_test', @@ -37676,16 +37713,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.27+261 +/// tallee 0.0.28+262 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.27+261', + version: '0.0.28+262', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], + dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('flutter_popup'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], devDependencies: [PackageRef('flutter_test'), PackageRef('build_runner'), PackageRef('dart_pubspec_licenses'), PackageRef('drift_dev'), PackageRef('flutter_lints')], license: '''GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 From 3923e955fdd0e74ba8d10afa4e386d0e586b2fa3 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 19:19:18 +0200 Subject: [PATCH 098/127] regenerate localization --- lib/l10n/generated/app_localizations.dart | 234 ++++++++++--------- lib/l10n/generated/app_localizations_de.dart | 15 +- lib/l10n/generated/app_localizations_en.dart | 15 +- 3 files changed, 157 insertions(+), 107 deletions(-) diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 1f6abff..fccffa1 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -98,37 +98,37 @@ abstract class AppLocalizations { Locale('en'), ]; - /// No description provided for @all_players. + /// Label for all players list /// /// In en, this message translates to: /// **'All players'** String get all_players; - /// No description provided for @all_players_selected. + /// Message when all players are added to selection /// /// In en, this message translates to: /// **'All players selected'** String get all_players_selected; - /// No description provided for @amount_of_matches. + /// Label for amount of matches statistic /// /// In en, this message translates to: /// **'Amount of Matches'** String get amount_of_matches; - /// No description provided for @app_name. + /// The name of the App /// /// In en, this message translates to: /// **'Tallee'** String get app_name; - /// No description provided for @best_player. + /// Label for best player statistic /// /// In en, this message translates to: /// **'Best Player'** String get best_player; - /// No description provided for @cancel. + /// Cancel button text /// /// In en, this message translates to: /// **'Cancel'** @@ -140,19 +140,19 @@ abstract class AppLocalizations { /// **'Choose Color'** String get choose_color; - /// No description provided for @choose_game. + /// Label for choosing a game /// /// In en, this message translates to: /// **'Choose Game'** String get choose_game; - /// No description provided for @choose_group. + /// Label for choosing a group /// /// In en, this message translates to: /// **'Choose Group'** String get choose_group; - /// No description provided for @choose_ruleset. + /// Label for choosing a ruleset /// /// In en, this message translates to: /// **'Choose Ruleset'** @@ -212,7 +212,7 @@ abstract class AppLocalizations { /// **'Yellow'** String get color_yellow; - /// No description provided for @could_not_add_player. + /// Error message when adding a player fails /// /// In en, this message translates to: /// **'Could not add player'** @@ -224,73 +224,73 @@ abstract class AppLocalizations { /// **'Create Game'** String get create_game; - /// No description provided for @create_group. + /// Button text to create a group /// /// In en, this message translates to: /// **'Create Group'** String get create_group; - /// No description provided for @create_match. + /// Button text to create a match /// /// In en, this message translates to: /// **'Create match'** String get create_match; - /// No description provided for @create_new_group. + /// Appbar text to create a new group /// /// In en, this message translates to: /// **'Create new group'** String get create_new_group; - /// No description provided for @created_on. + /// Label for creation date /// /// In en, this message translates to: /// **'Created on'** String get created_on; - /// No description provided for @create_new_match. + /// Appbar text to create a new match /// /// In en, this message translates to: /// **'Create new match'** String get create_new_match; - /// No description provided for @data. + /// Data label /// /// In en, this message translates to: /// **'Data'** String get data; - /// No description provided for @data_successfully_deleted. + /// Success message after deleting data /// /// In en, this message translates to: /// **'Data successfully deleted'** String get data_successfully_deleted; - /// No description provided for @data_successfully_exported. + /// Success message after exporting data /// /// In en, this message translates to: /// **'Data successfully exported'** String get data_successfully_exported; - /// No description provided for @data_successfully_imported. + /// Success message after importing data /// /// In en, this message translates to: /// **'Data successfully imported'** String get data_successfully_imported; - /// No description provided for @days_ago. + /// Date format for days ago /// /// In en, this message translates to: /// **'{count} days ago'** - String days_ago(Object count); + String days_ago(int count); - /// No description provided for @delete. + /// Delete button text /// /// In en, this message translates to: /// **'Delete'** String get delete; - /// No description provided for @delete_all_data. + /// Confirmation dialog for deleting all data /// /// In en, this message translates to: /// **'Delete all data'** @@ -308,18 +308,24 @@ abstract class AppLocalizations { /// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'** String delete_game_with_matches_warning(int count); - /// No description provided for @delete_group. + /// Confirmation dialog for deleting a group /// /// In en, this message translates to: /// **'Delete Group'** String get delete_group; - /// No description provided for @delete_match. + /// Button text to delete a match /// /// In en, this message translates to: /// **'Delete Match'** String get delete_match; + /// Label for dragging to set placement + /// + /// In en, this message translates to: + /// **'Drag to set placement'** + String get drag_to_set_placement; + /// No description provided for @description. /// /// In en, this message translates to: @@ -332,31 +338,31 @@ abstract class AppLocalizations { /// **'Edit Game'** String get edit_game; - /// No description provided for @edit_group. + /// Button & Appbar label for editing a group /// /// In en, this message translates to: /// **'Edit Group'** String get edit_group; - /// No description provided for @edit_match. + /// Button & Appbar label for editing a match /// /// In en, this message translates to: /// **'Edit Match'** String get edit_match; - /// No description provided for @enter_points. + /// Label to enter players points /// /// In en, this message translates to: /// **'Enter points'** String get enter_points; - /// No description provided for @enter_results. + /// Button text to enter match results /// /// In en, this message translates to: /// **'Enter Results'** String get enter_results; - /// No description provided for @error_creating_group. + /// Error message when group creation fails /// /// In en, this message translates to: /// **'Error while creating group, please try again'** @@ -368,169 +374,169 @@ abstract class AppLocalizations { /// **'Error while deleting game, please try again'** String get error_deleting_game; - /// No description provided for @error_deleting_group. + /// Error message when group deletion fails /// /// In en, this message translates to: /// **'Error while deleting group, please try again'** String get error_deleting_group; - /// No description provided for @error_editing_group. + /// Error message when group editing fails /// /// In en, this message translates to: /// **'Error while editing group, please try again'** String get error_editing_group; - /// No description provided for @error_reading_file. + /// Error message when file cannot be read /// /// In en, this message translates to: /// **'Error reading file'** String get error_reading_file; - /// No description provided for @export_canceled. + /// Message when export is canceled /// /// In en, this message translates to: /// **'Export canceled'** String get export_canceled; - /// No description provided for @export_data. + /// Export data menu item /// /// In en, this message translates to: /// **'Export data'** String get export_data; - /// No description provided for @format_exception. + /// Error message for format exceptions /// /// In en, this message translates to: /// **'Format Exception (see console)'** String get format_exception; - /// No description provided for @game. + /// Game label /// /// In en, this message translates to: /// **'Game'** String get game; - /// No description provided for @game_name. + /// Placeholder for game name search /// /// In en, this message translates to: /// **'Game Name'** String get game_name; - /// No description provided for @group. + /// Group label /// /// In en, this message translates to: /// **'Group'** String get group; - /// No description provided for @group_name. + /// Placeholder for group name input /// /// In en, this message translates to: /// **'Group name'** String get group_name; - /// No description provided for @group_profile. + /// Title for group profile view /// /// In en, this message translates to: /// **'Group Profile'** String get group_profile; - /// No description provided for @groups. + /// Label for groups /// /// In en, this message translates to: /// **'Groups'** String get groups; - /// No description provided for @home. + /// Home tab label /// /// In en, this message translates to: /// **'Home'** String get home; - /// No description provided for @import_canceled. + /// Message when import is canceled /// /// In en, this message translates to: /// **'Import canceled'** String get import_canceled; - /// No description provided for @import_data. + /// Import data menu item /// /// In en, this message translates to: /// **'Import data'** String get import_data; - /// No description provided for @info. + /// Info label /// /// In en, this message translates to: /// **'Info'** String get info; - /// No description provided for @invalid_schema. + /// Error message for invalid schema /// /// In en, this message translates to: /// **'Invalid Schema'** String get invalid_schema; - /// No description provided for @least_points. + /// Title for least points ruleset /// /// In en, this message translates to: /// **'Least Points'** String get least_points; - /// No description provided for @legal. + /// Legal section header /// /// In en, this message translates to: /// **'Legal'** String get legal; - /// No description provided for @legal_notice. + /// Legal notice menu item /// /// In en, this message translates to: /// **'Legal Notice'** String get legal_notice; - /// No description provided for @licenses. + /// Licenses menu item /// /// In en, this message translates to: /// **'Licenses'** String get licenses; - /// No description provided for @match_in_progress. + /// Message when match is in progress /// /// In en, this message translates to: /// **'Match in progress...'** String get match_in_progress; - /// No description provided for @match_name. + /// Placeholder for match name input /// /// In en, this message translates to: /// **'Match name'** String get match_name; - /// No description provided for @match_profile. + /// Title for match profile view /// /// In en, this message translates to: /// **'Match Profile'** String get match_profile; - /// No description provided for @matches. + /// Label for matches /// /// In en, this message translates to: /// **'Matches'** String get matches; - /// No description provided for @members. + /// Label for group members /// /// In en, this message translates to: /// **'Members'** String get members; - /// No description provided for @most_points. + /// Title for most points ruleset /// /// In en, this message translates to: /// **'Most Points'** String get most_points; - /// No description provided for @no_data_available. + /// Message when no data in the statistic tiles is given /// /// In en, this message translates to: /// **'No data available'** @@ -542,103 +548,115 @@ abstract class AppLocalizations { /// **'No games created yet'** String get no_games_created_yet; - /// No description provided for @no_groups_created_yet. + /// Message when no groups exist /// /// In en, this message translates to: /// **'No groups created yet'** String get no_groups_created_yet; - /// No description provided for @no_licenses_found. + /// Message when no licenses are found /// /// In en, this message translates to: /// **'No licenses found'** String get no_licenses_found; - /// No description provided for @no_license_text_available. + /// Message when no license text is available /// /// In en, this message translates to: /// **'No license text available'** String get no_license_text_available; - /// No description provided for @no_matches_created_yet. + /// Message when no matches exist /// /// In en, this message translates to: /// **'No matches created yet'** String get no_matches_created_yet; - /// No description provided for @no_players_created_yet. + /// Message when no players exist /// /// In en, this message translates to: /// **'No players created yet'** String get no_players_created_yet; - /// No description provided for @no_players_found_with_that_name. + /// Message when search returns no results /// /// In en, this message translates to: /// **'No players found with that name'** String get no_players_found_with_that_name; - /// No description provided for @no_players_selected. + /// Message when no players are selected /// /// In en, this message translates to: /// **'No players selected'** String get no_players_selected; - /// No description provided for @no_recent_matches_available. + /// Message when no recent matches exist /// /// In en, this message translates to: /// **'No recent matches available'** String get no_recent_matches_available; - /// No description provided for @no_results_entered_yet. + /// Message when no results have been entered yet /// /// In en, this message translates to: /// **'No results entered yet'** String get no_results_entered_yet; - /// No description provided for @no_second_match_available. + /// Message when no second match exists /// /// In en, this message translates to: /// **'No second match available'** String get no_second_match_available; - /// No description provided for @no_statistics_available. + /// Message when no statistics are available, because no matches were played yet /// /// In en, this message translates to: /// **'No statistics available'** String get no_statistics_available; - /// No description provided for @none. + /// None option label /// /// In en, this message translates to: /// **'None'** String get none; - /// No description provided for @none_group. + /// None group option label /// /// In en, this message translates to: /// **'None'** String get none_group; - /// No description provided for @not_available. + /// Abbreviation for not available /// /// In en, this message translates to: /// **'Not available'** String get not_available; - /// No description provided for @played_matches. + /// Title for placement ruleset + /// + /// In en, this message translates to: + /// **'Placement'** + String get placement; + + /// Label for placement text in match detail view + /// + /// In en, this message translates to: + /// **'place'** + String get place; + + /// Label for played matches statistic /// /// In en, this message translates to: /// **'Played Matches'** String get played_matches; - /// No description provided for @player_name. + /// Placeholder for player name input /// /// In en, this message translates to: /// **'Player name'** String get player_name; - /// No description provided for @players. + /// Players label /// /// In en, this message translates to: /// **'Players'** @@ -650,115 +668,121 @@ abstract class AppLocalizations { /// **'Point'** String get point; - /// No description provided for @points. + /// Points label /// /// In en, this message translates to: /// **'Points'** String get points; - /// No description provided for @privacy_policy. + /// Privacy policy menu item /// /// In en, this message translates to: /// **'Privacy Policy'** String get privacy_policy; - /// No description provided for @quick_create. + /// Title for quick create section /// /// In en, this message translates to: /// **'Quick Create'** String get quick_create; - /// No description provided for @recent_matches. + /// Title for recent matches section /// /// In en, this message translates to: /// **'Recent Matches'** String get recent_matches; - /// No description provided for @results. + /// Label for match results /// /// In en, this message translates to: /// **'Results'** String get results; - /// No description provided for @ruleset. + /// Ruleset label /// /// In en, this message translates to: /// **'Ruleset'** String get ruleset; - /// No description provided for @ruleset_least_points. + /// Description for least points ruleset /// /// In en, this message translates to: /// **'Inverse scoring: the player with the fewest points wins.'** String get ruleset_least_points; - /// No description provided for @ruleset_most_points. + /// Description for most points ruleset /// /// In en, this message translates to: /// **'Traditional ruleset: the player with the most points wins.'** String get ruleset_most_points; - /// No description provided for @ruleset_single_loser. + /// Description for placement ruleset + /// + /// In en, this message translates to: + /// **'Players can be arranged in an order, which reflects their placement.'** + String get ruleset_placement; + + /// Description for single loser ruleset /// /// In en, this message translates to: /// **'Exactly one loser is determined; last place receives the penalty or consequence.'** String get ruleset_single_loser; - /// No description provided for @ruleset_single_winner. + /// Description for single winner ruleset /// /// In en, this message translates to: /// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'** String get ruleset_single_winner; - /// No description provided for @save_changes. + /// Save changes button text /// /// In en, this message translates to: /// **'Save Changes'** String get save_changes; - /// No description provided for @search_for_groups. + /// Hint text for group search input field /// /// In en, this message translates to: /// **'Search for groups'** String get search_for_groups; - /// No description provided for @search_for_players. + /// Hint text for player search input field /// /// In en, this message translates to: /// **'Search for players'** String get search_for_players; - /// No description provided for @select_winner. + /// Label to select the winner /// /// In en, this message translates to: /// **'Select Winner'** String get select_winner; - /// No description provided for @select_loser. + /// Label to select the loser /// /// In en, this message translates to: /// **'Select Loser'** String get select_loser; - /// No description provided for @selected_players. + /// Shows the number of selected players /// /// In en, this message translates to: /// **'Selected players'** String get selected_players; - /// No description provided for @settings. + /// Label for the App Settings /// /// In en, this message translates to: /// **'Settings'** String get settings; - /// No description provided for @single_loser. + /// Title for single loser ruleset /// /// In en, this message translates to: /// **'Single Loser'** String get single_loser; - /// No description provided for @single_winner. + /// Title for single winner ruleset /// /// In en, this message translates to: /// **'Single Winner'** @@ -788,13 +812,13 @@ abstract class AppLocalizations { /// **'Multiple Winners'** String get multiple_winners; - /// No description provided for @statistics. + /// Statistics tab label /// /// In en, this message translates to: /// **'Statistics'** String get statistics; - /// No description provided for @stats. + /// Stats tab label (short) /// /// In en, this message translates to: /// **'Stats'** @@ -812,13 +836,13 @@ abstract class AppLocalizations { /// **'There are no games matching your search'** String get there_are_no_games_matching_your_search; - /// No description provided for @there_is_no_group_matching_your_search. + /// Message when search returns no groups /// /// In en, this message translates to: /// **'There is no group matching your search'** String get there_is_no_group_matching_your_search; - /// No description provided for @this_cannot_be_undone. + /// Warning message for irreversible actions /// /// In en, this message translates to: /// **'This can\'t be undone.'** @@ -830,43 +854,43 @@ abstract class AppLocalizations { /// **'Tie'** String get tie; - /// No description provided for @today_at. + /// Date format for today /// /// In en, this message translates to: /// **'Today at'** String get today_at; - /// No description provided for @undo. + /// Undo button text /// /// In en, this message translates to: /// **'Undo'** String get undo; - /// No description provided for @unknown_exception. + /// Error message for unknown exceptions /// /// In en, this message translates to: /// **'Unknown Exception (see console)'** String get unknown_exception; - /// No description provided for @winner. + /// Winner label /// /// In en, this message translates to: /// **'Winner'** String get winner; - /// No description provided for @winrate. + /// Label for winrate statistic /// /// In en, this message translates to: /// **'Winrate'** String get winrate; - /// No description provided for @wins. + /// Label for wins statistic /// /// In en, this message translates to: /// **'Wins'** String get wins; - /// No description provided for @yesterday_at. + /// Date format for yesterday /// /// In en, this message translates to: /// **'Yesterday at'** diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 3666d11..f91e0ba 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -101,7 +101,7 @@ class AppLocalizationsDe extends AppLocalizations { String get data_successfully_imported => 'Daten erfolgreich importiert'; @override - String days_ago(Object count) { + String days_ago(int count) { return 'vor $count Tagen'; } @@ -131,6 +131,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get delete_match => 'Spiel löschen'; + @override + String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen'; + @override String get description => 'Beschreibung'; @@ -289,6 +292,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get not_available => 'Nicht verfügbar'; + @override + String get placement => 'Platzierung'; + + @override + String get place => 'Platz'; + @override String get played_matches => 'Gespielte Spiele'; @@ -327,6 +336,10 @@ class AppLocalizationsDe extends AppLocalizations { String get ruleset_most_points => 'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.'; + @override + String get ruleset_placement => + 'Spieler:innen können in einer Reihenfolge angeordnet werden, die ihre Platzierung reflektiert.'; + @override String get ruleset_single_loser => 'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index ae7d813..3bc7b2f 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -101,7 +101,7 @@ class AppLocalizationsEn extends AppLocalizations { String get data_successfully_imported => 'Data successfully imported'; @override - String days_ago(Object count) { + String days_ago(int count) { return '$count days ago'; } @@ -131,6 +131,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get delete_match => 'Delete Match'; + @override + String get drag_to_set_placement => 'Drag to set placement'; + @override String get description => 'Description'; @@ -289,6 +292,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get not_available => 'Not available'; + @override + String get placement => 'Placement'; + + @override + String get place => 'place'; + @override String get played_matches => 'Played Matches'; @@ -327,6 +336,10 @@ class AppLocalizationsEn extends AppLocalizations { String get ruleset_most_points => 'Traditional ruleset: the player with the most points wins.'; + @override + String get ruleset_placement => + 'Players can be arranged in an order, which reflects their placement.'; + @override String get ruleset_single_loser => 'Exactly one loser is determined; last place receives the penalty or consequence.'; From 0eb27ab2848f756e228833765782f8ac9c2dd7ed Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 9 May 2026 19:48:02 +0200 Subject: [PATCH 099/127] add icon for placement ruleset --- lib/core/common.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/core/common.dart b/lib/core/common.dart index b298048..495759c 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -81,6 +81,8 @@ IconData getRulesetIcon(Ruleset ruleset) { return Icons.sentiment_dissatisfied; case Ruleset.multipleWinners: return Icons.group; + case Ruleset.placement: + return Icons.leaderboard; } } From 518bbb407c4e88bc02fb2116f2785721fb10538a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 19:54:24 +0200 Subject: [PATCH 100/127] Updated icon and match tile icon style --- lib/core/common.dart | 3 ++- .../widgets/tiles/match_tile.dart | 20 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 495759c..312e3fa 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; @@ -82,7 +83,7 @@ IconData getRulesetIcon(Ruleset ruleset) { case Ruleset.multipleWinners: return Icons.group; case Ruleset.placement: - return Icons.leaderboard; + return RpgAwesome.podium; } } diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 7c86c27..d034763 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -269,23 +269,21 @@ class _MatchTileState extends State { } Icon getMvpIcon() { - const Icon(Icons.emoji_events, size: 20, color: Colors.amber); + final icon = getRulesetIcon(widget.match.game.ruleset); switch (widget.match.game.ruleset) { case Ruleset.singleWinner: - return const Icon(Icons.emoji_events, size: 20, color: Colors.amber); + return Icon(icon, size: 20, color: Colors.amber); case Ruleset.singleLoser: - return const Icon( - Icons.sentiment_dissatisfied_outlined, - size: 20, - color: Colors.blue, - ); + return Icon(icon, size: 20, color: Colors.blue); case Ruleset.lowestScore: - return const Icon(Icons.arrow_downward, size: 20, color: Colors.orange); + return Icon(icon, size: 20, color: Colors.orange); case Ruleset.highestScore: - return const Icon(Icons.arrow_upward, size: 20, color: Colors.green); - default: - return const Icon(Icons.emoji_events, size: 20, color: Colors.amber); + return Icon(icon, size: 20, color: Colors.green); + case Ruleset.multipleWinners: + return Icon(icon, size: 20, color: Colors.amber); + case Ruleset.placement: + return Icon(icon, size: 20, color: Colors.deepOrangeAccent); } } } From cde64a26835d69dd5a17ff9885c8ee9b7db6b0b4 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Sat, 9 May 2026 17:59:32 +0000 Subject: [PATCH 101/127] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 1b3d91c..f514b01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.28+262 +version: 0.0.29+263 environment: sdk: ^3.8.1 From e24183eb9c194e0a40201d50def21ba0e37d3290 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Sat, 9 May 2026 18:00:12 +0000 Subject: [PATCH 102/127] Updated licenses [skip ci] --- lib/l10n/generated/app_localizations.dart | 12 +++++ .../settings_view/licenses/oss_licenses.dart | 44 +++++++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 1f6abff..749c8ed 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -386,6 +386,12 @@ abstract class AppLocalizations { /// **'Error reading file'** String get error_reading_file; + /// No description provided for @exit_view. + /// + /// In en, this message translates to: + /// **'Exit View'** + String get exit_view; + /// No description provided for @export_canceled. /// /// In en, this message translates to: @@ -494,6 +500,12 @@ abstract class AppLocalizations { /// **'Licenses'** String get licenses; + /// No description provided for @live_edit_mode. + /// + /// In en, this message translates to: + /// **'Live Edit Mode'** + String get live_edit_mode; + /// No description provided for @match_in_progress. /// /// In en, this message translates to: diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 92f3080..5b9a689 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -54,6 +54,7 @@ const allDependencies = [ _flutter, _flutter_lints, _flutter_localizations, + _flutter_numeric_text, _flutter_plugin_android_lifecycle, _flutter_popup, _flutter_test, @@ -169,6 +170,7 @@ const dependencies = [ _file_saver, _flutter, _flutter_localizations, + _flutter_numeric_text, _flutter_popup, _fluttericon, _font_awesome_flutter, @@ -2591,6 +2593,42 @@ const _flutter_localizations = Package( devDependencies: [PackageRef('flutter_test')], ); +/// flutter_numeric_text 1.3.3 +const _flutter_numeric_text = Package( + name: 'flutter_numeric_text', + description: 'This widget allows you to animate any text. The widget is easy to use and allows you to seamlessly replace Text(data) with NumericText(data).', + homepage: 'https://github.com/strash/flutter_numeric_text', + repository: 'https://github.com/strash/flutter_numeric_text', + authors: [], + version: '1.3.3', + spdxIdentifiers: ['MIT'], + isMarkdown: false, + isSdk: false, + dependencies: [PackageRef('flutter')], + devDependencies: [PackageRef('flutter_test'), PackageRef('flutter_lints')], + license: '''MIT License + +Copyright (c) 2025 Strash One + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''', + ); + /// flutter_plugin_android_lifecycle 2.0.34 const _flutter_plugin_android_lifecycle = Package( name: 'flutter_plugin_android_lifecycle', @@ -37713,16 +37751,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.28+262 +/// tallee 0.0.29+263 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.28+262', + version: '0.0.29+263', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('flutter_popup'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], + dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('flutter_numeric_text'), PackageRef('flutter_popup'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], devDependencies: [PackageRef('flutter_test'), PackageRef('build_runner'), PackageRef('dart_pubspec_licenses'), PackageRef('drift_dev'), PackageRef('flutter_lints')], license: '''GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 From f0ff4fbfc027ed2aa3ec4c083e0471b34c040a01 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 20:32:39 +0200 Subject: [PATCH 103/127] feat: placement text color --- .../match_view/match_detail_view.dart | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 9d1049e..04e7505 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -272,7 +272,7 @@ class _MatchDetailViewState extends State { children: getSingleResultRow(loc), ); } else { - return getScoreResultWidget(loc); + return getMultiResultRows(loc); } } @@ -323,7 +323,7 @@ class _MatchDetailViewState extends State { } /// Returns the result widget for scores or placement - Widget getScoreResultWidget(AppLocalizations loc) { + Widget getMultiResultRows(AppLocalizations loc) { List<(String, int)> playerScores = []; for (var player in match.players) { int score = match.scores[player.id]?.score ?? 0; @@ -351,33 +351,58 @@ class _MatchDetailViewState extends State { color: CustomTheme.textColor, ), ), - Text( - ruleset == Ruleset.placement - ? getPlacementText(i + 1, context, loc) - : getPointLabel(loc, playerScores[i].$2), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, - ), - ), + getResultValueText(loc, i, playerScores[i].$2), ], ), ], ); } + Widget getResultValueText(AppLocalizations loc, int index, int score) { + final ruleset = match.game.ruleset; + + if (ruleset == Ruleset.placement) { + return Text( + getPlacementText(context, index + 1), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getPlacementTextcolor(index), + ), + ); + } else { + return Text( + getPointLabel(loc, score), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), + ); + } + } + + Color getPlacementTextcolor(int placement) { + switch (placement) { + case 0: + return const Color(0xFFFFBF00); + case 1: + return const Color(0xBBFFFFFF); + case 2: + return const Color(0xFFCD7F32); + default: + return CustomTheme.textColor; + } + } + // Returns if the result can be displayed in a single row bool isSingleRowResult() { return match.game.ruleset == Ruleset.singleWinner || match.game.ruleset == Ruleset.singleLoser; } - String getPlacementText( - int rank, - BuildContext context, - AppLocalizations loc, - ) { + String getPlacementText(BuildContext context, int rank) { + final loc = AppLocalizations.of(context); final locale = Localizations.localeOf(context).languageCode; if (locale == 'de') { From df73fc87ebd648574216d9e99c6b92b169ba66df Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 23:16:56 +0200 Subject: [PATCH 104/127] localizations --- lib/l10n/generated/app_localizations.dart | 218 +++++++++---------- lib/l10n/generated/app_localizations_de.dart | 2 +- lib/l10n/generated/app_localizations_en.dart | 2 +- 3 files changed, 111 insertions(+), 111 deletions(-) diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 3fa991e..bfdb659 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -98,37 +98,37 @@ abstract class AppLocalizations { Locale('en'), ]; - /// Label for all players list + /// No description provided for @all_players. /// /// In en, this message translates to: /// **'All players'** String get all_players; - /// Message when all players are added to selection + /// No description provided for @all_players_selected. /// /// In en, this message translates to: /// **'All players selected'** String get all_players_selected; - /// Label for amount of matches statistic + /// No description provided for @amount_of_matches. /// /// In en, this message translates to: /// **'Amount of Matches'** String get amount_of_matches; - /// The name of the App + /// No description provided for @app_name. /// /// In en, this message translates to: /// **'Tallee'** String get app_name; - /// Label for best player statistic + /// No description provided for @best_player. /// /// In en, this message translates to: /// **'Best Player'** String get best_player; - /// Cancel button text + /// No description provided for @cancel. /// /// In en, this message translates to: /// **'Cancel'** @@ -140,19 +140,19 @@ abstract class AppLocalizations { /// **'Choose Color'** String get choose_color; - /// Label for choosing a game + /// No description provided for @choose_game. /// /// In en, this message translates to: /// **'Choose Game'** String get choose_game; - /// Label for choosing a group + /// No description provided for @choose_group. /// /// In en, this message translates to: /// **'Choose Group'** String get choose_group; - /// Label for choosing a ruleset + /// No description provided for @choose_ruleset. /// /// In en, this message translates to: /// **'Choose Ruleset'** @@ -212,7 +212,7 @@ abstract class AppLocalizations { /// **'Yellow'** String get color_yellow; - /// Error message when adding a player fails + /// No description provided for @could_not_add_player. /// /// In en, this message translates to: /// **'Could not add player'** @@ -224,73 +224,73 @@ abstract class AppLocalizations { /// **'Create Game'** String get create_game; - /// Button text to create a group + /// No description provided for @create_group. /// /// In en, this message translates to: /// **'Create Group'** String get create_group; - /// Button text to create a match + /// No description provided for @create_match. /// /// In en, this message translates to: /// **'Create match'** String get create_match; - /// Appbar text to create a new group + /// No description provided for @create_new_group. /// /// In en, this message translates to: /// **'Create new group'** String get create_new_group; - /// Label for creation date + /// No description provided for @created_on. /// /// In en, this message translates to: /// **'Created on'** String get created_on; - /// Appbar text to create a new match + /// No description provided for @create_new_match. /// /// In en, this message translates to: /// **'Create new match'** String get create_new_match; - /// Data label + /// No description provided for @data. /// /// In en, this message translates to: /// **'Data'** String get data; - /// Success message after deleting data + /// No description provided for @data_successfully_deleted. /// /// In en, this message translates to: /// **'Data successfully deleted'** String get data_successfully_deleted; - /// Success message after exporting data + /// No description provided for @data_successfully_exported. /// /// In en, this message translates to: /// **'Data successfully exported'** String get data_successfully_exported; - /// Success message after importing data + /// No description provided for @data_successfully_imported. /// /// In en, this message translates to: /// **'Data successfully imported'** String get data_successfully_imported; - /// Date format for days ago + /// No description provided for @days_ago. /// /// In en, this message translates to: /// **'{count} days ago'** - String days_ago(int count); + String days_ago(Object count); - /// Delete button text + /// No description provided for @delete. /// /// In en, this message translates to: /// **'Delete'** String get delete; - /// Confirmation dialog for deleting all data + /// No description provided for @delete_all_data. /// /// In en, this message translates to: /// **'Delete all data'** @@ -308,19 +308,19 @@ abstract class AppLocalizations { /// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'** String delete_game_with_matches_warning(int count); - /// Confirmation dialog for deleting a group + /// No description provided for @delete_group. /// /// In en, this message translates to: /// **'Delete Group'** String get delete_group; - /// Button text to delete a match + /// No description provided for @delete_match. /// /// In en, this message translates to: /// **'Delete Match'** String get delete_match; - /// Label for dragging to set placement + /// No description provided for @drag_to_set_placement. /// /// In en, this message translates to: /// **'Drag to set placement'** @@ -338,31 +338,31 @@ abstract class AppLocalizations { /// **'Edit Game'** String get edit_game; - /// Button & Appbar label for editing a group + /// No description provided for @edit_group. /// /// In en, this message translates to: /// **'Edit Group'** String get edit_group; - /// Button & Appbar label for editing a match + /// No description provided for @edit_match. /// /// In en, this message translates to: /// **'Edit Match'** String get edit_match; - /// Label to enter players points + /// No description provided for @enter_points. /// /// In en, this message translates to: /// **'Enter points'** String get enter_points; - /// Button text to enter match results + /// No description provided for @enter_results. /// /// In en, this message translates to: /// **'Enter Results'** String get enter_results; - /// Error message when group creation fails + /// No description provided for @error_creating_group. /// /// In en, this message translates to: /// **'Error while creating group, please try again'** @@ -374,19 +374,19 @@ abstract class AppLocalizations { /// **'Error while deleting game, please try again'** String get error_deleting_game; - /// Error message when group deletion fails + /// No description provided for @error_deleting_group. /// /// In en, this message translates to: /// **'Error while deleting group, please try again'** String get error_deleting_group; - /// Error message when group editing fails + /// No description provided for @error_editing_group. /// /// In en, this message translates to: /// **'Error while editing group, please try again'** String get error_editing_group; - /// Error message when file cannot be read + /// No description provided for @error_reading_file. /// /// In en, this message translates to: /// **'Error reading file'** @@ -398,109 +398,109 @@ abstract class AppLocalizations { /// **'Exit View'** String get exit_view; - /// Message when export is canceled + /// No description provided for @export_canceled. /// /// In en, this message translates to: /// **'Export canceled'** String get export_canceled; - /// Export data menu item + /// No description provided for @export_data. /// /// In en, this message translates to: /// **'Export data'** String get export_data; - /// Error message for format exceptions + /// No description provided for @format_exception. /// /// In en, this message translates to: /// **'Format Exception (see console)'** String get format_exception; - /// Game label + /// No description provided for @game. /// /// In en, this message translates to: /// **'Game'** String get game; - /// Placeholder for game name search + /// No description provided for @game_name. /// /// In en, this message translates to: /// **'Game Name'** String get game_name; - /// Group label + /// No description provided for @group. /// /// In en, this message translates to: /// **'Group'** String get group; - /// Placeholder for group name input + /// No description provided for @group_name. /// /// In en, this message translates to: /// **'Group name'** String get group_name; - /// Title for group profile view + /// No description provided for @group_profile. /// /// In en, this message translates to: /// **'Group Profile'** String get group_profile; - /// Label for groups + /// No description provided for @groups. /// /// In en, this message translates to: /// **'Groups'** String get groups; - /// Home tab label + /// No description provided for @home. /// /// In en, this message translates to: /// **'Home'** String get home; - /// Message when import is canceled + /// No description provided for @import_canceled. /// /// In en, this message translates to: /// **'Import canceled'** String get import_canceled; - /// Import data menu item + /// No description provided for @import_data. /// /// In en, this message translates to: /// **'Import data'** String get import_data; - /// Info label + /// No description provided for @info. /// /// In en, this message translates to: /// **'Info'** String get info; - /// Error message for invalid schema + /// No description provided for @invalid_schema. /// /// In en, this message translates to: /// **'Invalid Schema'** String get invalid_schema; - /// Title for least points ruleset + /// No description provided for @least_points. /// /// In en, this message translates to: /// **'Least Points'** String get least_points; - /// Legal section header + /// No description provided for @legal. /// /// In en, this message translates to: /// **'Legal'** String get legal; - /// Legal notice menu item + /// No description provided for @legal_notice. /// /// In en, this message translates to: /// **'Legal Notice'** String get legal_notice; - /// Licenses menu item + /// No description provided for @licenses. /// /// In en, this message translates to: /// **'Licenses'** @@ -512,43 +512,43 @@ abstract class AppLocalizations { /// **'Live Edit Mode'** String get live_edit_mode; - /// Message when match is in progress + /// No description provided for @match_in_progress. /// /// In en, this message translates to: /// **'Match in progress...'** String get match_in_progress; - /// Placeholder for match name input + /// No description provided for @match_name. /// /// In en, this message translates to: /// **'Match name'** String get match_name; - /// Title for match profile view + /// No description provided for @match_profile. /// /// In en, this message translates to: /// **'Match Profile'** String get match_profile; - /// Label for matches + /// No description provided for @matches. /// /// In en, this message translates to: /// **'Matches'** String get matches; - /// Label for group members + /// No description provided for @members. /// /// In en, this message translates to: /// **'Members'** String get members; - /// Title for most points ruleset + /// No description provided for @most_points. /// /// In en, this message translates to: /// **'Most Points'** String get most_points; - /// Message when no data in the statistic tiles is given + /// No description provided for @no_data_available. /// /// In en, this message translates to: /// **'No data available'** @@ -560,115 +560,115 @@ abstract class AppLocalizations { /// **'No games created yet'** String get no_games_created_yet; - /// Message when no groups exist + /// No description provided for @no_groups_created_yet. /// /// In en, this message translates to: /// **'No groups created yet'** String get no_groups_created_yet; - /// Message when no licenses are found + /// No description provided for @no_licenses_found. /// /// In en, this message translates to: /// **'No licenses found'** String get no_licenses_found; - /// Message when no license text is available + /// No description provided for @no_license_text_available. /// /// In en, this message translates to: /// **'No license text available'** String get no_license_text_available; - /// Message when no matches exist + /// No description provided for @no_matches_created_yet. /// /// In en, this message translates to: /// **'No matches created yet'** String get no_matches_created_yet; - /// Message when no players exist + /// No description provided for @no_players_created_yet. /// /// In en, this message translates to: /// **'No players created yet'** String get no_players_created_yet; - /// Message when search returns no results + /// No description provided for @no_players_found_with_that_name. /// /// In en, this message translates to: /// **'No players found with that name'** String get no_players_found_with_that_name; - /// Message when no players are selected + /// No description provided for @no_players_selected. /// /// In en, this message translates to: /// **'No players selected'** String get no_players_selected; - /// Message when no recent matches exist + /// No description provided for @no_recent_matches_available. /// /// In en, this message translates to: /// **'No recent matches available'** String get no_recent_matches_available; - /// Message when no results have been entered yet + /// No description provided for @no_results_entered_yet. /// /// In en, this message translates to: /// **'No results entered yet'** String get no_results_entered_yet; - /// Message when no second match exists + /// No description provided for @no_second_match_available. /// /// In en, this message translates to: /// **'No second match available'** String get no_second_match_available; - /// Message when no statistics are available, because no matches were played yet + /// No description provided for @no_statistics_available. /// /// In en, this message translates to: /// **'No statistics available'** String get no_statistics_available; - /// None option label + /// No description provided for @none. /// /// In en, this message translates to: /// **'None'** String get none; - /// None group option label + /// No description provided for @none_group. /// /// In en, this message translates to: /// **'None'** String get none_group; - /// Abbreviation for not available + /// No description provided for @not_available. /// /// In en, this message translates to: /// **'Not available'** String get not_available; - /// Title for placement ruleset + /// No description provided for @placement. /// /// In en, this message translates to: /// **'Placement'** String get placement; - /// Label for placement text in match detail view + /// No description provided for @place. /// /// In en, this message translates to: /// **'place'** String get place; - /// Label for played matches statistic + /// No description provided for @played_matches. /// /// In en, this message translates to: /// **'Played Matches'** String get played_matches; - /// Placeholder for player name input + /// No description provided for @player_name. /// /// In en, this message translates to: /// **'Player name'** String get player_name; - /// Players label + /// No description provided for @players. /// /// In en, this message translates to: /// **'Players'** @@ -680,121 +680,121 @@ abstract class AppLocalizations { /// **'Point'** String get point; - /// Points label + /// No description provided for @points. /// /// In en, this message translates to: /// **'Points'** String get points; - /// Privacy policy menu item + /// No description provided for @privacy_policy. /// /// In en, this message translates to: /// **'Privacy Policy'** String get privacy_policy; - /// Title for quick create section + /// No description provided for @quick_create. /// /// In en, this message translates to: /// **'Quick Create'** String get quick_create; - /// Title for recent matches section + /// No description provided for @recent_matches. /// /// In en, this message translates to: /// **'Recent Matches'** String get recent_matches; - /// Label for match results + /// No description provided for @results. /// /// In en, this message translates to: /// **'Results'** String get results; - /// Ruleset label + /// No description provided for @ruleset. /// /// In en, this message translates to: /// **'Ruleset'** String get ruleset; - /// Description for least points ruleset + /// No description provided for @ruleset_least_points. /// /// In en, this message translates to: /// **'Inverse scoring: the player with the fewest points wins.'** String get ruleset_least_points; - /// Description for most points ruleset + /// No description provided for @ruleset_most_points. /// /// In en, this message translates to: /// **'Traditional ruleset: the player with the most points wins.'** String get ruleset_most_points; - /// Description for placement ruleset + /// No description provided for @ruleset_placement. /// /// In en, this message translates to: /// **'Players can be arranged in an order, which reflects their placement.'** String get ruleset_placement; - /// Description for single loser ruleset + /// No description provided for @ruleset_single_loser. /// /// In en, this message translates to: /// **'Exactly one loser is determined; last place receives the penalty or consequence.'** String get ruleset_single_loser; - /// Description for single winner ruleset + /// No description provided for @ruleset_single_winner. /// /// In en, this message translates to: /// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'** String get ruleset_single_winner; - /// Save changes button text + /// No description provided for @save_changes. /// /// In en, this message translates to: /// **'Save Changes'** String get save_changes; - /// Hint text for group search input field + /// No description provided for @search_for_groups. /// /// In en, this message translates to: /// **'Search for groups'** String get search_for_groups; - /// Hint text for player search input field + /// No description provided for @search_for_players. /// /// In en, this message translates to: /// **'Search for players'** String get search_for_players; - /// Label to select the winner + /// No description provided for @select_winner. /// /// In en, this message translates to: /// **'Select Winner'** String get select_winner; - /// Label to select the loser + /// No description provided for @select_loser. /// /// In en, this message translates to: /// **'Select Loser'** String get select_loser; - /// Shows the number of selected players + /// No description provided for @selected_players. /// /// In en, this message translates to: /// **'Selected players'** String get selected_players; - /// Label for the App Settings + /// No description provided for @settings. /// /// In en, this message translates to: /// **'Settings'** String get settings; - /// Title for single loser ruleset + /// No description provided for @single_loser. /// /// In en, this message translates to: /// **'Single Loser'** String get single_loser; - /// Title for single winner ruleset + /// No description provided for @single_winner. /// /// In en, this message translates to: /// **'Single Winner'** @@ -824,13 +824,13 @@ abstract class AppLocalizations { /// **'Multiple Winners'** String get multiple_winners; - /// Statistics tab label + /// No description provided for @statistics. /// /// In en, this message translates to: /// **'Statistics'** String get statistics; - /// Stats tab label (short) + /// No description provided for @stats. /// /// In en, this message translates to: /// **'Stats'** @@ -848,13 +848,13 @@ abstract class AppLocalizations { /// **'There are no games matching your search'** String get there_are_no_games_matching_your_search; - /// Message when search returns no groups + /// No description provided for @there_is_no_group_matching_your_search. /// /// In en, this message translates to: /// **'There is no group matching your search'** String get there_is_no_group_matching_your_search; - /// Warning message for irreversible actions + /// No description provided for @this_cannot_be_undone. /// /// In en, this message translates to: /// **'This can\'t be undone.'** @@ -866,43 +866,43 @@ abstract class AppLocalizations { /// **'Tie'** String get tie; - /// Date format for today + /// No description provided for @today_at. /// /// In en, this message translates to: /// **'Today at'** String get today_at; - /// Undo button text + /// No description provided for @undo. /// /// In en, this message translates to: /// **'Undo'** String get undo; - /// Error message for unknown exceptions + /// No description provided for @unknown_exception. /// /// In en, this message translates to: /// **'Unknown Exception (see console)'** String get unknown_exception; - /// Winner label + /// No description provided for @winner. /// /// In en, this message translates to: /// **'Winner'** String get winner; - /// Label for winrate statistic + /// No description provided for @winrate. /// /// In en, this message translates to: /// **'Winrate'** String get winrate; - /// Label for wins statistic + /// No description provided for @wins. /// /// In en, this message translates to: /// **'Wins'** String get wins; - /// Date format for yesterday + /// No description provided for @yesterday_at. /// /// In en, this message translates to: /// **'Yesterday at'** diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 344eb38..8567ba0 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -101,7 +101,7 @@ class AppLocalizationsDe extends AppLocalizations { String get data_successfully_imported => 'Daten erfolgreich importiert'; @override - String days_ago(int count) { + String days_ago(Object count) { return 'vor $count Tagen'; } diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index a1ebd35..04e68b4 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -101,7 +101,7 @@ class AppLocalizationsEn extends AppLocalizations { String get data_successfully_imported => 'Data successfully imported'; @override - String days_ago(int count) { + String days_ago(Object count) { return '$count days ago'; } From e5fd54f112f4dfbcc9e1bad80ea3532493854b2e Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Sat, 9 May 2026 21:38:23 +0000 Subject: [PATCH 105/127] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index f514b01..b4b261e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.29+263 +version: 0.0.30+264 environment: sdk: ^3.8.1 From 881382b3997506b1f0203b46650987ad5d8e4976 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Sat, 9 May 2026 21:39:01 +0000 Subject: [PATCH 106/127] Updated licenses [skip ci] --- .../views/main_menu/settings_view/licenses/oss_licenses.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 5b9a689..0ea0662 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -37751,12 +37751,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.29+263 +/// tallee 0.0.30+264 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.29+263', + version: '0.0.30+264', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, From 03ab2045b2f8924ea8cbe14e6f39679708acee94 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 14:54:00 +0200 Subject: [PATCH 107/127] Add support for selecting multiple winners and update localization --- lib/l10n/arb/app_de.arb | 1 + lib/l10n/arb/app_en.arb | 1 + lib/l10n/generated/app_localizations.dart | 6 +++ lib/l10n/generated/app_localizations_de.dart | 3 ++ lib/l10n/generated/app_localizations_en.dart | 3 ++ .../match_view/match_detail_view.dart | 1 + .../match_view/match_result_view.dart | 53 ++++++++++++++++++- .../custom_checkbox_list_tile.dart | 52 ++++++++++++++++++ 8 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 5492bab..622d9cb 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -120,6 +120,7 @@ "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", "select_winner": "Gewinner:in wählen", + "select_winners": "Gewinner:innen wählen", "select_loser": "Verlierer:in wählen", "selected_players": "Ausgewählte Spieler:innen", "settings": "Einstellungen", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7fb944b..b5d617e 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -120,6 +120,7 @@ "search_for_groups": "Search for groups", "search_for_players": "Search for players", "select_winner": "Select Winner", + "select_winners": "Select Winners", "select_loser": "Select Loser", "selected_players": "Selected players", "settings": "Settings", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index bfdb659..dd7617e 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -770,6 +770,12 @@ abstract class AppLocalizations { /// **'Select Winner'** String get select_winner; + /// No description provided for @select_winners. + /// + /// In en, this message translates to: + /// **'Select Winners'** + String get select_winners; + /// No description provided for @select_loser. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 8567ba0..e9e6451 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -366,6 +366,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get select_winner => 'Gewinner:in wählen'; + @override + String get select_winners => 'Gewinner:innen wählen'; + @override String get select_loser => 'Verlierer:in wählen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 04e68b4..3b90592 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -366,6 +366,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get select_winner => 'Select Winner'; + @override + String get select_winners => 'Select Winners'; + @override String get select_loser => 'Select Loser'; diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 86c26c6..d8cd627 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -269,6 +269,7 @@ class _MatchDetailViewState extends State { /// Returns the widget to be displayed in the result [InfoTile] Widget getResultWidget(AppLocalizations loc) { + ///TODO: add support for multiple winners if (isSingleRowResult()) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, 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 61b2a55..fd2c35a 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 @@ -8,6 +8,7 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart'; @@ -45,9 +46,12 @@ class _MatchResultViewState extends State { /// Flag to indicate if the save button should be enabled late bool canSave; - /// Currently selected winner player + /// Currently selected winner player (single winner) Player? _selectedPlayer; + /// Currently selected winners (multiple winners) + Set _selectedWinners = {}; + @override void initState() { db = Provider.of(context, listen: false); @@ -80,7 +84,10 @@ class _MatchResultViewState extends State { final scoreB = widget.match.scores[b.id]?.score ?? 0; return scoreB.compareTo(scoreA); }); + } else if (rulesetSupportsMultipleWinners()) { + //TODO: Implement winners pre filling } + ; super.initState(); } } @@ -319,6 +326,36 @@ class _MatchResultViewState extends State { ], ), ), + + // Show multiple winner selection + if (rulesetSupportsMultipleWinners()) + Expanded( + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomCheckboxListTile( + text: allPlayers[index].name, + value: _selectedWinners.contains( + allPlayers[index].id, + ), + onChanged: (bool value) { + setState(() { + if (value) { + _selectedWinners.add( + allPlayers[index].id, + ); + } else { + _selectedWinners.remove( + allPlayers[index].id, + ); + } + }); + }, + ); + }, + ), + ), ], ), ), @@ -382,6 +419,8 @@ class _MatchResultViewState extends State { await _handleScores(); } else if (ruleset == Ruleset.placement) { await _handlePlacement(); + } else if (ruleset == Ruleset.multipleWinners) { + await _handleWinners(); } widget.onWinnerChanged?.call(); @@ -399,6 +438,12 @@ class _MatchResultViewState extends State { } } + /// Handles saving the winners to the database. + Future _handleWinners() async { + //TODO: Implement winner handling + return true; + } + /// Handles saving or removing the loser in the database. Future _handleLoser() async { if (_selectedPlayer == null) { @@ -443,6 +488,8 @@ class _MatchResultViewState extends State { return loc.select_loser; case Ruleset.placement: return loc.drag_to_set_placement; + case Ruleset.multipleWinners: + return loc.select_winners; default: return loc.enter_points; } @@ -459,4 +506,8 @@ class _MatchResultViewState extends State { bool rulesetSupportsPlacement() { return ruleset == Ruleset.placement; } + + bool rulesetSupportsMultipleWinners() { + return ruleset == Ruleset.multipleWinners; + } } diff --git a/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart new file mode 100644 index 0000000..77c9242 --- /dev/null +++ b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/custom_theme.dart'; + +class CustomCheckboxListTile extends StatelessWidget { + const CustomCheckboxListTile({ + super.key, + required this.text, + required this.value, + required this.onChanged, + }); + + final String text; + final bool value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onChanged(!value), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorderColor), + borderRadius: CustomTheme.standardBorderRadiusAll, + ), + child: Row( + children: [ + Checkbox( + value: value, + onChanged: (bool? v) { + if (v == null) return; + onChanged(v); + }, + ), + Expanded( + child: Text( + text, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ); + } +} From 3c5c0dbf2068896beb6fc9ffc448cd8e9319fc7f Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 16:26:51 +0200 Subject: [PATCH 108/127] Add support for multiple winners and update localization --- lib/data/dao/score_entry_dao.dart | 48 ++++++++++++++++++- lib/data/models/match.dart | 2 +- lib/l10n/arb/app_de.arb | 1 + lib/l10n/arb/app_en.arb | 1 + lib/l10n/generated/app_localizations.dart | 6 +++ lib/l10n/generated/app_localizations_de.dart | 3 ++ lib/l10n/generated/app_localizations_en.dart | 3 ++ .../match_view/match_detail_view.dart | 30 ++++++++++-- .../match_view/match_result_view.dart | 29 +++++++---- .../widgets/tiles/match_tile.dart | 3 ++ 10 files changed, 110 insertions(+), 16 deletions(-) diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index cf6a449..d59f40a 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -228,7 +228,7 @@ class ScoreEntryDao extends DatabaseAccessor required String playerId, }) async { // Clear previous winner if exists - deleteAllScoresForMatch(matchId: matchId); + await deleteAllScoresForMatch(matchId: matchId); // Set the winner's score to 1 final rowsAffected = await into(scoreEntryTable).insert( @@ -245,7 +245,7 @@ class ScoreEntryDao extends DatabaseAccessor return rowsAffected > 0; } - // Retrieves the winner of a match by looking for a score entry where score + /// Retrieves the winner of a match by looking for a score entry where score /// is 1. Returns `null` if no player found, else the first with the score. Future getWinner({required String matchId}) async { final query = @@ -285,6 +285,48 @@ class ScoreEntryDao extends DatabaseAccessor } } + /* multiple winners handling */ + + /// Sets the winners for a match. + /// + /// Returns `true` if more than 0 rows were affected + Future setWinners({ + required List winners, + required String matchId, + }) async { + // Clear previous winners if exists + await deleteAllScoresForMatch(matchId: matchId); + + if (winners.isEmpty) return false; + + await batch((batch) { + batch.insertAll( + scoreEntryTable, + winners + .map( + (player) => ScoreEntryTableCompanion.insert( + playerId: player.id, + matchId: matchId, + roundNumber: 0, + score: 1, + change: 0, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ); + }); + + return true; + } + + /// Removes the winners of a match. + /// + /// Returns `true` if more than 0 rows were affected, `false` otherwise. + Future removeWinners({required String matchId}) async { + return await deleteAllScoresForMatch(matchId: matchId); + } + /* Loser handling */ Future hasLoser({required String matchId}) async { @@ -354,6 +396,8 @@ class ScoreEntryDao extends DatabaseAccessor } } + /* placement handling */ + /// Sets the placement for each player in a match. /// The highest score is assigned to the first player, the second highest to the second player, and so on. Future setPlacements({ diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 679f8a4..2c43fe3 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -155,7 +155,7 @@ class Match { return _getPlayersWithLowestScore().take(1).toList(); case Ruleset.multipleWinners: - return []; + return _getPlayersWithHighestScore().toList(); case Ruleset.placement: return _getPlayersWithHighestScore().take(1).toList(); diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 622d9cb..f9093a2 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -141,6 +141,7 @@ "undo": "Rückgängig", "unknown_exception": "Unbekannter Fehler (siehe Konsole)", "winner": "Gewinner:in", + "winners": "Gewinner:innen", "winrate": "Siegquote", "wins": "Siege", "yesterday_at": "Gestern um" diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b5d617e..b7da7f2 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -150,6 +150,7 @@ "undo": "Undo", "unknown_exception": "Unknown Exception (see console)", "winner": "Winner", + "winners": "Winners", "winrate": "Winrate", "wins": "Wins", "yesterday_at": "Yesterday at" diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index dd7617e..1bff731 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -896,6 +896,12 @@ abstract class AppLocalizations { /// **'Winner'** String get winner; + /// No description provided for @winners. + /// + /// In en, this message translates to: + /// **'Winners'** + String get winners; + /// No description provided for @winrate. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index e9e6451..ea8e1f2 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -434,6 +434,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get winner => 'Gewinner:in'; + @override + String get winners => 'Gewinner:innen'; + @override String get winrate => 'Siegquote'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 3b90592..48f054b 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -433,6 +433,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get winner => 'Winner'; + @override + String get winners => 'Winners'; + @override String get winrate => 'Winrate'; diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index d8cd627..952cb60 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -269,9 +269,9 @@ class _MatchDetailViewState extends State { /// Returns the widget to be displayed in the result [InfoTile] Widget getResultWidget(AppLocalizations loc) { - ///TODO: add support for multiple winners if (isSingleRowResult()) { return Row( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: getSingleResultRow(loc), ); @@ -300,7 +300,8 @@ class _MatchDetailViewState extends State { ), ]; // Single Loser - } else if (match.game.ruleset == Ruleset.singleLoser) { + } else if (match.mvp.isNotEmpty && + match.game.ruleset == Ruleset.singleLoser) { return [ Text( loc.loser, @@ -315,6 +316,28 @@ class _MatchDetailViewState extends State { ), ), ]; + // Multiple Winners + } else if (match.mvp.isNotEmpty && + match.game.ruleset == Ruleset.multipleWinners) { + return [ + Text( + loc.winners, + style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), + ), + Flexible( + child: Container( + padding: EdgeInsets.only(left: 40), + child: Text( + match.mvp.map((player) => player.name).join(', '), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), + ), + ), + ), + ]; // No result entered yet } else { return [ @@ -402,7 +425,8 @@ class _MatchDetailViewState extends State { // Returns if the result can be displayed in a single row bool isSingleRowResult() { return match.game.ruleset == Ruleset.singleWinner || - match.game.ruleset == Ruleset.singleLoser; + match.game.ruleset == Ruleset.singleLoser || + match.game.ruleset == Ruleset.multipleWinners; } String getPlacementText(BuildContext context, int rank) { 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 fd2c35a..de31967 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 @@ -50,7 +50,7 @@ class _MatchResultViewState extends State { Player? _selectedPlayer; /// Currently selected winners (multiple winners) - Set _selectedWinners = {}; + Set _selectedWinners = {}; @override void initState() { @@ -85,9 +85,12 @@ class _MatchResultViewState extends State { return scoreB.compareTo(scoreA); }); } else if (rulesetSupportsMultipleWinners()) { - //TODO: Implement winners pre filling + for (int i = 0; i < allPlayers.length; i++) { + if (widget.match.scores[allPlayers[i].id]?.score == 1) { + _selectedWinners.add(allPlayers[i]); + } + } } - ; super.initState(); } } @@ -337,17 +340,17 @@ class _MatchResultViewState extends State { return CustomCheckboxListTile( text: allPlayers[index].name, value: _selectedWinners.contains( - allPlayers[index].id, + allPlayers[index], ), onChanged: (bool value) { setState(() { if (value) { _selectedWinners.add( - allPlayers[index].id, + allPlayers[index], ); } else { _selectedWinners.remove( - allPlayers[index].id, + allPlayers[index], ); } }); @@ -426,7 +429,7 @@ class _MatchResultViewState extends State { widget.onWinnerChanged?.call(); } - /// Handles saving or removing the winner in the database. + /// Handles saving or removing the (single) winner in the database. Future _handleWinner() async { if (_selectedPlayer == null) { return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); @@ -438,10 +441,16 @@ class _MatchResultViewState extends State { } } - /// Handles saving the winners to the database. + /// Handles saving the (multiple) winners to the database. Future _handleWinners() async { - //TODO: Implement winner handling - return true; + if (_selectedWinners.isEmpty) { + return await db.scoreEntryDao.removeWinners(matchId: widget.match.id); + } else { + return await db.scoreEntryDao.setWinners( + matchId: widget.match.id, + winners: allPlayers.where((p) => _selectedWinners.contains(p)).toList(), + ); + } } /// Handles saving or removing the loser in the database. diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index d034763..018c896 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -264,6 +264,9 @@ class _MatchTileState extends State { return '${loc.winner}: $mvpNames (${getPointLabel(loc, mvpScore)})'; } else if (ruleset == Ruleset.placement) { return '${loc.winner}: ${widget.match.mvp.first.name}'; + } else if (ruleset == Ruleset.multipleWinners) { + final mvpNames = widget.match.mvp.map((player) => player.name).join(', '); + return '${loc.winners}: $mvpNames'; } return '${loc.winner}: n.A.'; } From 8076f082bc7a7c09c5eb4dae73872db04739b01c Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 16:28:08 +0200 Subject: [PATCH 109/127] fix lint --- .../views/main_menu/match_view/match_detail_view.dart | 2 +- .../views/main_menu/match_view/match_result_view.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 952cb60..10fc324 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -326,7 +326,7 @@ class _MatchDetailViewState extends State { ), Flexible( child: Container( - padding: EdgeInsets.only(left: 40), + padding: const EdgeInsets.only(left: 40), child: Text( match.mvp.map((player) => player.name).join(', '), style: const TextStyle( 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 de31967..d14c259 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 @@ -50,7 +50,7 @@ class _MatchResultViewState extends State { Player? _selectedPlayer; /// Currently selected winners (multiple winners) - Set _selectedWinners = {}; + final Set _selectedWinners = {}; @override void initState() { From 009c53ad89fafe138b2678e1c82f88ca6e77958c Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 16:36:46 +0200 Subject: [PATCH 110/127] fix title and ruleset/color choose order in game creation --- .../match_view/create_match/create_game_view.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index e0f9d85..0e96508 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -98,6 +98,7 @@ class _CreateGameViewState extends State { Ruleset.multipleWinners, translateRulesetToString(Ruleset.multipleWinners, context), ), + (Ruleset.placement, translateRulesetToString(Ruleset.placement, context)), ]; _colors = [ (GameColor.green, translateGameColorToString(GameColor.green, context)), @@ -212,12 +213,12 @@ class _CreateGameViewState extends State { ), ), + // Choose color tile + ChooseTile(title: loc.ruleset, trailing: getRulesetDropdown(loc)), + // Choose ruleset tile if (!isEditMode()) - ChooseTile(title: loc.ruleset, trailing: getColorDropdown(loc)), - - // Choose color tile - ChooseTile(title: loc.color, trailing: getRulesetDropdown(loc)), + ChooseTile(title: loc.color, trailing: getColorDropdown(loc)), // Description input field Container( From 2a3c0fc98c32cef7b64115ff7c92f09ace15011b Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 17:50:16 +0200 Subject: [PATCH 111/127] fix schema --- assets/schema.json | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/assets/schema.json b/assets/schema.json index 6bcbe45..7f6aebd 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -102,36 +102,6 @@ ] } }, - "teams": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "memberIds": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "required": [ - "id", - "name", - "createdAt", - "memberIds" - ] - } - }, "matches": { "type": "array", "items": { @@ -195,6 +165,9 @@ }, "notes": { "type": "string" + }, + "teams": { + "type": ["array", "null"] } }, "additionalProperties": false, @@ -214,7 +187,6 @@ "players", "games", "groups", - "teams", "matches" ] } \ No newline at end of file From f98208b5089e08e89b0c1ed9ba229727b56422ed Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:17:01 +0200 Subject: [PATCH 112/127] fix: button enabled condition --- .../match_view/create_match/create_match_view.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 14908b6..fd98691 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 @@ -231,11 +231,11 @@ class _CreateMatchViewState extends State { /// Determines whether the "Create Match" button should be enabled. /// /// Returns `true` if: - /// - A ruleset is selected AND + /// - A game is selected AND /// - Either a group is selected OR at least 2 players are selected. bool _enableCreateGameButton() { - return (selectedGroup != null || - (selectedPlayers.length > 1) && selectedGame != null); + return ((selectedGroup != null || selectedPlayers.length > 1) && + selectedGame != null); } /// Handles navigation when the create or save button is pressed. From f5f97f676c34a78c7dec367a2ee4b60b561f042d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:22:56 +0200 Subject: [PATCH 113/127] fix: wrong tile hidden --- .../match_view/create_match/create_game_view.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 0e96508..d8dbe68 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -213,12 +213,15 @@ class _CreateGameViewState extends State { ), ), - // Choose color tile - ChooseTile(title: loc.ruleset, trailing: getRulesetDropdown(loc)), - // Choose ruleset tile if (!isEditMode()) - ChooseTile(title: loc.color, trailing: getColorDropdown(loc)), + ChooseTile( + title: loc.ruleset, + trailing: getRulesetDropdown(loc), + ), + + // Choose color tile + ChooseTile(title: loc.color, trailing: getColorDropdown(loc)), // Description input field Container( From 9ae562d92a8572979d49fc03903a42cc37aac487 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 18:23:39 +0200 Subject: [PATCH 114/127] update schema test --- test/services/data_transfer_service_test.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index fec70b7..586138a 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -883,14 +883,6 @@ void main() { 'createdAt': testGroup.createdAt.toIso8601String(), }, ], - 'teams': [ - { - 'id': testTeam.id, - 'name': testTeam.name, - 'memberIds': [testPlayer1.id, testPlayer2.id], - 'createdAt': testTeam.createdAt.toIso8601String(), - }, - ], 'matches': [ { 'id': testMatch.id, From 0c44c54bd7e7df1224b916dbf57197b4f17f6df1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:24:12 +0200 Subject: [PATCH 115/127] fix: schema test created json --- test/services/data_transfer_service_test.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index fec70b7..586138a 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -883,14 +883,6 @@ void main() { 'createdAt': testGroup.createdAt.toIso8601String(), }, ], - 'teams': [ - { - 'id': testTeam.id, - 'name': testTeam.name, - 'memberIds': [testPlayer1.id, testPlayer2.id], - 'createdAt': testTeam.createdAt.toIso8601String(), - }, - ], 'matches': [ { 'id': testMatch.id, From 6db265ea993550301356c5d3dfb75060f8cc63cd Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:39:11 +0200 Subject: [PATCH 116/127] feat: replaced variable content with generated lists --- lib/core/enums.dart | 18 ++----- .../create_match/create_game_view.dart | 47 ++++++------------- 2 files changed, 19 insertions(+), 46 deletions(-) diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 605d3aa..99141e4 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -34,21 +34,13 @@ enum ExportResult { success, canceled, unknownException } /// - [Ruleset.multipleWinners]: Multiple players can be winners. /// - [Ruleset.placement]: The player with the highest placement wins. enum Ruleset { + singleWinner, + multipleWinners, highestScore, lowestScore, - singleWinner, - singleLoser, - multipleWinners, placement, + singleLoser, } -/// Different colors available for games -/// - [GameColor.red]: Red color -/// - [GameColor.blue]: Blue color -/// - [GameColor.green]: Green color -/// - [GameColor.yellow]: Yellow color -/// - [GameColor.purple]: Purple color -/// - [GameColor.orange]: Orange color -/// - [GameColor.pink]: Pink color -/// - [GameColor.teal]: Teal color -enum GameColor { red, blue, green, yellow, purple, orange, pink, teal } +/// Different colors for highlighting games +enum GameColor { red, orange, yellow, green, teal, blue, purple, pink } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index d8dbe68..3156476 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -47,9 +47,9 @@ class _CreateGameViewState extends State { late final AppDatabase db; late List<(Ruleset, String)> _rulesets; - Ruleset? selectedRuleset = Ruleset.singleWinner; - late List<(GameColor, String)> _colors; + + Ruleset? selectedRuleset = Ruleset.singleWinner; GameColor? selectedColor = GameColor.orange; /// Controller for the game name input field. @@ -77,39 +77,20 @@ class _CreateGameViewState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - _rulesets = [ - ( - Ruleset.singleWinner, - translateRulesetToString(Ruleset.singleWinner, context), + _rulesets = List.generate( + Ruleset.values.length, + (index) => ( + Ruleset.values[index], + translateRulesetToString(Ruleset.values[index], context), ), - ( - Ruleset.singleLoser, - translateRulesetToString(Ruleset.singleLoser, context), + ); + _colors = List.generate( + GameColor.values.length, + (index) => ( + GameColor.values[index], + translateGameColorToString(GameColor.values[index], context), ), - ( - Ruleset.highestScore, - translateRulesetToString(Ruleset.highestScore, context), - ), - ( - Ruleset.lowestScore, - translateRulesetToString(Ruleset.lowestScore, context), - ), - ( - Ruleset.multipleWinners, - translateRulesetToString(Ruleset.multipleWinners, context), - ), - (Ruleset.placement, translateRulesetToString(Ruleset.placement, context)), - ]; - _colors = [ - (GameColor.green, translateGameColorToString(GameColor.green, context)), - (GameColor.teal, translateGameColorToString(GameColor.teal, context)), - (GameColor.blue, translateGameColorToString(GameColor.blue, context)), - (GameColor.purple, translateGameColorToString(GameColor.purple, context)), - (GameColor.pink, translateGameColorToString(GameColor.pink, context)), - (GameColor.red, translateGameColorToString(GameColor.red, context)), - (GameColor.orange, translateGameColorToString(GameColor.orange, context)), - (GameColor.yellow, translateGameColorToString(GameColor.yellow, context)), - ]; + ); if (widget.gameToEdit != null) { _gameNameController.text = widget.gameToEdit!.name; From 341b293151082944e58b4ebc8ac11bed21940a8d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:41:34 +0200 Subject: [PATCH 117/127] fix: text alignment --- .../views/main_menu/match_view/match_detail_view.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 10fc324..9c04590 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -326,9 +326,10 @@ class _MatchDetailViewState extends State { ), Flexible( child: Container( - padding: const EdgeInsets.only(left: 40), + padding: const EdgeInsets.only(left: 10), child: Text( match.mvp.map((player) => player.name).join(', '), + textAlign: TextAlign.end, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, From 50bf111f0368788af660cd8011c7f6d9b04c59e9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:47:48 +0200 Subject: [PATCH 118/127] fix: single result row --- .../match_view/match_detail_view.dart | 102 ++++++++---------- 1 file changed, 43 insertions(+), 59 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 9c04590..73d534d 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -283,71 +283,55 @@ class _MatchDetailViewState extends State { /// Returns the result row for single winner/loser rulesets or a placeholder /// if no result is entered yet List getSingleResultRow(AppLocalizations loc) { - // Single Winner - if (match.mvp.isNotEmpty && match.game.ruleset == Ruleset.singleWinner) { - return [ - Text( - loc.winner, - style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), - ), - Text( - match.mvp.first.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, + if (match.mvp.isNotEmpty) { + final ruleset = match.game.ruleset; + + if (ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser) { + return [ + Text( + ruleset == Ruleset.singleWinner ? loc.winner : loc.loser, + style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), ), - ), - ]; - // Single Loser - } else if (match.mvp.isNotEmpty && - match.game.ruleset == Ruleset.singleLoser) { - return [ - Text( - loc.loser, - style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), - ), - Text( - match.mvp.first.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, + Text( + match.mvp.first.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), ), - ), - ]; - // Multiple Winners - } else if (match.mvp.isNotEmpty && - match.game.ruleset == Ruleset.multipleWinners) { - return [ - Text( - loc.winners, - style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), - ), - Flexible( - child: Container( - padding: const EdgeInsets.only(left: 10), - child: Text( - match.mvp.map((player) => player.name).join(', '), - textAlign: TextAlign.end, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, + ]; + } else if (match.game.ruleset == Ruleset.multipleWinners) { + return [ + Text( + loc.winners, + style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), + ), + Flexible( + child: Container( + padding: const EdgeInsets.only(left: 10), + child: Text( + match.mvp.map((player) => player.name).join(', '), + textAlign: TextAlign.end, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), ), ), ), - ), - ]; - // No result entered yet - } else { - return [ - Text( - loc.no_results_entered_yet, - style: const TextStyle(fontSize: 14, color: CustomTheme.textColor), - ), - ]; + ]; + } } + + // No results yet + return [ + Text( + loc.no_results_entered_yet, + style: const TextStyle(fontSize: 14, color: CustomTheme.textColor), + ), + ]; } /// Returns the result widget for scores or placement From e503db1c1b28b91be8176288c7607d642547fe52 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 18:51:59 +0200 Subject: [PATCH 119/127] fix: removed unnecessary method --- lib/data/dao/score_entry_dao.dart | 15 +-------------- .../main_menu/match_view/match_result_view.dart | 2 +- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index d59f40a..830135d 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -276,13 +276,7 @@ class ScoreEntryDao extends DatabaseAccessor /// Returns `true` if the winner was removed, `false` if there are multiple /// scores or if the winner cannot be removed. Future removeWinner({required String matchId}) async { - final scores = await getAllMatchScores(matchId: matchId); - - if (scores.length > 1) { - return false; - } else { - return await deleteAllScoresForMatch(matchId: matchId); - } + return await deleteAllScoresForMatch(matchId: matchId); } /* multiple winners handling */ @@ -320,13 +314,6 @@ class ScoreEntryDao extends DatabaseAccessor return true; } - /// Removes the winners of a match. - /// - /// Returns `true` if more than 0 rows were affected, `false` otherwise. - Future removeWinners({required String matchId}) async { - return await deleteAllScoresForMatch(matchId: matchId); - } - /* Loser handling */ Future hasLoser({required String matchId}) async { 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 d14c259..ad41a09 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 @@ -444,7 +444,7 @@ class _MatchResultViewState extends State { /// Handles saving the (multiple) winners to the database. Future _handleWinners() async { if (_selectedWinners.isEmpty) { - return await db.scoreEntryDao.removeWinners(matchId: widget.match.id); + return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); } else { return await db.scoreEntryDao.setWinners( matchId: widget.match.id, From 60a92dafe1943ae6a238dd3f8b57f04c0340ce0d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 19:07:20 +0200 Subject: [PATCH 120/127] updated logic --- .../match_view/match_result_view.dart | 162 +++++++++--------- 1 file changed, 81 insertions(+), 81 deletions(-) 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 ad41a09..a9bb08e 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 @@ -49,8 +49,8 @@ class _MatchResultViewState extends State { /// Currently selected winner player (single winner) Player? _selectedPlayer; - /// Currently selected winners (multiple winners) - final Set _selectedWinners = {}; + /// Currently selected players (multiple winners) + final Set _selectedPlayers = {}; @override void initState() { @@ -68,28 +68,30 @@ class _MatchResultViewState extends State { // Prefill fields if (widget.match.mvp.isNotEmpty) { - if (rulesetSupportsWinnerSelection()) { - _selectedPlayer = allPlayers.firstWhere( - (p) => p.id == widget.match.mvp.first.id, - ); + if (rulesetSupportsPlayerSelection()) { + if (ruleset == Ruleset.multipleWinners) { + for (int i = 0; i < allPlayers.length; i++) { + if (widget.match.scores[allPlayers[i].id]?.score == 1) { + _selectedPlayers.add(allPlayers[i]); + } + } + } else { + _selectedPlayer = allPlayers.firstWhere( + (p) => p.id == widget.match.mvp.first.id, + ); + } } else if (rulesetSupportsScoreEntry()) { for (int i = 0; i < allPlayers.length; i++) { final scoreList = widget.match.scores[allPlayers[i].id]; final score = scoreList?.score ?? 0; controller[i].text = score.toString(); } - } else if (rulesetSupportsPlacement()) { + } else if (rulesetSupportsDragBehaviour()) { allPlayers.sort((a, b) { final scoreA = widget.match.scores[a.id]?.score ?? 0; final scoreB = widget.match.scores[b.id]?.score ?? 0; return scoreB.compareTo(scoreA); }); - } else if (rulesetSupportsMultipleWinners()) { - for (int i = 0; i < allPlayers.length; i++) { - if (widget.match.scores[allPlayers[i].id]?.score == 1) { - _selectedWinners.add(allPlayers[i]); - } - } } super.initState(); } @@ -168,38 +170,68 @@ class _MatchResultViewState extends State { const SizedBox(height: 10), // Show player selection - if (rulesetSupportsWinnerSelection()) + if (rulesetSupportsPlayerSelection()) Expanded( - child: RadioGroup( - groupValue: _selectedPlayer, - onChanged: (Player? value) async { - setState(() { - _selectedPlayer = value; - }); - }, - child: ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return CustomRadioListTile( - text: allPlayers[index].name, - value: allPlayers[index], - onContainerTap: (value) async { + child: ruleset == Ruleset.multipleWinners + // Multiple winners + ? ListView.builder( + physics: + const NeverScrollableScrollPhysics(), + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomCheckboxListTile( + text: allPlayers[index].name, + value: _selectedPlayers.contains( + allPlayers[index], + ), + onChanged: (bool value) { + setState(() { + if (value) { + _selectedPlayers.add( + allPlayers[index], + ); + } else { + _selectedPlayers.remove( + allPlayers[index], + ); + } + }); + }, + ); + }, + ) + // Single winner / looser + : RadioGroup( + groupValue: _selectedPlayer, + onChanged: (Player? value) async { setState(() { - // Check if the already selected player is the same as the newly tapped player. - if (_selectedPlayer == value) { - // If yes deselected the player by setting it to null. - _selectedPlayer = null; - } else { - // If no assign the newly tapped player to the selected player. - (_selectedPlayer = value); - } + _selectedPlayer = value; }); }, - ); - }, - ), - ), + child: ListView.builder( + physics: + const NeverScrollableScrollPhysics(), + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomRadioListTile( + text: allPlayers[index].name, + value: allPlayers[index], + onContainerTap: (value) async { + setState(() { + // Check if the already selected player is the same as the newly tapped player. + if (_selectedPlayer == value) { + // If yes deselected the player by setting it to null. + _selectedPlayer = null; + } else { + // If no assign the newly tapped player to the selected player. + (_selectedPlayer = value); + } + }); + }, + ); + }, + ), + ), ), // Show score entry @@ -226,7 +258,7 @@ class _MatchResultViewState extends State { ), // Show draggable placement list - if (rulesetSupportsPlacement()) + if (rulesetSupportsDragBehaviour()) Expanded( child: Row( children: [ @@ -329,36 +361,6 @@ class _MatchResultViewState extends State { ], ), ), - - // Show multiple winner selection - if (rulesetSupportsMultipleWinners()) - Expanded( - child: ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return CustomCheckboxListTile( - text: allPlayers[index].name, - value: _selectedWinners.contains( - allPlayers[index], - ), - onChanged: (bool value) { - setState(() { - if (value) { - _selectedWinners.add( - allPlayers[index], - ); - } else { - _selectedWinners.remove( - allPlayers[index], - ); - } - }); - }, - ); - }, - ), - ), ], ), ), @@ -443,12 +445,12 @@ class _MatchResultViewState extends State { /// Handles saving the (multiple) winners to the database. Future _handleWinners() async { - if (_selectedWinners.isEmpty) { + if (_selectedPlayers.isEmpty) { return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); } else { return await db.scoreEntryDao.setWinners( matchId: widget.match.id, - winners: allPlayers.where((p) => _selectedWinners.contains(p)).toList(), + winners: allPlayers.where((p) => _selectedPlayers.contains(p)).toList(), ); } } @@ -504,19 +506,17 @@ class _MatchResultViewState extends State { } } - bool rulesetSupportsWinnerSelection() { - return ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser; + bool rulesetSupportsPlayerSelection() { + return ruleset == Ruleset.singleWinner || + ruleset == Ruleset.singleLoser || + ruleset == Ruleset.multipleWinners; } bool rulesetSupportsScoreEntry() { return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore; } - bool rulesetSupportsPlacement() { + bool rulesetSupportsDragBehaviour() { return ruleset == Ruleset.placement; } - - bool rulesetSupportsMultipleWinners() { - return ruleset == Ruleset.multipleWinners; - } } From c75b3e4a6d8dea26ef865b424e891b0a5598b37b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 10 May 2026 19:12:12 +0200 Subject: [PATCH 121/127] updated comment --- .../views/main_menu/match_view/match_result_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a9bb08e..ba138d6 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 @@ -46,7 +46,7 @@ class _MatchResultViewState extends State { /// Flag to indicate if the save button should be enabled late bool canSave; - /// Currently selected winner player (single winner) + /// Currently selected player (single winner / looser) Player? _selectedPlayer; /// Currently selected players (multiple winners) From 1c5735eb4154bd4142126b0fbfd8f1bddad147b4 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Sun, 10 May 2026 17:14:04 +0000 Subject: [PATCH 122/127] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index b4b261e..9b0042b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.30+264 +version: 0.0.31+265 environment: sdk: ^3.8.1 From 699d4378b2e78563b578a4cb9977afd86852e4f7 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Sun, 10 May 2026 17:14:44 +0000 Subject: [PATCH 123/127] Updated licenses [skip ci] --- .../views/main_menu/settings_view/licenses/oss_licenses.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 0ea0662..ee5558a 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -37751,12 +37751,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.30+264 +/// tallee 0.0.31+265 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.30+264', + version: '0.0.31+265', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, From 424fa34cabe59482e185f0b2199b1b2578692b86 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 11 May 2026 18:54:50 +0000 Subject: [PATCH 124/127] bump version to 3.41.0 --- .gitea/workflows/pull_request.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/pull_request.yaml b/.gitea/workflows/pull_request.yaml index 26f4404..825305b 100644 --- a/.gitea/workflows/pull_request.yaml +++ b/.gitea/workflows/pull_request.yaml @@ -20,11 +20,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Analyze Formatting @@ -46,11 +46,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Run tests From 2aed053c4202618c463e772c73e05de4c1d867e9 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 11 May 2026 18:55:51 +0000 Subject: [PATCH 125/127] bump flutter version --- .gitea/workflows/push.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/push.yaml b/.gitea/workflows/push.yaml index e24f7ad..cfe987a 100644 --- a/.gitea/workflows/push.yaml +++ b/.gitea/workflows/push.yaml @@ -32,11 +32,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Build APK @@ -58,11 +58,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Run tests @@ -118,11 +118,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Generate oss_licenses.dart @@ -161,11 +161,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Check code format From 0f3863d446e116c14643245de283444eb5d55094 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Mon, 11 May 2026 19:14:13 +0000 Subject: [PATCH 126/127] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9b0042b..8857c57 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.31+265 +version: 0.0.32+266 environment: sdk: ^3.8.1 From 7a22deea1b26fba63571b7ad4d29c6cfa28b0bfe Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Mon, 11 May 2026 19:15:48 +0000 Subject: [PATCH 127/127] Updated licenses [skip ci] --- .../settings_view/licenses/oss_licenses.dart | 478 ++++++++++-------- 1 file changed, 269 insertions(+), 209 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index ee5558a..1cab79e 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -73,7 +73,6 @@ const allDependencies = [ _io, _jni, _jni_flutter, - _js, _json_annotation, _json_schema, _leak_tracker, @@ -244,13 +243,13 @@ class PackageRef { Package resolve() => allDependencies.firstWhere((d) => d.name == name); } -/// _fe_analyzer_shared 91.0.0 +/// _fe_analyzer_shared 92.0.0 const __fe_analyzer_shared = Package( name: '_fe_analyzer_shared', description: 'Logic that is shared between the front_end and analyzer packages.', repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/_fe_analyzer_shared', authors: [], - version: '91.0.0', + version: '92.0.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -285,13 +284,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// analyzer 8.4.1 +/// analyzer 9.0.0 const _analyzer = Package( name: 'analyzer', description: 'This package provides a library that performs static analysis of Dart code.', repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/analyzer', authors: [], - version: '8.4.1', + version: '9.0.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -699,13 +698,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// characters 1.4.0 +/// characters 1.4.1 const _characters = Package( name: 'characters', description: 'String replacement with operations that are Unicode/grapheme cluster aware.', repository: 'https://github.com/dart-lang/core/tree/main/pkgs/characters', authors: [], - version: '1.4.0', + version: '1.4.1', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -2503,13 +2502,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// flutter 3.38.6 +/// flutter 3.41.0 const _flutter = Package( name: 'flutter', description: 'A framework for writing Flutter applications', homepage: 'https://flutter.dev', authors: [], - version: '3.38.6', + version: '3.41.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: true, @@ -3292,47 +3291,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// js 0.7.2 -const _js = Package( - name: 'js', - description: 'Annotations to create static Dart interfaces for JavaScript APIs.', - repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/js', - authors: [], - version: '0.7.2', - spdxIdentifiers: ['BSD-3-Clause'], - isMarkdown: false, - isSdk: false, - dependencies: [], - devDependencies: [PackageRef('lints')], - license: '''Copyright 2012, the Dart project authors. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google LLC nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', - ); - /// json_annotation 4.11.0 const _json_annotation = Package( name: 'json_annotation', @@ -3671,18 +3629,18 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// matcher 0.12.17 +/// matcher 0.12.18 const _matcher = Package( name: 'matcher', description: 'Support for specifying test expectations via an extensible Matcher class. Also includes a number of built-in Matcher implementations for common cases.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/matcher', authors: [], - version: '0.12.17', + version: '0.12.18', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, dependencies: [PackageRef('async'), PackageRef('meta'), PackageRef('stack_trace'), PackageRef('term_glyph'), PackageRef('test_api')], - devDependencies: [PackageRef('fake_async'), PackageRef('lints'), PackageRef('test')], + devDependencies: [PackageRef('fake_async'), PackageRef('test')], license: '''Copyright 2014, the Dart project authors. Redistribution and use in source and binary forms, with or without @@ -3712,18 +3670,18 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// material_color_utilities 0.11.1 +/// material_color_utilities 0.13.0 const _material_color_utilities = Package( name: 'material_color_utilities', description: 'Algorithms and utilities that power the Material Design 3 color system, including choosing theme colors from images and creating tones of colors; all in a new color space.', repository: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart', authors: [], - version: '0.11.1', + version: '0.13.0', spdxIdentifiers: ['Apache-2.0'], isMarkdown: false, isSdk: false, dependencies: [PackageRef('collection')], - devDependencies: [PackageRef('matcher'), PackageRef('lints'), PackageRef('test')], + devDependencies: [PackageRef('matcher'), PackageRef('test')], license: '''Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -6288,6 +6246,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- +icu json @@ -6913,6 +6872,7 @@ skia limitations under the License. -------------------------------------------------------------------------------- angle +benchmark boringssl cpu_features flatbuffers @@ -10342,6 +10302,7 @@ prospectively choose to deem waived or otherwise exclude such Section(s) of the License, but only in their entirety and only with respect to the Combined Software. -------------------------------------------------------------------------------- +icu include json @@ -13912,34 +13873,6 @@ License & terms of use: http://www.unicode.org/copyright.html All Rights Reserved. --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (C) 2002-2010, International Business Machines -Corporation and others. All Rights Reserved. - - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2001-2003 International Business Machines -Corporation and others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2001-2010 International Business Machines -Corporation and others. All Rights Reserved. - -------------------------------------------------------------------------------- icu @@ -13954,14 +13887,6 @@ icu Copyright (C) 2016 and later: Unicode, Inc. and others. License & terms of use: http://www.unicode.org/copyright.html -Copyright (c) 2002-2010, International Business Machines Corporation and others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - Copyright (c) 2002-2015, International Business Machines Corporation and others. All Rights Reserved. @@ -13976,22 +13901,6 @@ Copyright (c) 2002-2016 International Business Machines Corporation and others. All Rights Reserved. --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2003-2005, International Business Machines Corporation and others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2003-2010, International Business Machines Corporation and others. All Rights Reserved. - -------------------------------------------------------------------------------- icu @@ -14166,17 +14075,6 @@ License & terms of use: http://www.unicode.org/copyright.html -------------------------------------------------------------------------------- icu -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -*************************************************************************** -* -* Copyright (C) 2009 International Business Machines -* Corporation and others. All Rights Reserved. -* -*************************************************************************** --------------------------------------------------------------------------------- -icu - Copyright (C) 2016 and later: Unicode, Inc. and others. License & terms of use: http://www.unicode.org/copyright.html ***************************************************************************** @@ -14187,19 +14085,6 @@ License & terms of use: http://www.unicode.org/copyright.html ***************************************************************************** --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -******************************************************************************* -* -* Copyright (C) 1995-2001, International Business Machines -* Corporation and others. All Rights Reserved. -* -******************************************************************************* - - -------------------------------------------------------------------------------- icu @@ -14278,6 +14163,15 @@ License & terms of use: http://www.unicode.org/copyright.html ******************************************************************************* +-------------------------------------------------------------------------------- +icu + +Copyright (C) 2016 and later: Unicode, Inc. and others. +License & terms of use: http://www.unicode.org/copyright.html +--------------------------------------------------------- +Copyright (C) 2013, International Business Machines +Corporation and others. All Rights Reserved. + -------------------------------------------------------------------------------- icu @@ -15561,6 +15455,22 @@ angle Copyright (c) 2008-2021 The Khronos Group Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +angle + +Copyright (c) 2008-2023 The Khronos Group Inc. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -18045,6 +17955,28 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- +volk + +Copyright (c) 2018-2024 Arseny Kapoulkine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +-------------------------------------------------------------------------------- vulkan-validation-layers Copyright (c) 2018-2024 The Khronos Group Inc. @@ -18569,7 +18501,6 @@ Copyright (c) 2020 The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- -angle spirv-tools Copyright (c) 2020 The Khronos Group Inc. @@ -19380,6 +19311,22 @@ spirv-tools Copyright (c) 2023 LunarG Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +angle + +Copyright (c) 2023 The Khronos Group Inc. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -19417,6 +19364,7 @@ for details. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- dart +perfetto Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file for details. All rights reserved. Use of this source code is governed by a @@ -19950,6 +19898,12 @@ Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file for details. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +perfetto + +Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +for details. All rights reserved. Use of this source code is governed by a +BSD-style license that can be found in the LICENSE file. +-------------------------------------------------------------------------------- glfw Copyright (c) Camilla Löwy @@ -20809,11 +20763,6 @@ Copyright 2014-2022 The Khronos Group Inc. SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- swiftshader - -Copyright 2014-2023 The Khronos Group Inc. - -SPDX-License-Identifier: Apache-2.0 --------------------------------------------------------------------------------- vulkan vulkan-headers @@ -20851,6 +20800,7 @@ tree. An additional intellectual property rights grant can be found in the file PATENTS. All contributing project authors may be found in the AUTHORS file in the root of the source tree. -------------------------------------------------------------------------------- +benchmark flatbuffers Copyright 2015 Google Inc. All rights reserved. @@ -20936,12 +20886,6 @@ See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- swiftshader - -Copyright 2015-2023 The Khronos Group Inc. - -SPDX-License-Identifier: Apache-2.0 --------------------------------------------------------------------------------- -swiftshader vulkan vulkan-headers @@ -20951,6 +20895,7 @@ Copyright 2015-2023 LunarG, Inc. SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- +swiftshader vulkan vulkan-headers @@ -20978,18 +20923,6 @@ skia Copyright 2016 Google Inc. -Use of this source code is governed by a BSD-style license that can be -found in the LICENSE file. --------------------------------------------------------------------------------- -skia - -Copyright 2016 Google Inc. - -Use of this source code is governed by a BSD-style license that can be -found in the LICENSE file. - -Copyright 2014 Google Inc. - Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -21007,6 +20940,39 @@ flatbuffers Copyright 2016 Google Inc. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +benchmark + +Copyright 2016 Ismael Jimenez Martinez. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +benchmark + +Copyright 2016 Ismael Jimenez Martinez. All rights reserved. +Copyright 2017 Roman Lebedev. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -21437,6 +21403,7 @@ tree. An additional intellectual property rights grant can be found in the file PATENTS. All contributing project authors may be found in the AUTHORS file in the root of the source tree. -------------------------------------------------------------------------------- +benchmark flatbuffers Copyright 2018 Google Inc. All rights reserved. @@ -22213,7 +22180,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- -angle swiftshader Copyright 2020 The SwiftShader Authors. All Rights Reserved. @@ -22263,6 +22229,7 @@ tree. An additional intellectual property rights grant can be found in the file PATENTS. All contributing project authors may be found in the AUTHORS file in the root of the source tree. -------------------------------------------------------------------------------- +benchmark flatbuffers Copyright 2021 Google Inc. All rights reserved. @@ -22695,6 +22662,22 @@ Copyright 2023 The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +angle + +Copyright 2023 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- skia Copyright 2023 The Android Open Source Project @@ -22772,6 +22755,7 @@ Copyright 2023-2025 The Khronos Group Inc. SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- +swiftshader vulkan-utility-libraries Copyright 2023-2025 The Khronos Group Inc. @@ -22789,6 +22773,12 @@ skia Copyright 2024 Google Inc. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +angle + +Copyright 2024 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -22824,6 +22814,12 @@ found in the LICENSE file. -------------------------------------------------------------------------------- skia +Copyright 2024 Google LLC. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +skia + Copyright 2024 Google LLC. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -22834,6 +22830,17 @@ Copyright 2024 Google, LLC Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +angle + +Copyright 2024 The ANGLE Project Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +angle + +Copyright 2024 The ANGLE Project Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. +-------------------------------------------------------------------------------- skia Copyright 2024 The Android Open Source Project @@ -22915,6 +22922,19 @@ skia Copyright 2025 Google LLC. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +skia + +Copyright 2025 Google, LLC + +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +angle + +Copyright 2025 The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -22979,6 +22999,12 @@ Copyright The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +angle + +Copyright {copyright_year} The ANGLE Project Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- harfbuzz Copyright © 1998-2004 David Turner and Werner Lemberg @@ -26723,6 +26749,48 @@ FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -------------------------------------------------------------------------------- +flutter + +License for the Ahem font embedded below is from: +https://www.w3.org/Style/CSS/Test/Fonts/Ahem/COPYING + +The Ahem font in this directory belongs to the public domain. In +jurisdictions that do not recognize public domain ownership of these +files, the following Creative Commons Zero declaration applies: + + + +which is quoted below: + + The person who has associated a work with this document (the "Work") + affirms that he or she (the "Affirmer") is the/an author or owner of + the Work. The Work may be any work of authorship, including a + database. + + The Affirmer hereby fully, permanently and irrevocably waives and + relinquishes all of her or his copyright and related or neighboring + legal rights in the Work available under any federal or state law, + treaty or contract, including but not limited to moral rights, + publicity and privacy rights, rights protecting against unfair + competition and any rights protecting the extraction, dissemination + and reuse of data, whether such rights are present or future, vested + or contingent (the "Waiver"). The Affirmer makes the Waiver for the + benefit of the public at large and to the detriment of the Affirmer's + heirs or successors. + + The Affirmer understands and intends that the Waiver has the effect + of eliminating and entirely removing from the Affirmer's control all + the copyright and related or neighboring legal rights previously held + by the Affirmer in the Work, to that extent making the Work freely + available to the public for any and all uses and purposes without + restriction of any kind, including commercial use and uses in media + and formats or by methods that have not yet been invented or + conceived. Should the Waiver for any reason be judged legally + ineffective in any jurisdiction, the Affirmer hereby grants a free, + full, permanent, irrevocable, nonexclusive and worldwide license for + all her or his copyright and related or neighboring legal rights in + the Work. +-------------------------------------------------------------------------------- fallback_root_certificates Mozilla Public License Version 2.0 @@ -27105,8 +27173,8 @@ libpng PNG Reference Library License version 2 --------------------------------------- - * Copyright (c) 1995-2024 The PNG Reference Library Authors. - * Copyright (c) 2018-2024 Cosmin Truta. + * Copyright (c) 1995-2025 The PNG Reference Library Authors. + * Copyright (c) 2018-2025 Cosmin Truta. * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. * Copyright (c) 1996-1997 Andreas Dilger. * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. @@ -27240,8 +27308,8 @@ libpng PNG Reference Library License version 2 --------------------------------------- - * Copyright (c) 1995-2024 The PNG Reference Library Authors. - * Copyright (c) 2018-2024 Cosmin Truta. + * Copyright (c) 1995-2025 The PNG Reference Library Authors. + * Copyright (c) 2018-2025 Cosmin Truta. * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. * Copyright (c) 1996-1997 Andreas Dilger. * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. @@ -28708,7 +28776,7 @@ UNICODE LICENSE V3 COPYRIGHT AND PERMISSION NOTICE -Copyright © 2016-2023 Unicode, Inc. +Copyright © 2016-2025 Unicode, Inc. NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR @@ -28744,6 +28812,8 @@ not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder. +SPDX-License-Identifier: Unicode-3.0 + ---------------------------------------------------------------------- Third-Party Software Licenses @@ -29137,6 +29207,34 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------- +JSON parsing library (nlohmann/json) + +File: vendor/json/upstream/single_include/nlohmann/json.hpp (only for ICU4C) + +MIT License + +Copyright (c) 2013-2022 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---------------------------------------------------------------------- + File: install-sh (only for ICU4C) @@ -32422,17 +32520,6 @@ Copyright (C) 2003-2016, International Business Machines --------------------------------------------------------------------------------- -icu - -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - - -Copyright (C) 2008-2013, International Business Machines Corporation and -others. All Rights Reserved. - - -------------------------------------------------------------------------------- icu @@ -32472,16 +32559,6 @@ icu License & terms of use: http://www.unicode.org/copyright.html -Copyright (c) 1999-2007, International Business Machines Corporation and -others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - - Copyright (c) 1999-2010, International Business Machines Corporation and others. All Rights Reserved. @@ -35972,15 +36049,6 @@ others. All Rights Reserved. --------------------------------------------------------------------------------- -icu - -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -Copyright (C) 2008-2014, International Business Machines Corporation and -others. All Rights Reserved. - - -------------------------------------------------------------------------------- icu @@ -36220,14 +36288,6 @@ others. All Rights Reserved. -------------------------------------------------------------------------------- icu -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -Copyright (c) 2008-2014, International Business Machines Corporation and -others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - © 2016 and later: Unicode, Inc. and others. License & terms of use: http://www.unicode.org/copyright.html Copyright (c) IBM Corporation, 2000-2010. All rights reserved. @@ -36755,17 +36815,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// test 1.26.3 +/// test 1.28.0 const _test = Package( name: 'test', description: 'A full featured library for writing and running Dart tests across platforms.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/test', authors: [], - version: '1.26.3', + version: '1.28.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('analyzer'), PackageRef('async'), PackageRef('boolean_selector'), PackageRef('collection'), PackageRef('coverage'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('js'), PackageRef('matcher'), PackageRef('node_preamble'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('shelf'), PackageRef('shelf_packages_handler'), PackageRef('shelf_static'), PackageRef('shelf_web_socket'), PackageRef('source_span'), PackageRef('stack_trace'), PackageRef('stream_channel'), PackageRef('test_api'), PackageRef('test_core'), PackageRef('typed_data'), PackageRef('web_socket_channel'), PackageRef('webkit_inspection_protocol'), PackageRef('yaml')], + dependencies: [PackageRef('analyzer'), PackageRef('async'), PackageRef('boolean_selector'), PackageRef('collection'), PackageRef('coverage'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('matcher'), PackageRef('node_preamble'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('shelf'), PackageRef('shelf_packages_handler'), PackageRef('shelf_static'), PackageRef('shelf_web_socket'), PackageRef('source_span'), PackageRef('stack_trace'), PackageRef('stream_channel'), PackageRef('test_api'), PackageRef('test_core'), PackageRef('typed_data'), PackageRef('web_socket_channel'), PackageRef('webkit_inspection_protocol'), PackageRef('yaml')], devDependencies: [PackageRef('fake_async'), PackageRef('glob')], license: '''Copyright 2014, the Dart project authors. @@ -36796,13 +36856,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// test_api 0.7.7 +/// test_api 0.7.8 const _test_api = Package( name: 'test_api', description: 'The user facing API for structuring Dart tests and checking expectations.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/test_api', authors: [], - version: '0.7.7', + version: '0.7.8', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -36837,13 +36897,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// test_core 0.6.12 +/// test_core 0.6.14 const _test_core = Package( name: 'test_core', description: 'A basic library for writing tests and running them on the VM.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/test_core', authors: [], - version: '0.6.12', + version: '0.6.14', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -37751,12 +37811,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.31+265 +/// tallee 0.0.32+266 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.31+265', + version: '0.0.32+266', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false,