From 1536e2b2af72bd80a40cd31009ff4431429c0cc6 Mon Sep 17 00:00:00 2001 From: Yannick <69087944+GelbEinhalb@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:31:50 +0100 Subject: [PATCH 01/95] move navbar up --- lib/presentation/views/main_menu/custom_navigation_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 20ced7a..3f6a82a 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -55,7 +55,7 @@ class _CustomNavigationBarState extends State body: tabs[currentIndex], extendBody: true, bottomNavigationBar: Padding( - padding: const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 8.0), + padding: const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 18.0), child: Material( elevation: 10, borderRadius: BorderRadius.circular(24), From 36aa4722a329bd051c40c29df0129264c0c9ae19 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 15 Nov 2025 17:11:48 +0100 Subject: [PATCH 02/95] wrapped custom_navigation_bar in safearea --- .../main_menu/custom_navigation_bar.dart | 126 +++++++++--------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 3f6a82a..28331b8 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -31,71 +31,73 @@ class _CustomNavigationBarState extends State @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - centerTitle: true, - title: Text( - _currentTabTitle(), - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + return SafeArea( + child: Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + _currentTabTitle(), + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + backgroundColor: CustomTheme.backgroundColor, + scrolledUnderElevation: 0, + actions: [ + IconButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const SettingsView()), + ), + icon: const Icon(Icons.settings), + ), + ], + elevation: 0, ), backgroundColor: CustomTheme.backgroundColor, - scrolledUnderElevation: 0, - actions: [ - IconButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => const SettingsView()), - ), - icon: const Icon(Icons.settings), - ), - ], - elevation: 0, - ), - backgroundColor: CustomTheme.backgroundColor, - body: tabs[currentIndex], - extendBody: true, - bottomNavigationBar: Padding( - padding: const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 18.0), - child: Material( - elevation: 10, - borderRadius: BorderRadius.circular(24), - color: CustomTheme.primaryColor, - child: ClipRRect( + body: tabs[currentIndex], + extendBody: true, + bottomNavigationBar: Padding( + padding: const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 18.0), + child: Material( + elevation: 10, borderRadius: BorderRadius.circular(24), - child: SizedBox( - height: 60, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - NavbarItem( - index: 0, - isSelected: currentIndex == 0, - icon: Icons.home_rounded, - label: 'Home', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 1, - isSelected: currentIndex == 1, - icon: Icons.gamepad_rounded, - label: 'Games', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 2, - isSelected: currentIndex == 2, - icon: Icons.group_rounded, - label: 'Groups', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 3, - isSelected: currentIndex == 3, - icon: Icons.bar_chart_rounded, - label: 'Stats', - onTabTapped: onTabTapped, - ), - ], + color: CustomTheme.primaryColor, + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: SizedBox( + height: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + NavbarItem( + index: 0, + isSelected: currentIndex == 0, + icon: Icons.home_rounded, + label: 'Home', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 1, + isSelected: currentIndex == 1, + icon: Icons.gamepad_rounded, + label: 'Games', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 2, + isSelected: currentIndex == 2, + icon: Icons.group_rounded, + label: 'Groups', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 3, + isSelected: currentIndex == 3, + icon: Icons.bar_chart_rounded, + label: 'Stats', + onTabTapped: onTabTapped, + ), + ], + ), ), ), ), From 3ca081419b9f3ec43d741727fad5b60ef0b4e689 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 17 Nov 2025 23:23:04 +0100 Subject: [PATCH 03/95] Implemented new SettingsListTile --- .../widgets/tiles/settings_list_tile.dart | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 lib/presentation/widgets/tiles/settings_list_tile.dart diff --git a/lib/presentation/widgets/tiles/settings_list_tile.dart b/lib/presentation/widgets/tiles/settings_list_tile.dart new file mode 100644 index 0000000..1174627 --- /dev/null +++ b/lib/presentation/widgets/tiles/settings_list_tile.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +class SettingsListTile extends StatelessWidget { + final VoidCallback? onPressed; + final IconData icon; + final String title; + final Widget? suffixWidget; + const SettingsListTile({ + super.key, + required this.title, + required this.icon, + this.suffixWidget, + this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Center( + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.95, + child: Container( + margin: EdgeInsets.zero, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorder), + borderRadius: BorderRadius.circular(12), + ), + child: GestureDetector( + onTap: onPressed ?? () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: CustomTheme.primaryColor, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 24), + ), + const SizedBox(width: 16), + Text(title, style: const TextStyle(fontSize: 18)), + ], + ), + if (suffixWidget != null) + suffixWidget! + else + const SizedBox.shrink(), + ], + ), + ), + ), + ), + ), + ); + } +} From 62acc87e0e804c3a366145d2af32ab0d7ecb9ab0 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 17 Nov 2025 23:23:41 +0100 Subject: [PATCH 04/95] Moved game tile in tiles folder --- lib/presentation/views/main_menu/home_view.dart | 2 +- lib/presentation/widgets/{ => tiles}/game_tile.dart | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/presentation/widgets/{ => tiles}/game_tile.dart (100%) diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index cf6288a..53816d0 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/presentation/widgets/game_tile.dart'; import 'package:game_tracker/presentation/widgets/quick_create_button.dart'; +import 'package:game_tracker/presentation/widgets/tiles/game_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart'; import 'package:provider/provider.dart'; diff --git a/lib/presentation/widgets/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart similarity index 100% rename from lib/presentation/widgets/game_tile.dart rename to lib/presentation/widgets/tiles/game_tile.dart From 2076e45fd505f1405d71e6efba05d87f86a9354d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 17 Nov 2025 23:23:52 +0100 Subject: [PATCH 05/95] Created new layout for settings view --- .../views/main_menu/settings_view.dart | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index c3e75f3..75ea163 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; class SettingsView extends StatelessWidget { const SettingsView({super.key}); @@ -6,8 +8,77 @@ class SettingsView extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Einstellungen')), - body: const Center(child: Text('Settings View')), + appBar: AppBar(backgroundColor: CustomTheme.backgroundColor), + backgroundColor: CustomTheme.backgroundColor, + body: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) => + SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(24, 0, 24, 10), + child: Text( + textAlign: TextAlign.start, + 'MenĂ¼', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10), + child: Text( + textAlign: TextAlign.start, + 'Einstellungen', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ), + + SettingsListTile( + title: 'Export Data', + icon: Icons.upload_outlined, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () => print('Export Data'), + ), + SettingsListTile( + title: 'Import Data', + icon: Icons.download_outlined, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () => print('Import Data'), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10), + child: Text( + textAlign: TextAlign.start, + 'Example Headline', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ), + SettingsListTile( + title: 'Example Tile', + icon: Icons.upload_outlined, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () => print('Example Tile'), + ), + SettingsListTile( + title: 'Example Tile', + icon: Icons.download_outlined, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () => print('Example Tile'), + ), + ], + ), + ), + ), ); } } From 178aaa964323e4ea7fbcb2dae09fc53a4c892b94 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 17 Nov 2025 23:25:12 +0100 Subject: [PATCH 06/95] Added function headers --- .../views/main_menu/settings_view.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index 75ea163..b05ae8d 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -21,7 +21,7 @@ class SettingsView extends StatelessWidget { padding: EdgeInsets.fromLTRB(24, 0, 24, 10), child: Text( textAlign: TextAlign.start, - 'MenĂ¼', + 'Menu', style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, @@ -32,7 +32,7 @@ class SettingsView extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10), child: Text( textAlign: TextAlign.start, - 'Einstellungen', + 'Settings', style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, @@ -44,13 +44,13 @@ class SettingsView extends StatelessWidget { title: 'Export Data', icon: Icons.upload_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => print('Export Data'), + onPressed: () => exportData(), ), SettingsListTile( title: 'Import Data', icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => print('Import Data'), + onPressed: () => importData(), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10), @@ -81,4 +81,10 @@ class SettingsView extends StatelessWidget { ), ); } + + // TODO: Implement export functionality + void exportData() {} + + // TODO: Implement import functionality + void importData() {} } From a8a81c21513049fa6391517ad90ef6b322e8bb6d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 18 Nov 2025 22:33:46 +0100 Subject: [PATCH 07/95] Fixed gesture detector area --- .../widgets/tiles/settings_list_tile.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/presentation/widgets/tiles/settings_list_tile.dart b/lib/presentation/widgets/tiles/settings_list_tile.dart index 1174627..d5c421f 100644 --- a/lib/presentation/widgets/tiles/settings_list_tile.dart +++ b/lib/presentation/widgets/tiles/settings_list_tile.dart @@ -21,16 +21,16 @@ class SettingsListTile extends StatelessWidget { child: Center( child: SizedBox( width: MediaQuery.of(context).size.width * 0.95, - child: Container( - margin: EdgeInsets.zero, - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), - decoration: BoxDecoration( - color: CustomTheme.boxColor, - border: Border.all(color: CustomTheme.boxBorder), - borderRadius: BorderRadius.circular(12), - ), - child: GestureDetector( - onTap: onPressed ?? () {}, + child: GestureDetector( + onTap: onPressed ?? () {}, + child: Container( + margin: EdgeInsets.zero, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorder), + borderRadius: BorderRadius.circular(12), + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ From d86de0904285a7acfd8685f20af68b554cdd8d52 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 18 Nov 2025 23:16:57 +0100 Subject: [PATCH 08/95] Added fromJson, toJson --- lib/data/dto/game.dart | 17 +++++++++++++++++ lib/data/dto/group.dart | 13 +++++++++++++ lib/data/dto/player.dart | 10 ++++++++++ 3 files changed, 40 insertions(+) diff --git a/lib/data/dto/game.dart b/lib/data/dto/game.dart index c84779d..a52ee29 100644 --- a/lib/data/dto/game.dart +++ b/lib/data/dto/game.dart @@ -21,4 +21,21 @@ class Game { String toString() { return 'Game{\n\tid: $id,\n\tname: $name,\n\tplayers: $players,\n\tgroup: $group,\n\twinner: $winner\n}'; } + + /// Creates a Game instance from a JSON object. + Game.fromJson(Map json) + : id = json['id'], + name = json['name'], + players = json['players'] != null + ? (json['players'] as List) + .map((playerJson) => Player.fromJson(playerJson)) + .toList() + : null, + group = json['group'] != null ? Group.fromJson(json['group']) : null, + winner = json['winner'] ?? ''; + + /// Converts the Game instance to a JSON object. + String toJson() { + return 'Game{id: $id,name: $name,players: $players,group: $group,winner: $winner}'; + } } diff --git a/lib/data/dto/group.dart b/lib/data/dto/group.dart index 0420477..0546dbd 100644 --- a/lib/data/dto/group.dart +++ b/lib/data/dto/group.dart @@ -13,4 +13,17 @@ class Group { String toString() { return 'Group{id: $id, name: $name,members: $members}'; } + + /// Creates a Group instance from a JSON object. + Group.fromJson(Map json) + : id = json['id'], + name = json['name'], + members = (json['members'] as List) + .map((memberJson) => Player.fromJson(memberJson)) + .toList(); + + /// Converts the Group instance to a JSON object. + String toJson() { + return 'Group{id: $id, name: $name,members: $members}'; + } } diff --git a/lib/data/dto/player.dart b/lib/data/dto/player.dart index 1b00c2c..9f10729 100644 --- a/lib/data/dto/player.dart +++ b/lib/data/dto/player.dart @@ -10,4 +10,14 @@ class Player { String toString() { return 'Player{id: $id,name: $name}'; } + + /// Creates a Player instance from a JSON object. + Player.fromJson(Map json) + : id = json['id'], + name = json['name']; + + /// Converts the Player instance to a JSON object. + String toJson() { + return 'Player{id: $id,name: $name}'; + } } From 2da2e28cb66fd31f2074befb3fa0fcc6686b767f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 18 Nov 2025 23:20:26 +0100 Subject: [PATCH 09/95] Added json schema --- assets/schema.json | 0 pubspec.yaml | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 assets/schema.json diff --git a/assets/schema.json b/assets/schema.json new file mode 100644 index 0000000..e69de29 diff --git a/pubspec.yaml b/pubspec.yaml index fbbc01a..c7e55d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,3 +30,6 @@ dev_dependencies: flutter: uses-material-design: true + +assets: + - assets/schema.json From fd86f5193fbe68df9a5bf32fb00a0ad50030011f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 18 Nov 2025 23:46:32 +0100 Subject: [PATCH 10/95] Fixed toJson methods --- lib/data/dto/game.dart | 10 +++++++--- lib/data/dto/group.dart | 8 +++++--- lib/data/dto/player.dart | 4 +--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/data/dto/game.dart b/lib/data/dto/game.dart index a52ee29..bec22fb 100644 --- a/lib/data/dto/game.dart +++ b/lib/data/dto/game.dart @@ -35,7 +35,11 @@ class Game { winner = json['winner'] ?? ''; /// Converts the Game instance to a JSON object. - String toJson() { - return 'Game{id: $id,name: $name,players: $players,group: $group,winner: $winner}'; - } + Map toJson() => { + 'id': id, + 'name': name, + 'players': players?.map((player) => player.toJson()).toList(), + 'group': group?.toJson(), + 'winner': winner, + }; } diff --git a/lib/data/dto/group.dart b/lib/data/dto/group.dart index 0546dbd..71347b1 100644 --- a/lib/data/dto/group.dart +++ b/lib/data/dto/group.dart @@ -23,7 +23,9 @@ class Group { .toList(); /// Converts the Group instance to a JSON object. - String toJson() { - return 'Group{id: $id, name: $name,members: $members}'; - } + Map toJson() => { + 'id': id, + 'name': name, + 'members': members.map((member) => member.toJson()).toList(), + }; } diff --git a/lib/data/dto/player.dart b/lib/data/dto/player.dart index 9f10729..cc73f87 100644 --- a/lib/data/dto/player.dart +++ b/lib/data/dto/player.dart @@ -17,7 +17,5 @@ class Player { name = json['name']; /// Converts the Player instance to a JSON object. - String toJson() { - return 'Player{id: $id,name: $name}'; - } + Map toJson() => {'id': id, 'name': name}; } From 08fcaa35ee31ee83e891b343ab6c03ca4a4dbdd3 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 18 Nov 2025 23:59:18 +0100 Subject: [PATCH 11/95] Added methods for deleting all entities --- lib/data/dao/game_dao.dart | 8 ++++++++ lib/data/dao/group_dao.dart | 8 ++++++++ lib/data/dao/player_dao.dart | 8 ++++++++ 3 files changed, 24 insertions(+) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index fc931ad..a5946f2 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -88,4 +88,12 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { final result = await query.getSingleOrNull(); return result != null; } + + /// Deletes all games from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllGames() async { + final query = delete(gameTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } } diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 8eb3a1a..39f8c45 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -103,4 +103,12 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { final result = await query.getSingleOrNull(); return result != null; } + + /// Deletes all groups from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllGroups() async { + final query = delete(groupTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } } diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 591634c..e0aa165 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -70,4 +70,12 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { .getSingle(); return count ?? 0; } + + /// Deletes all players from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllPlayers() async { + final query = delete(playerTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } } From 42ce69f4d3faf4fd210364bf504543d2887c6290 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 18 Nov 2025 23:59:28 +0100 Subject: [PATCH 12/95] Added schema.json --- assets/schema.json | 137 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/assets/schema.json b/assets/schema.json index e69de29..c33fab2 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -0,0 +1,137 @@ + + +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "games": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "players": { + "type": "null" + }, + "group": { + "type": "null" + }, + "winner": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "players", + "group", + "winner" + ] + } + ] + }, + "groups": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "members": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + ] + } + }, + "required": [ + "id", + "name", + "members" + ] + } + ] + }, + "players": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + ] + } + }, + "required": [ + "games", + "groups", + "players" + ] +} + From 69c95ca672731b7562b28ec7aab7c9b905f510e0 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 00:22:13 +0100 Subject: [PATCH 13/95] Added custom statement for cascade deleting --- lib/data/db/database.dart | 9 ++ lib/data/db/database.g.dart | 304 +++++------------------------------- 2 files changed, 52 insertions(+), 261 deletions(-) diff --git a/lib/data/db/database.dart b/lib/data/db/database.dart index 73ad73e..704e1f0 100644 --- a/lib/data/db/database.dart +++ b/lib/data/db/database.dart @@ -40,6 +40,15 @@ class AppDatabase extends _$AppDatabase { @override int get schemaVersion => 1; + @override + MigrationStrategy get migration { + return MigrationStrategy( + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON'); + }, + ); + } + static QueryExecutor _openConnection() { return driftDatabase( name: 'gametracker_db', diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index 03b7a10..20e4cc1 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -444,12 +444,9 @@ class $GameTableTable extends GameTable late final GeneratedColumn winnerId = GeneratedColumn( 'winner_id', aliasedName, - false, + true, type: DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES player_table (id) ON DELETE CASCADE', - ), + requiredDuringInsert: false, ); @override List get $columns => [id, name, winnerId]; @@ -483,8 +480,6 @@ class $GameTableTable extends GameTable _winnerIdMeta, winnerId.isAcceptableOrUnknown(data['winner_id']!, _winnerIdMeta), ); - } else if (isInserting) { - context.missing(_winnerIdMeta); } return context; } @@ -506,7 +501,7 @@ class $GameTableTable extends GameTable winnerId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}winner_id'], - )!, + ), ); } @@ -519,18 +514,16 @@ class $GameTableTable extends GameTable class GameTableData extends DataClass implements Insertable { final String id; final String name; - final String winnerId; - const GameTableData({ - required this.id, - required this.name, - required this.winnerId, - }); + final String? winnerId; + const GameTableData({required this.id, required this.name, this.winnerId}); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); map['name'] = Variable(name); - map['winner_id'] = Variable(winnerId); + if (!nullToAbsent || winnerId != null) { + map['winner_id'] = Variable(winnerId); + } return map; } @@ -538,7 +531,9 @@ class GameTableData extends DataClass implements Insertable { return GameTableCompanion( id: Value(id), name: Value(name), - winnerId: Value(winnerId), + winnerId: winnerId == null && nullToAbsent + ? const Value.absent() + : Value(winnerId), ); } @@ -550,7 +545,7 @@ class GameTableData extends DataClass implements Insertable { return GameTableData( id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), - winnerId: serializer.fromJson(json['winnerId']), + winnerId: serializer.fromJson(json['winnerId']), ); } @override @@ -559,16 +554,19 @@ class GameTableData extends DataClass implements Insertable { return { 'id': serializer.toJson(id), 'name': serializer.toJson(name), - 'winnerId': serializer.toJson(winnerId), + 'winnerId': serializer.toJson(winnerId), }; } - GameTableData copyWith({String? id, String? name, String? winnerId}) => - GameTableData( - id: id ?? this.id, - name: name ?? this.name, - winnerId: winnerId ?? this.winnerId, - ); + GameTableData copyWith({ + String? id, + String? name, + Value winnerId = const Value.absent(), + }) => GameTableData( + id: id ?? this.id, + name: name ?? this.name, + winnerId: winnerId.present ? winnerId.value : this.winnerId, + ); GameTableData copyWithCompanion(GameTableCompanion data) { return GameTableData( id: data.id.present ? data.id.value : this.id, @@ -601,7 +599,7 @@ class GameTableData extends DataClass implements Insertable { class GameTableCompanion extends UpdateCompanion { final Value id; final Value name; - final Value winnerId; + final Value winnerId; final Value rowid; const GameTableCompanion({ this.id = const Value.absent(), @@ -612,11 +610,10 @@ class GameTableCompanion extends UpdateCompanion { GameTableCompanion.insert({ required String id, required String name, - required String winnerId, + this.winnerId = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), - name = Value(name), - winnerId = Value(winnerId); + name = Value(name); static Insertable custom({ Expression? id, Expression? name, @@ -634,7 +631,7 @@ class GameTableCompanion extends UpdateCompanion { GameTableCompanion copyWith({ Value? id, Value? name, - Value? winnerId, + Value? winnerId, Value? rowid, }) { return GameTableCompanion( @@ -1381,13 +1378,6 @@ abstract class _$AppDatabase extends GeneratedDatabase { ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ - WritePropagation( - on: TableUpdateQuery.onTableName( - 'player_table', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('game_table', kind: UpdateKind.delete)], - ), WritePropagation( on: TableUpdateQuery.onTableName( 'player_table', @@ -1450,24 +1440,6 @@ final class $$PlayerTableTableReferences extends BaseReferences<_$AppDatabase, $PlayerTableTable, PlayerTableData> { $$PlayerTableTableReferences(super.$_db, super.$_table, super.$_typedResult); - static MultiTypedResultKey<$GameTableTable, List> - _gameTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( - db.gameTable, - aliasName: $_aliasNameGenerator(db.playerTable.id, db.gameTable.winnerId), - ); - - $$GameTableTableProcessedTableManager get gameTableRefs { - final manager = $$GameTableTableTableManager( - $_db, - $_db.gameTable, - ).filter((f) => f.winnerId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull(_gameTableRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } - static MultiTypedResultKey<$PlayerGroupTableTable, List> _playerGroupTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( db.playerGroupTable, @@ -1534,31 +1506,6 @@ class $$PlayerTableTableFilterComposer builder: (column) => ColumnFilters(column), ); - Expression gameTableRefs( - Expression Function($$GameTableTableFilterComposer f) f, - ) { - final $$GameTableTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.gameTable, - getReferencedColumn: (t) => t.winnerId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$GameTableTableFilterComposer( - $db: $db, - $table: $db.gameTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - Expression playerGroupTableRefs( Expression Function($$PlayerGroupTableTableFilterComposer f) f, ) { @@ -1645,31 +1592,6 @@ class $$PlayerTableTableAnnotationComposer GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); - Expression gameTableRefs( - Expression Function($$GameTableTableAnnotationComposer a) f, - ) { - final $$GameTableTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.gameTable, - getReferencedColumn: (t) => t.winnerId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$GameTableTableAnnotationComposer( - $db: $db, - $table: $db.gameTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - Expression playerGroupTableRefs( Expression Function($$PlayerGroupTableTableAnnotationComposer a) f, ) { @@ -1735,7 +1657,6 @@ class $$PlayerTableTableTableManager (PlayerTableData, $$PlayerTableTableReferences), PlayerTableData, PrefetchHooks Function({ - bool gameTableRefs, bool playerGroupTableRefs, bool playerGameTableRefs, }) @@ -1773,42 +1694,16 @@ class $$PlayerTableTableTableManager ) .toList(), prefetchHooksCallback: - ({ - gameTableRefs = false, - playerGroupTableRefs = false, - playerGameTableRefs = false, - }) { + ({playerGroupTableRefs = false, playerGameTableRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ - if (gameTableRefs) db.gameTable, if (playerGroupTableRefs) db.playerGroupTable, if (playerGameTableRefs) db.playerGameTable, ], addJoins: null, getPrefetchedDataCallback: (items) async { return [ - if (gameTableRefs) - await $_getPrefetchedData< - PlayerTableData, - $PlayerTableTable, - GameTableData - >( - currentTable: table, - referencedTable: $$PlayerTableTableReferences - ._gameTableRefsTable(db), - managerFromTypedResult: (p0) => - $$PlayerTableTableReferences( - db, - table, - p0, - ).gameTableRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.winnerId == item.id, - ), - typedResults: items, - ), if (playerGroupTableRefs) await $_getPrefetchedData< PlayerTableData, @@ -1872,7 +1767,6 @@ typedef $$PlayerTableTableProcessedTableManager = (PlayerTableData, $$PlayerTableTableReferences), PlayerTableData, PrefetchHooks Function({ - bool gameTableRefs, bool playerGroupTableRefs, bool playerGameTableRefs, }) @@ -2227,14 +2121,14 @@ typedef $$GameTableTableCreateCompanionBuilder = GameTableCompanion Function({ required String id, required String name, - required String winnerId, + Value winnerId, Value rowid, }); typedef $$GameTableTableUpdateCompanionBuilder = GameTableCompanion Function({ Value id, Value name, - Value winnerId, + Value winnerId, Value rowid, }); @@ -2242,25 +2136,6 @@ final class $$GameTableTableReferences extends BaseReferences<_$AppDatabase, $GameTableTable, GameTableData> { $$GameTableTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $PlayerTableTable _winnerIdTable(_$AppDatabase db) => - db.playerTable.createAlias( - $_aliasNameGenerator(db.gameTable.winnerId, db.playerTable.id), - ); - - $$PlayerTableTableProcessedTableManager get winnerId { - final $_column = $_itemColumn('winner_id')!; - - final manager = $$PlayerTableTableTableManager( - $_db, - $_db.playerTable, - ).filter((f) => f.id.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_winnerIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item]), - ); - } - static MultiTypedResultKey<$PlayerGameTableTable, List> _playerGameTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( db.playerGameTable, @@ -2319,28 +2194,10 @@ class $$GameTableTableFilterComposer builder: (column) => ColumnFilters(column), ); - $$PlayerTableTableFilterComposer get winnerId { - final $$PlayerTableTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.winnerId, - referencedTable: $db.playerTable, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$PlayerTableTableFilterComposer( - $db: $db, - $table: $db.playerTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } + ColumnFilters get winnerId => $composableBuilder( + column: $table.winnerId, + builder: (column) => ColumnFilters(column), + ); Expression playerGameTableRefs( Expression Function($$PlayerGameTableTableFilterComposer f) f, @@ -2412,28 +2269,10 @@ class $$GameTableTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - $$PlayerTableTableOrderingComposer get winnerId { - final $$PlayerTableTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.winnerId, - referencedTable: $db.playerTable, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$PlayerTableTableOrderingComposer( - $db: $db, - $table: $db.playerTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } + ColumnOrderings get winnerId => $composableBuilder( + column: $table.winnerId, + builder: (column) => ColumnOrderings(column), + ); } class $$GameTableTableAnnotationComposer @@ -2451,28 +2290,8 @@ class $$GameTableTableAnnotationComposer GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); - $$PlayerTableTableAnnotationComposer get winnerId { - final $$PlayerTableTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.winnerId, - referencedTable: $db.playerTable, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$PlayerTableTableAnnotationComposer( - $db: $db, - $table: $db.playerTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } + GeneratedColumn get winnerId => + $composableBuilder(column: $table.winnerId, builder: (column) => column); Expression playerGameTableRefs( Expression Function($$PlayerGameTableTableAnnotationComposer a) f, @@ -2539,7 +2358,6 @@ class $$GameTableTableTableManager (GameTableData, $$GameTableTableReferences), GameTableData, PrefetchHooks Function({ - bool winnerId, bool playerGameTableRefs, bool groupGameTableRefs, }) @@ -2559,7 +2377,7 @@ class $$GameTableTableTableManager ({ Value id = const Value.absent(), Value name = const Value.absent(), - Value winnerId = const Value.absent(), + Value winnerId = const Value.absent(), Value rowid = const Value.absent(), }) => GameTableCompanion( id: id, @@ -2571,7 +2389,7 @@ class $$GameTableTableTableManager ({ required String id, required String name, - required String winnerId, + Value winnerId = const Value.absent(), Value rowid = const Value.absent(), }) => GameTableCompanion.insert( id: id, @@ -2588,49 +2406,14 @@ class $$GameTableTableTableManager ) .toList(), prefetchHooksCallback: - ({ - winnerId = false, - playerGameTableRefs = false, - groupGameTableRefs = false, - }) { + ({playerGameTableRefs = false, groupGameTableRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ if (playerGameTableRefs) db.playerGameTable, if (groupGameTableRefs) db.groupGameTable, ], - addJoins: - < - T extends TableManagerState< - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic - > - >(state) { - if (winnerId) { - state = - state.withJoin( - currentTable: table, - currentColumn: table.winnerId, - referencedTable: $$GameTableTableReferences - ._winnerIdTable(db), - referencedColumn: $$GameTableTableReferences - ._winnerIdTable(db) - .id, - ) - as T; - } - - return state; - }, + addJoins: null, getPrefetchedDataCallback: (items) async { return [ if (playerGameTableRefs) @@ -2696,7 +2479,6 @@ typedef $$GameTableTableProcessedTableManager = (GameTableData, $$GameTableTableReferences), GameTableData, PrefetchHooks Function({ - bool winnerId, bool playerGameTableRefs, bool groupGameTableRefs, }) From f6ebda7984414ac429c5e45227da508272070d30 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 00:22:35 +0100 Subject: [PATCH 14/95] Changed table column because of importing issues --- lib/data/db/tables/game_table.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/data/db/tables/game_table.dart b/lib/data/db/tables/game_table.dart index 9651a79..b2dc6ba 100644 --- a/lib/data/db/tables/game_table.dart +++ b/lib/data/db/tables/game_table.dart @@ -1,11 +1,9 @@ import 'package:drift/drift.dart'; -import 'package:game_tracker/data/db/tables/player_table.dart'; class GameTable extends Table { TextColumn get id => text()(); TextColumn get name => text()(); - TextColumn get winnerId => - text().references(PlayerTable, #id, onDelete: KeyAction.cascade)(); + late final winnerId = text().nullable()(); @override Set> get primaryKey => {id}; From 5dcd0826bdd5b221170b4780c7f425be09706247 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 00:23:28 +0100 Subject: [PATCH 15/95] Adjusted attributes to table definition --- lib/data/dao/game_dao.dart | 2 +- lib/data/dao/game_dao.g.dart | 1 - lib/data/dao/group_game_dao.g.dart | 1 - lib/data/dto/game.dart | 11 +++-------- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index a5946f2..7df03b0 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -57,7 +57,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { GameTableCompanion.insert( id: game.id, name: game.name, - winnerId: game.winner, + winnerId: Value(game.winner), ), mode: InsertMode.insertOrReplace, ); diff --git a/lib/data/dao/game_dao.g.dart b/lib/data/dao/game_dao.g.dart index ebf5524..b5a29fe 100644 --- a/lib/data/dao/game_dao.g.dart +++ b/lib/data/dao/game_dao.g.dart @@ -4,6 +4,5 @@ part of 'game_dao.dart'; // ignore_for_file: type=lint mixin _$GameDaoMixin on DatabaseAccessor { - $PlayerTableTable get playerTable => attachedDatabase.playerTable; $GameTableTable get gameTable => attachedDatabase.gameTable; } diff --git a/lib/data/dao/group_game_dao.g.dart b/lib/data/dao/group_game_dao.g.dart index 426f192..735a35f 100644 --- a/lib/data/dao/group_game_dao.g.dart +++ b/lib/data/dao/group_game_dao.g.dart @@ -5,7 +5,6 @@ part of 'group_game_dao.dart'; // ignore_for_file: type=lint mixin _$GroupGameDaoMixin on DatabaseAccessor { $GroupTableTable get groupTable => attachedDatabase.groupTable; - $PlayerTableTable get playerTable => attachedDatabase.playerTable; $GameTableTable get gameTable => attachedDatabase.gameTable; $GroupGameTableTable get groupGameTable => attachedDatabase.groupGameTable; } diff --git a/lib/data/dto/game.dart b/lib/data/dto/game.dart index bec22fb..60bcff0 100644 --- a/lib/data/dto/game.dart +++ b/lib/data/dto/game.dart @@ -7,15 +7,10 @@ class Game { final String name; final List? players; final Group? group; - final String winner; + final String? winner; - Game({ - String? id, - required this.name, - this.players, - this.group, - this.winner = '', - }) : id = id ?? const Uuid().v4(); + Game({String? id, required this.name, this.players, this.group, this.winner}) + : id = id ?? const Uuid().v4(); @override String toString() { From f2a749cb0fe7b43f6b232437f8487c43e07f3e3c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 00:24:08 +0100 Subject: [PATCH 16/95] First version of settings view --- .../views/main_menu/settings_view.dart | 218 +++++++++++++++--- pubspec.yaml | 8 +- 2 files changed, 192 insertions(+), 34 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index b05ae8d..2a1d193 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -1,10 +1,55 @@ -import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; +import 'dart:convert'; +import 'dart:io'; -class SettingsView extends StatelessWidget { +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/game.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; +import 'package:json_schema/json_schema.dart'; +import 'package:provider/provider.dart'; + +class SettingsView extends StatefulWidget { const SettingsView({super.key}); + @override + State createState() => _SettingsViewState(); + + /// Helper method to read file content from either bytes or path + static Future _readFileContent(PlatformFile file) async { + if (file.bytes != null) return utf8.decode(file.bytes!); + if (file.path != null) return await File(file.path!).readAsString(); + + throw Exception('Die Datei hat keinen lesbaren Inhalt'); + } + + static Future validateJsonSchema(String jsonString) async { + final String schemaString; + + schemaString = await rootBundle.loadString('assets/schema.json'); + + try { + final schema = JsonSchema.create(json.decode(schemaString)); + final jsonData = json.decode(jsonString); + final result = schema.validate(jsonData); + + if (result.isValid) { + return true; + } + return false; + } catch (e, stack) { + print('[validateJsonSchema] $e'); + print(stack); + return false; + } + } +} + +class _SettingsViewState extends State { @override Widget build(BuildContext context) { return Scaffold( @@ -41,39 +86,31 @@ class SettingsView extends StatelessWidget { ), SettingsListTile( - title: 'Export Data', + title: 'Export data', icon: Icons.upload_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => exportData(), + onPressed: () async { + final String json = await _getAppDataAsJson(context); + await exportData(json, 'export'); + }, ), SettingsListTile( - title: 'Import Data', + title: 'Import data', icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => importData(), - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10), - child: Text( - textAlign: TextAlign.start, - 'Example Headline', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), + onPressed: () => importData(context), ), SettingsListTile( - title: 'Example Tile', + title: 'Delete all data', + icon: Icons.download_outlined, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () => deleteAllData(context), + ), + SettingsListTile( + title: 'Add Sample Data', icon: Icons.upload_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => print('Example Tile'), - ), - SettingsListTile( - title: 'Example Tile', - icon: Icons.download_outlined, - suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => print('Example Tile'), + onPressed: () => addSampleData(context), ), ], ), @@ -82,9 +119,128 @@ class SettingsView extends StatelessWidget { ); } - // TODO: Implement export functionality - void exportData() {} + Future deleteAllData(BuildContext context) async { + final db = Provider.of(context, listen: false); + await db.gameDao.deleteAllGames(); + await db.groupDao.deleteAllGroups(); + await db.playerDao.deleteAllPlayers(); + print('[deleteAllData] All data deleted'); + } - // TODO: Implement import functionality - void importData() {} + Future addSampleData(BuildContext context) async { + final db = Provider.of(context, listen: false); + + final player1 = Player(name: 'Alice'); + final player2 = Player(name: 'Bob'); + final group = Group(name: 'Friends', members: [player1, player2]); + final game = Game(name: 'Sample Game', group: group, winner: 'Alice'); + + await db.playerDao.addPlayer(player: player1); + await db.playerDao.addPlayer(player: player2); + await db.groupDao.addGroup(group: group); + await db.gameDao.addGame(game: game); + } + + Future _getAppDataAsJson(BuildContext context) async { + final db = Provider.of(context, listen: false); + final games = await db.gameDao.getAllGames(); + final groups = await db.groupDao.getAllGroups(); + final players = await db.playerDao.getAllPlayers(); + + // Construct a JSON representation of the data + final Map jsonMap = { + 'games': games.map((game) => game.toJson()).toList(), + 'groups': groups.map((group) => group.toJson()).toList(), + 'players': players.map((player) => player.toJson()).toList(), + }; + + return json.encode(jsonMap); + } + + Future exportData(String jsonString, String fileName) async { + try { + final bytes = Uint8List.fromList(utf8.encode(jsonString)); + await FilePicker.platform.saveFile( + fileName: '$fileName.json', + bytes: bytes, + ); + return true; + } catch (e, stack) { + print('[exportData] $e'); + print(stack); + return false; + } + } + + Future importData(BuildContext context) async { + final db = Provider.of(context, listen: false); + + final path = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + ); + + if (path == null) { + print('[importData] No file selected'); + return; + } + + try { + final jsonString = await SettingsView._readFileContent(path.files.single); + + // Checks if the JSON String is in the gameList format + if (await SettingsView.validateJsonSchema(jsonString)) { + final Map jsonData = + json.decode(jsonString) as Map; + print('[importData] : $jsonData'); + final List? gamesJson = jsonData['games'] as List?; + final List? groupsJson = jsonData['groups'] as List?; + final List? playersJson = + jsonData['players'] as List?; + + final List importedGames = + gamesJson + ?.map((g) => Game.fromJson(g as Map)) + .toList() ?? + []; + final List importedGroups = + groupsJson + ?.map((g) => Group.fromJson(g as Map)) + .toList() ?? + []; + final List importedPlayers = + playersJson + ?.map((p) => Player.fromJson(p as Map)) + .toList() ?? + []; + + for (Player player in importedPlayers) { + await db.playerDao.addPlayer(player: player); + } + + for (Group group in importedGroups) { + await db.groupDao.addGroup(group: group); + } + + for (Game game in importedGames) { + await db.gameDao.addGame(game: game); + } + } else { + print('[importData] Invalid JSON schema'); + return; + } + print('[importData] Data imported successfully'); + return; + } on FormatException catch (e, stack) { + print('[importData] FormatException'); + print('[importData] $e'); + print(stack); + return; + } on Exception catch (e, stack) { + print('[importData] Exception'); + print('[importData] $e'); + print(stack); + return; + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index c7e55d7..bce31f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,9 @@ dependencies: provider: ^6.1.5 skeletonizer: ^2.1.0+1 uuid: ^4.5.2 + file_picker: ^10.3.6 + json_schema: ^5.2.2 + file_saver: ^0.3.1 dev_dependencies: flutter_test: @@ -30,6 +33,5 @@ dev_dependencies: flutter: uses-material-design: true - -assets: - - assets/schema.json + assets: + - assets/schema.json From 82e28b7509a29bc23dd59f8a01a0bc4a156f1ea8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 00:32:16 +0100 Subject: [PATCH 17/95] Refactored whole export/import methods in DataTransferService --- .../views/main_menu/settings_view.dart | 160 +----------------- lib/services/data_transfer_service.dart | 135 +++++++++++++++ 2 files changed, 144 insertions(+), 151 deletions(-) create mode 100644 lib/services/data_transfer_service.dart diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index 2a1d193..f0a530b 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -1,17 +1,11 @@ import 'dart:convert'; -import 'dart:io'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/game.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; +import 'package:game_tracker/services/data_transfer_service.dart'; import 'package:json_schema/json_schema.dart'; -import 'package:provider/provider.dart'; class SettingsView extends StatefulWidget { const SettingsView({super.key}); @@ -19,14 +13,6 @@ class SettingsView extends StatefulWidget { @override State createState() => _SettingsViewState(); - /// Helper method to read file content from either bytes or path - static Future _readFileContent(PlatformFile file) async { - if (file.bytes != null) return utf8.decode(file.bytes!); - if (file.path != null) return await File(file.path!).readAsString(); - - throw Exception('Die Datei hat keinen lesbaren Inhalt'); - } - static Future validateJsonSchema(String jsonString) async { final String schemaString; @@ -84,33 +70,30 @@ class _SettingsViewState extends State { ), ), ), - SettingsListTile( title: 'Export data', icon: Icons.upload_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), onPressed: () async { - final String json = await _getAppDataAsJson(context); - await exportData(json, 'export'); + final String json = + await DataTransferService.getAppDataAsJson(context); + await DataTransferService.exportData( + json, + 'exported_data', + ); }, ), SettingsListTile( title: 'Import data', icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => importData(context), + onPressed: () => DataTransferService.importData(context), ), SettingsListTile( title: 'Delete all data', icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => deleteAllData(context), - ), - SettingsListTile( - title: 'Add Sample Data', - icon: Icons.upload_outlined, - suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => addSampleData(context), + onPressed: () => DataTransferService.deleteAllData(context), ), ], ), @@ -118,129 +101,4 @@ class _SettingsViewState extends State { ), ); } - - Future deleteAllData(BuildContext context) async { - final db = Provider.of(context, listen: false); - await db.gameDao.deleteAllGames(); - await db.groupDao.deleteAllGroups(); - await db.playerDao.deleteAllPlayers(); - print('[deleteAllData] All data deleted'); - } - - Future addSampleData(BuildContext context) async { - final db = Provider.of(context, listen: false); - - final player1 = Player(name: 'Alice'); - final player2 = Player(name: 'Bob'); - final group = Group(name: 'Friends', members: [player1, player2]); - final game = Game(name: 'Sample Game', group: group, winner: 'Alice'); - - await db.playerDao.addPlayer(player: player1); - await db.playerDao.addPlayer(player: player2); - await db.groupDao.addGroup(group: group); - await db.gameDao.addGame(game: game); - } - - Future _getAppDataAsJson(BuildContext context) async { - final db = Provider.of(context, listen: false); - final games = await db.gameDao.getAllGames(); - final groups = await db.groupDao.getAllGroups(); - final players = await db.playerDao.getAllPlayers(); - - // Construct a JSON representation of the data - final Map jsonMap = { - 'games': games.map((game) => game.toJson()).toList(), - 'groups': groups.map((group) => group.toJson()).toList(), - 'players': players.map((player) => player.toJson()).toList(), - }; - - return json.encode(jsonMap); - } - - Future exportData(String jsonString, String fileName) async { - try { - final bytes = Uint8List.fromList(utf8.encode(jsonString)); - await FilePicker.platform.saveFile( - fileName: '$fileName.json', - bytes: bytes, - ); - return true; - } catch (e, stack) { - print('[exportData] $e'); - print(stack); - return false; - } - } - - Future importData(BuildContext context) async { - final db = Provider.of(context, listen: false); - - final path = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['json'], - ); - - if (path == null) { - print('[importData] No file selected'); - return; - } - - try { - final jsonString = await SettingsView._readFileContent(path.files.single); - - // Checks if the JSON String is in the gameList format - if (await SettingsView.validateJsonSchema(jsonString)) { - final Map jsonData = - json.decode(jsonString) as Map; - print('[importData] : $jsonData'); - final List? gamesJson = jsonData['games'] as List?; - final List? groupsJson = jsonData['groups'] as List?; - final List? playersJson = - jsonData['players'] as List?; - - final List importedGames = - gamesJson - ?.map((g) => Game.fromJson(g as Map)) - .toList() ?? - []; - final List importedGroups = - groupsJson - ?.map((g) => Group.fromJson(g as Map)) - .toList() ?? - []; - final List importedPlayers = - playersJson - ?.map((p) => Player.fromJson(p as Map)) - .toList() ?? - []; - - for (Player player in importedPlayers) { - await db.playerDao.addPlayer(player: player); - } - - for (Group group in importedGroups) { - await db.groupDao.addGroup(group: group); - } - - for (Game game in importedGames) { - await db.gameDao.addGame(game: game); - } - } else { - print('[importData] Invalid JSON schema'); - return; - } - print('[importData] Data imported successfully'); - return; - } on FormatException catch (e, stack) { - print('[importData] FormatException'); - print('[importData] $e'); - print(stack); - return; - } on Exception catch (e, stack) { - print('[importData] Exception'); - print('[importData] $e'); - print(stack); - return; - } - } } diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart new file mode 100644 index 0000000..839c0e0 --- /dev/null +++ b/lib/services/data_transfer_service.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/game.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/presentation/views/main_menu/settings_view.dart'; +import 'package:provider/provider.dart'; + +class DataTransferService { + /// Deletes all data from the database. + static Future deleteAllData(BuildContext context) async { + final db = Provider.of(context, listen: false); + await db.gameDao.deleteAllGames(); + await db.groupDao.deleteAllGroups(); + await db.playerDao.deleteAllPlayers(); + print('[deleteAllData] All data deleted'); + } + + static Future getAppDataAsJson(BuildContext context) async { + final db = Provider.of(context, listen: false); + final games = await db.gameDao.getAllGames(); + final groups = await db.groupDao.getAllGroups(); + final players = await db.playerDao.getAllPlayers(); + + // Construct a JSON representation of the data + final Map jsonMap = { + 'games': games.map((game) => game.toJson()).toList(), + 'groups': groups.map((group) => group.toJson()).toList(), + 'players': players.map((player) => player.toJson()).toList(), + }; + + return json.encode(jsonMap); + } + + /// Exports the given JSON string to a file with the specified name. + static Future exportData(String jsonString, String fileName) async { + try { + final bytes = Uint8List.fromList(utf8.encode(jsonString)); + await FilePicker.platform.saveFile( + fileName: '$fileName.json', + bytes: bytes, + ); + return true; + } catch (e, stack) { + print('[exportData] $e'); + print(stack); + return false; + } + } + + /// Imports data from a selected JSON file into the database. + static Future importData(BuildContext context) async { + final db = Provider.of(context, listen: false); + + final path = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + ); + + if (path == null) { + print('[importData] No file selected'); + return; + } + + try { + final jsonString = await _readFileContent(path.files.single); + + if (await SettingsView.validateJsonSchema(jsonString)) { + final Map jsonData = + json.decode(jsonString) as Map; + + final List? gamesJson = jsonData['games'] as List?; + final List? groupsJson = jsonData['groups'] as List?; + final List? playersJson = + jsonData['players'] as List?; + + final List importedGames = + gamesJson + ?.map((g) => Game.fromJson(g as Map)) + .toList() ?? + []; + final List importedGroups = + groupsJson + ?.map((g) => Group.fromJson(g as Map)) + .toList() ?? + []; + final List importedPlayers = + playersJson + ?.map((p) => Player.fromJson(p as Map)) + .toList() ?? + []; + + for (Player player in importedPlayers) { + await db.playerDao.addPlayer(player: player); + } + + for (Group group in importedGroups) { + await db.groupDao.addGroup(group: group); + } + + for (Game game in importedGames) { + await db.gameDao.addGame(game: game); + } + } else { + print('[importData] Invalid JSON schema'); + return; + } + print('[importData] Data imported successfully'); + return; + } on FormatException catch (e, stack) { + print('[importData] FormatException'); + print('[importData] $e'); + print(stack); + return; + } on Exception catch (e, stack) { + print('[importData] Exception'); + print('[importData] $e'); + print(stack); + return; + } + } + + /// Helper method to read file content from either bytes or path + static Future _readFileContent(PlatformFile file) async { + if (file.bytes != null) return utf8.decode(file.bytes!); + if (file.path != null) return await File(file.path!).readAsString(); + + throw Exception('Die Datei hat keinen lesbaren Inhalt'); + } +} From 322c51a7646da556e4c8a92924c5aa9ad237d872 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 09:44:48 +0100 Subject: [PATCH 18/95] Added documentation and feedback in snackbar --- .../views/main_menu/settings_view.dart | 97 ++++++++++++++++++- lib/services/data_transfer_service.dart | 56 +++++++---- 2 files changed, 133 insertions(+), 20 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index f0a530b..1a762ce 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -77,23 +77,37 @@ class _SettingsViewState extends State { onPressed: () async { final String json = await DataTransferService.getAppDataAsJson(context); - await DataTransferService.exportData( + final result = await DataTransferService.exportData( json, 'exported_data', ); + if (!context.mounted) return; + showExportSnackBar(context: context, result: result); }, ), SettingsListTile( title: 'Import data', icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => DataTransferService.importData(context), + onPressed: () async { + final result = await DataTransferService.importData( + context, + ); + if (!context.mounted) return; + showImportSnackBar(context: context, result: result); + }, ), SettingsListTile( title: 'Delete all data', icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => DataTransferService.deleteAllData(context), + onPressed: () { + DataTransferService.deleteAllData(context); + showSnackbar( + context: context, + message: 'Data successfully deleted', + ); + }, ), ], ), @@ -101,4 +115,81 @@ class _SettingsViewState extends State { ), ); } + + /// Displays a snackbar based on the import result. + /// + /// [context] The BuildContext to show the snackbar in. + /// [result] The result of the import operation. + void showImportSnackBar({ + required BuildContext context, + required ImportResult result, + }) { + switch (result) { + case ImportResult.success: + showSnackbar(context: context, message: 'Data successfully imported'); + case ImportResult.invalidSchema: + showSnackbar(context: context, message: 'Invalid Schema'); + case ImportResult.fileReadError: + showSnackbar(context: context, message: 'Error reading file'); + case ImportResult.canceled: + showSnackbar(context: context, message: 'Import canceled'); + case ImportResult.formatException: + showSnackbar( + context: context, + message: 'Format Exception (see console)', + ); + case ImportResult.unknownException: + showSnackbar( + context: context, + message: 'Unknown Exception (see console)', + ); + } + } + + /// Displays a snackbar based on the export result. + /// + /// [context] The BuildContext to show the snackbar in. + /// [result] The result of the export operation. + void showExportSnackBar({ + required BuildContext context, + required ExportResult result, + }) { + switch (result) { + case ExportResult.success: + showSnackbar(context: context, message: 'Data successfully exported'); + case ExportResult.canceled: + showSnackbar(context: context, message: 'Export canceled'); + case ExportResult.unknownException: + showSnackbar( + context: context, + message: 'Unknown Exception (see console)', + ); + } + } + + /// Displays a snackbar with the given message and optional action. + /// + /// [context] The BuildContext to show the snackbar in. + /// [message] The message to display in the snackbar. + /// [duration] The duration for which the snackbar is displayed. + /// [action] An optional callback function to execute when the action button is pressed. + void showSnackbar({ + required BuildContext context, + required String message, + Duration duration = const Duration(seconds: 3), + VoidCallback? action, + }) { + final messenger = ScaffoldMessenger.of(context); + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message, style: const TextStyle(color: Colors.white)), + backgroundColor: CustomTheme.onBoxColor, + duration: duration, + action: action != null + ? SnackBarAction(label: 'RĂ¼ckgängig', onPressed: action) + : null, + ), + ); + } } diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 839c0e0..6eba1ee 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -11,6 +11,17 @@ import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view.dart'; import 'package:provider/provider.dart'; +enum ImportResult { + success, + canceled, + fileReadError, + invalidSchema, + formatException, + unknownException, +} + +enum ExportResult { success, canceled, unknownException } + class DataTransferService { /// Deletes all data from the database. static Future deleteAllData(BuildContext context) async { @@ -18,9 +29,10 @@ class DataTransferService { await db.gameDao.deleteAllGames(); await db.groupDao.deleteAllGroups(); await db.playerDao.deleteAllPlayers(); - print('[deleteAllData] All data deleted'); } + /// Retrieves all application data and converts it to a JSON string. + /// Returns the JSON string representation of the data. static Future getAppDataAsJson(BuildContext context) async { final db = Provider.of(context, listen: false); final games = await db.gameDao.getAllGames(); @@ -38,23 +50,34 @@ class DataTransferService { } /// Exports the given JSON string to a file with the specified name. - static Future exportData(String jsonString, String fileName) async { + /// Returns an [ExportResult] indicating the outcome. + /// + /// [jsonString] The JSON string to be exported. + /// [fileName] The desired name for the exported file (without extension). + static Future exportData( + String jsonString, + String fileName, + ) async { try { final bytes = Uint8List.fromList(utf8.encode(jsonString)); - await FilePicker.platform.saveFile( + final path = await FilePicker.platform.saveFile( fileName: '$fileName.json', bytes: bytes, ); - return true; + if (path == null) { + return ExportResult.canceled; + } else { + return ExportResult.success; + } } catch (e, stack) { print('[exportData] $e'); print(stack); - return false; + return ExportResult.unknownException; } } /// Imports data from a selected JSON file into the database. - static Future importData(BuildContext context) async { + static Future importData(BuildContext context) async { final db = Provider.of(context, listen: false); final path = await FilePicker.platform.pickFiles( @@ -63,12 +86,14 @@ class DataTransferService { ); if (path == null) { - print('[importData] No file selected'); - return; + return ImportResult.canceled; } try { final jsonString = await _readFileContent(path.files.single); + if (jsonString == null) { + return ImportResult.fileReadError; + } if (await SettingsView.validateJsonSchema(jsonString)) { final Map jsonData = @@ -107,29 +132,26 @@ class DataTransferService { await db.gameDao.addGame(game: game); } } else { - print('[importData] Invalid JSON schema'); - return; + return ImportResult.invalidSchema; } - print('[importData] Data imported successfully'); - return; + return ImportResult.success; } on FormatException catch (e, stack) { print('[importData] FormatException'); print('[importData] $e'); print(stack); - return; + return ImportResult.formatException; } on Exception catch (e, stack) { print('[importData] Exception'); print('[importData] $e'); print(stack); - return; + return ImportResult.unknownException; } } /// Helper method to read file content from either bytes or path - static Future _readFileContent(PlatformFile file) async { + static Future _readFileContent(PlatformFile file) async { if (file.bytes != null) return utf8.decode(file.bytes!); if (file.path != null) return await File(file.path!).readAsString(); - - throw Exception('Die Datei hat keinen lesbaren Inhalt'); + return null; } } From 87b1a7d57f547db37ad2ff868e3476d862605672 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 19:26:35 +0100 Subject: [PATCH 19/95] Moved ImportStatus & ExportStatus to enums.dart --- lib/core/enums.dart | 22 +++++++++++++++++++ .../views/main_menu/settings_view.dart | 1 + lib/services/data_transfer_service.dart | 12 +--------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 320eaf7..8c809b0 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -1,2 +1,24 @@ /// Button types used for styling the [CustomWidthButton] enum ButtonType { primary, secondary, tertiary } + +/// Result types for import operations in the [SettingsView] +/// - [ImportResult.success]: The import operation was successful. +/// - [ImportResult.canceled]: The import operation was canceled by the user. +/// - [ImportResult.fileReadError]: There was an error reading the selected file. +/// - [ImportResult.invalidSchema]: The JSON schema of the imported data is invalid. +/// - [ImportResult.formatException]: A format exception occurred during import. +/// - [ImportResult.unknownException]: An exception occurred during import. +enum ImportResult { + success, + canceled, + fileReadError, + invalidSchema, + formatException, + unknownException, +} + +/// Result types for export operations in the [SettingsView] +/// - [ExportResult.success]: The export operation was successful. +/// - [ExportResult.canceled]: The export operation was canceled by the user. +/// - [ExportResult.unknownException]: An exception occurred during export. +enum ExportResult { success, canceled, unknownException } diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index 1a762ce..e679467 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; import 'package:game_tracker/services/data_transfer_service.dart'; import 'package:json_schema/json_schema.dart'; diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 6eba1ee..93c788d 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/game.dart'; import 'package:game_tracker/data/dto/group.dart'; @@ -11,17 +12,6 @@ import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view.dart'; import 'package:provider/provider.dart'; -enum ImportResult { - success, - canceled, - fileReadError, - invalidSchema, - formatException, - unknownException, -} - -enum ExportResult { success, canceled, unknownException } - class DataTransferService { /// Deletes all data from the database. static Future deleteAllData(BuildContext context) async { From 9434282ed194b089c1c075ea5410c5c549301728 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 20:20:21 +0100 Subject: [PATCH 20/95] Added createdAt attribute in dto classes and json schema --- assets/schema.json | 45 ++++++++++++---------------------------- lib/data/dto/game.dart | 4 +++- lib/data/dto/group.dart | 4 +++- lib/data/dto/player.dart | 9 ++++++-- 4 files changed, 26 insertions(+), 36 deletions(-) diff --git a/assets/schema.json b/assets/schema.json index c33fab2..eedfb05 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -13,6 +13,9 @@ "id": { "type": "string" }, + "createdAt": { + "type": "string" + }, "name": { "type": "string" }, @@ -28,6 +31,7 @@ }, "required": [ "id", + "createdAt", "name", "players", "group", @@ -45,6 +49,9 @@ "id": { "type": "string" }, + "createdAt": { + "type": "string" + }, "name": { "type": "string" }, @@ -57,19 +64,7 @@ "id": { "type": "string" }, - "name": { - "type": "string" - } - }, - "required": [ - "id", - "name" - ] - }, - { - "type": "object", - "properties": { - "id": { + "createdAt": { "type": "string" }, "name": { @@ -78,6 +73,7 @@ }, "required": [ "id", + "createdAt", "name" ] } @@ -86,6 +82,7 @@ }, "required": [ "id", + "createdAt", "name", "members" ] @@ -101,19 +98,7 @@ "id": { "type": "string" }, - "name": { - "type": "string" - } - }, - "required": [ - "id", - "name" - ] - }, - { - "type": "object", - "properties": { - "id": { + "createdAt": { "type": "string" }, "name": { @@ -122,16 +107,12 @@ }, "required": [ "id", + "createdAt", "name" ] } ] } - }, - "required": [ - "games", - "groups", - "players" - ] + } } diff --git a/lib/data/dto/game.dart b/lib/data/dto/game.dart index 30898e5..4188bc4 100644 --- a/lib/data/dto/game.dart +++ b/lib/data/dto/game.dart @@ -5,11 +5,11 @@ import 'package:uuid/uuid.dart'; class Game { final String id; + final DateTime createdAt; final String name; final List? players; final Group? group; final String? winner; - final DateTime createdAt; Game({ String? id, @@ -30,6 +30,7 @@ class Game { Game.fromJson(Map json) : id = json['id'], name = json['name'], + createdAt = DateTime.parse(json['createdAt']), players = json['players'] != null ? (json['players'] as List) .map((playerJson) => Player.fromJson(playerJson)) @@ -41,6 +42,7 @@ class Game { /// Converts the Game instance to a JSON object. Map toJson() => { 'id': id, + 'createdAt': createdAt.toIso8601String(), 'name': name, 'players': players?.map((player) => player.toJson()).toList(), 'group': group?.toJson(), diff --git a/lib/data/dto/group.dart b/lib/data/dto/group.dart index 3f00bf5..92dbd09 100644 --- a/lib/data/dto/group.dart +++ b/lib/data/dto/group.dart @@ -4,9 +4,9 @@ import 'package:uuid/uuid.dart'; class Group { final String id; + final DateTime createdAt; final String name; final List members; - final DateTime createdAt; Group({ String? id, @@ -24,6 +24,7 @@ class Group { /// Creates a Group instance from a JSON object. Group.fromJson(Map json) : id = json['id'], + createdAt = DateTime.parse(json['createdAt']), name = json['name'], members = (json['members'] as List) .map((memberJson) => Player.fromJson(memberJson)) @@ -32,6 +33,7 @@ class Group { /// Converts the Group instance to a JSON object. Map toJson() => { 'id': id, + 'createdAt': createdAt.toIso8601String(), 'name': name, 'members': members.map((member) => member.toJson()).toList(), }; diff --git a/lib/data/dto/player.dart b/lib/data/dto/player.dart index f7e05d2..cfb4f4b 100644 --- a/lib/data/dto/player.dart +++ b/lib/data/dto/player.dart @@ -3,8 +3,8 @@ import 'package:uuid/uuid.dart'; class Player { final String id; - final String name; final DateTime createdAt; + final String name; Player({String? id, DateTime? createdAt, required this.name}) : id = id ?? const Uuid().v4(), @@ -18,8 +18,13 @@ class Player { /// Creates a Player instance from a JSON object. Player.fromJson(Map json) : id = json['id'], + createdAt = DateTime.parse(json['createdAt']), name = json['name']; /// Converts the Player instance to a JSON object. - Map toJson() => {'id': id, 'name': name}; + Map toJson() => { + 'id': id, + 'createdAt': createdAt.toIso8601String(), + 'name': name, + }; } From f7073a83a42c334a404e1ed21bf69d717f3d2345 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 20:21:30 +0100 Subject: [PATCH 21/95] Added insert mode --- lib/data/dao/player_game_dao.dart | 1 + lib/data/dao/player_group_dao.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/data/dao/player_game_dao.dart b/lib/data/dao/player_game_dao.dart index 8f367f8..d58417e 100644 --- a/lib/data/dao/player_game_dao.dart +++ b/lib/data/dao/player_game_dao.dart @@ -46,6 +46,7 @@ class PlayerGameDao extends DatabaseAccessor }) async { await into(playerGameTable).insert( PlayerGameTableCompanion.insert(playerId: playerId, gameId: gameId), + mode: InsertMode.insertOrReplace, ); } } diff --git a/lib/data/dao/player_group_dao.dart b/lib/data/dao/player_group_dao.dart index fe067ae..e200958 100644 --- a/lib/data/dao/player_group_dao.dart +++ b/lib/data/dao/player_group_dao.dart @@ -56,6 +56,7 @@ class PlayerGroupDao extends DatabaseAccessor await into(playerGroupTable).insert( PlayerGroupTableCompanion.insert(playerId: player.id, groupId: groupId), + mode: InsertMode.insertOrReplace, ); return true; From 412cfff9f53bebe9ea2df6a453c1c3687b440f2d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 20:21:55 +0100 Subject: [PATCH 22/95] Added methods for inserting list of players, groups, games --- lib/data/dao/game_dao.dart | 116 +++++++++- lib/data/dao/group_dao.dart | 50 +++- lib/data/dao/player_dao.dart | 24 ++ lib/data/db/database.g.dart | 293 +++--------------------- lib/services/data_transfer_service.dart | 14 +- 5 files changed, 220 insertions(+), 277 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index 9ed9849..018866a 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -50,14 +50,6 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /// Also adds associated players and group if they exist. Future addGame({required Game game}) async { await db.transaction(() async { - for (final p in game.players ?? []) { - await db.playerDao.addPlayer(player: p); - await db.playerGameDao.addPlayerToGame(gameId: game.id, playerId: p.id); - } - if (game.group != null) { - await db.groupDao.addGroup(group: game.group!); - await db.groupGameDao.addGroupToGame(game.id, game.group!.id); - } await into(gameTable).insert( GameTableCompanion.insert( id: game.id, @@ -67,6 +59,114 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { ), mode: InsertMode.insertOrReplace, ); + + if (game.players != null) { + await db.playerDao.addPlayers(players: game.players!); + for (final p in game.players ?? []) { + await db.playerGameDao.addPlayerToGame( + gameId: game.id, + playerId: p.id, + ); + } + } + + if (game.group != null) { + await db.groupDao.addGroup(group: game.group!); + await db.groupGameDao.addGroupToGame(game.id, game.group!.id); + } + }); + } + + Future addGames({required List games}) async { + if (games.isEmpty) return; + await db.transaction(() async { + // Add all games in batch + await db.batch( + (b) => b.insertAll( + gameTable, + games + .map( + (game) => GameTableCompanion.insert( + id: game.id, + name: game.name, + createdAt: game.createdAt, + winnerId: Value(game.winner), + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + // Add all groups of the games in batch + await db.batch( + (b) => b.insertAll( + db.groupTable, + games + .where((game) => game.group != null) + .map( + (game) => GroupTableCompanion.insert( + id: game.group!.id, + name: game.group!.name, + createdAt: game.group!.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + + // Add all players of the games in batch + await db.batch((b) async { + for (final game in games) { + if (game.players != null) { + for (final p in game.players ?? []) { + b.insert( + db.playerGameTable, + PlayerGameTableCompanion.insert( + gameId: game.id, + playerId: p.id, + ), + mode: InsertMode.insertOrReplace, + ); + } + } + } + }); + + // Add all group-game associations in batch + await db.batch((b) async { + for (final game in games) { + if (game.group != null) { + b.insert( + db.groupGameTable, + GroupGameTableCompanion.insert( + gameId: game.id, + groupId: game.group!.id, + ), + mode: InsertMode.insertOrReplace, + ); + } + } + }); + + // Add all player-game associations in batch + await db.batch((b) async { + for (final game in games) { + if (game.players != null) { + for (final p in game.players ?? []) { + b.insert( + db.playerTable, + PlayerTableCompanion.insert( + id: p.id, + name: p.name, + createdAt: p.createdAt, + ), + mode: InsertMode.insertOrReplace, + ); + } + } + } + }); }); } diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 6eaea09..695d78a 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -57,6 +57,10 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { name: group.name, createdAt: group.createdAt, ), + mode: InsertMode.insertOrReplace, + ); + await Future.wait( + group.members.map((player) => db.playerDao.addPlayer(player: player)), ); await db.batch( (b) => b.insertAll( @@ -69,17 +73,57 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { ), ) .toList(), + mode: InsertMode.insertOrReplace, ), ); - await Future.wait( - group.members.map((player) => db.playerDao.addPlayer(player: player)), - ); }); return true; } return false; } + /// Adds multiple groups to the database. + /// Also adds the group's members to the [PlayerGroupTable]. + Future addGroups({required List groups}) async { + if (groups.isEmpty) return; + await db.transaction(() async { + await db.batch( + (b) => b.insertAll( + groupTable, + groups + .map( + (group) => GroupTableCompanion.insert( + id: group.id, + name: group.name, + createdAt: group.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + + for (final group in groups) { + await db.playerDao.addPlayers(players: group.members); + + await db.batch( + (b) => b.insertAll( + db.playerGroupTable, + group.members + .map( + (member) => PlayerGroupTableCompanion.insert( + playerId: member.id, + groupId: group.id, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + } + }); + } + /// Deletes the group with the given [id] from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteGroup({required String groupId}) async { diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 353819c..53e251f 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -42,12 +42,36 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { name: player.name, createdAt: player.createdAt, ), + mode: InsertMode.insertOrReplace, ); return true; } return false; } + /// Adds multiple [players] to the database in a batch operation. + Future addPlayers({required List players}) async { + if (players.isEmpty) return false; + + await db.batch( + (b) => b.insertAll( + playerTable, + players + .map( + (player) => PlayerTableCompanion.insert( + id: player.id, + name: player.name, + createdAt: player.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + + return true; + } + /// Deletes the player with the given [id] from the database. /// Returns `true` if the player was deleted, `false` if the player did not exist. Future deletePlayer({required String playerId}) async { diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index 3f10169..f211d0c 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -552,12 +552,9 @@ class $GameTableTable extends GameTable late final GeneratedColumn winnerId = GeneratedColumn( 'winner_id', aliasedName, - false, + true, type: DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES player_table (id) ON DELETE CASCADE', - ), + requiredDuringInsert: false, ); static const VerificationMeta _createdAtMeta = const VerificationMeta( 'createdAt', @@ -602,8 +599,6 @@ class $GameTableTable extends GameTable _winnerIdMeta, winnerId.isAcceptableOrUnknown(data['winner_id']!, _winnerIdMeta), ); - } else if (isInserting) { - context.missing(_winnerIdMeta); } if (data.containsKey('created_at')) { context.handle( @@ -633,7 +628,7 @@ class $GameTableTable extends GameTable winnerId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}winner_id'], - )!, + ), createdAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}created_at'], @@ -650,12 +645,12 @@ class $GameTableTable extends GameTable class GameTableData extends DataClass implements Insertable { final String id; final String name; - final String winnerId; + final String? winnerId; final DateTime createdAt; const GameTableData({ required this.id, required this.name, - required this.winnerId, + this.winnerId, required this.createdAt, }); @override @@ -663,7 +658,9 @@ class GameTableData extends DataClass implements Insertable { final map = {}; map['id'] = Variable(id); map['name'] = Variable(name); - map['winner_id'] = Variable(winnerId); + if (!nullToAbsent || winnerId != null) { + map['winner_id'] = Variable(winnerId); + } map['created_at'] = Variable(createdAt); return map; } @@ -672,7 +669,9 @@ class GameTableData extends DataClass implements Insertable { return GameTableCompanion( id: Value(id), name: Value(name), - winnerId: Value(winnerId), + winnerId: winnerId == null && nullToAbsent + ? const Value.absent() + : Value(winnerId), createdAt: Value(createdAt), ); } @@ -685,7 +684,7 @@ class GameTableData extends DataClass implements Insertable { return GameTableData( id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), - winnerId: serializer.fromJson(json['winnerId']), + winnerId: serializer.fromJson(json['winnerId']), createdAt: serializer.fromJson(json['createdAt']), ); } @@ -695,7 +694,7 @@ class GameTableData extends DataClass implements Insertable { return { 'id': serializer.toJson(id), 'name': serializer.toJson(name), - 'winnerId': serializer.toJson(winnerId), + 'winnerId': serializer.toJson(winnerId), 'createdAt': serializer.toJson(createdAt), }; } @@ -703,12 +702,12 @@ class GameTableData extends DataClass implements Insertable { GameTableData copyWith({ String? id, String? name, - String? winnerId, + Value winnerId = const Value.absent(), DateTime? createdAt, }) => GameTableData( id: id ?? this.id, name: name ?? this.name, - winnerId: winnerId ?? this.winnerId, + winnerId: winnerId.present ? winnerId.value : this.winnerId, createdAt: createdAt ?? this.createdAt, ); GameTableData copyWithCompanion(GameTableCompanion data) { @@ -746,7 +745,7 @@ class GameTableData extends DataClass implements Insertable { class GameTableCompanion extends UpdateCompanion { final Value id; final Value name; - final Value winnerId; + final Value winnerId; final Value createdAt; final Value rowid; const GameTableCompanion({ @@ -759,12 +758,11 @@ class GameTableCompanion extends UpdateCompanion { GameTableCompanion.insert({ required String id, required String name, - required String winnerId, + this.winnerId = const Value.absent(), required DateTime createdAt, this.rowid = const Value.absent(), }) : id = Value(id), name = Value(name), - winnerId = Value(winnerId), createdAt = Value(createdAt); static Insertable custom({ Expression? id, @@ -785,7 +783,7 @@ class GameTableCompanion extends UpdateCompanion { GameTableCompanion copyWith({ Value? id, Value? name, - Value? winnerId, + Value? winnerId, Value? createdAt, Value? rowid, }) { @@ -1538,13 +1536,6 @@ abstract class _$AppDatabase extends GeneratedDatabase { ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ - WritePropagation( - on: TableUpdateQuery.onTableName( - 'player_table', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('game_table', kind: UpdateKind.delete)], - ), WritePropagation( on: TableUpdateQuery.onTableName( 'player_table', @@ -1609,24 +1600,6 @@ final class $$PlayerTableTableReferences extends BaseReferences<_$AppDatabase, $PlayerTableTable, PlayerTableData> { $$PlayerTableTableReferences(super.$_db, super.$_table, super.$_typedResult); - static MultiTypedResultKey<$GameTableTable, List> - _gameTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( - db.gameTable, - aliasName: $_aliasNameGenerator(db.playerTable.id, db.gameTable.winnerId), - ); - - $$GameTableTableProcessedTableManager get gameTableRefs { - final manager = $$GameTableTableTableManager( - $_db, - $_db.gameTable, - ).filter((f) => f.winnerId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull(_gameTableRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } - static MultiTypedResultKey<$PlayerGroupTableTable, List> _playerGroupTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( db.playerGroupTable, @@ -1698,31 +1671,6 @@ class $$PlayerTableTableFilterComposer builder: (column) => ColumnFilters(column), ); - Expression gameTableRefs( - Expression Function($$GameTableTableFilterComposer f) f, - ) { - final $$GameTableTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.gameTable, - getReferencedColumn: (t) => t.winnerId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$GameTableTableFilterComposer( - $db: $db, - $table: $db.gameTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - Expression playerGroupTableRefs( Expression Function($$PlayerGroupTableTableFilterComposer f) f, ) { @@ -1817,31 +1765,6 @@ class $$PlayerTableTableAnnotationComposer GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); - Expression gameTableRefs( - Expression Function($$GameTableTableAnnotationComposer a) f, - ) { - final $$GameTableTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.gameTable, - getReferencedColumn: (t) => t.winnerId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$GameTableTableAnnotationComposer( - $db: $db, - $table: $db.gameTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - Expression playerGroupTableRefs( Expression Function($$PlayerGroupTableTableAnnotationComposer a) f, ) { @@ -1907,7 +1830,6 @@ class $$PlayerTableTableTableManager (PlayerTableData, $$PlayerTableTableReferences), PlayerTableData, PrefetchHooks Function({ - bool gameTableRefs, bool playerGroupTableRefs, bool playerGameTableRefs, }) @@ -1956,42 +1878,16 @@ class $$PlayerTableTableTableManager ) .toList(), prefetchHooksCallback: - ({ - gameTableRefs = false, - playerGroupTableRefs = false, - playerGameTableRefs = false, - }) { + ({playerGroupTableRefs = false, playerGameTableRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ - if (gameTableRefs) db.gameTable, if (playerGroupTableRefs) db.playerGroupTable, if (playerGameTableRefs) db.playerGameTable, ], addJoins: null, getPrefetchedDataCallback: (items) async { return [ - if (gameTableRefs) - await $_getPrefetchedData< - PlayerTableData, - $PlayerTableTable, - GameTableData - >( - currentTable: table, - referencedTable: $$PlayerTableTableReferences - ._gameTableRefsTable(db), - managerFromTypedResult: (p0) => - $$PlayerTableTableReferences( - db, - table, - p0, - ).gameTableRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.winnerId == item.id, - ), - typedResults: items, - ), if (playerGroupTableRefs) await $_getPrefetchedData< PlayerTableData, @@ -2055,7 +1951,6 @@ typedef $$PlayerTableTableProcessedTableManager = (PlayerTableData, $$PlayerTableTableReferences), PlayerTableData, PrefetchHooks Function({ - bool gameTableRefs, bool playerGroupTableRefs, bool playerGameTableRefs, }) @@ -2436,7 +2331,7 @@ typedef $$GameTableTableCreateCompanionBuilder = GameTableCompanion Function({ required String id, required String name, - required String winnerId, + Value winnerId, required DateTime createdAt, Value rowid, }); @@ -2444,7 +2339,7 @@ typedef $$GameTableTableUpdateCompanionBuilder = GameTableCompanion Function({ Value id, Value name, - Value winnerId, + Value winnerId, Value createdAt, Value rowid, }); @@ -2453,25 +2348,6 @@ final class $$GameTableTableReferences extends BaseReferences<_$AppDatabase, $GameTableTable, GameTableData> { $$GameTableTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $PlayerTableTable _winnerIdTable(_$AppDatabase db) => - db.playerTable.createAlias( - $_aliasNameGenerator(db.gameTable.winnerId, db.playerTable.id), - ); - - $$PlayerTableTableProcessedTableManager get winnerId { - final $_column = $_itemColumn('winner_id')!; - - final manager = $$PlayerTableTableTableManager( - $_db, - $_db.playerTable, - ).filter((f) => f.id.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_winnerIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item]), - ); - } - static MultiTypedResultKey<$PlayerGameTableTable, List> _playerGameTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( db.playerGameTable, @@ -2530,34 +2406,16 @@ class $$GameTableTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get winnerId => $composableBuilder( + column: $table.winnerId, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column), ); - $$PlayerTableTableFilterComposer get winnerId { - final $$PlayerTableTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.winnerId, - referencedTable: $db.playerTable, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$PlayerTableTableFilterComposer( - $db: $db, - $table: $db.playerTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } - Expression playerGameTableRefs( Expression Function($$PlayerGameTableTableFilterComposer f) f, ) { @@ -2628,33 +2486,15 @@ class $$GameTableTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get winnerId => $composableBuilder( + column: $table.winnerId, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column), ); - - $$PlayerTableTableOrderingComposer get winnerId { - final $$PlayerTableTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.winnerId, - referencedTable: $db.playerTable, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$PlayerTableTableOrderingComposer( - $db: $db, - $table: $db.playerTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } } class $$GameTableTableAnnotationComposer @@ -2672,32 +2512,12 @@ class $$GameTableTableAnnotationComposer GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); + GeneratedColumn get winnerId => + $composableBuilder(column: $table.winnerId, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); - $$PlayerTableTableAnnotationComposer get winnerId { - final $$PlayerTableTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.winnerId, - referencedTable: $db.playerTable, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$PlayerTableTableAnnotationComposer( - $db: $db, - $table: $db.playerTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } - Expression playerGameTableRefs( Expression Function($$PlayerGameTableTableAnnotationComposer a) f, ) { @@ -2763,7 +2583,6 @@ class $$GameTableTableTableManager (GameTableData, $$GameTableTableReferences), GameTableData, PrefetchHooks Function({ - bool winnerId, bool playerGameTableRefs, bool groupGameTableRefs, }) @@ -2783,7 +2602,7 @@ class $$GameTableTableTableManager ({ Value id = const Value.absent(), Value name = const Value.absent(), - Value winnerId = const Value.absent(), + Value winnerId = const Value.absent(), Value createdAt = const Value.absent(), Value rowid = const Value.absent(), }) => GameTableCompanion( @@ -2797,7 +2616,7 @@ class $$GameTableTableTableManager ({ required String id, required String name, - required String winnerId, + Value winnerId = const Value.absent(), required DateTime createdAt, Value rowid = const Value.absent(), }) => GameTableCompanion.insert( @@ -2816,49 +2635,14 @@ class $$GameTableTableTableManager ) .toList(), prefetchHooksCallback: - ({ - winnerId = false, - playerGameTableRefs = false, - groupGameTableRefs = false, - }) { + ({playerGameTableRefs = false, groupGameTableRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ if (playerGameTableRefs) db.playerGameTable, if (groupGameTableRefs) db.groupGameTable, ], - addJoins: - < - T extends TableManagerState< - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic - > - >(state) { - if (winnerId) { - state = - state.withJoin( - currentTable: table, - currentColumn: table.winnerId, - referencedTable: $$GameTableTableReferences - ._winnerIdTable(db), - referencedColumn: $$GameTableTableReferences - ._winnerIdTable(db) - .id, - ) - as T; - } - - return state; - }, + addJoins: null, getPrefetchedDataCallback: (items) async { return [ if (playerGameTableRefs) @@ -2924,7 +2708,6 @@ typedef $$GameTableTableProcessedTableManager = (GameTableData, $$GameTableTableReferences), GameTableData, PrefetchHooks Function({ - bool winnerId, bool playerGameTableRefs, bool groupGameTableRefs, }) diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 93c788d..13eb658 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -110,17 +110,9 @@ class DataTransferService { .toList() ?? []; - for (Player player in importedPlayers) { - await db.playerDao.addPlayer(player: player); - } - - for (Group group in importedGroups) { - await db.groupDao.addGroup(group: group); - } - - for (Game game in importedGames) { - await db.gameDao.addGame(game: game); - } + await db.playerDao.addPlayers(players: importedPlayers); + await db.groupDao.addGroups(groups: importedGroups); + await db.gameDao.addGames(games: importedGames); } else { return ImportResult.invalidSchema; } From f7c1d6e975c6719122810426c47b9fd691f1b8e1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 21:13:29 +0100 Subject: [PATCH 23/95] Added missing await --- lib/data/dao/player_group_dao.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/data/dao/player_group_dao.dart b/lib/data/dao/player_group_dao.dart index e200958..93acf0b 100644 --- a/lib/data/dao/player_group_dao.dart +++ b/lib/data/dao/player_group_dao.dart @@ -51,7 +51,7 @@ class PlayerGroupDao extends DatabaseAccessor } if (await db.playerDao.playerExists(playerId: player.id) == false) { - db.playerDao.addPlayer(player: player); + await db.playerDao.addPlayer(player: player); } await into(playerGroupTable).insert( From cf71b40718d3a13005fb8f277b96ee9fcd351e0a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 21:19:54 +0100 Subject: [PATCH 24/95] changed export file name --- lib/presentation/views/main_menu/settings_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index e679467..a6e66fa 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -80,7 +80,7 @@ class _SettingsViewState extends State { await DataTransferService.getAppDataAsJson(context); final result = await DataTransferService.exportData( json, - 'exported_data', + 'game_tracker-data', ); if (!context.mounted) return; showExportSnackBar(context: context, result: result); From a8d4e640cfaeee074deff45956fec538c550039c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 21:39:01 +0100 Subject: [PATCH 25/95] Tabs update themselves after settings view --- .../main_menu/custom_navigation_bar.dart | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 20ced7a..9dd0658 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -17,12 +17,7 @@ class CustomNavigationBar extends StatefulWidget { class _CustomNavigationBarState extends State with SingleTickerProviderStateMixin { int currentIndex = 0; - final List tabs = [ - const HomeView(), - const GameHistoryView(), - const GroupsView(), - const StatisticsView(), - ]; + int tabKeyCount = 0; @override void initState() { @@ -31,6 +26,22 @@ class _CustomNavigationBarState extends State @override Widget build(BuildContext context) { + // Pretty ugly but works + final List tabs = [ + KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()), + KeyedSubtree( + key: ValueKey('games_$tabKeyCount'), + child: const GameHistoryView(), + ), + KeyedSubtree( + key: ValueKey('groups_$tabKeyCount'), + child: const GroupsView(), + ), + KeyedSubtree( + key: ValueKey('stats_$tabKeyCount'), + child: const StatisticsView(), + ), + ]; return Scaffold( appBar: AppBar( centerTitle: true, @@ -42,10 +53,15 @@ class _CustomNavigationBarState extends State scrolledUnderElevation: 0, actions: [ IconButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => const SettingsView()), - ), + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute(builder: (_) => const SettingsView()), + ); + setState(() { + tabKeyCount++; + }); + }, icon: const Icon(Icons.settings), ), ], From 822bc03c83363789ef412f7bcdfbecf3d1ebcf45 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 21:41:30 +0100 Subject: [PATCH 26/95] Added dialog --- .../views/main_menu/settings_view.dart | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index a6e66fa..70f7663 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -103,11 +103,31 @@ class _SettingsViewState extends State { icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), onPressed: () { - DataTransferService.deleteAllData(context); - showSnackbar( + showDialog( context: context, - message: 'Data successfully deleted', - ); + builder: (context) => AlertDialog( + title: const Text('Delete all data?'), + content: const Text('This can\'t be undone'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Abbrechen'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Löschen'), + ), + ], + ), + ).then((confirmed) { + if (confirmed == true && context.mounted) { + DataTransferService.deleteAllData(context); + showSnackbar( + context: context, + message: 'Daten erfolgreich gelöscht', + ); + } + }); }, ), ], From f40a9ad09b0c0f119a35e788cb2c44e6c38ebc28 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 21:42:07 +0100 Subject: [PATCH 27/95] Updated schema --- assets/schema.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/assets/schema.json b/assets/schema.json index eedfb05..4bbd5d3 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -20,10 +20,10 @@ "type": "string" }, "players": { - "type": "null" + "type": "object" }, "group": { - "type": "null" + "type": "object" }, "winner": { "type": "string" @@ -33,8 +33,6 @@ "id", "createdAt", "name", - "players", - "group", "winner" ] } From 45650133a7daa9b41bdd7a989e1dd6a95339865d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 21:51:13 +0100 Subject: [PATCH 28/95] Corrected schema again --- assets/schema.json | 84 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/assets/schema.json b/assets/schema.json index 4bbd5d3..69f889b 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -1,5 +1,3 @@ - - { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", @@ -20,15 +18,78 @@ "type": "string" }, "players": { - "type": "object" - }, - "group": { - "type": "object" - }, - "winner": { - "type": "string" + "type": [ + "object", + "null" + ], + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "createdAt", + "name" + ] } }, + "group": { + "type": [ + "object", + "null" + ], + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "name": { + "type": "string" + }, + "members": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "createdAt", + "name" + ] + } + ] + } + }, + "required": [ + "id", + "createdAt", + "name", + "members" + ] + }, + "winner": { + "type": "string" + }, "required": [ "id", "createdAt", @@ -91,7 +152,10 @@ "type": "array", "items": [ { - "type": "object", + "type": [ + "object", + "null" + ], "properties": { "id": { "type": "string" From a61818dd77613c3ed6294086963a1de23e0dbf78 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 20 Nov 2025 22:40:56 +0100 Subject: [PATCH 29/95] Fixed error in getAllGames method --- lib/data/dao/game_dao.dart | 22 +++++++++++++++++----- lib/data/dao/group_game_dao.dart | 9 +++++++-- lib/data/dao/player_game_dao.dart | 8 ++++---- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index 94d010c..20c90c7 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -15,11 +15,23 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { Future> getAllGames() async { final query = select(gameTable); final result = await query.get(); - return result - .map( - (row) => Game(id: row.id, name: row.name, createdAt: row.createdAt), - ) - .toList(); + + return Future.wait( + result.map((row) async { + final group = await db.groupGameDao.getGroupByGameId(gameId: row.id); + final player = await db.playerGameDao.getPlayersByGameId( + gameId: row.id, + ); + return Game( + id: row.id, + name: row.name, + group: group, + players: player, + createdAt: row.createdAt, + winner: row.winnerId, + ); + }), + ); } /// Retrieves a [Game] by its [gameId]. diff --git a/lib/data/dao/group_game_dao.dart b/lib/data/dao/group_game_dao.dart index d3b30ca..66ebdec 100644 --- a/lib/data/dao/group_game_dao.dart +++ b/lib/data/dao/group_game_dao.dart @@ -23,10 +23,15 @@ class GroupGameDao extends DatabaseAccessor } /// Retrieves the [Group] associated with the given [gameId]. - Future getGroupByGameId({required String gameId}) async { + /// Returns `null` if no group is found. + Future getGroupByGameId({required String gameId}) async { final result = await (select( groupGameTable, - )..where((g) => g.gameId.equals(gameId))).getSingle(); + )..where((g) => g.gameId.equals(gameId))).getSingleOrNull(); + + if (result == null) { + return null; + } final group = await db.groupDao.getGroupById(groupId: result.groupId); return group; diff --git a/lib/data/dao/player_game_dao.dart b/lib/data/dao/player_game_dao.dart index 8f367f8..05c9e10 100644 --- a/lib/data/dao/player_game_dao.dart +++ b/lib/data/dao/player_game_dao.dart @@ -23,19 +23,19 @@ class PlayerGameDao extends DatabaseAccessor } /// Retrieves a list of [Player]s associated with the given [gameId]. - /// Returns an empty list if no players are found. - Future> getPlayersByGameId({required String gameId}) async { + /// Returns null if no players are found. + Future?> getPlayersByGameId({required String gameId}) async { final result = await (select( playerGameTable, )..where((p) => p.gameId.equals(gameId))).get(); - if (result.isEmpty) return []; + if (result.isEmpty) return null; final futures = result.map( (row) => db.playerDao.getPlayerById(playerId: row.playerId), ); final players = await Future.wait(futures); - return players.whereType().toList(); + return players; } /// Associates a player with a game by inserting a record into the From 72067863c2c7d04f781159411e4b094726a08a72 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 20 Nov 2025 23:14:44 +0100 Subject: [PATCH 30/95] Updated insert modes --- lib/data/dao/group_game_dao.dart | 2 +- lib/data/dao/player_game_dao.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/data/dao/group_game_dao.dart b/lib/data/dao/group_game_dao.dart index 66ebdec..8a55b91 100644 --- a/lib/data/dao/group_game_dao.dart +++ b/lib/data/dao/group_game_dao.dart @@ -42,6 +42,6 @@ class GroupGameDao extends DatabaseAccessor Future addGroupToGame(String gameId, String groupId) async { await into( groupGameTable, - ).insert(GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId)); + ).insert(GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId), mode: InsertMode.insertOrReplace); } } diff --git a/lib/data/dao/player_game_dao.dart b/lib/data/dao/player_game_dao.dart index 05c9e10..92937cb 100644 --- a/lib/data/dao/player_game_dao.dart +++ b/lib/data/dao/player_game_dao.dart @@ -46,6 +46,7 @@ class PlayerGameDao extends DatabaseAccessor }) async { await into(playerGameTable).insert( PlayerGameTableCompanion.insert(playerId: playerId, gameId: gameId), + mode: InsertMode.insertOrReplace, ); } } From 89b3f1ff69cc5e5e8fecad8e5536f602f1edb03a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 20 Nov 2025 23:40:46 +0100 Subject: [PATCH 31/95] Overhauled tests --- test/db_tests/game_test.dart | 161 ++++++++++++++++++++++++++++++--- test/db_tests/group_test.dart | 64 +++++++++---- test/db_tests/player_test.dart | 81 ++++++++++++----- 3 files changed, 251 insertions(+), 55 deletions(-) diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index d726425..0b68648 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -15,7 +15,9 @@ void main() { late Player player4; late Player player5; late Group testgroup; + late Group testgroup2; late Game testgame; + late Game testgame2; final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -38,19 +40,28 @@ void main() { name: 'Test Group', members: [player1, player2, player3], ); + testgroup2 = Group( + name: 'Test Group', + members: [player1, player2, player3], + ); testgame = Game( name: 'Test Game', group: testgroup, players: [player4, player5], ); + testgame2 = Game( + name: 'Second Test Game', + group: testgroup2, + players: [player1, player2, player3], + ); }); }); tearDown(() async { await database.close(); }); - group('game tests', () { - test('game is added correctly', () async { + group('Game Tests', () { + test('Adding and fetching single game works correclty', () async { await database.gameDao.addGame(game: testgame); final result = await database.gameDao.getGameById(gameId: testgame.id); @@ -83,7 +94,116 @@ void main() { } }); - test('game is deleted correctly', () async { + // TODO: Use upcoming addGames() method + test('Adding and fetching multiple games works correclty', () async { + await database.gameDao.addGame(game: testgame); + await database.gameDao.addGame(game: testgame2); + + final allGames = await database.gameDao.getAllGames(); + expect(allGames.length, 2); + + final fetchedGame1 = allGames.firstWhere((g) => g.id == testgame.id); + // game checks + expect(fetchedGame1.id, testgame.id); + expect(fetchedGame1.name, testgame.name); + expect(fetchedGame1.createdAt, testgame.createdAt); + expect(fetchedGame1.winner, testgame.winner); + + // group checks + expect(fetchedGame1.group!.id, testgame.group!.id); + expect(fetchedGame1.group!.name, testgame.group!.name); + expect(fetchedGame1.group!.createdAt, testgame.group!.createdAt); + // group members checks + expect( + fetchedGame1.group!.members.length, + testgame.group!.members.length, + ); + for (int i = 0; i < testgame.group!.members.length; i++) { + expect( + fetchedGame1.group!.members[i].id, + testgame.group!.members[i].id, + ); + expect( + fetchedGame1.group!.members[i].name, + testgame.group!.members[i].name, + ); + expect( + fetchedGame1.group!.members[i].createdAt, + testgame.group!.members[i].createdAt, + ); + } + + // players checks + for (int i = 0; i < fetchedGame1.players!.length; i++) { + expect(fetchedGame1.players![i].id, testgame.players![i].id); + expect(fetchedGame1.players![i].name, testgame.players![i].name); + expect( + fetchedGame1.players![i].createdAt, + testgame.players![i].createdAt, + ); + } + + final fetchedGame2 = allGames.firstWhere((g) => g.id == testgame2.id); + // game checks + expect(fetchedGame2.id, testgame2.id); + expect(fetchedGame2.name, testgame2.name); + expect(fetchedGame2.createdAt, testgame2.createdAt); + expect(fetchedGame2.winner, testgame2.winner); + + // group checks + expect(fetchedGame2.group!.id, testgame2.group!.id); + expect(fetchedGame2.group!.name, testgame2.group!.name); + expect(fetchedGame2.group!.createdAt, testgame2.group!.createdAt); + // group members checks + expect( + fetchedGame2.group!.members.length, + testgame2.group!.members.length, + ); + for (int i = 0; i < testgame2.group!.members.length; i++) { + expect( + fetchedGame2.group!.members[i].id, + testgame2.group!.members[i].id, + ); + expect( + fetchedGame2.group!.members[i].name, + testgame2.group!.members[i].name, + ); + expect( + fetchedGame2.group!.members[i].createdAt, + testgame2.group!.members[i].createdAt, + ); + } + + // players checks + for (int i = 0; i < fetchedGame2.players!.length; i++) { + expect(fetchedGame2.players![i].id, testgame2.players![i].id); + expect(fetchedGame2.players![i].name, testgame2.players![i].name); + expect( + fetchedGame2.players![i].createdAt, + testgame2.players![i].createdAt, + ); + } + }); + + test('Adding the same game twice does not create duplicates', () async { + await database.gameDao.addGame(game: testgame); + await database.gameDao.addGame(game: testgame); + + final gameCount = await database.gameDao.getGameCount(); + expect(gameCount, 1); + }); + + test('Game existence check works correctly', () async { + var gameExists = await database.gameDao.gameExists(gameId: testgame.id); + expect(gameExists, false); + + await database.gameDao.addGame(game: testgame); + + gameExists = await database.gameDao.gameExists(gameId: testgame.id); + expect(gameExists, true); + }); + + test('Deleting a game works correclty', () async { await database.gameDao.addGame(game: testgame); final gameDeleted = await database.gameDao.deleteGame( @@ -95,22 +215,35 @@ void main() { expect(gameExists, false); }); - test('get game count works correctly', () async { - final initialCount = await database.gameDao.getGameCount(); - expect(initialCount, 0); + test('Getting the game count works correctly', () async { + var gameCount = await database.gameDao.getGameCount(); + expect(gameCount, 0); await database.gameDao.addGame(game: testgame); - final gameAdded = await database.gameDao.getGameCount(); - expect(gameAdded, 1); + gameCount = await database.gameDao.getGameCount(); + expect(gameCount, 1); - final gameRemoved = await database.gameDao.deleteGame( - gameId: testgame.id, - ); - expect(gameRemoved, true); + await database.gameDao.addGame(game: testgame2); - final finalCount = await database.gameDao.getGameCount(); - expect(finalCount, 0); + gameCount = await database.gameDao.getGameCount(); + expect(gameCount, 2); + + await database.gameDao.deleteGame(gameId: testgame.id); + + gameCount = await database.gameDao.getGameCount(); + expect(gameCount, 1); + + await database.gameDao.deleteGame(gameId: testgame2.id); + + gameCount = await database.gameDao.getGameCount(); + expect(gameCount, 0); }); + + // TODO: Implement + test('Adding a player to a game works correclty', () async {}); + + // TODO: Implement + test('Adding a group to a game works correclty', () async {}); }); } diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart index a076ab0..2572f52 100644 --- a/test/db_tests/group_test.dart +++ b/test/db_tests/group_test.dart @@ -45,8 +45,31 @@ void main() { tearDown(() async { await database.close(); }); - group('group tests', () { - test('all groups get fetched correctly', () async { + group('Group Tests', () { + test('Adding and fetching a single group works correctly', () async { + await database.groupDao.addGroup(group: testgroup); + + final fetchedGroup = await database.groupDao.getGroupById( + groupId: testgroup.id, + ); + + expect(fetchedGroup.id, testgroup.id); + expect(fetchedGroup.name, testgroup.name); + expect(fetchedGroup.createdAt, testgroup.createdAt); + + expect(fetchedGroup.members.length, testgroup.members.length); + for (int i = 0; i < testgroup.members.length; i++) { + expect(fetchedGroup.members[i].id, testgroup.members[i].id); + expect(fetchedGroup.members[i].name, testgroup.members[i].name); + expect( + fetchedGroup.members[i].createdAt, + testgroup.members[i].createdAt, + ); + } + }); + + // TODO: Use upcoming addGroups() method + test('Adding and fetching a single group works correctly', () async { await database.groupDao.addGroup(group: testgroup); await database.groupDao.addGroup(group: testgroup2); @@ -66,26 +89,27 @@ void main() { expect(fetchedGroup2.members.elementAt(0).createdAt, player2.createdAt); }); - test('group and group members gets added correctly', () async { + test('Adding the same group twice does not create duplicates', () async { + await database.groupDao.addGroup(group: testgroup); await database.groupDao.addGroup(group: testgroup); - final result = await database.groupDao.getGroupById( - groupId: testgroup.id, - ); - - expect(result.id, testgroup.id); - expect(result.name, testgroup.name); - expect(result.createdAt, testgroup.createdAt); - - expect(result.members.length, testgroup.members.length); - for (int i = 0; i < testgroup.members.length; i++) { - expect(result.members[i].id, testgroup.members[i].id); - expect(result.members[i].name, testgroup.members[i].name); - expect(result.members[i].createdAt, testgroup.members[i].createdAt); - } + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 1); }); - test('group gets deleted correctly', () async { + test('Group existence check works correctly', () async { + var groupExists = await database.groupDao.groupExists( + groupId: testgroup.id, + ); + expect(groupExists, false); + + await database.groupDao.addGroup(group: testgroup); + + groupExists = await database.groupDao.groupExists(groupId: testgroup.id); + expect(groupExists, true); + }); + + test('Deleting a group works correclty', () async { await database.groupDao.addGroup(group: testgroup); final groupDeleted = await database.groupDao.deleteGroup( @@ -99,7 +123,7 @@ void main() { expect(groupExists, false); }); - test('group name gets updated correcly ', () async { + test('Updating a group name works correcly', () async { await database.groupDao.addGroup(group: testgroup); const newGroupName = 'new group name'; @@ -167,7 +191,7 @@ void main() { expect(playerExists, false); }); - test('get group count works correctly', () async { + test('Getting the group count works correctly', () async { final initialCount = await database.groupDao.getGroupCount(); expect(initialCount, 0); diff --git a/test/db_tests/player_test.dart b/test/db_tests/player_test.dart index fa65f67..d894836 100644 --- a/test/db_tests/player_test.dart +++ b/test/db_tests/player_test.dart @@ -31,7 +31,7 @@ void main() { }); group('player tests', () { - test('all players get fetched correctly', () async { + test('Adding and fetching single player works correclty', () async { await database.playerDao.addPlayer(player: testPlayer); await database.playerDao.addPlayer(player: testPlayer2); @@ -51,18 +51,50 @@ void main() { expect(fetchedPlayer2.createdAt, testPlayer2.createdAt); }); - test('players get inserted correcly ', () async { + // TODO: Use upcoming addPlayers() method + test('Adding and fetching multiple players works correclty', () async { await database.playerDao.addPlayer(player: testPlayer); - final result = await database.playerDao.getPlayerById( - playerId: testPlayer.id, - ); + await database.playerDao.addPlayer(player: testPlayer2); - expect(result.id, testPlayer.id); - expect(result.name, testPlayer.name); - expect(result.createdAt, testPlayer.createdAt); + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 2); + + final fetchedPlayer1 = allPlayers.firstWhere( + (g) => g.id == testPlayer.id, + ); + expect(fetchedPlayer1.name, testPlayer.name); + expect(fetchedPlayer1.createdAt, testPlayer.createdAt); + + final fetchedPlayer2 = allPlayers.firstWhere( + (g) => g.id == testPlayer2.id, + ); + expect(fetchedPlayer2.name, testPlayer2.name); + expect(fetchedPlayer2.createdAt, testPlayer2.createdAt); }); - test('players get deleted correcly ', () async { + test('Adding the same player twice does not create duplicates', () async { + await database.playerDao.addPlayer(player: testPlayer); + await database.playerDao.addPlayer(player: testPlayer); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 1); + }); + + test('Player existence check works correctly', () async { + var playerExists = await database.playerDao.playerExists( + playerId: testPlayer.id, + ); + expect(playerExists, false); + + await database.playerDao.addPlayer(player: testPlayer); + + playerExists = await database.playerDao.playerExists( + playerId: testPlayer.id, + ); + expect(playerExists, true); + }); + + test('Deleting a player works correclty', () async { await database.playerDao.addPlayer(player: testPlayer); final playerDeleted = await database.playerDao.deletePlayer( playerId: testPlayer.id, @@ -75,7 +107,7 @@ void main() { expect(playerExists, false); }); - test('player name gets updated correcly ', () async { + test('Updating a player name works correcly', () async { await database.playerDao.addPlayer(player: testPlayer); const newPlayerName = 'new player name'; @@ -91,22 +123,29 @@ void main() { expect(result.name, newPlayerName); }); - test('get player count works correctly', () async { - final initialCount = await database.playerDao.getPlayerCount(); - expect(initialCount, 0); + test('Getting the player count works correctly', () async { + var playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 0); await database.playerDao.addPlayer(player: testPlayer); - final playerAdded = await database.playerDao.getPlayerCount(); - expect(playerAdded, 1); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 1); - final playerRemoved = await database.playerDao.deletePlayer( - playerId: testPlayer.id, - ); - expect(playerRemoved, true); + await database.playerDao.addPlayer(player: testPlayer2); - final finalCount = await database.playerDao.getPlayerCount(); - expect(finalCount, 0); + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 2); + + await database.playerDao.deletePlayer(playerId: testPlayer.id); + + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 1); + + await database.playerDao.deletePlayer(playerId: testPlayer2.id); + + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 0); }); }); } From 8e63a01705868772df469f43ed2afc6f788bb482 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 20 Nov 2025 23:49:57 +0100 Subject: [PATCH 32/95] Added methods for multiple inserts to tests --- test/db_tests/game_test.dart | 4 +--- test/db_tests/group_test.dart | 4 +--- test/db_tests/player_test.dart | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index 0b68648..477965f 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -94,10 +94,8 @@ void main() { } }); - // TODO: Use upcoming addGames() method test('Adding and fetching multiple games works correclty', () async { - await database.gameDao.addGame(game: testgame); - await database.gameDao.addGame(game: testgame2); + await database.gameDao.addGames(games: [testgame, testgame2]); final allGames = await database.gameDao.getAllGames(); expect(allGames.length, 2); diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart index 2572f52..62416fd 100644 --- a/test/db_tests/group_test.dart +++ b/test/db_tests/group_test.dart @@ -68,10 +68,8 @@ void main() { } }); - // TODO: Use upcoming addGroups() method test('Adding and fetching a single group works correctly', () async { - await database.groupDao.addGroup(group: testgroup); - await database.groupDao.addGroup(group: testgroup2); + await database.groupDao.addGroups(groups: [testgroup, testgroup2]); final allGroups = await database.groupDao.getAllGroups(); expect(allGroups.length, 2); diff --git a/test/db_tests/player_test.dart b/test/db_tests/player_test.dart index d894836..bc95533 100644 --- a/test/db_tests/player_test.dart +++ b/test/db_tests/player_test.dart @@ -51,10 +51,8 @@ void main() { expect(fetchedPlayer2.createdAt, testPlayer2.createdAt); }); - // TODO: Use upcoming addPlayers() method test('Adding and fetching multiple players works correclty', () async { - await database.playerDao.addPlayer(player: testPlayer); - await database.playerDao.addPlayer(player: testPlayer2); + await database.playerDao.addPlayers(players: [testPlayer, testPlayer2]); final allPlayers = await database.playerDao.getAllPlayers(); expect(allPlayers.length, 2); From 31589855f28e6eb8c6f2d402f389df8f9bfcf28b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 00:06:09 +0100 Subject: [PATCH 33/95] Added methods of todos --- lib/data/dao/group_game_dao.dart | 57 +++++++++++++++++++-------- lib/data/dao/player_game_dao.dart | 64 +++++++++++++++++++++++-------- test/db_tests/game_test.dart | 59 ++++++++++++++++++++++++++-- 3 files changed, 143 insertions(+), 37 deletions(-) diff --git a/lib/data/dao/group_game_dao.dart b/lib/data/dao/group_game_dao.dart index 8a55b91..633bb1c 100644 --- a/lib/data/dao/group_game_dao.dart +++ b/lib/data/dao/group_game_dao.dart @@ -10,16 +10,13 @@ class GroupGameDao extends DatabaseAccessor with _$GroupGameDaoMixin { GroupGameDao(super.db); - /// Checks if there is a group associated with the given [gameId]. - /// Returns `true` if there is a group, otherwise `false`. - Future hasGameGroup({required String gameId}) async { - final count = - await (selectOnly(groupGameTable) - ..where(groupGameTable.gameId.equals(gameId)) - ..addColumns([groupGameTable.groupId.count()])) - .map((row) => row.read(groupGameTable.groupId.count())) - .getSingle(); - return (count ?? 0) > 0; + /// Associates a group with a game by inserting a record into the + /// [GroupGameTable]. + Future addGroupToGame(String gameId, String groupId) async { + await into(groupGameTable).insert( + GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId), + mode: InsertMode.insertOrReplace, + ); } /// Retrieves the [Group] associated with the given [gameId]. @@ -37,11 +34,39 @@ class GroupGameDao extends DatabaseAccessor return group; } - /// Associates a group with a game by inserting a record into the - /// [GroupGameTable]. - Future addGroupToGame(String gameId, String groupId) async { - await into( - groupGameTable, - ).insert(GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId), mode: InsertMode.insertOrReplace); + /// Checks if there is a group associated with the given [gameId]. + /// Returns `true` if there is a group, otherwise `false`. + Future gameHasGroup({required String gameId}) async { + final count = + await (selectOnly(groupGameTable) + ..where(groupGameTable.gameId.equals(gameId)) + ..addColumns([groupGameTable.groupId.count()])) + .map((row) => row.read(groupGameTable.groupId.count())) + .getSingle(); + return (count ?? 0) > 0; + } + + /// Checks if a specific group is associated with a specific game. + /// Returns `true` if the group is in the game, otherwise `false`. + Future isGroupInGame({ + required String gameId, + required String groupId, + }) async { + final count = + await (selectOnly(groupGameTable) + ..where( + groupGameTable.gameId.equals(gameId) & + groupGameTable.groupId.equals(groupId), + ) + ..addColumns([groupGameTable.groupId.count()])) + .map((row) => row.read(groupGameTable.groupId.count())) + .getSingle(); + return (count ?? 0) > 0; + } + + Future removeGroupFromGame({required String gameId}) async { + final query = delete(groupGameTable)..where((g) => g.gameId.equals(gameId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; } } diff --git a/lib/data/dao/player_game_dao.dart b/lib/data/dao/player_game_dao.dart index 92937cb..87fd1d0 100644 --- a/lib/data/dao/player_game_dao.dart +++ b/lib/data/dao/player_game_dao.dart @@ -10,16 +10,16 @@ class PlayerGameDao extends DatabaseAccessor with _$PlayerGameDaoMixin { PlayerGameDao(super.db); - /// Checks if there are any players associated with the given [gameId]. - /// Returns `true` if there are players, otherwise `false`. - Future gameHasPlayers({required String gameId}) async { - final count = - await (selectOnly(playerGameTable) - ..where(playerGameTable.gameId.equals(gameId)) - ..addColumns([playerGameTable.playerId.count()])) - .map((row) => row.read(playerGameTable.playerId.count())) - .getSingle(); - return (count ?? 0) > 0; + /// Associates a player with a game by inserting a record into the + /// [PlayerGameTable]. + Future addPlayerToGame({ + required String gameId, + required String playerId, + }) async { + await into(playerGameTable).insert( + PlayerGameTableCompanion.insert(playerId: playerId, gameId: gameId), + mode: InsertMode.insertOrReplace, + ); } /// Retrieves a list of [Player]s associated with the given [gameId]. @@ -38,15 +38,45 @@ class PlayerGameDao extends DatabaseAccessor return players; } - /// Associates a player with a game by inserting a record into the - /// [PlayerGameTable]. - Future addPlayerToGame({ + /// Checks if there are any players associated with the given [gameId]. + /// Returns `true` if there are players, otherwise `false`. + Future gameHasPlayers({required String gameId}) async { + final count = + await (selectOnly(playerGameTable) + ..where(playerGameTable.gameId.equals(gameId)) + ..addColumns([playerGameTable.playerId.count()])) + .map((row) => row.read(playerGameTable.playerId.count())) + .getSingle(); + return (count ?? 0) > 0; + } + + /// Checks if a specific player is associated with a specific game. + /// Returns `true` if the player is in the game, otherwise `false`. + Future isPlayerInGame({ required String gameId, required String playerId, }) async { - await into(playerGameTable).insert( - PlayerGameTableCompanion.insert(playerId: playerId, gameId: gameId), - mode: InsertMode.insertOrReplace, - ); + final count = + await (selectOnly(playerGameTable) + ..where(playerGameTable.gameId.equals(gameId)) + ..where(playerGameTable.playerId.equals(playerId)) + ..addColumns([playerGameTable.playerId.count()])) + .map((row) => row.read(playerGameTable.playerId.count())) + .getSingle(); + return (count ?? 0) > 0; + } + + /// Removes the association of a player with a game by deleting the record + /// from the [PlayerGameTable]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future removePlayerFromGame({ + required String gameId, + required String playerId, + }) async { + final query = delete(playerGameTable) + ..where((pg) => pg.gameId.equals(gameId)) + ..where((pg) => pg.playerId.equals(playerId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; } } diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index 0b68648..05cf9d0 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -14,10 +14,12 @@ void main() { late Player player3; late Player player4; late Player player5; + late Player player6; late Group testgroup; late Group testgroup2; late Game testgame; late Game testgame2; + late Game testgame3; final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -36,6 +38,7 @@ void main() { player3 = Player(name: 'Charlie'); player4 = Player(name: 'Diana'); player5 = Player(name: 'Eve'); + player6 = Player(name: 'Frank'); testgroup = Group( name: 'Test Group', members: [player1, player2, player3], @@ -54,6 +57,7 @@ void main() { group: testgroup2, players: [player1, player2, player3], ); + testgame3 = Game(name: 'Third Test Game', players: [player4, player5]); }); }); tearDown(() async { @@ -240,10 +244,57 @@ void main() { expect(gameCount, 0); }); - // TODO: Implement - test('Adding a player to a game works correclty', () async {}); + test( + 'Adding and removing player to and from a game works correclty', + () async { + database.gameDao.addGame(game: testgame); + database.playerDao.addPlayer(player: player6); + database.playerGameDao.addPlayerToGame( + gameId: testgame.id, + playerId: player6.id, + ); - // TODO: Implement - test('Adding a group to a game works correclty', () async {}); + var playerInGame = await database.playerGameDao.isPlayerInGame( + gameId: testgame.id, + playerId: player6.id, + ); + + expect(playerInGame, true); + + final playerRemoved = await database.playerGameDao.removePlayerFromGame( + gameId: testgame.id, + playerId: player6.id, + ); + + expect(playerRemoved, true); + + playerInGame = await database.playerGameDao.isPlayerInGame( + gameId: testgame.id, + playerId: player6.id, + ); + expect(playerInGame, false); + }, + ); + + test( + 'Adding and removing a group to and from a game works correclty', + () async { + database.gameDao.addGame(game: testgame3); + database.groupDao.addGroup(group: testgroup); + database.groupGameDao.addGroupToGame(testgame3.id, testgroup.id); + var gameHasGroup = await database.groupGameDao.gameHasGroup( + gameId: testgame3.id, + ); + expect(gameHasGroup, true); + final groupRemoved = await database.groupGameDao.removeGroupFromGame( + gameId: testgame3.id, + ); + expect(groupRemoved, true); + gameHasGroup = await database.groupGameDao.gameHasGroup( + gameId: testgame3.id, + ); + expect(gameHasGroup, false); + }, + ); }); } From 6055eb63a85ffb43143a63f4e9083aff1a718c1d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 00:07:34 +0100 Subject: [PATCH 34/95] Refactoring --- lib/data/dao/game_dao.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index 20c90c7..2024eca 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -44,7 +44,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { players = await db.playerGameDao.getPlayersByGameId(gameId: gameId); } Group? group; - if (await db.groupGameDao.hasGameGroup(gameId: gameId)) { + if (await db.groupGameDao.gameHasGroup(gameId: gameId)) { group = await db.groupGameDao.getGroupByGameId(gameId: gameId); } From b21ca5467232c9e35b341ea5c374353c8ee77210 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 00:10:29 +0100 Subject: [PATCH 35/95] Refactoring --- lib/data/dao/game_dao.dart | 10 +++---- lib/data/dao/group_dao.dart | 4 +-- lib/data/dao/group_game_dao.dart | 2 +- lib/data/dao/player_game_dao.dart | 2 +- lib/data/dao/player_group_dao.dart | 48 +++++++++++++++--------------- 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index 2024eca..f29d553 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -18,10 +18,8 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { return Future.wait( result.map((row) async { - final group = await db.groupGameDao.getGroupByGameId(gameId: row.id); - final player = await db.playerGameDao.getPlayersByGameId( - gameId: row.id, - ); + final group = await db.groupGameDao.getGroupOfGame(gameId: row.id); + final player = await db.playerGameDao.getPlayersOfGame(gameId: row.id); return Game( id: row.id, name: row.name, @@ -41,11 +39,11 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { List? players; if (await db.playerGameDao.gameHasPlayers(gameId: gameId)) { - players = await db.playerGameDao.getPlayersByGameId(gameId: gameId); + players = await db.playerGameDao.getPlayersOfGame(gameId: gameId); } Group? group; if (await db.groupGameDao.gameHasGroup(gameId: gameId)) { - group = await db.groupGameDao.getGroupByGameId(gameId: gameId); + group = await db.groupGameDao.getGroupOfGame(gameId: gameId); } return Game( diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 3378948..9b16801 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -16,7 +16,7 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { final result = await query.get(); return Future.wait( result.map((groupData) async { - final members = await db.playerGroupDao.getPlayersOfGroupById( + final members = await db.playerGroupDao.getPlayersOfGroup( groupId: groupData.id, ); return Group( @@ -34,7 +34,7 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { final query = select(groupTable)..where((g) => g.id.equals(groupId)); final result = await query.getSingle(); - List members = await db.playerGroupDao.getPlayersOfGroupById( + List members = await db.playerGroupDao.getPlayersOfGroup( groupId: groupId, ); diff --git a/lib/data/dao/group_game_dao.dart b/lib/data/dao/group_game_dao.dart index 633bb1c..8081c6f 100644 --- a/lib/data/dao/group_game_dao.dart +++ b/lib/data/dao/group_game_dao.dart @@ -21,7 +21,7 @@ class GroupGameDao extends DatabaseAccessor /// Retrieves the [Group] associated with the given [gameId]. /// Returns `null` if no group is found. - Future getGroupByGameId({required String gameId}) async { + Future getGroupOfGame({required String gameId}) async { final result = await (select( groupGameTable, )..where((g) => g.gameId.equals(gameId))).getSingleOrNull(); diff --git a/lib/data/dao/player_game_dao.dart b/lib/data/dao/player_game_dao.dart index 87fd1d0..ef15a80 100644 --- a/lib/data/dao/player_game_dao.dart +++ b/lib/data/dao/player_game_dao.dart @@ -24,7 +24,7 @@ class PlayerGameDao extends DatabaseAccessor /// Retrieves a list of [Player]s associated with the given [gameId]. /// Returns null if no players are found. - Future?> getPlayersByGameId({required String gameId}) async { + Future?> getPlayersOfGame({required String gameId}) async { final result = await (select( playerGameTable, )..where((p) => p.gameId.equals(gameId))).get(); diff --git a/lib/data/dao/player_group_dao.dart b/lib/data/dao/player_group_dao.dart index fe067ae..5484bf7 100644 --- a/lib/data/dao/player_group_dao.dart +++ b/lib/data/dao/player_group_dao.dart @@ -10,8 +10,31 @@ class PlayerGroupDao extends DatabaseAccessor with _$PlayerGroupDaoMixin { PlayerGroupDao(super.db); + /// Adds a [player] to a group with the given [groupId]. + /// If the player is already in the group, no action is taken. + /// If the player does not exist in the player table, they are added. + /// Returns `true` if the player was added, otherwise `false`. + Future addPlayerToGroup({ + required Player player, + required String groupId, + }) async { + if (await isPlayerInGroup(playerId: player.id, groupId: groupId)) { + return false; + } + + if (await db.playerDao.playerExists(playerId: player.id) == false) { + db.playerDao.addPlayer(player: player); + } + + await into(playerGroupTable).insert( + PlayerGroupTableCompanion.insert(playerId: player.id, groupId: groupId), + ); + + return true; + } + /// Retrieves all players belonging to a specific group by [groupId]. - Future> getPlayersOfGroupById({required String groupId}) async { + Future> getPlayersOfGroup({required String groupId}) async { final query = select(playerGroupTable) ..where((pG) => pG.groupId.equals(groupId)); final result = await query.get(); @@ -38,29 +61,6 @@ class PlayerGroupDao extends DatabaseAccessor return rowsAffected > 0; } - /// Adds a [player] to a group with the given [groupId]. - /// If the player is already in the group, no action is taken. - /// If the player does not exist in the player table, they are added. - /// Returns `true` if the player was added, otherwise `false`. - Future addPlayerToGroup({ - required Player player, - required String groupId, - }) async { - if (await isPlayerInGroup(playerId: player.id, groupId: groupId)) { - return false; - } - - if (await db.playerDao.playerExists(playerId: player.id) == false) { - db.playerDao.addPlayer(player: player); - } - - await into(playerGroupTable).insert( - PlayerGroupTableCompanion.insert(playerId: player.id, groupId: groupId), - ); - - return true; - } - /// 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({ From 961c6bb679b77492dcb60cb7a61e83c132fba855 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 01:05:35 +0100 Subject: [PATCH 36/95] Refactored tests in own files --- lib/data/dao/group_game_dao.dart | 11 +- test/db_tests/game_test.dart | 158 ++++++++++----------------- test/db_tests/group_game_test.dart | 110 +++++++++++++++++++ test/db_tests/group_test.dart | 55 +--------- test/db_tests/player_game_test.dart | 126 +++++++++++++++++++++ test/db_tests/player_group_test.dart | 90 +++++++++++++++ test/db_tests/player_test.dart | 3 +- 7 files changed, 394 insertions(+), 159 deletions(-) create mode 100644 test/db_tests/group_game_test.dart create mode 100644 test/db_tests/player_game_test.dart create mode 100644 test/db_tests/player_group_test.dart diff --git a/lib/data/dao/group_game_dao.dart b/lib/data/dao/group_game_dao.dart index 8081c6f..f3ddcc7 100644 --- a/lib/data/dao/group_game_dao.dart +++ b/lib/data/dao/group_game_dao.dart @@ -64,8 +64,15 @@ class GroupGameDao extends DatabaseAccessor return (count ?? 0) > 0; } - Future removeGroupFromGame({required String gameId}) async { - final query = delete(groupGameTable)..where((g) => g.gameId.equals(gameId)); + /// Removes the association of a group from a game based on [groupId] and + /// [gameId]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future removeGroupFromGame({ + required String gameId, + required String groupId, + }) async { + final query = delete(groupGameTable) + ..where((g) => g.gameId.equals(gameId) & g.groupId.equals(groupId)); final rowsAffected = await query.go(); return rowsAffected > 0; } diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index 05cf9d0..d5b0856 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -14,12 +14,12 @@ void main() { late Player player3; late Player player4; late Player player5; - late Player player6; late Group testgroup; late Group testgroup2; - late Game testgame; + late Game testgame1; late Game testgame2; - late Game testgame3; + late Game testgameWithPlayer; + late Game testgameWithGroup; final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -38,16 +38,12 @@ void main() { player3 = Player(name: 'Charlie'); player4 = Player(name: 'Diana'); player5 = Player(name: 'Eve'); - player6 = Player(name: 'Frank'); testgroup = Group( - name: 'Test Group', + name: 'Test Group 2', members: [player1, player2, player3], ); - testgroup2 = Group( - name: 'Test Group', - members: [player1, player2, player3], - ); - testgame = Game( + testgroup2 = Group(name: 'Test Group 2', members: [player4, player5]); + testgame1 = Game( name: 'Test Game', group: testgroup, players: [player4, player5], @@ -57,7 +53,11 @@ void main() { group: testgroup2, players: [player1, player2, player3], ); - testgame3 = Game(name: 'Third Test Game', players: [player4, player5]); + testgameWithPlayer = Game( + name: 'Second Test Game', + players: [player1, player2, player3], + ); + testgameWithGroup = Game(name: 'Second Test Game', group: testgroup2); }); }); tearDown(() async { @@ -66,14 +66,14 @@ void main() { group('Game Tests', () { test('Adding and fetching single game works correclty', () async { - await database.gameDao.addGame(game: testgame); + await database.gameDao.addGame(game: testgame1); - final result = await database.gameDao.getGameById(gameId: testgame.id); + final result = await database.gameDao.getGameById(gameId: testgame1.id); - expect(result.id, testgame.id); - expect(result.name, testgame.name); - expect(result.winner, testgame.winner); - expect(result.createdAt, testgame.createdAt); + expect(result.id, testgame1.id); + expect(result.name, testgame1.name); + expect(result.winner, testgame1.winner); + expect(result.createdAt, testgame1.createdAt); if (result.group != null) { expect(result.group!.members.length, testgroup.members.length); @@ -86,12 +86,12 @@ void main() { fail('Group is null'); } if (result.players != null) { - expect(result.players!.length, testgame.players!.length); + expect(result.players!.length, testgame1.players!.length); - for (int i = 0; i < testgame.players!.length; i++) { - expect(result.players![i].id, testgame.players![i].id); - expect(result.players![i].name, testgame.players![i].name); - expect(result.players![i].createdAt, testgame.players![i].createdAt); + for (int i = 0; i < testgame1.players!.length; i++) { + expect(result.players![i].id, testgame1.players![i].id); + expect(result.players![i].name, testgame1.players![i].name); + expect(result.players![i].createdAt, testgame1.players![i].createdAt); } } else { fail('Players is null'); @@ -99,51 +99,54 @@ void main() { }); // TODO: Use upcoming addGames() method + // TODO: Iterate through games test('Adding and fetching multiple games works correclty', () async { - await database.gameDao.addGame(game: testgame); + await database.gameDao.addGame(game: testgame1); await database.gameDao.addGame(game: testgame2); + await database.gameDao.addGame(game: testgameWithGroup); + await database.gameDao.addGame(game: testgameWithPlayer); final allGames = await database.gameDao.getAllGames(); - expect(allGames.length, 2); + expect(allGames.length, 4); - final fetchedGame1 = allGames.firstWhere((g) => g.id == testgame.id); + final fetchedGame1 = allGames.firstWhere((g) => g.id == testgame1.id); // game checks - expect(fetchedGame1.id, testgame.id); - expect(fetchedGame1.name, testgame.name); - expect(fetchedGame1.createdAt, testgame.createdAt); - expect(fetchedGame1.winner, testgame.winner); + expect(fetchedGame1.id, testgame1.id); + expect(fetchedGame1.name, testgame1.name); + expect(fetchedGame1.createdAt, testgame1.createdAt); + expect(fetchedGame1.winner, testgame1.winner); // group checks - expect(fetchedGame1.group!.id, testgame.group!.id); - expect(fetchedGame1.group!.name, testgame.group!.name); - expect(fetchedGame1.group!.createdAt, testgame.group!.createdAt); + expect(fetchedGame1.group!.id, testgame1.group!.id); + expect(fetchedGame1.group!.name, testgame1.group!.name); + expect(fetchedGame1.group!.createdAt, testgame1.group!.createdAt); // group members checks expect( fetchedGame1.group!.members.length, - testgame.group!.members.length, + testgame1.group!.members.length, ); - for (int i = 0; i < testgame.group!.members.length; i++) { + for (int i = 0; i < testgame1.group!.members.length; i++) { expect( fetchedGame1.group!.members[i].id, - testgame.group!.members[i].id, + testgame1.group!.members[i].id, ); expect( fetchedGame1.group!.members[i].name, - testgame.group!.members[i].name, + testgame1.group!.members[i].name, ); expect( fetchedGame1.group!.members[i].createdAt, - testgame.group!.members[i].createdAt, + testgame1.group!.members[i].createdAt, ); } // players checks for (int i = 0; i < fetchedGame1.players!.length; i++) { - expect(fetchedGame1.players![i].id, testgame.players![i].id); - expect(fetchedGame1.players![i].name, testgame.players![i].name); + expect(fetchedGame1.players![i].id, testgame1.players![i].id); + expect(fetchedGame1.players![i].name, testgame1.players![i].name); expect( fetchedGame1.players![i].createdAt, - testgame.players![i].createdAt, + testgame1.players![i].createdAt, ); } @@ -190,32 +193,34 @@ void main() { }); test('Adding the same game twice does not create duplicates', () async { - await database.gameDao.addGame(game: testgame); - await database.gameDao.addGame(game: testgame); + await database.gameDao.addGame(game: testgame1); + await database.gameDao.addGame(game: testgame1); final gameCount = await database.gameDao.getGameCount(); expect(gameCount, 1); }); test('Game existence check works correctly', () async { - var gameExists = await database.gameDao.gameExists(gameId: testgame.id); + var gameExists = await database.gameDao.gameExists(gameId: testgame1.id); expect(gameExists, false); - await database.gameDao.addGame(game: testgame); + await database.gameDao.addGame(game: testgame1); - gameExists = await database.gameDao.gameExists(gameId: testgame.id); + gameExists = await database.gameDao.gameExists(gameId: testgame1.id); expect(gameExists, true); }); test('Deleting a game works correclty', () async { - await database.gameDao.addGame(game: testgame); + await database.gameDao.addGame(game: testgame1); final gameDeleted = await database.gameDao.deleteGame( - gameId: testgame.id, + gameId: testgame1.id, ); expect(gameDeleted, true); - final gameExists = await database.gameDao.gameExists(gameId: testgame.id); + final gameExists = await database.gameDao.gameExists( + gameId: testgame1.id, + ); expect(gameExists, false); }); @@ -223,7 +228,7 @@ void main() { var gameCount = await database.gameDao.getGameCount(); expect(gameCount, 0); - await database.gameDao.addGame(game: testgame); + await database.gameDao.addGame(game: testgame1); gameCount = await database.gameDao.getGameCount(); expect(gameCount, 1); @@ -233,7 +238,7 @@ void main() { gameCount = await database.gameDao.getGameCount(); expect(gameCount, 2); - await database.gameDao.deleteGame(gameId: testgame.id); + await database.gameDao.deleteGame(gameId: testgame1.id); gameCount = await database.gameDao.getGameCount(); expect(gameCount, 1); @@ -243,58 +248,5 @@ void main() { gameCount = await database.gameDao.getGameCount(); expect(gameCount, 0); }); - - test( - 'Adding and removing player to and from a game works correclty', - () async { - database.gameDao.addGame(game: testgame); - database.playerDao.addPlayer(player: player6); - database.playerGameDao.addPlayerToGame( - gameId: testgame.id, - playerId: player6.id, - ); - - var playerInGame = await database.playerGameDao.isPlayerInGame( - gameId: testgame.id, - playerId: player6.id, - ); - - expect(playerInGame, true); - - final playerRemoved = await database.playerGameDao.removePlayerFromGame( - gameId: testgame.id, - playerId: player6.id, - ); - - expect(playerRemoved, true); - - playerInGame = await database.playerGameDao.isPlayerInGame( - gameId: testgame.id, - playerId: player6.id, - ); - expect(playerInGame, false); - }, - ); - - test( - 'Adding and removing a group to and from a game works correclty', - () async { - database.gameDao.addGame(game: testgame3); - database.groupDao.addGroup(group: testgroup); - database.groupGameDao.addGroupToGame(testgame3.id, testgroup.id); - var gameHasGroup = await database.groupGameDao.gameHasGroup( - gameId: testgame3.id, - ); - expect(gameHasGroup, true); - final groupRemoved = await database.groupGameDao.removeGroupFromGame( - gameId: testgame3.id, - ); - expect(groupRemoved, true); - gameHasGroup = await database.groupGameDao.gameHasGroup( - gameId: testgame3.id, - ); - expect(gameHasGroup, false); - }, - ); }); } diff --git a/test/db_tests/group_game_test.dart b/test/db_tests/group_game_test.dart new file mode 100644 index 0000000..2e208d9 --- /dev/null +++ b/test/db_tests/group_game_test.dart @@ -0,0 +1,110 @@ +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/game.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +void main() { + late AppDatabase database; + late Player player1; + late Player player2; + late Player player3; + late Player player4; + late Player player5; + late Group testgroup; + late Game gameWithGroup; + late Game gameWithPlayers; + final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); + final fakeClock = Clock(() => fixedDate); + + setUp(() { + database = AppDatabase( + DatabaseConnection( + NativeDatabase.memory(), + // Recommended for widget tests to avoid test errors. + closeStreamsSynchronously: true, + ), + ); + + withClock(fakeClock, () { + player1 = Player(name: 'Alice'); + player2 = Player(name: 'Bob'); + player3 = Player(name: 'Charlie'); + player4 = Player(name: 'Diana'); + player5 = Player(name: 'Eve'); + testgroup = Group( + name: 'Test Group', + members: [player1, player2, player3], + ); + gameWithPlayers = Game( + name: 'Game with Players', + players: [player4, player5], + ); + gameWithGroup = Game(name: 'Game with Group', group: testgroup); + }); + }); + tearDown(() async { + await database.close(); + }); + group('Group-Game Tests', () { + test('Game has group works correctly', () async { + database.gameDao.addGame(game: gameWithPlayers); + database.groupDao.addGroup(group: testgroup); + + var gameHasGroup = await database.groupGameDao.gameHasGroup( + gameId: gameWithPlayers.id, + ); + + expect(gameHasGroup, false); + + database.groupGameDao.addGroupToGame(gameWithPlayers.id, testgroup.id); + + gameHasGroup = await database.groupGameDao.gameHasGroup( + gameId: gameWithPlayers.id, + ); + + expect(gameHasGroup, true); + }); + + test('Adding a group to a game works correctly', () async { + database.gameDao.addGame(game: gameWithPlayers); + database.groupDao.addGroup(group: testgroup); + database.groupGameDao.addGroupToGame(gameWithPlayers.id, testgroup.id); + + var groupAdded = await database.groupGameDao.isGroupInGame( + gameId: gameWithPlayers.id, + groupId: testgroup.id, + ); + expect(groupAdded, true); + + groupAdded = await database.groupGameDao.isGroupInGame( + gameId: gameWithPlayers.id, + groupId: '', + ); + expect(groupAdded, false); + }); + + test('Removing group from game works correctly', () async { + await database.gameDao.addGame(game: gameWithGroup); + + final groupToRemove = gameWithGroup.group!; + + final removed = await database.groupGameDao.removeGroupFromGame( + groupId: groupToRemove.id, + gameId: gameWithGroup.id, + ); + expect(removed, true); + + final result = await database.gameDao.getGameById( + gameId: gameWithGroup.id, + ); + expect(result.group, null); + }); + + // TODO: test getGroupOfGame() + test('Retrieving group of a game works correctly', () async {}); + }); +} diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart index 2572f52..b18942e 100644 --- a/test/db_tests/group_test.dart +++ b/test/db_tests/group_test.dart @@ -68,7 +68,6 @@ void main() { } }); - // TODO: Use upcoming addGroups() method test('Adding and fetching a single group works correctly', () async { await database.groupDao.addGroup(group: testgroup); await database.groupDao.addGroup(group: testgroup2); @@ -89,6 +88,8 @@ void main() { expect(fetchedGroup2.members.elementAt(0).createdAt, player2.createdAt); }); + // TODO: Use upcoming addGroups() method + // TODO: An Test in Game Tests orientieren test('Adding the same group twice does not create duplicates', () async { await database.groupDao.addGroup(group: testgroup); await database.groupDao.addGroup(group: testgroup); @@ -139,58 +140,6 @@ void main() { expect(result.name, newGroupName); }); - test('Adding player to group works correctly', () async { - await database.groupDao.addGroup(group: testgroup); - - await database.playerGroupDao.addPlayerToGroup( - player: player4, - groupId: testgroup.id, - ); - - final playerAdded = await database.playerGroupDao.isPlayerInGroup( - playerId: player4.id, - groupId: testgroup.id, - ); - - expect(playerAdded, true); - - final playerNotAdded = !await database.playerGroupDao.isPlayerInGroup( - playerId: '', - groupId: testgroup.id, - ); - - expect(playerNotAdded, true); - - final result = await database.groupDao.getGroupById( - groupId: testgroup.id, - ); - expect(result.members.length, testgroup.members.length + 1); - - final addedPlayer = result.members.firstWhere((p) => p.id == player4.id); - expect(addedPlayer.name, player4.name); - expect(addedPlayer.createdAt, player4.createdAt); - }); - - 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); - }); - test('Getting the group count works correctly', () async { final initialCount = await database.groupDao.getGroupCount(); expect(initialCount, 0); diff --git a/test/db_tests/player_game_test.dart b/test/db_tests/player_game_test.dart new file mode 100644 index 0000000..7df18a1 --- /dev/null +++ b/test/db_tests/player_game_test.dart @@ -0,0 +1,126 @@ +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/game.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +void main() { + late AppDatabase database; + late Player player1; + late Player player2; + late Player player3; + late Player player4; + late Player player5; + late Player player6; + late Group testgroup; + late Game testgameWithGroup; + late Game testgameWithPlayers; + final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); + final fakeClock = Clock(() => fixedDate); + + setUp(() { + database = AppDatabase( + DatabaseConnection( + NativeDatabase.memory(), + // Recommended for widget tests to avoid test errors. + closeStreamsSynchronously: true, + ), + ); + + withClock(fakeClock, () { + player1 = Player(name: 'Alice'); + player2 = Player(name: 'Bob'); + player3 = Player(name: 'Charlie'); + player4 = Player(name: 'Diana'); + player5 = Player(name: 'Eve'); + player6 = Player(name: 'Frank'); + testgroup = Group( + name: 'Test Group', + members: [player1, player2, player3], + ); + testgameWithGroup = Game(name: 'Test Game', group: testgroup); + testgameWithPlayers = Game( + name: 'Test Game with Players', + players: [player4, player5, player6], + ); + }); + }); + tearDown(() async { + await database.close(); + }); + + group('Player-Game Tests', () { + test('Game has player works correctly', () async { + database.gameDao.addGame(game: testgameWithGroup); + database.playerDao.addPlayer(player: player1); + + var gameHasPlayers = await database.playerGameDao.gameHasPlayers( + gameId: testgameWithGroup.id, + ); + + expect(gameHasPlayers, false); + + database.playerGameDao.addPlayerToGame( + gameId: testgameWithGroup.id, + playerId: player1.id, + ); + + gameHasPlayers = await database.playerGameDao.gameHasPlayers( + gameId: testgameWithGroup.id, + ); + + expect(gameHasPlayers, true); + }); + + test('Adding a player to a game works correctly', () async { + database.gameDao.addGame(game: testgameWithGroup); + database.playerDao.addPlayer(player: player5); + database.playerGameDao.addPlayerToGame( + gameId: testgameWithGroup.id, + playerId: player5.id, + ); + + var playerAdded = await database.playerGameDao.isPlayerInGame( + gameId: testgameWithGroup.id, + playerId: player5.id, + ); + + expect(playerAdded, true); + + playerAdded = await database.playerGameDao.isPlayerInGame( + gameId: testgameWithGroup.id, + playerId: '', + ); + + expect(playerAdded, false); + }); + + test('Removing player from game works correctly', () async { + await database.gameDao.addGame(game: testgameWithPlayers); + + final playerToRemove = testgameWithPlayers.players![0]; + + final removed = await database.playerGameDao.removePlayerFromGame( + playerId: playerToRemove.id, + gameId: testgameWithPlayers.id, + ); + expect(removed, true); + + final result = await database.gameDao.getGameById( + gameId: testgameWithGroup.id, + ); + expect(result.players!.length, testgameWithGroup.players!.length - 1); + + final playerExists = result.players!.any( + (p) => p.id == playerToRemove.id, + ); + expect(playerExists, false); + }); + + //TODO: test getPlayersOfGame() + test('Retrieving players of a game works correctly', () async {}); + }); +} diff --git a/test/db_tests/player_group_test.dart b/test/db_tests/player_group_test.dart new file mode 100644 index 0000000..74d7658 --- /dev/null +++ b/test/db_tests/player_group_test.dart @@ -0,0 +1,90 @@ +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +void main() { + late AppDatabase database; + late Player player1; + late Player player2; + late Player player3; + late Player player4; + late Group testgroup; + final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); + final fakeClock = Clock(() => fixedDate); + + setUp(() { + database = AppDatabase( + DatabaseConnection( + NativeDatabase.memory(), + // Recommended for widget tests to avoid test errors. + closeStreamsSynchronously: true, + ), + ); + + withClock(fakeClock, () { + player1 = Player(name: 'Alice'); + player2 = Player(name: 'Bob'); + player3 = Player(name: 'Charlie'); + player4 = Player(name: 'Diana'); + testgroup = Group( + name: 'Test Group', + members: [player1, player2, player3], + ); + }); + }); + tearDown(() async { + await database.close(); + }); + + group('Player-Group Tests', () { + test('Adding a player to a group works correctly', () async { + await database.groupDao.addGroup(group: testgroup); + await database.playerDao.addPlayer(player: player4); + await database.playerGroupDao.addPlayerToGroup( + groupId: testgroup.id, + player: player4, + ); + + var playerAdded = await database.playerGroupDao.isPlayerInGroup( + groupId: testgroup.id, + playerId: player4.id, + ); + + expect(playerAdded, true); + + playerAdded = await database.playerGroupDao.isPlayerInGroup( + groupId: testgroup.id, + playerId: '', + ); + + expect(playerAdded, false); + }); + + 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); + }); + + //TODO: test getPlayersOfGroup() + test('Retrieving players of a group works correctly', () async {}); + }); +} diff --git a/test/db_tests/player_test.dart b/test/db_tests/player_test.dart index d894836..9430433 100644 --- a/test/db_tests/player_test.dart +++ b/test/db_tests/player_test.dart @@ -23,7 +23,7 @@ void main() { withClock(fakeClock, () { testPlayer = Player(name: 'Test Player'); - testPlayer2 = Player(name: 'Second Group'); + testPlayer2 = Player(name: 'Second Player'); }); }); tearDown(() async { @@ -52,6 +52,7 @@ void main() { }); // TODO: Use upcoming addPlayers() method + // TODO: An Tests in Game orientieren test('Adding and fetching multiple players works correclty', () async { await database.playerDao.addPlayer(player: testPlayer); await database.playerDao.addPlayer(player: testPlayer2); From fe9239ee02138fdf0751c069a928b1b41e61700d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 12:45:48 +0100 Subject: [PATCH 37/95] Added missing test --- test/db_tests/group_game_test.dart | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/test/db_tests/group_game_test.dart b/test/db_tests/group_game_test.dart index 2e208d9..6621d9a 100644 --- a/test/db_tests/group_game_test.dart +++ b/test/db_tests/group_game_test.dart @@ -104,7 +104,25 @@ void main() { expect(result.group, null); }); - // TODO: test getGroupOfGame() - test('Retrieving group of a game works correctly', () async {}); + test('Retrieving group of a game works correctly', () async { + await database.gameDao.addGame(game: gameWithGroup); + final group = await database.groupGameDao.getGroupOfGame( + gameId: gameWithGroup.id, + ); + + if (group == null) { + fail('Group should not be null'); + } + + 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 < group.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); + } + }); }); } From 229750ffcffda1c68ff2974b48c06fe138c30517 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 12:46:04 +0100 Subject: [PATCH 38/95] Fixed test --- test/db_tests/player_game_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/db_tests/player_game_test.dart b/test/db_tests/player_game_test.dart index 7df18a1..d6f282e 100644 --- a/test/db_tests/player_game_test.dart +++ b/test/db_tests/player_game_test.dart @@ -110,9 +110,9 @@ void main() { expect(removed, true); final result = await database.gameDao.getGameById( - gameId: testgameWithGroup.id, + gameId: testgameWithPlayers.id, ); - expect(result.players!.length, testgameWithGroup.players!.length - 1); + expect(result.players!.length, testgameWithPlayers.players!.length - 1); final playerExists = result.players!.any( (p) => p.id == playerToRemove.id, From 32f3f68da9083c2f60a9f664146d4b180de007bc Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 12:46:18 +0100 Subject: [PATCH 39/95] Annotation for missing test & method --- lib/data/dao/player_group_dao.dart | 3 +++ test/db_tests/player_group_test.dart | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/data/dao/player_group_dao.dart b/lib/data/dao/player_group_dao.dart index 5484bf7..4024629 100644 --- a/lib/data/dao/player_group_dao.dart +++ b/lib/data/dao/player_group_dao.dart @@ -10,6 +10,9 @@ class PlayerGroupDao extends DatabaseAccessor with _$PlayerGroupDaoMixin { PlayerGroupDao(super.db); + /// No need for a groupHasPlayers method since the members attribute is + /// not nullable + /// Adds a [player] to a group with the given [groupId]. /// If the player is already in the group, no action is taken. /// If the player does not exist in the player table, they are added. diff --git a/test/db_tests/player_group_test.dart b/test/db_tests/player_group_test.dart index 74d7658..1181eda 100644 --- a/test/db_tests/player_group_test.dart +++ b/test/db_tests/player_group_test.dart @@ -41,6 +41,9 @@ void main() { }); group('Player-Group Tests', () { + /// No need to test if group has players since the members attribute is + /// not nullable + test('Adding a player to a group works correctly', () async { await database.groupDao.addGroup(group: testgroup); await database.playerDao.addPlayer(player: player4); From e15f5d163db0b40c3bf1c5198e71d147e6f99035 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 13:12:36 +0100 Subject: [PATCH 40/95] Added missing methods --- test/db_tests/group_game_test.dart | 42 ++++++++++++++++------------ test/db_tests/player_game_test.dart | 18 ++++++++++-- test/db_tests/player_group_test.dart | 14 ++++++++-- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/test/db_tests/group_game_test.dart b/test/db_tests/group_game_test.dart index 6621d9a..e231284 100644 --- a/test/db_tests/group_game_test.dart +++ b/test/db_tests/group_game_test.dart @@ -15,8 +15,8 @@ void main() { late Player player4; late Player player5; late Group testgroup; - late Game gameWithGroup; - late Game gameWithPlayers; + late Game testgameWithGroup; + late Game testgameWithPlayers; final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -39,11 +39,11 @@ void main() { name: 'Test Group', members: [player1, player2, player3], ); - gameWithPlayers = Game( + testgameWithPlayers = Game( name: 'Game with Players', players: [player4, player5], ); - gameWithGroup = Game(name: 'Game with Group', group: testgroup); + testgameWithGroup = Game(name: 'Game with Group', group: testgroup); }); }); tearDown(() async { @@ -51,63 +51,69 @@ void main() { }); group('Group-Game Tests', () { test('Game has group works correctly', () async { - database.gameDao.addGame(game: gameWithPlayers); + database.gameDao.addGame(game: testgameWithPlayers); database.groupDao.addGroup(group: testgroup); var gameHasGroup = await database.groupGameDao.gameHasGroup( - gameId: gameWithPlayers.id, + gameId: testgameWithPlayers.id, ); expect(gameHasGroup, false); - database.groupGameDao.addGroupToGame(gameWithPlayers.id, testgroup.id); + database.groupGameDao.addGroupToGame( + testgameWithPlayers.id, + testgroup.id, + ); gameHasGroup = await database.groupGameDao.gameHasGroup( - gameId: gameWithPlayers.id, + gameId: testgameWithPlayers.id, ); expect(gameHasGroup, true); }); test('Adding a group to a game works correctly', () async { - database.gameDao.addGame(game: gameWithPlayers); + database.gameDao.addGame(game: testgameWithPlayers); database.groupDao.addGroup(group: testgroup); - database.groupGameDao.addGroupToGame(gameWithPlayers.id, testgroup.id); + database.groupGameDao.addGroupToGame( + testgameWithPlayers.id, + testgroup.id, + ); var groupAdded = await database.groupGameDao.isGroupInGame( - gameId: gameWithPlayers.id, + gameId: testgameWithPlayers.id, groupId: testgroup.id, ); expect(groupAdded, true); groupAdded = await database.groupGameDao.isGroupInGame( - gameId: gameWithPlayers.id, + gameId: testgameWithPlayers.id, groupId: '', ); expect(groupAdded, false); }); test('Removing group from game works correctly', () async { - await database.gameDao.addGame(game: gameWithGroup); + await database.gameDao.addGame(game: testgameWithGroup); - final groupToRemove = gameWithGroup.group!; + final groupToRemove = testgameWithGroup.group!; final removed = await database.groupGameDao.removeGroupFromGame( groupId: groupToRemove.id, - gameId: gameWithGroup.id, + gameId: testgameWithGroup.id, ); expect(removed, true); final result = await database.gameDao.getGameById( - gameId: gameWithGroup.id, + gameId: testgameWithGroup.id, ); expect(result.group, null); }); test('Retrieving group of a game works correctly', () async { - await database.gameDao.addGame(game: gameWithGroup); + await database.gameDao.addGame(game: testgameWithGroup); final group = await database.groupGameDao.getGroupOfGame( - gameId: gameWithGroup.id, + gameId: testgameWithGroup.id, ); if (group == null) { diff --git a/test/db_tests/player_game_test.dart b/test/db_tests/player_game_test.dart index d6f282e..1fd0128 100644 --- a/test/db_tests/player_game_test.dart +++ b/test/db_tests/player_game_test.dart @@ -120,7 +120,21 @@ void main() { expect(playerExists, false); }); - //TODO: test getPlayersOfGame() - test('Retrieving players of a game works correctly', () async {}); + test('Retrieving players of a game works correctly', () async { + await database.gameDao.addGame(game: testgameWithPlayers); + final players = await database.playerGameDao.getPlayersOfGame( + gameId: testgameWithPlayers.id, + ); + + if (players == null) { + fail('Players should not be null'); + } + + for (int i = 0; i < players.length; i++) { + expect(players[i].id, testgameWithPlayers.players![i].id); + expect(players[i].name, testgameWithPlayers.players![i].name); + expect(players[i].createdAt, testgameWithPlayers.players![i].createdAt); + } + }); }); } diff --git a/test/db_tests/player_group_test.dart b/test/db_tests/player_group_test.dart index 1181eda..9e367e0 100644 --- a/test/db_tests/player_group_test.dart +++ b/test/db_tests/player_group_test.dart @@ -87,7 +87,17 @@ void main() { expect(playerExists, false); }); - //TODO: test getPlayersOfGroup() - test('Retrieving players of a group works correctly', () async {}); + 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); + } + }); }); } From d948f2f13d6888996783f40b9761b87510c668f0 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 13:42:27 +0100 Subject: [PATCH 41/95] Added iteration for multiple items test --- test/db_tests/game_test.dart | 130 +++++++++++++-------------------- test/db_tests/group_test.dart | 47 ++++++++---- test/db_tests/player_test.dart | 77 ++++++++++--------- 3 files changed, 129 insertions(+), 125 deletions(-) diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index d5b0856..71b7573 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -98,9 +98,8 @@ void main() { } }); - // TODO: Use upcoming addGames() method - // TODO: Iterate through games - test('Adding and fetching multiple games works correclty', () async { + test('Adding and fetching multiple games works correctly', () async { + // TODO: Use upcoming addGames() method await database.gameDao.addGame(game: testgame1); await database.gameDao.addGame(game: testgame2); await database.gameDao.addGame(game: testgameWithGroup); @@ -109,86 +108,61 @@ void main() { final allGames = await database.gameDao.getAllGames(); expect(allGames.length, 4); - final fetchedGame1 = allGames.firstWhere((g) => g.id == testgame1.id); - // game checks - expect(fetchedGame1.id, testgame1.id); - expect(fetchedGame1.name, testgame1.name); - expect(fetchedGame1.createdAt, testgame1.createdAt); - expect(fetchedGame1.winner, testgame1.winner); + final testGames = { + testgame1.id: testgame1, + testgame2.id: testgame2, + testgameWithGroup.id: testgameWithGroup, + testgameWithPlayer.id: testgameWithPlayer, + }; - // group checks - expect(fetchedGame1.group!.id, testgame1.group!.id); - expect(fetchedGame1.group!.name, testgame1.group!.name); - expect(fetchedGame1.group!.createdAt, testgame1.group!.createdAt); - // group members checks - expect( - fetchedGame1.group!.members.length, - testgame1.group!.members.length, - ); - for (int i = 0; i < testgame1.group!.members.length; i++) { - expect( - fetchedGame1.group!.members[i].id, - testgame1.group!.members[i].id, - ); - expect( - fetchedGame1.group!.members[i].name, - testgame1.group!.members[i].name, - ); - expect( - fetchedGame1.group!.members[i].createdAt, - testgame1.group!.members[i].createdAt, - ); - } + for (final game in allGames) { + final expectedGame = testGames[game.id]!; - // players checks - for (int i = 0; i < fetchedGame1.players!.length; i++) { - expect(fetchedGame1.players![i].id, testgame1.players![i].id); - expect(fetchedGame1.players![i].name, testgame1.players![i].name); - expect( - fetchedGame1.players![i].createdAt, - testgame1.players![i].createdAt, - ); - } + // Game-Checks + expect(game.id, expectedGame.id); + expect(game.name, expectedGame.name); + expect(game.createdAt, expectedGame.createdAt); + expect(game.winner, expectedGame.winner); - final fetchedGame2 = allGames.firstWhere((g) => g.id == testgame2.id); - // game checks - expect(fetchedGame2.id, testgame2.id); - expect(fetchedGame2.name, testgame2.name); - expect(fetchedGame2.createdAt, testgame2.createdAt); - expect(fetchedGame2.winner, testgame2.winner); + // Group-Checks + if (expectedGame.group != null) { + expect(game.group!.id, expectedGame.group!.id); + expect(game.group!.name, expectedGame.group!.name); + expect(game.group!.createdAt, expectedGame.group!.createdAt); - // group checks - expect(fetchedGame2.group!.id, testgame2.group!.id); - expect(fetchedGame2.group!.name, testgame2.group!.name); - expect(fetchedGame2.group!.createdAt, testgame2.group!.createdAt); - // group members checks - expect( - fetchedGame2.group!.members.length, - testgame2.group!.members.length, - ); - for (int i = 0; i < testgame2.group!.members.length; i++) { - expect( - fetchedGame2.group!.members[i].id, - testgame2.group!.members[i].id, - ); - expect( - fetchedGame2.group!.members[i].name, - testgame2.group!.members[i].name, - ); - expect( - fetchedGame2.group!.members[i].createdAt, - testgame2.group!.members[i].createdAt, - ); - } + // Group Members-Checks + expect( + game.group!.members.length, + expectedGame.group!.members.length, + ); + for (int i = 0; i < expectedGame.group!.members.length; i++) { + expect( + game.group!.members[i].id, + expectedGame.group!.members[i].id, + ); + expect( + game.group!.members[i].name, + expectedGame.group!.members[i].name, + ); + expect( + game.group!.members[i].createdAt, + expectedGame.group!.members[i].createdAt, + ); + } + } - // players checks - for (int i = 0; i < fetchedGame2.players!.length; i++) { - expect(fetchedGame2.players![i].id, testgame2.players![i].id); - expect(fetchedGame2.players![i].name, testgame2.players![i].name); - expect( - fetchedGame2.players![i].createdAt, - testgame2.players![i].createdAt, - ); + // Players-Checks + if (expectedGame.players != null) { + expect(game.players!.length, expectedGame.players!.length); + for (int i = 0; i < expectedGame.players!.length; i++) { + expect(game.players![i].id, expectedGame.players![i].id); + expect(game.players![i].name, expectedGame.players![i].name); + expect( + game.players![i].createdAt, + expectedGame.players![i].createdAt, + ); + } + } } }); diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart index b18942e..189e4c3 100644 --- a/test/db_tests/group_test.dart +++ b/test/db_tests/group_test.dart @@ -14,6 +14,8 @@ void main() { late Player player4; late Group testgroup; late Group testgroup2; + late Group testgroup3; + late Group testgroup4; final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -40,6 +42,16 @@ void main() { name: 'Second Group', members: [player2, player3, player4], ); + testgroup3 = Group( + id: 'gr2', + name: 'Second Group', + members: [player2, player4], + ); + testgroup4 = Group( + id: 'gr2', + name: 'Second Group', + members: [player1, player2, player3, player4], + ); }); }); tearDown(() async { @@ -68,28 +80,37 @@ void main() { } }); - test('Adding and fetching a single group works correctly', () async { + test('Adding and fetching multiple groups works correctly', () async { + // TODO: Use upcoming addGroups() method await database.groupDao.addGroup(group: testgroup); await database.groupDao.addGroup(group: testgroup2); + await database.groupDao.addGroup(group: testgroup3); + await database.groupDao.addGroup(group: testgroup4); final allGroups = await database.groupDao.getAllGroups(); expect(allGroups.length, 2); - final fetchedGroup1 = allGroups.firstWhere((g) => g.id == testgroup.id); - expect(fetchedGroup1.name, testgroup.name); - expect(fetchedGroup1.members.length, testgroup.members.length); - expect(fetchedGroup1.members.elementAt(0).id, player1.id); - expect(fetchedGroup1.members.elementAt(0).createdAt, player1.createdAt); + final testGroups = {testgroup.id: testgroup, testgroup2.id: testgroup2}; - final fetchedGroup2 = allGroups.firstWhere((g) => g.id == testgroup2.id); - expect(fetchedGroup2.name, testgroup2.name); - expect(fetchedGroup2.members.length, testgroup2.members.length); - expect(fetchedGroup2.members.elementAt(0).id, player2.id); - expect(fetchedGroup2.members.elementAt(0).createdAt, player2.createdAt); + for (final group in allGroups) { + final expectedGroup = testGroups[group.id]!; + + expect(group.id, expectedGroup.id); + expect(group.name, expectedGroup.name); + expect(group.createdAt, expectedGroup.createdAt); + + expect(group.members.length, expectedGroup.members.length); + for (int i = 0; i < expectedGroup.members.length; i++) { + expect(group.members[i].id, expectedGroup.members[i].id); + expect(group.members[i].name, expectedGroup.members[i].name); + expect( + group.members[i].createdAt, + expectedGroup.members[i].createdAt, + ); + } + } }); - // TODO: Use upcoming addGroups() method - // TODO: An Test in Game Tests orientieren test('Adding the same group twice does not create duplicates', () async { await database.groupDao.addGroup(group: testgroup); await database.groupDao.addGroup(group: testgroup); diff --git a/test/db_tests/player_test.dart b/test/db_tests/player_test.dart index 9430433..aa5d09e 100644 --- a/test/db_tests/player_test.dart +++ b/test/db_tests/player_test.dart @@ -7,8 +7,10 @@ import 'package:game_tracker/data/dto/player.dart'; void main() { late AppDatabase database; - late Player testPlayer; + late Player testPlayer1; late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -22,27 +24,29 @@ void main() { ); withClock(fakeClock, () { - testPlayer = Player(name: 'Test Player'); + testPlayer1 = Player(name: 'Test Player'); testPlayer2 = Player(name: 'Second Player'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); }); }); tearDown(() async { await database.close(); }); - group('player tests', () { + group('Player Tests', () { test('Adding and fetching single player works correclty', () async { - await database.playerDao.addPlayer(player: testPlayer); + await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer2); final allPlayers = await database.playerDao.getAllPlayers(); expect(allPlayers.length, 2); final fetchedPlayer1 = allPlayers.firstWhere( - (g) => g.id == testPlayer.id, + (g) => g.id == testPlayer1.id, ); - expect(fetchedPlayer1.name, testPlayer.name); - expect(fetchedPlayer1.createdAt, testPlayer.createdAt); + expect(fetchedPlayer1.name, testPlayer1.name); + expect(fetchedPlayer1.createdAt, testPlayer1.createdAt); final fetchedPlayer2 = allPlayers.firstWhere( (g) => g.id == testPlayer2.id, @@ -51,31 +55,36 @@ void main() { expect(fetchedPlayer2.createdAt, testPlayer2.createdAt); }); - // TODO: Use upcoming addPlayers() method - // TODO: An Tests in Game orientieren test('Adding and fetching multiple players works correclty', () async { - await database.playerDao.addPlayer(player: testPlayer); + // TODO: Use upcoming addPlayers() method + await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer2); + await database.playerDao.addPlayer(player: testPlayer3); + await database.playerDao.addPlayer(player: testPlayer4); final allPlayers = await database.playerDao.getAllPlayers(); - expect(allPlayers.length, 2); + expect(allPlayers.length, 4); - final fetchedPlayer1 = allPlayers.firstWhere( - (g) => g.id == testPlayer.id, - ); - expect(fetchedPlayer1.name, testPlayer.name); - expect(fetchedPlayer1.createdAt, testPlayer.createdAt); + // Map for connencting fetched players with expected players + final testPlayer = { + testPlayer1.id: testPlayer1, + testPlayer2.id: testPlayer2, + testPlayer3.id: testPlayer3, + testPlayer4.id: testPlayer4, + }; - final fetchedPlayer2 = allPlayers.firstWhere( - (g) => g.id == testPlayer2.id, - ); - expect(fetchedPlayer2.name, testPlayer2.name); - expect(fetchedPlayer2.createdAt, testPlayer2.createdAt); + for (final player in allPlayers) { + final expectedPlayer = testPlayer[player.id]!; + + expect(player.id, expectedPlayer.id); + expect(player.name, expectedPlayer.name); + expect(player.createdAt, expectedPlayer.createdAt); + } }); test('Adding the same player twice does not create duplicates', () async { - await database.playerDao.addPlayer(player: testPlayer); - await database.playerDao.addPlayer(player: testPlayer); + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer1); final allPlayers = await database.playerDao.getAllPlayers(); expect(allPlayers.length, 1); @@ -83,43 +92,43 @@ void main() { test('Player existence check works correctly', () async { var playerExists = await database.playerDao.playerExists( - playerId: testPlayer.id, + playerId: testPlayer1.id, ); expect(playerExists, false); - await database.playerDao.addPlayer(player: testPlayer); + await database.playerDao.addPlayer(player: testPlayer1); playerExists = await database.playerDao.playerExists( - playerId: testPlayer.id, + playerId: testPlayer1.id, ); expect(playerExists, true); }); test('Deleting a player works correclty', () async { - await database.playerDao.addPlayer(player: testPlayer); + await database.playerDao.addPlayer(player: testPlayer1); final playerDeleted = await database.playerDao.deletePlayer( - playerId: testPlayer.id, + playerId: testPlayer1.id, ); expect(playerDeleted, true); final playerExists = await database.playerDao.playerExists( - playerId: testPlayer.id, + playerId: testPlayer1.id, ); expect(playerExists, false); }); test('Updating a player name works correcly', () async { - await database.playerDao.addPlayer(player: testPlayer); + await database.playerDao.addPlayer(player: testPlayer1); const newPlayerName = 'new player name'; await database.playerDao.updatePlayername( - playerId: testPlayer.id, + playerId: testPlayer1.id, newName: newPlayerName, ); final result = await database.playerDao.getPlayerById( - playerId: testPlayer.id, + playerId: testPlayer1.id, ); expect(result.name, newPlayerName); }); @@ -128,7 +137,7 @@ void main() { var playerCount = await database.playerDao.getPlayerCount(); expect(playerCount, 0); - await database.playerDao.addPlayer(player: testPlayer); + await database.playerDao.addPlayer(player: testPlayer1); playerCount = await database.playerDao.getPlayerCount(); expect(playerCount, 1); @@ -138,7 +147,7 @@ void main() { playerCount = await database.playerDao.getPlayerCount(); expect(playerCount, 2); - await database.playerDao.deletePlayer(playerId: testPlayer.id); + await database.playerDao.deletePlayer(playerId: testPlayer1.id); playerCount = await database.playerDao.getPlayerCount(); expect(playerCount, 1); From 8c0538520353ba9e42ff893a7f800d32591acf97 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 13:47:27 +0100 Subject: [PATCH 42/95] Renamed variables to be consistent --- test/db_tests/game_test.dart | 133 ++++++++++++++------------- test/db_tests/group_game_test.dart | 28 +++--- test/db_tests/group_test.dart | 96 +++++++++---------- test/db_tests/player_game_test.dart | 82 ++++++++--------- test/db_tests/player_group_test.dart | 24 ++--- 5 files changed, 183 insertions(+), 180 deletions(-) diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index 71b7573..a7163a3 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -9,17 +9,17 @@ import 'package:game_tracker/data/dto/player.dart'; void main() { late AppDatabase database; - late Player player1; - late Player player2; - late Player player3; - late Player player4; - late Player player5; - late Group testgroup; - late Group testgroup2; - late Game testgame1; - late Game testgame2; - late Game testgameWithPlayer; - late Game testgameWithGroup; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; + late Player testPlayer5; + late Group testGroup1; + late Group testGroup2; + late Game testGame1; + late Game testGame2; + late Game testGameOnlyPlayers; + late Game testGameOnlyGroup; final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -33,31 +33,34 @@ void main() { ); withClock(fakeClock, () { - player1 = Player(name: 'Alice'); - player2 = Player(name: 'Bob'); - player3 = Player(name: 'Charlie'); - player4 = Player(name: 'Diana'); - player5 = Player(name: 'Eve'); - testgroup = Group( + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testPlayer5 = Player(name: 'Eve'); + testGroup1 = Group( name: 'Test Group 2', - members: [player1, player2, player3], + members: [testPlayer1, testPlayer2, testPlayer3], ); - testgroup2 = Group(name: 'Test Group 2', members: [player4, player5]); - testgame1 = Game( - name: 'Test Game', - group: testgroup, - players: [player4, player5], + testGroup2 = Group( + name: 'Test Group 2', + members: [testPlayer4, testPlayer5], ); - testgame2 = Game( + testGame1 = Game( + name: 'First Test Game', + group: testGroup1, + players: [testPlayer4, testPlayer5], + ); + testGame2 = Game( name: 'Second Test Game', - group: testgroup2, - players: [player1, player2, player3], + group: testGroup2, + players: [testPlayer1, testPlayer2, testPlayer3], ); - testgameWithPlayer = Game( - name: 'Second Test Game', - players: [player1, player2, player3], + testGameOnlyPlayers = Game( + name: 'Test Game with Players', + players: [testPlayer1, testPlayer2, testPlayer3], ); - testgameWithGroup = Game(name: 'Second Test Game', group: testgroup2); + testGameOnlyGroup = Game(name: 'Test Game with Group', group: testGroup2); }); }); tearDown(() async { @@ -66,32 +69,32 @@ void main() { group('Game Tests', () { test('Adding and fetching single game works correclty', () async { - await database.gameDao.addGame(game: testgame1); + await database.gameDao.addGame(game: testGame1); - final result = await database.gameDao.getGameById(gameId: testgame1.id); + final result = await database.gameDao.getGameById(gameId: testGame1.id); - expect(result.id, testgame1.id); - expect(result.name, testgame1.name); - expect(result.winner, testgame1.winner); - expect(result.createdAt, testgame1.createdAt); + expect(result.id, testGame1.id); + expect(result.name, testGame1.name); + expect(result.winner, testGame1.winner); + expect(result.createdAt, testGame1.createdAt); if (result.group != null) { - expect(result.group!.members.length, testgroup.members.length); + expect(result.group!.members.length, testGroup1.members.length); - for (int i = 0; i < testgroup.members.length; i++) { - expect(result.group!.members[i].id, testgroup.members[i].id); - expect(result.group!.members[i].name, testgroup.members[i].name); + 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'); } if (result.players != null) { - expect(result.players!.length, testgame1.players!.length); + expect(result.players!.length, testGame1.players!.length); - for (int i = 0; i < testgame1.players!.length; i++) { - expect(result.players![i].id, testgame1.players![i].id); - expect(result.players![i].name, testgame1.players![i].name); - expect(result.players![i].createdAt, testgame1.players![i].createdAt); + for (int i = 0; i < testGame1.players!.length; i++) { + expect(result.players![i].id, testGame1.players![i].id); + expect(result.players![i].name, testGame1.players![i].name); + expect(result.players![i].createdAt, testGame1.players![i].createdAt); } } else { fail('Players is null'); @@ -100,19 +103,19 @@ void main() { test('Adding and fetching multiple games works correctly', () async { // TODO: Use upcoming addGames() method - await database.gameDao.addGame(game: testgame1); - await database.gameDao.addGame(game: testgame2); - await database.gameDao.addGame(game: testgameWithGroup); - await database.gameDao.addGame(game: testgameWithPlayer); + await database.gameDao.addGame(game: testGame1); + await database.gameDao.addGame(game: testGame2); + await database.gameDao.addGame(game: testGameOnlyGroup); + await database.gameDao.addGame(game: testGameOnlyPlayers); final allGames = await database.gameDao.getAllGames(); expect(allGames.length, 4); final testGames = { - testgame1.id: testgame1, - testgame2.id: testgame2, - testgameWithGroup.id: testgameWithGroup, - testgameWithPlayer.id: testgameWithPlayer, + testGame1.id: testGame1, + testGame2.id: testGame2, + testGameOnlyGroup.id: testGameOnlyGroup, + testGameOnlyPlayers.id: testGameOnlyPlayers, }; for (final game in allGames) { @@ -167,33 +170,33 @@ void main() { }); test('Adding the same game twice does not create duplicates', () async { - await database.gameDao.addGame(game: testgame1); - await database.gameDao.addGame(game: testgame1); + await database.gameDao.addGame(game: testGame1); + await database.gameDao.addGame(game: testGame1); final gameCount = await database.gameDao.getGameCount(); expect(gameCount, 1); }); test('Game existence check works correctly', () async { - var gameExists = await database.gameDao.gameExists(gameId: testgame1.id); + var gameExists = await database.gameDao.gameExists(gameId: testGame1.id); expect(gameExists, false); - await database.gameDao.addGame(game: testgame1); + await database.gameDao.addGame(game: testGame1); - gameExists = await database.gameDao.gameExists(gameId: testgame1.id); + gameExists = await database.gameDao.gameExists(gameId: testGame1.id); expect(gameExists, true); }); test('Deleting a game works correclty', () async { - await database.gameDao.addGame(game: testgame1); + await database.gameDao.addGame(game: testGame1); final gameDeleted = await database.gameDao.deleteGame( - gameId: testgame1.id, + gameId: testGame1.id, ); expect(gameDeleted, true); final gameExists = await database.gameDao.gameExists( - gameId: testgame1.id, + gameId: testGame1.id, ); expect(gameExists, false); }); @@ -202,22 +205,22 @@ void main() { var gameCount = await database.gameDao.getGameCount(); expect(gameCount, 0); - await database.gameDao.addGame(game: testgame1); + await database.gameDao.addGame(game: testGame1); gameCount = await database.gameDao.getGameCount(); expect(gameCount, 1); - await database.gameDao.addGame(game: testgame2); + await database.gameDao.addGame(game: testGame2); gameCount = await database.gameDao.getGameCount(); expect(gameCount, 2); - await database.gameDao.deleteGame(gameId: testgame1.id); + await database.gameDao.deleteGame(gameId: testGame1.id); gameCount = await database.gameDao.getGameCount(); expect(gameCount, 1); - await database.gameDao.deleteGame(gameId: testgame2.id); + await database.gameDao.deleteGame(gameId: testGame2.id); gameCount = await database.gameDao.getGameCount(); expect(gameCount, 0); diff --git a/test/db_tests/group_game_test.dart b/test/db_tests/group_game_test.dart index e231284..49d3cdb 100644 --- a/test/db_tests/group_game_test.dart +++ b/test/db_tests/group_game_test.dart @@ -9,11 +9,11 @@ import 'package:game_tracker/data/dto/player.dart'; void main() { late AppDatabase database; - late Player player1; - late Player player2; - late Player player3; - late Player player4; - late Player player5; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; + late Player testPlayer5; late Group testgroup; late Game testgameWithGroup; late Game testgameWithPlayers; @@ -30,20 +30,20 @@ void main() { ); withClock(fakeClock, () { - player1 = Player(name: 'Alice'); - player2 = Player(name: 'Bob'); - player3 = Player(name: 'Charlie'); - player4 = Player(name: 'Diana'); - player5 = Player(name: 'Eve'); + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testPlayer5 = Player(name: 'Eve'); testgroup = Group( name: 'Test Group', - members: [player1, player2, player3], + members: [testPlayer1, testPlayer2, testPlayer3], ); testgameWithPlayers = Game( - name: 'Game with Players', - players: [player4, player5], + name: 'Test Game with Players', + players: [testPlayer4, testPlayer5], ); - testgameWithGroup = Game(name: 'Game with Group', group: testgroup); + testgameWithGroup = Game(name: 'Test Game with Group', group: testgroup); }); }); tearDown(() async { diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart index 189e4c3..2cf9bba 100644 --- a/test/db_tests/group_test.dart +++ b/test/db_tests/group_test.dart @@ -8,14 +8,14 @@ import 'package:game_tracker/data/dto/player.dart'; void main() { late AppDatabase database; - late Player player1; - late Player player2; - late Player player3; - late Player player4; - late Group testgroup; - late Group testgroup2; - late Group testgroup3; - late Group testgroup4; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; + late Group testGroup1; + late Group testGroup2; + late Group testGroup3; + late Group testGroup4; final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -29,28 +29,28 @@ void main() { ); withClock(fakeClock, () { - player1 = Player(name: 'Alice'); - player2 = Player(name: 'Bob'); - player3 = Player(name: 'Charlie'); - player4 = Player(name: 'Diana'); - testgroup = Group( + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testGroup1 = Group( name: 'Test Group', - members: [player1, player2, player3], + members: [testPlayer1, testPlayer2, testPlayer3], ); - testgroup2 = Group( + testGroup2 = Group( id: 'gr2', name: 'Second Group', - members: [player2, player3, player4], + members: [testPlayer2, testPlayer3, testPlayer4], ); - testgroup3 = Group( + testGroup3 = Group( id: 'gr2', name: 'Second Group', - members: [player2, player4], + members: [testPlayer2, testPlayer4], ); - testgroup4 = Group( + testGroup4 = Group( id: 'gr2', name: 'Second Group', - members: [player1, player2, player3, player4], + members: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], ); }); }); @@ -59,38 +59,38 @@ void main() { }); group('Group Tests', () { test('Adding and fetching a single group works correctly', () async { - await database.groupDao.addGroup(group: testgroup); + await database.groupDao.addGroup(group: testGroup1); final fetchedGroup = await database.groupDao.getGroupById( - groupId: testgroup.id, + groupId: testGroup1.id, ); - expect(fetchedGroup.id, testgroup.id); - expect(fetchedGroup.name, testgroup.name); - expect(fetchedGroup.createdAt, testgroup.createdAt); + expect(fetchedGroup.id, testGroup1.id); + expect(fetchedGroup.name, testGroup1.name); + expect(fetchedGroup.createdAt, testGroup1.createdAt); - expect(fetchedGroup.members.length, testgroup.members.length); - for (int i = 0; i < testgroup.members.length; i++) { - expect(fetchedGroup.members[i].id, testgroup.members[i].id); - expect(fetchedGroup.members[i].name, testgroup.members[i].name); + 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, - testgroup.members[i].createdAt, + testGroup1.members[i].createdAt, ); } }); test('Adding and fetching multiple groups works correctly', () async { // TODO: Use upcoming addGroups() method - await database.groupDao.addGroup(group: testgroup); - await database.groupDao.addGroup(group: testgroup2); - await database.groupDao.addGroup(group: testgroup3); - await database.groupDao.addGroup(group: testgroup4); + await database.groupDao.addGroup(group: testGroup1); + await database.groupDao.addGroup(group: testGroup2); + await database.groupDao.addGroup(group: testGroup3); + await database.groupDao.addGroup(group: testGroup4); final allGroups = await database.groupDao.getAllGroups(); expect(allGroups.length, 2); - final testGroups = {testgroup.id: testgroup, testgroup2.id: testgroup2}; + final testGroups = {testGroup1.id: testGroup1, testGroup2.id: testGroup2}; for (final group in allGroups) { final expectedGroup = testGroups[group.id]!; @@ -112,8 +112,8 @@ void main() { }); test('Adding the same group twice does not create duplicates', () async { - await database.groupDao.addGroup(group: testgroup); - await database.groupDao.addGroup(group: testgroup); + await database.groupDao.addGroup(group: testGroup1); + await database.groupDao.addGroup(group: testGroup1); final allGroups = await database.groupDao.getAllGroups(); expect(allGroups.length, 1); @@ -121,42 +121,42 @@ void main() { test('Group existence check works correctly', () async { var groupExists = await database.groupDao.groupExists( - groupId: testgroup.id, + groupId: testGroup1.id, ); expect(groupExists, false); - await database.groupDao.addGroup(group: testgroup); + await database.groupDao.addGroup(group: testGroup1); - groupExists = await database.groupDao.groupExists(groupId: testgroup.id); + groupExists = await database.groupDao.groupExists(groupId: testGroup1.id); expect(groupExists, true); }); test('Deleting a group works correclty', () async { - await database.groupDao.addGroup(group: testgroup); + await database.groupDao.addGroup(group: testGroup1); final groupDeleted = await database.groupDao.deleteGroup( - groupId: testgroup.id, + groupId: testGroup1.id, ); expect(groupDeleted, true); final groupExists = await database.groupDao.groupExists( - groupId: testgroup.id, + groupId: testGroup1.id, ); expect(groupExists, false); }); test('Updating a group name works correcly', () async { - await database.groupDao.addGroup(group: testgroup); + await database.groupDao.addGroup(group: testGroup1); const newGroupName = 'new group name'; await database.groupDao.updateGroupname( - groupId: testgroup.id, + groupId: testGroup1.id, newName: newGroupName, ); final result = await database.groupDao.getGroupById( - groupId: testgroup.id, + groupId: testGroup1.id, ); expect(result.name, newGroupName); }); @@ -165,13 +165,13 @@ void main() { final initialCount = await database.groupDao.getGroupCount(); expect(initialCount, 0); - await database.groupDao.addGroup(group: testgroup); + await database.groupDao.addGroup(group: testGroup1); final groupAdded = await database.groupDao.getGroupCount(); expect(groupAdded, 1); final groupRemoved = await database.groupDao.deleteGroup( - groupId: testgroup.id, + groupId: testGroup1.id, ); expect(groupRemoved, true); diff --git a/test/db_tests/player_game_test.dart b/test/db_tests/player_game_test.dart index 1fd0128..0eca9ff 100644 --- a/test/db_tests/player_game_test.dart +++ b/test/db_tests/player_game_test.dart @@ -9,15 +9,15 @@ import 'package:game_tracker/data/dto/player.dart'; void main() { late AppDatabase database; - late Player player1; - late Player player2; - late Player player3; - late Player player4; - late Player player5; - late Player player6; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; + late Player testPlayer5; + late Player testPlayer6; late Group testgroup; - late Game testgameWithGroup; - late Game testgameWithPlayers; + late Game testGameOnlyGroup; + late Game testGameOnlyPlayers; final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -31,20 +31,20 @@ void main() { ); withClock(fakeClock, () { - player1 = Player(name: 'Alice'); - player2 = Player(name: 'Bob'); - player3 = Player(name: 'Charlie'); - player4 = Player(name: 'Diana'); - player5 = Player(name: 'Eve'); - player6 = Player(name: 'Frank'); + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testPlayer5 = Player(name: 'Eve'); + testPlayer6 = Player(name: 'Frank'); testgroup = Group( name: 'Test Group', - members: [player1, player2, player3], + members: [testPlayer1, testPlayer2, testPlayer3], ); - testgameWithGroup = Game(name: 'Test Game', group: testgroup); - testgameWithPlayers = Game( + testGameOnlyGroup = Game(name: 'Test Game with Group', group: testgroup); + testGameOnlyPlayers = Game( name: 'Test Game with Players', - players: [player4, player5, player6], + players: [testPlayer4, testPlayer5, testPlayer6], ); }); }); @@ -54,44 +54,44 @@ void main() { group('Player-Game Tests', () { test('Game has player works correctly', () async { - database.gameDao.addGame(game: testgameWithGroup); - database.playerDao.addPlayer(player: player1); + database.gameDao.addGame(game: testGameOnlyGroup); + database.playerDao.addPlayer(player: testPlayer1); var gameHasPlayers = await database.playerGameDao.gameHasPlayers( - gameId: testgameWithGroup.id, + gameId: testGameOnlyGroup.id, ); expect(gameHasPlayers, false); database.playerGameDao.addPlayerToGame( - gameId: testgameWithGroup.id, - playerId: player1.id, + gameId: testGameOnlyGroup.id, + playerId: testPlayer1.id, ); gameHasPlayers = await database.playerGameDao.gameHasPlayers( - gameId: testgameWithGroup.id, + gameId: testGameOnlyGroup.id, ); expect(gameHasPlayers, true); }); test('Adding a player to a game works correctly', () async { - database.gameDao.addGame(game: testgameWithGroup); - database.playerDao.addPlayer(player: player5); + database.gameDao.addGame(game: testGameOnlyGroup); + database.playerDao.addPlayer(player: testPlayer5); database.playerGameDao.addPlayerToGame( - gameId: testgameWithGroup.id, - playerId: player5.id, + gameId: testGameOnlyGroup.id, + playerId: testPlayer5.id, ); var playerAdded = await database.playerGameDao.isPlayerInGame( - gameId: testgameWithGroup.id, - playerId: player5.id, + gameId: testGameOnlyGroup.id, + playerId: testPlayer5.id, ); expect(playerAdded, true); playerAdded = await database.playerGameDao.isPlayerInGame( - gameId: testgameWithGroup.id, + gameId: testGameOnlyGroup.id, playerId: '', ); @@ -99,20 +99,20 @@ void main() { }); test('Removing player from game works correctly', () async { - await database.gameDao.addGame(game: testgameWithPlayers); + await database.gameDao.addGame(game: testGameOnlyPlayers); - final playerToRemove = testgameWithPlayers.players![0]; + final playerToRemove = testGameOnlyPlayers.players![0]; final removed = await database.playerGameDao.removePlayerFromGame( playerId: playerToRemove.id, - gameId: testgameWithPlayers.id, + gameId: testGameOnlyPlayers.id, ); expect(removed, true); final result = await database.gameDao.getGameById( - gameId: testgameWithPlayers.id, + gameId: testGameOnlyPlayers.id, ); - expect(result.players!.length, testgameWithPlayers.players!.length - 1); + expect(result.players!.length, testGameOnlyPlayers.players!.length - 1); final playerExists = result.players!.any( (p) => p.id == playerToRemove.id, @@ -121,9 +121,9 @@ void main() { }); test('Retrieving players of a game works correctly', () async { - await database.gameDao.addGame(game: testgameWithPlayers); + await database.gameDao.addGame(game: testGameOnlyPlayers); final players = await database.playerGameDao.getPlayersOfGame( - gameId: testgameWithPlayers.id, + gameId: testGameOnlyPlayers.id, ); if (players == null) { @@ -131,9 +131,9 @@ void main() { } for (int i = 0; i < players.length; i++) { - expect(players[i].id, testgameWithPlayers.players![i].id); - expect(players[i].name, testgameWithPlayers.players![i].name); - expect(players[i].createdAt, testgameWithPlayers.players![i].createdAt); + expect(players[i].id, testGameOnlyPlayers.players![i].id); + expect(players[i].name, testGameOnlyPlayers.players![i].name); + expect(players[i].createdAt, testGameOnlyPlayers.players![i].createdAt); } }); }); diff --git a/test/db_tests/player_group_test.dart b/test/db_tests/player_group_test.dart index 9e367e0..2783430 100644 --- a/test/db_tests/player_group_test.dart +++ b/test/db_tests/player_group_test.dart @@ -8,10 +8,10 @@ import 'package:game_tracker/data/dto/player.dart'; void main() { late AppDatabase database; - late Player player1; - late Player player2; - late Player player3; - late Player player4; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; late Group testgroup; final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -26,13 +26,13 @@ void main() { ); withClock(fakeClock, () { - player1 = Player(name: 'Alice'); - player2 = Player(name: 'Bob'); - player3 = Player(name: 'Charlie'); - player4 = Player(name: 'Diana'); + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); testgroup = Group( name: 'Test Group', - members: [player1, player2, player3], + members: [testPlayer1, testPlayer2, testPlayer3], ); }); }); @@ -46,15 +46,15 @@ void main() { test('Adding a player to a group works correctly', () async { await database.groupDao.addGroup(group: testgroup); - await database.playerDao.addPlayer(player: player4); + await database.playerDao.addPlayer(player: testPlayer4); await database.playerGroupDao.addPlayerToGroup( groupId: testgroup.id, - player: player4, + player: testPlayer4, ); var playerAdded = await database.playerGroupDao.isPlayerInGroup( groupId: testgroup.id, - playerId: player4.id, + playerId: testPlayer4.id, ); expect(playerAdded, true); From 51722eb7fd25dbfe321df3f853f705e46df89d0f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 14:01:45 +0100 Subject: [PATCH 43/95] Added batch insert methods to tests --- test/db_tests/game_test.dart | 8 +++----- test/db_tests/group_test.dart | 8 +++----- test/db_tests/player_test.dart | 8 +++----- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index a7163a3..86ca34b 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -102,11 +102,9 @@ void main() { }); test('Adding and fetching multiple games works correctly', () async { - // TODO: Use upcoming addGames() method - await database.gameDao.addGame(game: testGame1); - await database.gameDao.addGame(game: testGame2); - await database.gameDao.addGame(game: testGameOnlyGroup); - await database.gameDao.addGame(game: testGameOnlyPlayers); + await database.gameDao.addGames( + games: [testGame1, testGame2, testGameOnlyGroup, testGameOnlyPlayers], + ); final allGames = await database.gameDao.getAllGames(); expect(allGames.length, 4); diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart index 2cf9bba..b2e63eb 100644 --- a/test/db_tests/group_test.dart +++ b/test/db_tests/group_test.dart @@ -81,11 +81,9 @@ void main() { }); test('Adding and fetching multiple groups works correctly', () async { - // TODO: Use upcoming addGroups() method - await database.groupDao.addGroup(group: testGroup1); - await database.groupDao.addGroup(group: testGroup2); - await database.groupDao.addGroup(group: testGroup3); - await database.groupDao.addGroup(group: testGroup4); + await database.groupDao.addGroups( + groups: [testGroup1, testGroup2, testGroup3, testGroup4], + ); final allGroups = await database.groupDao.getAllGroups(); expect(allGroups.length, 2); diff --git a/test/db_tests/player_test.dart b/test/db_tests/player_test.dart index aa5d09e..0d72ed5 100644 --- a/test/db_tests/player_test.dart +++ b/test/db_tests/player_test.dart @@ -56,11 +56,9 @@ void main() { }); test('Adding and fetching multiple players works correclty', () async { - // TODO: Use upcoming addPlayers() method - await database.playerDao.addPlayer(player: testPlayer1); - await database.playerDao.addPlayer(player: testPlayer2); - await database.playerDao.addPlayer(player: testPlayer3); - await database.playerDao.addPlayer(player: testPlayer4); + await database.playerDao.addPlayers( + players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], + ); final allPlayers = await database.playerDao.getAllPlayers(); expect(allPlayers.length, 4); From c56663d15e1a4caef27193254846c507f52be363 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 14:06:11 +0100 Subject: [PATCH 44/95] Added missing await --- test/db_tests/group_game_test.dart | 12 ++++++------ test/db_tests/player_game_test.dart | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/db_tests/group_game_test.dart b/test/db_tests/group_game_test.dart index 49d3cdb..1733243 100644 --- a/test/db_tests/group_game_test.dart +++ b/test/db_tests/group_game_test.dart @@ -51,8 +51,8 @@ void main() { }); group('Group-Game Tests', () { test('Game has group works correctly', () async { - database.gameDao.addGame(game: testgameWithPlayers); - database.groupDao.addGroup(group: testgroup); + await database.gameDao.addGame(game: testgameWithPlayers); + await database.groupDao.addGroup(group: testgroup); var gameHasGroup = await database.groupGameDao.gameHasGroup( gameId: testgameWithPlayers.id, @@ -60,7 +60,7 @@ void main() { expect(gameHasGroup, false); - database.groupGameDao.addGroupToGame( + await database.groupGameDao.addGroupToGame( testgameWithPlayers.id, testgroup.id, ); @@ -73,9 +73,9 @@ void main() { }); test('Adding a group to a game works correctly', () async { - database.gameDao.addGame(game: testgameWithPlayers); - database.groupDao.addGroup(group: testgroup); - database.groupGameDao.addGroupToGame( + await database.gameDao.addGame(game: testgameWithPlayers); + await database.groupDao.addGroup(group: testgroup); + await database.groupGameDao.addGroupToGame( testgameWithPlayers.id, testgroup.id, ); diff --git a/test/db_tests/player_game_test.dart b/test/db_tests/player_game_test.dart index 0eca9ff..e8fd707 100644 --- a/test/db_tests/player_game_test.dart +++ b/test/db_tests/player_game_test.dart @@ -54,8 +54,8 @@ void main() { group('Player-Game Tests', () { test('Game has player works correctly', () async { - database.gameDao.addGame(game: testGameOnlyGroup); - database.playerDao.addPlayer(player: testPlayer1); + await database.gameDao.addGame(game: testGameOnlyGroup); + await database.playerDao.addPlayer(player: testPlayer1); var gameHasPlayers = await database.playerGameDao.gameHasPlayers( gameId: testGameOnlyGroup.id, @@ -63,7 +63,7 @@ void main() { expect(gameHasPlayers, false); - database.playerGameDao.addPlayerToGame( + await database.playerGameDao.addPlayerToGame( gameId: testGameOnlyGroup.id, playerId: testPlayer1.id, ); @@ -76,9 +76,9 @@ void main() { }); test('Adding a player to a game works correctly', () async { - database.gameDao.addGame(game: testGameOnlyGroup); - database.playerDao.addPlayer(player: testPlayer5); - database.playerGameDao.addPlayerToGame( + await database.gameDao.addGame(game: testGameOnlyGroup); + await database.playerDao.addPlayer(player: testPlayer5); + await database.playerGameDao.addPlayerToGame( gameId: testGameOnlyGroup.id, playerId: testPlayer5.id, ); From dbb52cfc48546f69245297a0bf3b883d5bd8ba29 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 14:20:42 +0100 Subject: [PATCH 45/95] Added missing awaits --- test/db_tests/group_game_test.dart | 12 ++++++------ test/db_tests/player_game_test.dart | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/db_tests/group_game_test.dart b/test/db_tests/group_game_test.dart index 49d3cdb..1733243 100644 --- a/test/db_tests/group_game_test.dart +++ b/test/db_tests/group_game_test.dart @@ -51,8 +51,8 @@ void main() { }); group('Group-Game Tests', () { test('Game has group works correctly', () async { - database.gameDao.addGame(game: testgameWithPlayers); - database.groupDao.addGroup(group: testgroup); + await database.gameDao.addGame(game: testgameWithPlayers); + await database.groupDao.addGroup(group: testgroup); var gameHasGroup = await database.groupGameDao.gameHasGroup( gameId: testgameWithPlayers.id, @@ -60,7 +60,7 @@ void main() { expect(gameHasGroup, false); - database.groupGameDao.addGroupToGame( + await database.groupGameDao.addGroupToGame( testgameWithPlayers.id, testgroup.id, ); @@ -73,9 +73,9 @@ void main() { }); test('Adding a group to a game works correctly', () async { - database.gameDao.addGame(game: testgameWithPlayers); - database.groupDao.addGroup(group: testgroup); - database.groupGameDao.addGroupToGame( + await database.gameDao.addGame(game: testgameWithPlayers); + await database.groupDao.addGroup(group: testgroup); + await database.groupGameDao.addGroupToGame( testgameWithPlayers.id, testgroup.id, ); diff --git a/test/db_tests/player_game_test.dart b/test/db_tests/player_game_test.dart index 0eca9ff..f50afc1 100644 --- a/test/db_tests/player_game_test.dart +++ b/test/db_tests/player_game_test.dart @@ -54,8 +54,8 @@ void main() { group('Player-Game Tests', () { test('Game has player works correctly', () async { - database.gameDao.addGame(game: testGameOnlyGroup); - database.playerDao.addPlayer(player: testPlayer1); + await database.gameDao.addGame(game: testGameOnlyGroup); + await database.playerDao.addPlayer(player: testPlayer1); var gameHasPlayers = await database.playerGameDao.gameHasPlayers( gameId: testGameOnlyGroup.id, @@ -76,9 +76,9 @@ void main() { }); test('Adding a player to a game works correctly', () async { - database.gameDao.addGame(game: testGameOnlyGroup); - database.playerDao.addPlayer(player: testPlayer5); - database.playerGameDao.addPlayerToGame( + await database.gameDao.addGame(game: testGameOnlyGroup); + await database.playerDao.addPlayer(player: testPlayer5); + await database.playerGameDao.addPlayerToGame( gameId: testGameOnlyGroup.id, playerId: testPlayer5.id, ); From ab250e2df43d1880d6d0e0c190a2ca00b7be76d2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 21 Nov 2025 14:24:57 +0100 Subject: [PATCH 46/95] Typo --- test/db_tests/game_test.dart | 4 ++-- test/db_tests/group_test.dart | 2 +- test/db_tests/player_test.dart | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index a7163a3..311e30f 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -68,7 +68,7 @@ void main() { }); group('Game Tests', () { - test('Adding and fetching single game works correclty', () async { + test('Adding and fetching single game works correctly', () async { await database.gameDao.addGame(game: testGame1); final result = await database.gameDao.getGameById(gameId: testGame1.id); @@ -187,7 +187,7 @@ void main() { expect(gameExists, true); }); - test('Deleting a game works correclty', () async { + test('Deleting a game works correctly', () async { await database.gameDao.addGame(game: testGame1); final gameDeleted = await database.gameDao.deleteGame( diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart index 2cf9bba..9104b74 100644 --- a/test/db_tests/group_test.dart +++ b/test/db_tests/group_test.dart @@ -131,7 +131,7 @@ void main() { expect(groupExists, true); }); - test('Deleting a group works correclty', () async { + test('Deleting a group works correctly', () async { await database.groupDao.addGroup(group: testGroup1); final groupDeleted = await database.groupDao.deleteGroup( diff --git a/test/db_tests/player_test.dart b/test/db_tests/player_test.dart index aa5d09e..2ec57e5 100644 --- a/test/db_tests/player_test.dart +++ b/test/db_tests/player_test.dart @@ -35,7 +35,7 @@ void main() { }); group('Player Tests', () { - test('Adding and fetching single player works correclty', () async { + test('Adding and fetching single player works correctly', () async { await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer2); @@ -55,7 +55,7 @@ void main() { expect(fetchedPlayer2.createdAt, testPlayer2.createdAt); }); - test('Adding and fetching multiple players works correclty', () async { + test('Adding and fetching multiple players works correctly', () async { // TODO: Use upcoming addPlayers() method await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer2); @@ -104,7 +104,7 @@ void main() { expect(playerExists, true); }); - test('Deleting a player works correclty', () async { + test('Deleting a player works correctly', () async { await database.playerDao.addPlayer(player: testPlayer1); final playerDeleted = await database.playerDao.deletePlayer( playerId: testPlayer1.id, From 1b3334f3e0c7439d74931ce6554c393cbecd4079 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 00:47:24 +0100 Subject: [PATCH 47/95] Fixed addGroups method --- lib/data/dao/group_dao.dart | 53 +++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 3489f5c..fbb4d6f 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -87,10 +87,17 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { Future addGroups({required List groups}) async { if (groups.isEmpty) return; await db.transaction(() async { + // Deduplicate groups by id - keep first occurrence + final Map uniqueGroups = {}; + for (final g in groups) { + uniqueGroups.putIfAbsent(g.id, () => g); + } + + // Insert unique groups in batch await db.batch( (b) => b.insertAll( groupTable, - groups + uniqueGroups.values .map( (group) => GroupTableCompanion.insert( id: group.id, @@ -103,17 +110,24 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { ), ); - for (final group in groups) { - await db.playerDao.addPlayers(players: group.members); + // Collect unique players from all groups + final uniquePlayers = {}; + for (final g in uniqueGroups.values) { + for (final m in g.members) { + uniquePlayers[m.id] = m; + } + } + if (uniquePlayers.isNotEmpty) { await db.batch( (b) => b.insertAll( - db.playerGroupTable, - group.members + db.playerTable, + uniquePlayers.values .map( - (member) => PlayerGroupTableCompanion.insert( - playerId: member.id, - groupId: group.id, + (p) => PlayerTableCompanion.insert( + id: p.id, + name: p.name, + createdAt: p.createdAt, ), ) .toList(), @@ -121,6 +135,29 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { ), ); } + + // Prepare all player-group associations in one list (unique pairs) + final Set seenPairs = {}; + final List pgRows = []; + for (final g in uniqueGroups.values) { + for (final m in g.members) { + final key = '${m.id}|${g.id}'; + if (!seenPairs.contains(key)) { + seenPairs.add(key); + pgRows.add( + PlayerGroupTableCompanion.insert(playerId: m.id, groupId: g.id), + ); + } + } + } + + if (pgRows.isNotEmpty) { + await db.batch((b) { + for (final pg in pgRows) { + b.insert(db.playerGroupTable, pg, mode: InsertMode.insertOrReplace); + } + }); + } }); } From 70307016b36581f9e7495a8c149a74776ad5f6a2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 00:47:32 +0100 Subject: [PATCH 48/95] Fixed addGames method --- lib/data/dao/game_dao.dart | 78 +++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index 12893db..47b5848 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -107,6 +107,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { mode: InsertMode.insertOrReplace, ), ); + // Add all groups of the games in batch await db.batch( (b) => b.insertAll( @@ -125,8 +126,42 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { ), ); - // Add all players of the games in batch - await db.batch((b) async { + // Add all players of the games in batch (unique) + final uniquePlayers = {}; + for (final game in games) { + if (game.players != null) { + for (final p in game.players!) { + uniquePlayers[p.id] = p; + } + } + // Also include members of groups + if (game.group != null) { + for (final m in game.group!.members) { + uniquePlayers[m.id] = m; + } + } + } + + if (uniquePlayers.isNotEmpty) { + await db.batch( + (b) => b.insertAll( + db.playerTable, + uniquePlayers.values + .map( + (p) => PlayerTableCompanion.insert( + id: p.id, + name: p.name, + createdAt: p.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + } + + // Add all player-game associations in batch + await db.batch((b) { for (final game in games) { if (game.players != null) { for (final p in game.players ?? []) { @@ -143,8 +178,26 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { } }); + // Add all player-group associations in batch + await db.batch((b) { + for (final game in games) { + if (game.group != null) { + for (final m in game.group!.members) { + b.insert( + db.playerGroupTable, + PlayerGroupTableCompanion.insert( + playerId: m.id, + groupId: game.group!.id, + ), + mode: InsertMode.insertOrReplace, + ); + } + } + } + }); + // Add all group-game associations in batch - await db.batch((b) async { + await db.batch((b) { for (final game in games) { if (game.group != null) { b.insert( @@ -158,25 +211,6 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { } } }); - - // Add all player-game associations in batch - await db.batch((b) async { - for (final game in games) { - if (game.players != null) { - for (final p in game.players ?? []) { - b.insert( - db.playerTable, - PlayerTableCompanion.insert( - id: p.id, - name: p.name, - createdAt: p.createdAt, - ), - mode: InsertMode.insertOrReplace, - ); - } - } - } - }); }); } From 2ebd4274f05b22bde0dcb9f927de2e55d49f3f1a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 00:47:44 +0100 Subject: [PATCH 49/95] Moved method validateJsonSchema() --- .../views/main_menu/settings_view.dart | 25 ----------------- lib/services/data_transfer_service.dart | 28 +++++++++++++++++-- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index 70f7663..6ebb7fb 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -1,39 +1,14 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; import 'package:game_tracker/services/data_transfer_service.dart'; -import 'package:json_schema/json_schema.dart'; class SettingsView extends StatefulWidget { const SettingsView({super.key}); @override State createState() => _SettingsViewState(); - - static Future validateJsonSchema(String jsonString) async { - final String schemaString; - - schemaString = await rootBundle.loadString('assets/schema.json'); - - try { - final schema = JsonSchema.create(json.decode(schemaString)); - final jsonData = json.decode(jsonString); - final result = schema.validate(jsonData); - - if (result.isValid) { - return true; - } - return false; - } catch (e, stack) { - print('[validateJsonSchema] $e'); - print(stack); - return false; - } - } } class _SettingsViewState extends State { diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 13eb658..eaa9633 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -1,15 +1,15 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/game.dart'; import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/presentation/views/main_menu/settings_view.dart'; +import 'package:json_schema/json_schema.dart'; import 'package:provider/provider.dart'; class DataTransferService { @@ -85,7 +85,7 @@ class DataTransferService { return ImportResult.fileReadError; } - if (await SettingsView.validateJsonSchema(jsonString)) { + if (await _validateJsonSchema(jsonString)) { final Map jsonData = json.decode(jsonString) as Map; @@ -136,4 +136,26 @@ class DataTransferService { if (file.path != null) return await File(file.path!).readAsString(); return null; } + + /// Validates the given JSON string against the predefined schema. + static Future _validateJsonSchema(String jsonString) async { + final String schemaString; + + schemaString = await rootBundle.loadString('assets/schema.json'); + + try { + final schema = JsonSchema.create(json.decode(schemaString)); + final jsonData = json.decode(jsonString); + final result = schema.validate(jsonData); + + if (result.isValid) { + return true; + } + return false; + } catch (e, stack) { + print('[validateJsonSchema] $e'); + print(stack); + return false; + } + } } From 893eb91143a67ff175a1ee8fed77d7a3b4c7ba2b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 01:12:39 +0100 Subject: [PATCH 50/95] Schema corrected --- assets/schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/schema.json b/assets/schema.json index 69f889b..1883122 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -19,7 +19,7 @@ }, "players": { "type": [ - "object", + "array", "null" ], "properties": { @@ -88,7 +88,7 @@ ] }, "winner": { - "type": "string" + "type": ["string","null"] }, "required": [ "id", From 62eea086144522acde8a8396aa584f3dbd24b9b2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 14:12:41 +0100 Subject: [PATCH 51/95] Renamed variable --- lib/data/dao/game_dao.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index f29d553..283c02f 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -19,12 +19,12 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { return Future.wait( result.map((row) async { final group = await db.groupGameDao.getGroupOfGame(gameId: row.id); - final player = await db.playerGameDao.getPlayersOfGame(gameId: row.id); + final players = await db.playerGameDao.getPlayersOfGame(gameId: row.id); return Game( id: row.id, name: row.name, group: group, - players: player, + players: players, createdAt: row.createdAt, winner: row.winnerId, ); From 24f18f5c655088ecfa127495d8e34e8592aa1b9d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 14:13:15 +0100 Subject: [PATCH 52/95] Removed false comparison --- lib/data/dao/player_group_dao.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/data/dao/player_group_dao.dart b/lib/data/dao/player_group_dao.dart index 4024629..8cf96c2 100644 --- a/lib/data/dao/player_group_dao.dart +++ b/lib/data/dao/player_group_dao.dart @@ -25,7 +25,7 @@ class PlayerGroupDao extends DatabaseAccessor return false; } - if (await db.playerDao.playerExists(playerId: player.id) == false) { + if (!await db.playerDao.playerExists(playerId: player.id)) { db.playerDao.addPlayer(player: player); } From bef812502cbe4ca7ed4a38974fb443bfc913232d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 14:20:51 +0100 Subject: [PATCH 53/95] Renamed variable and added null checks --- test/db_tests/game_test.dart | 53 ++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index 311e30f..7590a97 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -119,52 +119,47 @@ void main() { }; for (final game in allGames) { - final expectedGame = testGames[game.id]!; + final testGame = testGames[game.id]!; // Game-Checks - expect(game.id, expectedGame.id); - expect(game.name, expectedGame.name); - expect(game.createdAt, expectedGame.createdAt); - expect(game.winner, expectedGame.winner); + expect(game.id, testGame.id); + expect(game.name, testGame.name); + expect(game.createdAt, testGame.createdAt); + expect(game.winner, testGame.winner); // Group-Checks - if (expectedGame.group != null) { - expect(game.group!.id, expectedGame.group!.id); - expect(game.group!.name, expectedGame.group!.name); - expect(game.group!.createdAt, expectedGame.group!.createdAt); + if (testGame.group != null) { + expect(game.group!.id, testGame.group!.id); + expect(game.group!.name, testGame.group!.name); + expect(game.group!.createdAt, testGame.group!.createdAt); // Group Members-Checks - expect( - game.group!.members.length, - expectedGame.group!.members.length, - ); - for (int i = 0; i < expectedGame.group!.members.length; i++) { - expect( - game.group!.members[i].id, - expectedGame.group!.members[i].id, - ); + expect(game.group!.members.length, testGame.group!.members.length); + for (int i = 0; i < testGame.group!.members.length; i++) { + expect(game.group!.members[i].id, testGame.group!.members[i].id); expect( game.group!.members[i].name, - expectedGame.group!.members[i].name, + testGame.group!.members[i].name, ); expect( game.group!.members[i].createdAt, - expectedGame.group!.members[i].createdAt, + testGame.group!.members[i].createdAt, ); } + } else { + expect(game.group, null); } // Players-Checks - if (expectedGame.players != null) { - expect(game.players!.length, expectedGame.players!.length); - for (int i = 0; i < expectedGame.players!.length; i++) { - expect(game.players![i].id, expectedGame.players![i].id); - expect(game.players![i].name, expectedGame.players![i].name); - expect( - game.players![i].createdAt, - expectedGame.players![i].createdAt, - ); + if (testGame.players != null) { + expect(game.players!.length, testGame.players!.length); + for (int i = 0; i < testGame.players!.length; i++) { + expect(game.players![i].id, testGame.players![i].id); + expect(game.players![i].name, testGame.players![i].name); + expect(game.players![i].createdAt, testGame.players![i].createdAt); } + } else { + expect(game.players, null); } } }); From 9346f61d141c1658e33101527e04ff6e88689f56 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 14:21:28 +0100 Subject: [PATCH 54/95] Renamed variable --- test/db_tests/group_test.dart | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart index 9104b74..6c8e6c9 100644 --- a/test/db_tests/group_test.dart +++ b/test/db_tests/group_test.dart @@ -93,20 +93,17 @@ void main() { final testGroups = {testGroup1.id: testGroup1, testGroup2.id: testGroup2}; for (final group in allGroups) { - final expectedGroup = testGroups[group.id]!; + final testGroup = testGroups[group.id]!; - expect(group.id, expectedGroup.id); - expect(group.name, expectedGroup.name); - expect(group.createdAt, expectedGroup.createdAt); + expect(group.id, testGroup.id); + expect(group.name, testGroup.name); + expect(group.createdAt, testGroup.createdAt); - expect(group.members.length, expectedGroup.members.length); - for (int i = 0; i < expectedGroup.members.length; i++) { - expect(group.members[i].id, expectedGroup.members[i].id); - expect(group.members[i].name, expectedGroup.members[i].name); - expect( - group.members[i].createdAt, - expectedGroup.members[i].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); } } }); From 30645f06f82c2c7cf20f9f79d1bb1f22aef3d59f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 14:21:55 +0100 Subject: [PATCH 55/95] Renamed variable --- test/db_tests/player_test.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/db_tests/player_test.dart b/test/db_tests/player_test.dart index 2ec57e5..30d9a14 100644 --- a/test/db_tests/player_test.dart +++ b/test/db_tests/player_test.dart @@ -66,7 +66,7 @@ void main() { expect(allPlayers.length, 4); // Map for connencting fetched players with expected players - final testPlayer = { + final testPlayers = { testPlayer1.id: testPlayer1, testPlayer2.id: testPlayer2, testPlayer3.id: testPlayer3, @@ -74,11 +74,11 @@ void main() { }; for (final player in allPlayers) { - final expectedPlayer = testPlayer[player.id]!; + final testPlayer = testPlayers[player.id]!; - expect(player.id, expectedPlayer.id); - expect(player.name, expectedPlayer.name); - expect(player.createdAt, expectedPlayer.createdAt); + expect(player.id, testPlayer.id); + expect(player.name, testPlayer.name); + expect(player.createdAt, testPlayer.createdAt); } }); From 63d9ed400d1fc61614dee1df4934ebfaf9cddee6 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 16:18:37 +0100 Subject: [PATCH 56/95] Adjust padding in CustomNavigationBar - Add vertical minimum padding to SafeArea - Remove bottom padding in bottomNavigationBar - replaced left/right padding in bottomNavigationBar with EdgeInsets.symmetric - removed bottom padding from bottomNavigationBar --- lib/presentation/views/main_menu/custom_navigation_bar.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 28331b8..2805689 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -32,6 +32,7 @@ class _CustomNavigationBarState extends State @override Widget build(BuildContext context) { return SafeArea( + minimum: EdgeInsets.symmetric(vertical: 30), child: Scaffold( appBar: AppBar( centerTitle: true, @@ -56,7 +57,7 @@ class _CustomNavigationBarState extends State body: tabs[currentIndex], extendBody: true, bottomNavigationBar: Padding( - padding: const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 18.0), + padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Material( elevation: 10, borderRadius: BorderRadius.circular(24), From b668f6b9ae54375b6e5b59a9f984dd6a63a5d2a0 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 16:31:15 +0100 Subject: [PATCH 57/95] fix black top/bottom bar when wrapping scaffold in safearea --- .../main_menu/custom_navigation_bar.dart | 131 +++++++++--------- 1 file changed, 67 insertions(+), 64 deletions(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 2805689..709d541 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -31,73 +31,76 @@ class _CustomNavigationBarState extends State @override Widget build(BuildContext context) { - return SafeArea( - minimum: EdgeInsets.symmetric(vertical: 30), - child: Scaffold( - appBar: AppBar( - centerTitle: true, - title: Text( - _currentTabTitle(), - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + return Container( + decoration: BoxDecoration(color: CustomTheme.backgroundColor), + child: SafeArea( + minimum: EdgeInsets.symmetric(vertical: 30), + child: Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + _currentTabTitle(), + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + backgroundColor: CustomTheme.backgroundColor, + scrolledUnderElevation: 0, + actions: [ + IconButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const SettingsView()), + ), + icon: const Icon(Icons.settings), + ), + ], + elevation: 0, ), backgroundColor: CustomTheme.backgroundColor, - scrolledUnderElevation: 0, - actions: [ - IconButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => const SettingsView()), - ), - icon: const Icon(Icons.settings), - ), - ], - elevation: 0, - ), - backgroundColor: CustomTheme.backgroundColor, - body: tabs[currentIndex], - extendBody: true, - bottomNavigationBar: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Material( - elevation: 10, - borderRadius: BorderRadius.circular(24), - color: CustomTheme.primaryColor, - child: ClipRRect( + body: tabs[currentIndex], + extendBody: true, + bottomNavigationBar: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Material( + elevation: 10, borderRadius: BorderRadius.circular(24), - child: SizedBox( - height: 60, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - NavbarItem( - index: 0, - isSelected: currentIndex == 0, - icon: Icons.home_rounded, - label: 'Home', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 1, - isSelected: currentIndex == 1, - icon: Icons.gamepad_rounded, - label: 'Games', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 2, - isSelected: currentIndex == 2, - icon: Icons.group_rounded, - label: 'Groups', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 3, - isSelected: currentIndex == 3, - icon: Icons.bar_chart_rounded, - label: 'Stats', - onTabTapped: onTabTapped, - ), - ], + color: CustomTheme.primaryColor, + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: SizedBox( + height: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + NavbarItem( + index: 0, + isSelected: currentIndex == 0, + icon: Icons.home_rounded, + label: 'Home', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 1, + isSelected: currentIndex == 1, + icon: Icons.gamepad_rounded, + label: 'Games', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 2, + isSelected: currentIndex == 2, + icon: Icons.group_rounded, + label: 'Groups', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 3, + isSelected: currentIndex == 3, + icon: Icons.bar_chart_rounded, + label: 'Stats', + onTabTapped: onTabTapped, + ), + ], + ), ), ), ), From 28ed22ce732bdfe9ee1fabf6c0e9f59c6733df89 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 17:40:00 +0100 Subject: [PATCH 58/95] wrap navbar in SafeArea and replaced Material with Container --- .../main_menu/custom_navigation_bar.dart | 129 +++++++++--------- 1 file changed, 63 insertions(+), 66 deletions(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 709d541..9727bc0 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -31,76 +31,73 @@ class _CustomNavigationBarState extends State @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration(color: CustomTheme.backgroundColor), - child: SafeArea( - minimum: EdgeInsets.symmetric(vertical: 30), - child: Scaffold( - appBar: AppBar( - centerTitle: true, - title: Text( - _currentTabTitle(), - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + _currentTabTitle(), + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + backgroundColor: CustomTheme.backgroundColor, + scrolledUnderElevation: 0, + actions: [ + IconButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const SettingsView()), ), - backgroundColor: CustomTheme.backgroundColor, - scrolledUnderElevation: 0, - actions: [ - IconButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => const SettingsView()), - ), - icon: const Icon(Icons.settings), - ), - ], - elevation: 0, + icon: const Icon(Icons.settings), ), - backgroundColor: CustomTheme.backgroundColor, - body: tabs[currentIndex], - extendBody: true, - bottomNavigationBar: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Material( - elevation: 10, + ], + elevation: 0, + ), + backgroundColor: CustomTheme.backgroundColor, + body: tabs[currentIndex], + extendBody: true, + bottomNavigationBar: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Container( + decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), color: CustomTheme.primaryColor, - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: SizedBox( - height: 60, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - NavbarItem( - index: 0, - isSelected: currentIndex == 0, - icon: Icons.home_rounded, - label: 'Home', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 1, - isSelected: currentIndex == 1, - icon: Icons.gamepad_rounded, - label: 'Games', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 2, - isSelected: currentIndex == 2, - icon: Icons.group_rounded, - label: 'Groups', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 3, - isSelected: currentIndex == 3, - icon: Icons.bar_chart_rounded, - label: 'Stats', - onTabTapped: onTabTapped, - ), - ], - ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: SizedBox( + height: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + NavbarItem( + index: 0, + isSelected: currentIndex == 0, + icon: Icons.home_rounded, + label: 'Home', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 1, + isSelected: currentIndex == 1, + icon: Icons.gamepad_rounded, + label: 'Games', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 2, + isSelected: currentIndex == 2, + icon: Icons.group_rounded, + label: 'Groups', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 3, + isSelected: currentIndex == 3, + icon: Icons.bar_chart_rounded, + label: 'Stats', + onTabTapped: onTabTapped, + ), + ], ), ), ), From df3215ef761e4264a31ec1441bf2e05ddd6731d6 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 17:40:24 +0100 Subject: [PATCH 59/95] Made bottom padding adaptive to screen --- lib/presentation/views/main_menu/groups_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/groups_view.dart b/lib/presentation/views/main_menu/groups_view.dart index c45cf21..5a7a2eb 100644 --- a/lib/presentation/views/main_menu/groups_view.dart +++ b/lib/presentation/views/main_menu/groups_view.dart @@ -103,7 +103,7 @@ class _GroupsViewState extends State { ), Positioned( - bottom: 80, + bottom: MediaQuery.paddingOf(context).bottom + 15, child: CustomWidthButton( text: 'Create Group', sizeRelativeToWidth: 0.90, From 5062196463e0df16477d2901f01db291e377bc30 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 17:57:58 +0100 Subject: [PATCH 60/95] Adjust safeareas minimum padding for custom navigation bar --- .../main_menu/custom_navigation_bar.dart | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 9727bc0..7176505 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -55,50 +55,49 @@ class _CustomNavigationBarState extends State body: tabs[currentIndex], extendBody: true, bottomNavigationBar: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: CustomTheme.primaryColor, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: SizedBox( - height: 60, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - NavbarItem( - index: 0, - isSelected: currentIndex == 0, - icon: Icons.home_rounded, - label: 'Home', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 1, - isSelected: currentIndex == 1, - icon: Icons.gamepad_rounded, - label: 'Games', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 2, - isSelected: currentIndex == 2, - icon: Icons.group_rounded, - label: 'Groups', - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 3, - isSelected: currentIndex == 3, - icon: Icons.bar_chart_rounded, - label: 'Stats', - onTabTapped: onTabTapped, - ), - ], - ), + minimum: const EdgeInsets.only(bottom: 30), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: CustomTheme.primaryColor, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: SizedBox( + height: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + NavbarItem( + index: 0, + isSelected: currentIndex == 0, + icon: Icons.home_rounded, + label: 'Home', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 1, + isSelected: currentIndex == 1, + icon: Icons.gamepad_rounded, + label: 'Games', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 2, + isSelected: currentIndex == 2, + icon: Icons.group_rounded, + label: 'Groups', + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 3, + isSelected: currentIndex == 3, + icon: Icons.bar_chart_rounded, + label: 'Stats', + onTabTapped: onTabTapped, + ), + ], ), ), ), From fd13fe6e903bc61d4843200385203bdbe9210da4 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 17:58:33 +0100 Subject: [PATCH 61/95] made CustomWidthButton's position adaptive to bottom padding of safearea --- lib/presentation/views/main_menu/groups_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/groups_view.dart b/lib/presentation/views/main_menu/groups_view.dart index 5a7a2eb..4bf9cba 100644 --- a/lib/presentation/views/main_menu/groups_view.dart +++ b/lib/presentation/views/main_menu/groups_view.dart @@ -103,7 +103,7 @@ class _GroupsViewState extends State { ), Positioned( - bottom: MediaQuery.paddingOf(context).bottom + 15, + bottom: MediaQuery.paddingOf(context).bottom, child: CustomWidthButton( text: 'Create Group', sizeRelativeToWidth: 0.90, From 310b9aa43bf356731e3a5188e03c91ee66306c9d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 22:10:02 +0100 Subject: [PATCH 62/95] Implemented StatisticsWidget tile --- .../widgets/tiles/statistics_tile.dart | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 lib/presentation/widgets/tiles/statistics_tile.dart diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart new file mode 100644 index 0000000..0d01159 --- /dev/null +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -0,0 +1,100 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; + +class StatisticsTile extends StatelessWidget { + const StatisticsTile({ + super.key, + required this.icon, + required this.title, + required this.width, + required this.values, + required this.itemCount, + required this.barColor, + }); + + final IconData icon; + final String title; + final double width; + final List<(String, int)> values; + final int itemCount; + final Color barColor; + + @override + Widget build(BuildContext context) { + final maxBarWidth = MediaQuery.of(context).size.width * 0.7; + + return InfoTile( + width: width, + title: title, + icon: icon, + content: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Visibility( + visible: values.isNotEmpty, + replacement: const Center( + heightFactor: 4, + child: Text('No data available.'), + ), + child: Column( + children: List.generate(min(values.length, itemCount), (index) { + /// The maximum wins among all players + final maxGames = values.isNotEmpty ? values[0].$2 : 0; + + /// Fraction of wins + final double fraction = (maxGames > 0) + ? (values[index].$2 / maxGames) + : 0.0; + + /// Calculated width for current the bar + final double barWidth = maxBarWidth * fraction; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Stack( + children: [ + Container( + height: 24, + width: barWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: barColor, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + values[index].$1, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const Spacer(), + Center( + child: Text( + values[index].$2.toString(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + }), + ), + ), + ), + ); + } +} From b2036e4e6811439da75dc5d0b3a15af2b41fb553 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 22:10:16 +0100 Subject: [PATCH 63/95] Implemented first version of statistics view --- .../views/main_menu/statistics_view.dart | 199 +++++++++++++++++- 1 file changed, 196 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 84ccf77..fc7b262 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -1,10 +1,203 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/game.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart'; +import 'package:provider/provider.dart'; +import 'package:skeletonizer/skeletonizer.dart'; -class StatisticsView extends StatelessWidget { +class StatisticsView extends StatefulWidget { const StatisticsView({super.key}); + @override + State createState() => _StatisticsViewState(); +} + +class _StatisticsViewState extends State { + late Future> _gamesFuture; + late Future> _playersFuture; + List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 5)); + List<(String, int)> gameCounts = List.filled(6, ('Skeleton Player', 5)); + + bool isLoading = true; + + @override + void initState() { + super.initState(); + final db = Provider.of(context, listen: false); + _gamesFuture = db.gameDao.getAllGames(); + _playersFuture = db.playerDao.getAllPlayers(); + + Future.wait([_gamesFuture, _playersFuture]).then((results) async { + await Future.delayed(const Duration(milliseconds: 500)); + final games = results[0] as List; + final players = results[1] as List; + winCounts = _calculateWinsForAllPlayers(games, players); + gameCounts = _calculateGameAmountsForAllPlayers(games, players); + if (mounted) { + setState(() { + isLoading = false; + }); + } + }); + } + @override Widget build(BuildContext context) { - return const Center(child: Text('Statistics View')); + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Skeletonizer( + effect: PulseEffect( + from: Colors.grey[800]!, + to: Colors.grey[600]!, + duration: const Duration(milliseconds: 800), + ), + enabled: isLoading, + enableSwitchAnimation: true, + switchAnimationConfig: const SwitchAnimationConfig( + duration: Duration(milliseconds: 200), + switchInCurve: Curves.linear, + switchOutCurve: Curves.linear, + transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, + layoutBuilder: AnimatedSwitcher.defaultLayoutBuilder, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: constraints.maxHeight * 0.01), + StatisticsTile( + icon: Icons.sports_score, + title: 'Wins per Player', + width: constraints.maxWidth * 0.95, + values: winCounts, + itemCount: 6, + barColor: Colors.blue, + ), + SizedBox(height: constraints.maxHeight * 0.02), + StatisticsTile( + icon: Icons.casino, + title: 'Games per Player', + width: constraints.maxWidth * 0.95, + values: gameCounts, + itemCount: 6, + barColor: Colors.green, + ), + SizedBox(height: MediaQuery.paddingOf(context).bottom), + ], + ), + ), + ), + ); + }, + ); + } + + /// Calculates the number of wins for each player + /// and returns a sorted list of tuples (playerName, winCount) + List<(String, int)> _calculateWinsForAllPlayers( + List games, + List players, + ) { + List<(String, int)> winCounts = []; + + // Getting the winners + for (var game in games) { + final winner = game.winner; + print('Game: ${game.id}, Winner: $winner'); + if (winner != null && winner.isNotEmpty) { + final index = winCounts.indexWhere((entry) => entry.$1 == winner); + if (index != -1) { + final current = winCounts[index].$2; + winCounts[index] = (winner, current + 1); + } else { + winCounts.add((winner, 1)); + } + } + } + + // Adding all players with zero wins + for (var player in players) { + final index = winCounts.indexWhere((entry) => entry.$1 == player.id); + if (index == -1) { + winCounts.add((player.id, 0)); + } + } + + // Replace player IDs with names + for (int i = 0; i < winCounts.length; i++) { + final playerId = winCounts[i].$1; + final player = players.firstWhere( + (p) => p.id == playerId, + orElse: () => Player(id: playerId, name: 'N.a.'), + ); + winCounts[i] = (player.name, winCounts[i].$2); + } + + winCounts.sort((a, b) => b.$2.compareTo(a.$2)); + + return winCounts; + } + + /// Calculates the number of games played for each player + /// and returns a sorted list of tuples (playerName, gameCount) + List<(String, int)> _calculateGameAmountsForAllPlayers( + List games, + List players, + ) { + List<(String, int)> gameCounts = []; + + // Counting games for each player + for (var game in games) { + if (game.group != null) { + final members = game.group!.members.map((p) => p.id).toList(); + for (var playerId in members) { + final index = gameCounts.indexWhere((entry) => entry.$1 == playerId); + if (index != -1) { + final current = gameCounts[index].$2; + gameCounts[index] = (playerId, current + 1); + } else { + gameCounts.add((playerId, 1)); + } + } + } + if (game.players != null) { + final members = game.players!.map((p) => p.id).toList(); + for (var playerId in members) { + final index = gameCounts.indexWhere((entry) => entry.$1 == playerId); + if (index != -1) { + final current = gameCounts[index].$2; + gameCounts[index] = (playerId, current + 1); + } else { + gameCounts.add((playerId, 1)); + } + } + } + } + + // Adding all players with zero games + for (var player in players) { + final index = gameCounts.indexWhere((entry) => entry.$1 == player.id); + if (index == -1) { + gameCounts.add((player.id, 0)); + } + } + + // Replace player IDs with names + for (int i = 0; i < gameCounts.length; i++) { + final playerId = gameCounts[i].$1; + final player = players.firstWhere( + (p) => p.id == playerId, + orElse: () => Player(id: playerId, name: 'N.a.'), + ); + gameCounts[i] = (player.name, gameCounts[i].$2); + } + + gameCounts.sort((a, b) => b.$2.compareTo(a.$2)); + + return gameCounts; } } From 546a3e37174b4187e4e2d9c48b55e6bbba69f0d4 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 22:49:17 +0100 Subject: [PATCH 64/95] implemented feature to automatically add newly created player to selected players --- .../views/main_menu/create_group_view.dart | 85 +++++++++---------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/lib/presentation/views/main_menu/create_group_view.dart b/lib/presentation/views/main_menu/create_group_view.dart index c54369e..75fdb83 100644 --- a/lib/presentation/views/main_menu/create_group_view.dart +++ b/lib/presentation/views/main_menu/create_group_view.dart @@ -49,8 +49,7 @@ class _CreateGroupViewState extends State { @override void dispose() { _groupNameController.dispose(); - _searchBarController - .dispose(); // Listener entfernen und Controller aufräumen + _searchBarController.dispose(); super.dispose(); } @@ -123,12 +122,7 @@ class _CreateGroupViewState extends State { .trim() .isNotEmpty, onTrailingButtonPressed: () async { - addNewPlayerFromSearch( - context: context, - searchBarController: _searchBarController, - db: db, - loadPlayerList: loadPlayerList, - ); + addNewPlayerFromSearch(context: context); }, onChanged: (value) { setState(() { @@ -339,48 +333,47 @@ class _CreateGroupViewState extends State { ), ); } -} -/// Adds a new player to the database from the search bar input. -/// Shows a snackbar indicating success or failure. -/// [context] - BuildContext to show the snackbar. -/// [searchBarController] - TextEditingController of the search bar. -/// [db] - AppDatabase instance to interact with the database. -/// [loadPlayerList] - Function to reload the player list after adding. -void addNewPlayerFromSearch({ - required BuildContext context, - required TextEditingController searchBarController, - required AppDatabase db, - required Function loadPlayerList, -}) async { - String playerName = searchBarController.text.trim(); - bool success = await db.playerDao.addPlayer(player: Player(name: playerName)); - if (!context.mounted) return; - if (success) { - loadPlayerList(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: CustomTheme.boxColor, - content: Center( - child: Text( - 'Successfully added player $playerName.', - style: const TextStyle(color: Colors.white), + /// Adds a new player to the database from the search bar input. + /// Shows a snackbar indicating success or failure. + /// [context] - BuildContext to show the snackbar. + void addNewPlayerFromSearch({required BuildContext context}) async { + String playerName = _searchBarController.text.trim(); + Player createdPlayer = Player(name: playerName); + bool success = await db.playerDao.addPlayer(player: createdPlayer); + if (!context.mounted) return; + if (success) { + selectedPlayers.add(createdPlayer); + allPlayers.add(createdPlayer); + setState(() { + _searchBarController.clear(); + suggestedPlayers = allPlayers.where((player) { + return !selectedPlayers.contains(player); + }).toList(); + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: CustomTheme.boxColor, + content: Center( + child: Text( + 'Successfully added player $playerName.', + style: const TextStyle(color: Colors.white), + ), ), ), - ), - ); - searchBarController.clear(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: CustomTheme.boxColor, - content: Center( - child: Text( - 'Could not add player $playerName.', - style: const TextStyle(color: Colors.white), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: CustomTheme.boxColor, + content: Center( + child: Text( + 'Could not add player $playerName.', + style: const TextStyle(color: Colors.white), + ), ), ), - ), - ); + ); + } } } From cc04e0555768396560f28b20f108a6a0a5d29d02 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 22:49:28 +0100 Subject: [PATCH 65/95] Adjust bottom padding in GroupsView list based on media query padding --- lib/presentation/views/main_menu/groups_view.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/groups_view.dart b/lib/presentation/views/main_menu/groups_view.dart index 4bf9cba..a23a615 100644 --- a/lib/presentation/views/main_menu/groups_view.dart +++ b/lib/presentation/views/main_menu/groups_view.dart @@ -93,7 +93,9 @@ class _GroupsViewState extends State { itemCount: groups.length + 1, itemBuilder: (BuildContext context, int index) { if (index == groups.length) { - return const SizedBox(height: 60); + return SizedBox( + height: MediaQuery.paddingOf(context).bottom - 20, + ); } return GroupTile(group: groups[index]); }, From 692b412fe2e3d7d727717aa36bd24ad29a5cd349 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 22:52:09 +0100 Subject: [PATCH 66/95] Fix black bars on the screens bottom and top by not wrapping scaffold in safearea, but scaffolds body children --- .../views/main_menu/create_group_view.dart | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/presentation/views/main_menu/create_group_view.dart b/lib/presentation/views/main_menu/create_group_view.dart index 75fdb83..b077bbc 100644 --- a/lib/presentation/views/main_menu/create_group_view.dart +++ b/lib/presentation/views/main_menu/create_group_view.dart @@ -66,19 +66,19 @@ class _CreateGroupViewState extends State { @override Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar( - backgroundColor: CustomTheme.backgroundColor, - scrolledUnderElevation: 0, - title: const Text( - 'Create new group', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - centerTitle: true, + scrolledUnderElevation: 0, + title: const Text( + 'Create new group', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), - body: Column( + centerTitle: true, + ), + body: SafeArea( + child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Container( From c170aa17752f808954b85d6270e33df46d91cad8 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 22:55:19 +0100 Subject: [PATCH 67/95] sort groups by creation date in GroupsView --- lib/presentation/views/main_menu/groups_view.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/groups_view.dart b/lib/presentation/views/main_menu/groups_view.dart index a23a615..e9af8b9 100644 --- a/lib/presentation/views/main_menu/groups_view.dart +++ b/lib/presentation/views/main_menu/groups_view.dart @@ -69,9 +69,9 @@ class _GroupsViewState extends State { } final bool isLoading = snapshot.connectionState == ConnectionState.waiting; - final List groups = isLoading - ? skeletonData - : (snapshot.data ?? []); + final List groups = + isLoading ? skeletonData : (snapshot.data ?? []) + ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); return Skeletonizer( effect: PulseEffect( from: Colors.grey[800]!, From 59c041699dc3edd983959f207b33470b9a6673af Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 23:20:31 +0100 Subject: [PATCH 68/95] Changed values attribute & maxBarWidth --- lib/presentation/widgets/tiles/statistics_tile.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index 0d01159..279c492 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -17,13 +17,13 @@ class StatisticsTile extends StatelessWidget { final IconData icon; final String title; final double width; - final List<(String, int)> values; + final List<(String, num)> values; final int itemCount; final Color barColor; @override Widget build(BuildContext context) { - final maxBarWidth = MediaQuery.of(context).size.width * 0.7; + final maxBarWidth = MediaQuery.of(context).size.width * 0.65; return InfoTile( width: width, From e60961730f64ec78f2ab17da15297990bbdca4d0 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 23:20:58 +0100 Subject: [PATCH 69/95] Added new metric & changed layout builder of Skeletonizer --- .../views/main_menu/statistics_view.dart | 92 +++++++++++++++---- 1 file changed, 72 insertions(+), 20 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index fc7b262..2a8dedf 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -16,9 +16,9 @@ class StatisticsView extends StatefulWidget { class _StatisticsViewState extends State { late Future> _gamesFuture; late Future> _playersFuture; - List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 5)); - List<(String, int)> gameCounts = List.filled(6, ('Skeleton Player', 5)); - + List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1)); + List<(String, int)> gameCounts = List.filled(6, ('Skeleton Player', 1)); + List<(String, double)> winRates = List.filled(6, ('Skeleton Player', 1)); bool isLoading = true; @override @@ -29,11 +29,12 @@ class _StatisticsViewState extends State { _playersFuture = db.playerDao.getAllPlayers(); Future.wait([_gamesFuture, _playersFuture]).then((results) async { - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 200)); final games = results[0] as List; final players = results[1] as List; winCounts = _calculateWinsForAllPlayers(games, players); gameCounts = _calculateGameAmountsForAllPlayers(games, players); + winRates = computeWinRatePercent(wins: winCounts, games: gameCounts); if (mounted) { setState(() { isLoading = false; @@ -46,22 +47,31 @@ class _StatisticsViewState extends State { Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - return Skeletonizer( - effect: PulseEffect( - from: Colors.grey[800]!, - to: Colors.grey[600]!, - duration: const Duration(milliseconds: 800), - ), - enabled: isLoading, - enableSwitchAnimation: true, - switchAnimationConfig: const SwitchAnimationConfig( - duration: Duration(milliseconds: 200), - switchInCurve: Curves.linear, - switchOutCurve: Curves.linear, - transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, - layoutBuilder: AnimatedSwitcher.defaultLayoutBuilder, - ), - child: SingleChildScrollView( + return SingleChildScrollView( + child: Skeletonizer( + effect: PulseEffect( + from: Colors.grey[800]!, + to: Colors.grey[600]!, + duration: const Duration(milliseconds: 800), + ), + enabled: isLoading, + enableSwitchAnimation: true, + switchAnimationConfig: SwitchAnimationConfig( + duration: const Duration(milliseconds: 1000), + switchInCurve: Curves.linear, + switchOutCurve: Curves.linear, + transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, + layoutBuilder: + (Widget? currentChild, List previousChildren) { + return Stack( + alignment: Alignment.topCenter, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + ), child: ConstrainedBox( constraints: BoxConstraints(minWidth: constraints.maxWidth), child: Column( @@ -78,6 +88,15 @@ class _StatisticsViewState extends State { barColor: Colors.blue, ), SizedBox(height: constraints.maxHeight * 0.02), + StatisticsTile( + icon: Icons.casino, + title: 'Winrate per Player', + width: constraints.maxWidth * 0.95, + values: winRates, + itemCount: 6, + barColor: Colors.orange[700]!, + ), + SizedBox(height: constraints.maxHeight * 0.02), StatisticsTile( icon: Icons.casino, title: 'Games per Player', @@ -86,6 +105,7 @@ class _StatisticsViewState extends State { itemCount: 6, barColor: Colors.green, ), + SizedBox(height: MediaQuery.paddingOf(context).bottom), ], ), @@ -200,4 +220,36 @@ class _StatisticsViewState extends State { return gameCounts; } + + // dart + List<(String, double)> computeWinRatePercent({ + required List<(String, int)> wins, // [(name, wins)] + required List<(String, int)> games, // [(name, games)] + }) { + final Map winsMap = {for (var e in wins) e.$1: e.$2}; + final Map gamesMap = {for (var e in games) e.$1: e.$2}; + + final names = {...winsMap.keys, ...gamesMap.keys}; + + final result = names.map((name) { + final int w = winsMap[name] ?? 0; + final int g = gamesMap[name] ?? 0; + final double percent = (g > 0) + ? double.parse(((w / g)).toStringAsFixed(2)) + : 0; + return (name, percent); + }).toList(); + + // Sort the result: first by winrate descending, + // then by wins descending in case of a tie + result.sort((a, b) { + final cmp = b.$2.compareTo(a.$2); + if (cmp != 0) return cmp; + final wa = winsMap[a.$1] ?? 0; + final wb = winsMap[b.$1] ?? 0; + return wb.compareTo(wa); + }); + + return result; + } } From fba35521cbcfddfc401c91c67f344ba4b6203ab5 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 23:23:02 +0100 Subject: [PATCH 70/95] changed skeletonizer transition duration back to normal --- lib/presentation/views/main_menu/statistics_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 2a8dedf..a365b2e 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -57,7 +57,7 @@ class _StatisticsViewState extends State { enabled: isLoading, enableSwitchAnimation: true, switchAnimationConfig: SwitchAnimationConfig( - duration: const Duration(milliseconds: 1000), + duration: const Duration(milliseconds: 200), switchInCurve: Curves.linear, switchOutCurve: Curves.linear, transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, From feb5fa061557bc8b4fa9d206bde076fc7e559095 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 23:30:24 +0100 Subject: [PATCH 71/95] Docs, small changes --- .../views/main_menu/statistics_view.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index a365b2e..8830118 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -84,7 +84,7 @@ class _StatisticsViewState extends State { title: 'Wins per Player', width: constraints.maxWidth * 0.95, values: winCounts, - itemCount: 6, + itemCount: 3, barColor: Colors.blue, ), SizedBox(height: constraints.maxHeight * 0.02), @@ -93,7 +93,7 @@ class _StatisticsViewState extends State { title: 'Winrate per Player', width: constraints.maxWidth * 0.95, values: winRates, - itemCount: 6, + itemCount: 5, barColor: Colors.orange[700]!, ), SizedBox(height: constraints.maxHeight * 0.02), @@ -102,7 +102,7 @@ class _StatisticsViewState extends State { title: 'Games per Player', width: constraints.maxWidth * 0.95, values: gameCounts, - itemCount: 6, + itemCount: 10, barColor: Colors.green, ), @@ -127,7 +127,6 @@ class _StatisticsViewState extends State { // Getting the winners for (var game in games) { final winner = game.winner; - print('Game: ${game.id}, Winner: $winner'); if (winner != null && winner.isNotEmpty) { final index = winCounts.indexWhere((entry) => entry.$1 == winner); if (index != -1) { @@ -223,17 +222,21 @@ class _StatisticsViewState extends State { // dart List<(String, double)> computeWinRatePercent({ - required List<(String, int)> wins, // [(name, wins)] - required List<(String, int)> games, // [(name, games)] + required List<(String, int)> wins, + required List<(String, int)> games, }) { final Map winsMap = {for (var e in wins) e.$1: e.$2}; final Map gamesMap = {for (var e in games) e.$1: e.$2}; + // Get all unique player names final names = {...winsMap.keys, ...gamesMap.keys}; + // Calculate win rates final result = names.map((name) { final int w = winsMap[name] ?? 0; final int g = gamesMap[name] ?? 0; + // Calculate percentage and round to 2 decimal places + // Avoid division by zero final double percent = (g > 0) ? double.parse(((w / g)).toStringAsFixed(2)) : 0; From f658a88849caee39d07de784807722905308bd08 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 23:52:00 +0100 Subject: [PATCH 72/95] Implemented displaying real recent games in home view --- .../views/main_menu/home_view.dart | 158 +++++++++++++----- 1 file changed, 115 insertions(+), 43 deletions(-) diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index 34e4be3..194849e 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/game.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart'; import 'package:game_tracker/presentation/widgets/tiles/game_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart'; +import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:provider/provider.dart'; import 'package:skeletonizer/skeletonizer.dart'; @@ -17,23 +21,43 @@ class HomeView extends StatefulWidget { class _HomeViewState extends State { late Future _gameCountFuture; late Future _groupCountFuture; + late Future> _recentGamesFuture; bool isLoading = true; + late final List skeletonData = List.filled( + 2, + Game( + name: 'Skeleton Game', + group: Group( + name: 'Skeleton Group', + members: [ + Player(name: 'Skeleton Player 1'), + Player(name: 'Skeleton Player 2'), + ], + ), + winner: + "Winner ID", //TODO: Should be player object, but isnt yet, waiting for pr + ), + ); + @override initState() { super.initState(); final db = Provider.of(context, listen: false); _gameCountFuture = db.gameDao.getGameCount(); _groupCountFuture = db.groupDao.getGroupCount(); + _recentGamesFuture = db.gameDao.getAllGames(); - Future.wait([_gameCountFuture, _groupCountFuture]).then((_) async { - await Future.delayed(const Duration(milliseconds: 50)); - if (mounted) { - setState(() { - isLoading = false; - }); - } - }); + Future.wait([_gameCountFuture, _groupCountFuture, _recentGamesFuture]).then( + (_) async { + await Future.delayed(const Duration(milliseconds: 50)); + if (mounted) { + setState(() { + isLoading = false; + }); + } + }, + ); } @override @@ -48,12 +72,21 @@ class _HomeViewState extends State { ), enabled: isLoading, enableSwitchAnimation: true, - switchAnimationConfig: const SwitchAnimationConfig( + switchAnimationConfig: SwitchAnimationConfig( duration: Duration(milliseconds: 200), switchInCurve: Curves.linear, switchOutCurve: Curves.linear, transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, - layoutBuilder: AnimatedSwitcher.defaultLayoutBuilder, + layoutBuilder: + (Widget? currentChild, List previousChildren) { + return Stack( + alignment: Alignment.topCenter, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, ), child: SingleChildScrollView( child: Column( @@ -97,41 +130,70 @@ class _HomeViewState extends State { ), ], ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: InfoTile( - width: constraints.maxWidth * 0.95, - title: 'Recent Games', - icon: Icons.timer, - content: const Padding( - padding: EdgeInsets.symmetric(horizontal: 40.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GameTile( - gameTitle: 'Gamenight', - gameType: 'Cabo', - ruleset: 'Lowest Points', - players: '5 Players', - winner: 'Leonard', + FutureBuilder( + future: _recentGamesFuture, + builder: (context, snapshot) { + if (snapshot.hasError) { + return const Center( + child: TopCenteredMessage( + icon: Icons.report, + title: 'Error', + message: 'Group data couldn\'t\nbe loaded.', + ), + ); + } + if (snapshot.connectionState == ConnectionState.done && + (!snapshot.hasData || snapshot.data!.isEmpty)) { + return const Center( + child: TopCenteredMessage( + icon: Icons.info, + title: 'Info', + message: 'No games created yet.', + ), + ); + } + final List games = + isLoading ? skeletonData : (snapshot.data ?? []) + ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: InfoTile( + width: constraints.maxWidth * 0.95, + title: 'Recent Games', + icon: Icons.timer, + content: Padding( + padding: EdgeInsets.symmetric(horizontal: 40.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GameTile( + gameTitle: games[0].name, + gameType: "Gametype", + ruleset: 'Ruleset', + players: _getPlayerText(games[0]), + winner: + 'Leonard', //TODO: Replace Winner with real Winner + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Divider(), + ), + GameTile( + gameTitle: games[1].name, + gameType: 'Gametype', + ruleset: 'Ruleset', + players: _getPlayerText(games[1]), + winner: + 'Lina', //TODO: Replace Winner with real Winner + ), + SizedBox(height: 8), + ], ), - Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Divider(), - ), - GameTile( - gameTitle: 'Schoolbreak', - gameType: 'Uno', - ruleset: 'Highest Points', - players: 'The Gang', - winner: 'Lina', - ), - SizedBox(height: 8), - ], + ), ), - ), - ), + ); + }, ), InfoTile( width: constraints.maxWidth * 0.95, @@ -189,4 +251,14 @@ class _HomeViewState extends State { }, ); } + + String _getPlayerText(Game game) { + if (game.group == null) { + return game.players?.map((p) => p.name).join(', ') ?? 'No Players'; + } + if (game.players == null || game.players!.isEmpty) { + return game.group!.name; + } + return '${game.group!.name} + ${game.players!.length}'; + } } From fa841e328ec63a13655e43a22b7c24bf11a1eac1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 00:17:11 +0100 Subject: [PATCH 73/95] Altered game class --- lib/data/dto/game.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/data/dto/game.dart b/lib/data/dto/game.dart index 4188bc4..48ef902 100644 --- a/lib/data/dto/game.dart +++ b/lib/data/dto/game.dart @@ -9,7 +9,7 @@ class Game { final String name; final List? players; final Group? group; - final String? winner; + final Player? winner; Game({ String? id, @@ -17,7 +17,7 @@ class Game { required this.name, this.players, this.group, - this.winner = '', + this.winner, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? clock.now(); @@ -37,7 +37,7 @@ class Game { .toList() : null, group = json['group'] != null ? Group.fromJson(json['group']) : null, - winner = json['winner'] ?? ''; + winner = json['winner'] != null ? Player.fromJson(json['winner']) : null; /// Converts the Game instance to a JSON object. Map toJson() => { @@ -46,6 +46,6 @@ class Game { 'name': name, 'players': players?.map((player) => player.toJson()).toList(), 'group': group?.toJson(), - 'winner': winner, + 'winner': winner?.toJson(), }; } From cfed05595ca9cbec81213b8128c52c9d442f923e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 00:17:27 +0100 Subject: [PATCH 74/95] Updated methods in gameDao --- lib/data/dao/game_dao.dart | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index a211849..18792b5 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -20,13 +20,16 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { result.map((row) async { final group = await db.groupGameDao.getGroupOfGame(gameId: row.id); final players = await db.playerGameDao.getPlayersOfGame(gameId: row.id); + final winner = row.winnerId != null + ? await db.playerDao.getPlayerById(playerId: row.winnerId!) + : null; return Game( id: row.id, name: row.name, group: group, players: players, createdAt: row.createdAt, - winner: row.winnerId, + winner: winner, ); }), ); @@ -45,13 +48,17 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { if (await db.groupGameDao.gameHasGroup(gameId: gameId)) { group = await db.groupGameDao.getGroupOfGame(gameId: gameId); } + Player? winner; + if (result.winnerId != null) { + winner = await db.playerDao.getPlayerById(playerId: result.winnerId!); + } return Game( id: result.id, name: result.name, players: players, group: group, - winner: result.winnerId, + winner: winner, createdAt: result.createdAt, ); } @@ -64,7 +71,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { GameTableCompanion.insert( id: game.id, name: game.name, - winnerId: Value(game.winner), + winnerId: Value(game.winner?.id), createdAt: game.createdAt, ), mode: InsertMode.insertOrReplace, @@ -100,7 +107,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { id: game.id, name: game.name, createdAt: game.createdAt, - winnerId: Value(game.winner), + winnerId: Value(game.winner?.id), ), ) .toList(), From 338f4294dc17ee4f2e1288a3bcc07b654ef5a073 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 00:17:31 +0100 Subject: [PATCH 75/95] Updated json schema --- assets/schema.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/schema.json b/assets/schema.json index 1883122..c80915c 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -88,13 +88,12 @@ ] }, "winner": { - "type": ["string","null"] + "type": ["object","null"] }, "required": [ "id", "createdAt", - "name", - "winner" + "name" ] } ] From 82b344a145f829e7be92f42f6c05a140975fc79f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 00:17:48 +0100 Subject: [PATCH 76/95] Changed winner access in statistics view --- lib/presentation/views/main_menu/statistics_view.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 8830118..13e9aae 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -127,13 +127,13 @@ class _StatisticsViewState extends State { // Getting the winners for (var game in games) { final winner = game.winner; - if (winner != null && winner.isNotEmpty) { - final index = winCounts.indexWhere((entry) => entry.$1 == winner); + if (winner != null) { + final index = winCounts.indexWhere((entry) => entry.$1 == winner.id); if (index != -1) { final current = winCounts[index].$2; - winCounts[index] = (winner, current + 1); + winCounts[index] = (winner.id, current + 1); } else { - winCounts.add((winner, 1)); + winCounts.add((winner.id, 1)); } } } From 4ff131770e205f4c112dc703330c7ec69b25ea8d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 00:22:53 +0100 Subject: [PATCH 77/95] Adjust tests --- test/db_tests/game_test.dart | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index 0d33c1e..4cf6982 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -50,15 +50,18 @@ void main() { name: 'First Test Game', group: testGroup1, players: [testPlayer4, testPlayer5], + winner: testPlayer4, ); testGame2 = Game( name: 'Second Test Game', group: testGroup2, players: [testPlayer1, testPlayer2, testPlayer3], + winner: testPlayer2, ); testGameOnlyPlayers = Game( name: 'Test Game with Players', players: [testPlayer1, testPlayer2, testPlayer3], + winner: testPlayer3, ); testGameOnlyGroup = Game(name: 'Test Game with Group', group: testGroup2); }); @@ -75,9 +78,16 @@ void main() { expect(result.id, testGame1.id); expect(result.name, testGame1.name); - expect(result.winner, testGame1.winner); expect(result.createdAt, testGame1.createdAt); + if (result.winner != null && testGame1.winner != null) { + expect(result.winner!.id, testGame1.winner!.id); + expect(result.winner!.name, testGame1.winner!.name); + expect(result.winner!.createdAt, testGame1.winner!.createdAt); + } else { + expect(result.winner, testGame1.winner); + } + if (result.group != null) { expect(result.group!.members.length, testGroup1.members.length); @@ -123,7 +133,13 @@ void main() { expect(game.id, testGame.id); expect(game.name, testGame.name); expect(game.createdAt, testGame.createdAt); - expect(game.winner, testGame.winner); + if (game.winner != null && testGame.winner != null) { + expect(game.winner!.id, testGame.winner!.id); + expect(game.winner!.name, testGame.winner!.name); + expect(game.winner!.createdAt, testGame.winner!.createdAt); + } else { + expect(game.winner, testGame.winner); + } // Group-Checks if (testGame.group != null) { From fee5c57207f9f1c77039a0111e117698e957f239 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 12:13:30 +0100 Subject: [PATCH 78/95] Added comments for return value -1 --- lib/presentation/views/main_menu/statistics_view.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 8830118..58dec3a 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -129,6 +129,7 @@ class _StatisticsViewState extends State { final winner = game.winner; if (winner != null && winner.isNotEmpty) { final index = winCounts.indexWhere((entry) => entry.$1 == winner); + // -1 means winner not found in winCounts if (index != -1) { final current = winCounts[index].$2; winCounts[index] = (winner, current + 1); @@ -141,6 +142,7 @@ class _StatisticsViewState extends State { // Adding all players with zero wins for (var player in players) { final index = winCounts.indexWhere((entry) => entry.$1 == player.id); + // -1 means player not found in winCounts if (index == -1) { winCounts.add((player.id, 0)); } @@ -175,6 +177,7 @@ class _StatisticsViewState extends State { final members = game.group!.members.map((p) => p.id).toList(); for (var playerId in members) { final index = gameCounts.indexWhere((entry) => entry.$1 == playerId); + // -1 means player not found in gameCounts if (index != -1) { final current = gameCounts[index].$2; gameCounts[index] = (playerId, current + 1); @@ -187,6 +190,7 @@ class _StatisticsViewState extends State { final members = game.players!.map((p) => p.id).toList(); for (var playerId in members) { final index = gameCounts.indexWhere((entry) => entry.$1 == playerId); + // -1 means player not found in gameCounts if (index != -1) { final current = gameCounts[index].$2; gameCounts[index] = (playerId, current + 1); @@ -200,6 +204,7 @@ class _StatisticsViewState extends State { // Adding all players with zero games for (var player in players) { final index = gameCounts.indexWhere((entry) => entry.$1 == player.id); + // -1 means player not found in gameCounts if (index == -1) { gameCounts.add((player.id, 0)); } From d411f58134f256bfbb1e6aed73e10a9bc9ae8840 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 12:18:05 +0100 Subject: [PATCH 79/95] Changed icon for second statistics tile --- lib/presentation/views/main_menu/statistics_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 58dec3a..96e2203 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -89,7 +89,7 @@ class _StatisticsViewState extends State { ), SizedBox(height: constraints.maxHeight * 0.02), StatisticsTile( - icon: Icons.casino, + icon: Icons.percent, title: 'Winrate per Player', width: constraints.maxWidth * 0.95, values: winRates, From e9b041e43ac89fdf775e36704fddeac8700b96c7 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 12:33:13 +0100 Subject: [PATCH 80/95] Changed double depiction --- lib/presentation/views/main_menu/statistics_view.dart | 3 +-- lib/presentation/widgets/tiles/statistics_tile.dart | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 7a6f861..56bdcf5 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -93,7 +93,7 @@ class _StatisticsViewState extends State { title: 'Winrate per Player', width: constraints.maxWidth * 0.95, values: winRates, - itemCount: 5, + itemCount: 115, barColor: Colors.orange[700]!, ), SizedBox(height: constraints.maxHeight * 0.02), @@ -105,7 +105,6 @@ class _StatisticsViewState extends State { itemCount: 10, barColor: Colors.green, ), - SizedBox(height: MediaQuery.paddingOf(context).bottom), ], ), diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index 279c492..3692167 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -80,7 +80,9 @@ class StatisticsTile extends StatelessWidget { const Spacer(), Center( child: Text( - values[index].$2.toString(), + values[index].$2 <= 1 + ? values[index].$2.toStringAsFixed(2) + : values[index].$2.toString(), textAlign: TextAlign.center, style: const TextStyle( fontSize: 16, From 7cda25a380af58b4714bb537ac59d0ec696d11df Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 12:34:42 +0100 Subject: [PATCH 81/95] Changed item count back to normal --- lib/presentation/views/main_menu/statistics_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 56bdcf5..02eab46 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -93,7 +93,7 @@ class _StatisticsViewState extends State { title: 'Winrate per Player', width: constraints.maxWidth * 0.95, values: winRates, - itemCount: 115, + itemCount: 5, barColor: Colors.orange[700]!, ), SizedBox(height: constraints.maxHeight * 0.02), From 8ae85c925d4795338dba1d3656fb22abd4d56b95 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 14:29:23 +0100 Subject: [PATCH 82/95] Fixed double --- lib/presentation/widgets/tiles/statistics_tile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index 3692167..6e3b9b2 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -80,7 +80,7 @@ class StatisticsTile extends StatelessWidget { const Spacer(), Center( child: Text( - values[index].$2 <= 1 + values[index].$2 <= 1 && values[index].$2 is double ? values[index].$2.toStringAsFixed(2) : values[index].$2.toString(), textAlign: TextAlign.center, From def37aa6402b8054472da849755d3a709034484f Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sun, 23 Nov 2025 14:56:53 +0100 Subject: [PATCH 83/95] Refactor Recent Games tile in HomeView - Move FutureBuilder inside InfoTile content - Replace hardcoded winner and game type strings with actual game data - Limit displayed recent games to 2 items and handle cases with fewer games - Update player text generation to show player count instead of names - Remove TopCenteredMessage usage and replace with simple text for empty/error states - Update skeleton data to use Player object for winner --- .../views/main_menu/home_view.dart | 120 ++++++++++-------- 1 file changed, 64 insertions(+), 56 deletions(-) diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index 194849e..f0f1483 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -7,7 +7,6 @@ import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.da import 'package:game_tracker/presentation/widgets/tiles/game_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart'; -import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:provider/provider.dart'; import 'package:skeletonizer/skeletonizer.dart'; @@ -35,8 +34,7 @@ class _HomeViewState extends State { Player(name: 'Skeleton Player 2'), ], ), - winner: - "Winner ID", //TODO: Should be player object, but isnt yet, waiting for pr + winner: Player(name: 'Skeleton Player 1'), ), ); @@ -73,7 +71,7 @@ class _HomeViewState extends State { enabled: isLoading, enableSwitchAnimation: true, switchAnimationConfig: SwitchAnimationConfig( - duration: Duration(milliseconds: 200), + duration: const Duration(milliseconds: 200), switchInCurve: Curves.linear, switchOutCurve: Curves.linear, transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, @@ -130,70 +128,79 @@ class _HomeViewState extends State { ), ], ), - FutureBuilder( - future: _recentGamesFuture, - builder: (context, snapshot) { - if (snapshot.hasError) { - return const Center( - child: TopCenteredMessage( - icon: Icons.report, - title: 'Error', - message: 'Group data couldn\'t\nbe loaded.', - ), - ); - } - if (snapshot.connectionState == ConnectionState.done && - (!snapshot.hasData || snapshot.data!.isEmpty)) { - return const Center( - child: TopCenteredMessage( - icon: Icons.info, - title: 'Info', - message: 'No games created yet.', - ), - ); - } - final List games = - isLoading ? skeletonData : (snapshot.data ?? []) - ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: InfoTile( - width: constraints.maxWidth * 0.95, - title: 'Recent Games', - icon: Icons.timer, - content: Padding( - padding: EdgeInsets.symmetric(horizontal: 40.0), - child: Column( + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: InfoTile( + width: constraints.maxWidth * 0.95, + title: 'Recent Games', + icon: Icons.timer, + content: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40.0), + child: FutureBuilder( + future: _recentGamesFuture, + builder: (context, snapshot) { + if (snapshot.hasError) { + return const Center( + heightFactor: 4, + child: Text('Error while loading recent games.'), + ); + } + if (snapshot.connectionState == + ConnectionState.done && + (!snapshot.hasData || snapshot.data!.isEmpty)) { + return const Center( + heightFactor: 4, + child: Text('No recent games available.'), + ); + } + final List games = + (isLoading ? skeletonData : (snapshot.data ?? []) + ..sort( + (a, b) => + b.createdAt.compareTo(a.createdAt), + )) + .take(2) + .toList(); + return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ GameTile( gameTitle: games[0].name, - gameType: "Gametype", + gameType: 'Winner', ruleset: 'Ruleset', players: _getPlayerText(games[0]), - winner: - 'Leonard', //TODO: Replace Winner with real Winner + winner: games[0].winner == null + ? 'No winner set.' + : games[0].winner!.name, ), - Padding( + const Padding( padding: EdgeInsets.symmetric(vertical: 8.0), child: Divider(), ), - GameTile( - gameTitle: games[1].name, - gameType: 'Gametype', - ruleset: 'Ruleset', - players: _getPlayerText(games[1]), - winner: - 'Lina', //TODO: Replace Winner with real Winner - ), - SizedBox(height: 8), + if (games.length >= 2) ...[ + GameTile( + gameTitle: games[1].name, + gameType: 'Winner', + ruleset: 'Ruleset', + players: _getPlayerText(games[1]), + winner: games[1].winner == null + ? 'No winner set.' + : games[1].winner!.name, + ), + const SizedBox(height: 8), + ] else ...[ + const Center( + heightFactor: 4, + child: Text('No second game available.'), + ), + ], ], - ), - ), + ); + }, ), - ); - }, + ), + ), ), InfoTile( width: constraints.maxWidth * 0.95, @@ -254,7 +261,8 @@ class _HomeViewState extends State { String _getPlayerText(Game game) { if (game.group == null) { - return game.players?.map((p) => p.name).join(', ') ?? 'No Players'; + final playerCount = game.players?.length ?? 0; + return '$playerCount Player(s)'; } if (game.players == null || game.players!.isEmpty) { return game.group!.name; From 9e8bab1a6055be5732047074fc64e49a20203613 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sun, 23 Nov 2025 15:21:40 +0100 Subject: [PATCH 84/95] add artificial delay to group list loading --- lib/presentation/views/main_menu/groups_view.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/groups_view.dart b/lib/presentation/views/main_menu/groups_view.dart index e9af8b9..7d852bc 100644 --- a/lib/presentation/views/main_menu/groups_view.dart +++ b/lib/presentation/views/main_menu/groups_view.dart @@ -34,7 +34,10 @@ class _GroupsViewState extends State { void initState() { super.initState(); db = Provider.of(context, listen: false); - _allGroupsFuture = db.groupDao.getAllGroups(); + _allGroupsFuture = Future.delayed( + const Duration(milliseconds: 400), + () => db.groupDao.getAllGroups(), + ); } @override From 26fadf5093ee49ef0575fdfe7bba1d23042137fc Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sun, 23 Nov 2025 15:22:59 +0100 Subject: [PATCH 85/95] add artificial delay to loadPlayerList for skeleton loading --- lib/presentation/views/main_menu/create_group_view.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/create_group_view.dart b/lib/presentation/views/main_menu/create_group_view.dart index b077bbc..11f241c 100644 --- a/lib/presentation/views/main_menu/create_group_view.dart +++ b/lib/presentation/views/main_menu/create_group_view.dart @@ -54,7 +54,11 @@ class _CreateGroupViewState extends State { } void loadPlayerList() { - _allPlayersFuture = db.playerDao.getAllPlayers(); + _allPlayersFuture = Future.delayed( + const Duration(milliseconds: 400), + () => db.playerDao.getAllPlayers(), + ); + suggestedPlayers = skeletonData; _allPlayersFuture.then((loadedPlayers) { setState(() { loadedPlayers.sort((a, b) => a.name.compareTo(b.name)); From 604a541392c0bfb30b19e4bca62be7268f22ce93 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sun, 23 Nov 2025 17:09:52 +0100 Subject: [PATCH 86/95] set delay in all future builders to 250ms --- lib/presentation/views/main_menu/create_group_view.dart | 2 +- lib/presentation/views/main_menu/groups_view.dart | 2 +- lib/presentation/views/main_menu/home_view.dart | 2 +- lib/presentation/views/main_menu/statistics_view.dart | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/create_group_view.dart b/lib/presentation/views/main_menu/create_group_view.dart index 11f241c..59f72ed 100644 --- a/lib/presentation/views/main_menu/create_group_view.dart +++ b/lib/presentation/views/main_menu/create_group_view.dart @@ -55,7 +55,7 @@ class _CreateGroupViewState extends State { void loadPlayerList() { _allPlayersFuture = Future.delayed( - const Duration(milliseconds: 400), + const Duration(milliseconds: 250), () => db.playerDao.getAllPlayers(), ); suggestedPlayers = skeletonData; diff --git a/lib/presentation/views/main_menu/groups_view.dart b/lib/presentation/views/main_menu/groups_view.dart index 7d852bc..aaef1a5 100644 --- a/lib/presentation/views/main_menu/groups_view.dart +++ b/lib/presentation/views/main_menu/groups_view.dart @@ -35,7 +35,7 @@ class _GroupsViewState extends State { super.initState(); db = Provider.of(context, listen: false); _allGroupsFuture = Future.delayed( - const Duration(milliseconds: 400), + const Duration(milliseconds: 250), () => db.groupDao.getAllGroups(), ); } diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index 34e4be3..2230a91 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -27,7 +27,7 @@ class _HomeViewState extends State { _groupCountFuture = db.groupDao.getGroupCount(); Future.wait([_gameCountFuture, _groupCountFuture]).then((_) async { - await Future.delayed(const Duration(milliseconds: 50)); + await Future.delayed(const Duration(milliseconds: 250)); if (mounted) { setState(() { isLoading = false; diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 02eab46..6107586 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -29,7 +29,7 @@ class _StatisticsViewState extends State { _playersFuture = db.playerDao.getAllPlayers(); Future.wait([_gamesFuture, _playersFuture]).then((results) async { - await Future.delayed(const Duration(milliseconds: 200)); + await Future.delayed(const Duration(milliseconds: 250)); final games = results[0] as List; final players = results[1] as List; winCounts = _calculateWinsForAllPlayers(games, players); From 963edaf1d1be0444de244399d7561648b0809510 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sun, 23 Nov 2025 17:51:55 +0100 Subject: [PATCH 87/95] Update game status text and player count label in HomeView --- lib/presentation/views/main_menu/home_view.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index f0f1483..aa21f76 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -171,7 +171,7 @@ class _HomeViewState extends State { ruleset: 'Ruleset', players: _getPlayerText(games[0]), winner: games[0].winner == null - ? 'No winner set.' + ? 'Game in progress...' : games[0].winner!.name, ), const Padding( @@ -185,7 +185,7 @@ class _HomeViewState extends State { ruleset: 'Ruleset', players: _getPlayerText(games[1]), winner: games[1].winner == null - ? 'No winner set.' + ? 'Game in progress...' : games[1].winner!.name, ), const SizedBox(height: 8), @@ -262,7 +262,7 @@ class _HomeViewState extends State { String _getPlayerText(Game game) { if (game.group == null) { final playerCount = game.players?.length ?? 0; - return '$playerCount Player(s)'; + return '$playerCount Players'; } if (game.players == null || game.players!.isEmpty) { return game.group!.name; From 73c55868746055b3272335a6c28eb2a8c5514c00 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 17:54:33 +0100 Subject: [PATCH 88/95] Added icons for android --- android/app/src/main/ic_launcher-playstore.png | Bin 0 -> 6223 bytes .../main/res/mipmap-anydpi-v26/ic_launcher.xml | 5 +++++ .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 5 +++++ .../app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 700 bytes .../res/mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 276 bytes .../main/res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2164 bytes .../app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 588 bytes .../res/mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 210 bytes .../main/res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1400 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 904 bytes .../res/mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 334 bytes .../main/res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3060 bytes .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 0 bytes .../src/main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 1228 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 430 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 4656 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 0 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 1656 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.webp | Bin 0 -> 520 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 6724 bytes .../main/res/values/ic_launcher_background.xml | 4 ++++ 24 files changed, 14 insertions(+) create mode 100644 android/app/src/main/ic_launcher-playstore.png create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp delete mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp delete mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp delete mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp delete mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 android/app/src/main/res/values/ic_launcher_background.xml diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..e7244c6c07cf5749b8e233a7f2291a14fdbbce1f GIT binary patch literal 6223 zcmeI1X;hQh8OLt|BOn3_7LYA~b}}sjR@usups46%W@2@YiY#H#K{^OV1j3S#!39T_ zxEzRxBvgijQ-sK3Si>?7Ngb9TvIi0%Y_cST07*zl8dTcT)35zv51-!q;eVIsdGGx_ z_x-;=p8CdDZ@bxc0D#`f6Gu-2097}kKzp0|v5f0k1HjbyG2`nyII4hvHDN9Q|k3afk2Nw13`ieFoTEr0BT#7)o`yjnfxa)1Lgj zFR#9$;%pN;ZMtp!>T2mqA6FPHtYl9kr1R?#&^{Rxu7m)o3JpN>O-%s4ILw5+c=G?;(9*MU`hRZ_%M^WZM9bD|lZ#G;SnvGmXZ&Mr|( zMfYvypntpgHoPn#lc*jz@Uhp6I4FQxAo155aME|ZzWo9zJF_()dW5im6LhWiCQxg) zH4kY6?H7_l2~)^b^wvbSndG=Qj0e;mpH!HUr=91=h{!Y1BQg$d!(wj=MNluY{G^eh^*;dEug-eZR|3bFik+h)P>-ED3|4 zAQmS!d)b0ABk+f^-+`S+fH#2L^99~JAONE2Z3O{;(gd12L4qdubtl;A4JOn9`)rA} zQn0lPKIwztw5Tj*G4G$zVkt?I3@_M%SrQ_d?96fA0Gow-ZAvU9yc5UxhyZV_FB)bQ zjbB{Djpoalmh55A#+8{%{JUvQ2g?S2Lq=W-I(LQR7DbqSEE4w%tZ0@g2^LxMKsh`$wK83AhKN5|?Jn$69oA9Bw|zN6{7H8dYf$vjwf6VMMjWJeCh`!seJI z5+4~kvNBq;THZ!>AU@|YJ4~`LO`=MybmE7iL8{(IP@U>&^QFS_;)Z8BRTgz>Zk}^_ z06!Z#H`A@?|MDZSc2^DD6C|u7nUnra4cA3``|{Z%2Uk-X;v@9>{;ip7Qh5pzm8)2h zkBLK|;?7E@uggY>sS{wfX%ju6AXJ-}8iZ69W93=qo=Wsp(UdL=AppZ;osWJ)+Qc*&(F_vhyl<7lMSePABI|!!OXD@(w@2p{_%T| z_@1o0_kn9V#GoztR%W&~!TH=JCU0N`*8;` zteBLcN}O}Gi!XIi#Z0cj%PdF>vR61&Wf)34tuO>17)X8w+(hq0qE#&qRxigP$bB2u zfiUwF+aHt16d3UQE@*T*{%h3j#MZo71BN__CxO?DiR@Ts56HR?5xF zJL_6}2)!>fux?V9pIzvfA?e=(9IXg(!-^hPGJ?9{3UqvVb>aMl8!z4K3;~ibmb3v) zJD74-y3k2={6N0T?6i7jQ^Vr=h7V$&f8y7*)k2JUGSLdL{~Ph;@l< z3|1@`!Ll@9LVxvIh9qwUz0URO+dDbS=Sgd_a731_c6lT@eZi&1ZzSFK!&%w>O%>NI z#sd^xw@QQ;_S?$Z-vj=bJy%0I7E_&xs#b`*b&h33dnFFe#!Faenoy+rW!&y2Ss&@) zvQQHeIjoQsg_RT1Yq|9GHId?PmUDz~ezbnR=K%phmR6G2lgkQQH%6apl+6_JCDE$y zEIjbI2<1kJc-H!vK*TU0`B!7$&{TVYf|fAxpf|ny{zEPDcPVszLPd&ao!o%Wgg1ux z=+GZctIeR7Yo@YM_*_b;*cFcOK*UjSSB4w;$EW>Xv?kDTcV$$Xl7pLejJEHfh|yp- z9fy*=#*;EUsEpVEMny;6ZElMqZc(l;Ze<{y@5|;j zUaL+Vln&)MCf<$7QN~f!^{}nPCYe2_LU5I%nMDa_Zh$YMMEgcdjcB9};V_c0^owN) z(jcJWic-CT5WOfFznW!0Sr^nyXHqkf_S`ijqmissI;NA`)N#hH=)4L&Q+rAN5~VUo z;e~b>JDyc4OEEM;N^%*@Mktpu%2ya#EHtJrCUS_t5YO*VmXLU6@t#RyY3uj#H08I4 zmLl{-n%+imgcp6Svd9N6A!$(OFJ&5=Jl$KYPkEcE`YN5=Zj8nnvk+p{m{87h_Mdii z>I)TRH`_Yt<15_Q&p8+D`{oy4Yn1Fto{Ie0cgyVk#LQl`ORv + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..8049fd623b9a7c3441e6926651bf80c20e86a244 GIT binary patch literal 700 zcmV;t0z>^$Nk&Gr0ssJ4MM6+kP&iDd0ssInN5ByfKjgTPBt^>M`R(qJOXt6Io-j@x zIN>&uBt^=*duGtytoL6>oqwm@Mv|mRHM7qW+VB>=|0)@9yi;&%+g4SreR20f+*=^l zBFN~1R;&ghkDvxZ_b;ao03aYBAP@xsARr*%KnVDOfY-PO|GJYGPJYEAtp3i|%g(=?ek-uma=f@vY5P(>PRS1Y2PA^{{G1?uUr)hI- z)$zd(6uPj`;h>$1&N4p?QrB9s&syly%4m<+Ne3he%}NAN%98-fT+|Zfzz{#%X33Z@Tow6 z4vCQFKO literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..a3131a17d34303d3abda58d4d5a158bbbf437bf2 GIT binary patch literal 276 zcmV+v0qg!!Nk&Et0RRA3MM6+kP&iBg0RR9mp+G1QXMw1Vq;&5E{$*eajIcHkM6!+j zC}0#&N`FC&$Eb}YN8X5INgaO(_sn(4<85GsHK=s)ZqnJ<+e_Jw0%}bC(Yp9T^lnmcqsuD?*&GV+z3=8hXRM& aA7Cq4pz}J(>m;C_j(ko7y6ighzd;&|czuQd literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..358841497c4bcbb8692e6c7a010b20ea44b26573 GIT binary patch literal 2164 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 MXhJ!asU7T literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..0d0468ecc8eae0b6ededfdd8ee53faf2ddad7fa4 GIT binary patch literal 1400 zcmV-;1&8`lNk&F+1pok7MM6+kP&iCu1pojqFTe{B*XOFXZB<$O3QqqvBp``?y@%d= zHl9IpB*~2=c@?nyzxs=@imVo#9|4e%BimN3tiA93ifjI1kOV0(XXbv-*-&g-Teh70 zejo1c?heW6ZkYj)83cnNQOP9t=I-wBegFSI>i_@(0uBf`pg%x75D*Y>!2b+@2LuG< zkk8+OcL5}g4qJJvgk)(lKLWc2&6afdNtB40aNCE-^asn`5hCxQ+ z5P*OLj4vaJ#1k-HDIUSA7?N$L6(QY84n-%(cY+g0h$J8hj7UYM3SupsfDo(jF5(oi z3le;q{_l_2g%c3kpKqK%f)jiS0zO5YhJ?NrK1KWa!oa6Nh*fwmVl8eTuUtP~A;G8d zX%J!+2(gRpxX6A?4fWk_d3P3+%k8BuHfs}1lri@E_vg#{{=PVuz8NEns|B<5*@Vd^1=)XC*GKF#gRbUK0dXX1W3vD_m)wcbn5v~n>sRG^xdPFBF;t$n|0 zRzQR@g0IGbPp>T0%k6)j8g%GO?H9FkRFL^yLxI|VL@`jMB3YEA7Eq!Jsbmqp)QYKO z!a3;x$`=a3oYNV3&L+h%4VuM;DoA6b2}wo@@I**M0Kr4uKI$s%L<))O)t&_c(xD;d z46k^I(^lqm?frH7^8aHcow4*-wcK}a(zP{iug`Cuw0k6eXcyH>;_E#AU4K9335>ih=&NZKmyZ zQtw7=dl1{1Z_S>)4rAL+>QmchQY&@4YumPB?UmH+u45;^F0Y9GPXIzo-kHOfcjgi? z3kLU3UAAxU@)O)l#32}fZ9RMS8!&Ltpn(JW_3F6|027gQ27c<+XRxB6gZp&*0q7i! zur``#bXS0f;z0Kyq;nKuPScRh01v@|EsBgq zky?gmEWiT^ptsUCMQ&>vmjVwUficR~6r;B3%M_#^!zQIQ#j1x^e-7r;85@R>O+go* z>v{T|0A#E0|3GIC0gNI;fls^4^MZvz$k0b=2p7XfrLFj^p=*8z10e9@NXk)slvAJV zh5#6TR+^GPLsvfs1__KOlMF+zry)6hsU)2Y?M(XzyfZ#i7A1p*t$r4rIldq}DR%ku zDUQ@ve#6&$u8o0S%>aLW<9E_|p6AYtPW^g4@YuJ0XI|!c;j9=wyb<_!M3ScO0scYw^J9 zt$B(uzjfk&E%r%2h-~WzCpuNpRXG#yZcEtpb7j-u*GZ$G^9ShHFsL@i5onzrA+V$~Bbz4`w8 zZ@&EGsulY?G_6y*1~CejZP>I`$D;=i95{HiW2>eOvrHgnS@?5jwobhU4eHg&?(AHU G2n_&;$GCI= literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d4391482be68e9e4a07fab769b5de337d16eb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 721 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!iOI#yLg7ec#$`gxH85~pclTsBt za}(23gHjVyDhp4h+5i=O3-AeX1=1l$e`s#|#^}+&7(N@w0CIr{$Oe+Uk^K-ZP~83C zcc@hG6rikF&NPT(23>y!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..8aa87cc45b48978136c981269ed293717498fb24 GIT binary patch literal 904 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 CIniPO literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..71193874ff1e2cd9bb09b8af3de77c7e3ae27d1c GIT binary patch literal 1228 zcmV;-1T*_mNk&G*1ONb6MM6+kP&iDu1ONapkH8}kcLzC=Bt=qWHSHd={<__p8M-Tv zWq{Z=k|Rk~J#*gVAKWEj-p*|o#72@7Nm(^`@c;hX_Es=Y7ZlspmM!Obe|PtvDH`;`n?U5Uh~k{TdtOp;mhg zs}(lI4hyl+L?+Q;Pxr(45uS~5A#B2qOA0_DiI84ol>`Y%gpDMC08mgw5}7>JvhGqn z?!pOW4rC)4BxDoS0D%Mo0U!{}fNUfo2}uNj1Sfyfh0~`QP5KU@12t~wmCr=zVcy0+q zqz;jQ^yrcO`?7Nko}1PYCq?D}438byiBPi~QUalP zJhd0IIG8(e%_WHv$%~f{?ln1}JNfL!QeG1A@|nG<9Ms(NBV<7$k9v4-GAyak9vGs_RlMyS0(aWx-ic9 z2VIsM%8n;LwEcX&C6Ukhu7k-FIe+>?n|QI=Q25dtcQxN$oX9!9f5)lSVw`inKIbV0ZT~t{w8v#_@{7Xc)95iWl=3WTbPVV0#x`e+_B!bND6F zWcxmYM~xqA@Z&}g>D#tcZ1G?(yG#NVSKqKQG5PXWtW~GB!DGz|1@f6?!hBs7iKwi) z`GyuQT_i3afC72pQ0At}WRM9r@4)cLf<=oNJ}V?l>g}e=NSX;34{!gV;1J7c=$6Hq qzYm(KERknS*j?Q{JWVe{H%+A6E*O&>k}-t~06Aw29hd=#e<}r4S5sX8 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..d62b6730ab7352635ef1ebc231be8c96592e690f GIT binary patch literal 430 zcmV;f0a5-^Nk&Gd0RRA3MM6+kP&iDP0RR9mL%~oG|G_AdBq_PK3j9A%fguzSG=~XB zZ6rDJ#(N)z!*u+4q-n-&+XkZ3-%WK(-2RVSBT3l?09YnTZM*+}&`Ydcvl}HqfcyO+ z2u_-bI?(kVFGWaImRuns1(Pe3R$>H|2AA?#p!Q$ME@s1(f{;6{ZIeX|MWlo&rTZt*;IJuH=Z7wRtB)Od3MY) zkh5pU1Y`2-=wbBeKl+dU`-No&eA1ru^TSci$d|m&j$Jc@ShhYnEIEiw@X4{tV20>F z`j5p)&Ii&%xAye#7BlkM(bJ=zK`hvx99kA46Mc4MFkSQ?{r4}*rht*!*T?7|C}KDt z1FZ#7GxjYo7W-5iqPuzwRICF~bE*CMI4_qU>j&ZufJ#I?irZ9Jdbw6d+8kkFhfN)+ YC&JPptD^tufBK*Pr~m1H`v2c@06aL?9smFU literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..ab5d33715c7cf2e26c09cc29374e35444ad5d9cb GIT binary patch literal 4656 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..ae5a179a7dea0e707310319706535417ab8c0c91 GIT binary patch literal 1656 zcmV-;28a1lNk&F+1^@t8MM6+kP&iCu1^@srzrZgL)rQ)(Z5aCh&r{ckP?)p(G+sUKJHZT zqfe}N2wx9u0{|@Z|4X&qId*OC3<3Zw)4$AZqriCEW`X`s0ROhluOYsM`WpIcSB&FV zM!f;!RYqA7M=Rby@G8vAGwaJ~#`z7MdH#eu{&2|5T=B?#4H+|wX^vh0XYJ(5PQHk$ z#y&CFiQeId?L_-uZlc5-$HYdNXIy_WzB>O;>;yk35;(p2e(rbXQBtGKR&~r6V<#q& zn5@(!fmWi_>rG=!syfa*N+@QYFvd>wB2ldb(r6{BNc5)panC#;I40x8x@GJ{9dAly zz3^ofugx~2V~k1K%0Vz&Sjh*PN1kzdlJi+tn4PdlK zqoP0A$v0-MeDuQCdT`TsR_mu>Xxs-4sB~q)j5j58Jb&NJvcH6cr@EQStr3YyNUz|vEa#9xwp0Dc8 zm}YlRgrjh)YYwpYcarTBgUKd}jP%X(J;oDf3To$h;PtPkCjfs>Cq*O%kw8+O?J*v^ zQxqNRNgx4)+d66Ta^z0Q2#NgSn>*?9a`>*&#rt~|38*}fZkT4s%c1vK<_2mOD3F#5$Y!&N z(caemX!zWv!eMw@<@?d#z6!bRfhp35|853#k6j^@0|5LE_N1Vg+YDYVz480*N40r~ zdjc+OF@x*ek7{Ed^F$o$U+2SrXL5~Yk`NXk#0m&Hx){Chp48das3$OV*k%O;_at2; z44=!aJxOPg2f4&}_-~e}J`HhC#D#5caGgXnysu*6cwnh~#t^9o`OMlCLAh|l7J~}e zY^4py?@L6J@8cm1I~*(>s?BwoY3*y=mtYZ_^LGVxj!mVJj};(#J!y9IJeJG_1bwWG zP-7ma-#0_`sh1G;u_A>3(evz#9sN*&NR4v01493*X}U7vWY4mR-%TF^x%N!=xA(=6$L;f zYaszU;PuMM=QR8{z=mPk2`S>pfVMO%%xp zM53fq+%*e(Jx6DCeH=%8646RnuZlbHziRfym#Rot)qWroL9zE>4^EkVtK@Yf3ao0t zVs%`A4+y(**4{Hc_@ashy&#Z-Am}YfRWWw@?)xBY;cB>+Yl;M5+;A;?Kp==X``*_d z>cJZ|SX3+)twiZN{(=R?qDb&o6}QjA@%O$1A}3;C=K@ACLG1$`SOTZj2>>C}h}>hh zt}A=JAFF?qB2`7xPT=?=K{~}J30|$@{$99nw+}n*)}y%qoaRS9ASkfvy1*8=tw7Ln zgij-K8u7cnXaAYI?O(au=bLBy|6q^)kzS^UdyEeMa4*w?vkbTQ{J&!N&!;{6!H)kq zIE@6o9N~Wew>fTCF-brG3UI)@QC*6FHHid~(}-FZB`)*WAlxFH$D?b5DEtiWNK9{R?6JKe~15j(FlrMowDXn?nY6;}oT?7E^ Czc)?* literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..85dcbb73f50e37f8c5dc54eaba500b6fe6acbe45 GIT binary patch literal 520 zcmWIYbaP{1Vqge&bqWXzu<-f9$iSe#{$RF1{-G7QDxUB1Bo6;K)tGiZ=*C=Omc<^E zM4leHJA2FAdRdFND<-PAcbP@ScqjebKdoh__VtN5v#)K<{eRa)PPlsKt-9;g?^%o! zewP-OwQ+7*c5aGMkk^)Jg;%&r3_fjl|7xOj=|yGetCfeOw=Ll2U}-dH`f$91kylND zjY09_5-F3#4W!;5>HUAa-|g4FdAvs!x-1E@kGZse zk;w{|uHQFO@=xsiV8?&$|KsA{pWp78eY}&6W0K13ZRa-U-rkn`w%+yEXVdwA{apU5 zI~@4`!Tt~b|HEk1i+2|mEX%4IDPUuZ?W<_@KHJpP9~^?dL5z+t1!HFP(HgX2yC4{RL_JZ1z6q{MY>d zf&CBuf4|SEho7`~crEQn-SH`tE6yLR^;Z8W^gZ?E(H-kO^_if?t>40Bd;Z<6N1`XB zFMauyW3YI-MZ9sv3D3j4s#TU|5~VA5E_<|Vlg~Px{0*%ue}2vRa7E?rl$eP-AGIxC z^-THcYR|&e>)tMV@=A|IInF2Skl5B8m)C8*G_|bod1Zp?8cU8J^8ecZKdeWi{>z^L E08)Y(Pyhe` literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..7da3e2afb5a3f852ab740e6d50cda6d6efc9ba9d GIT binary patch literal 6724 zcmV-K8oT9ENk&FI8UO%SMM6+kP&iC48UO$6uwEGcz;e zGBY#tFf;QkHFpm-<1jZ2b2AilW#$>MJ9P5o$vm0o{4GI7bQ@yyIw};)marx&Ld>k( z@s8axhb)c3jEoh_uC!5wlE|u%e7yTg+t&yp3{?E!LE?V9BZ#bGh9SbrBh2 zS(_1ZYsg!9IbP=oELndI*+L4EY@4>N>VK1-^=;esKXOYmspmjC#Z@eBGt+E9nBI!<&5AvdVXO{1<(c7a(c}CaXIwMve;~FckF7 zL!h)6s8nr+A|a3<3WyK$cdzkA z@4V2s^j=Z5 z6-a7`i?`h1o)~!M_Z5-}L^UB1R>bzraFwBOaC3qEaa;Ng)=fq})Wyu~G?+;oAya>7w`h z*8lVZxSOYMZ_M{!s5DF9N^56KsgmyPwd#eQH40Pz^cjNoAG6`lA z3xx2y-)&VD9b3C&P_cTRU+%%GyjMO1PXPZ@$r z6$C4)EW#l?89F>zC0C`~ABTHJB^iXR1QIOu zViO2Yf6u=lm&Z_DqDhB5WYeVrNuP4oS$|@hb}{yp=Bu>j0M79`FFWI zjk-48@E~+ZAnp`35B<@nQq-kNlixoo9Tazpx`!4T0F6kxC2E=`Zgn0h;m{N@c^lVZpa< zL73rW=@aWfr;mMmmP=Oh7z(9d53iHK3^PlsU^1Nk^7DCGzEl~i7`eYI64m%2>_M~O zT0uYS=e+3{YCJ+!Y4T>3wy0DC&+cUD7lI)<^s{T`?{uWXbyaRT|56WB65^7U!`~Jz zc&U%vt&;f~fj0c%-uETJC24d;KUp%rGQc70WsHLtb;l=dhmS3pJMQa&GduO*pu98IuzBL*`GYf=ZoQ))Ebxm=^$RL}#qEV>{X0U(=i>|KSJWV=D>LX1) z1DFUjo{Kfn`+EL1^VqM)0}Dr*bWJJ;Z|I=xl6T!$;Flkm?dwme{q-kE9&>#AIgS7R z1O6DuYBZ6kt(XbuoWvK{BHkJ^^NE0NGeL*yN_AF`3#v!&1B}b|ChPlJRH?Q zON!S*K!YcplcCAyzrU&U+B1-&V~sC9M0QV~4mFdjYPm{!9sMc;Av_{WQEGhg0kV7g zuyt&vt~uHfBQ>p#Vk?k7V;W_VWy3j~Z$;D2(vmJ_3XpbSAL9TItfisM%=RJE#iXS9 zWW=J95Sm#4!30RmcvfhF}Z1bQm zsL0e8C?%dAYQYAhn>~>i->V~;CKprt+52Dy3pUt{dN$RnPpdCRqCQjZocYpkXaOCT zW7cr~kJiJ-G&X2UjJnD21PD3M!8zx3D4U+ImelMvA)p-~J;dBJK=$ToQrQUTo7|Gl zbNd!4keZsBO27TQftl?b@D3${ZNZfe;9NM%}0$CqS3_3h@tm;QhfOVG@YV5qr+CE2d+ncwOMAp=Gj=bSc0 zs>XT5zxGlM1X_lsLz|$P1tYAvj!jwDLBkKL#J^-jK+j)#22TMqys!!=-&oqrmk#wQxo*&MH61z*PXSIW^=--}S6#1id@*>g4UGMze4x_0YO3POz``Sj zAGS@9cIHWcpoJz-#5i3=uT3F;2sjH52{@GT?*}NYt=}!}aQHAT~NCF#lfHu(*Zj$BseHWbC+GO? z3(7osl+yR_gG5aB=pkx<^$A~2A9Ue4Mdw`O62m@0*V;hZ4z*I?Zp%G;oQ|Yol1C3x z|Ic5b5#~BJrK^rvZ;P0#18oEh8_O4^;;i@kPfYgY5jv8KI=}t^jmT&uQMTqih!_QQt%X-b@})+wk|&UuIn{iKJ=3gWm?Rx5kc3XaQqvI+ z+N&Kg-+_R^BJb#IFVfm>yO? zNt7&*WT2&HA|JfdRllZ*fbUS2sF@^?#Gs|7BRTZBx+q_Oh!;>6NyqagA!sc%67ivX zbx`gJ42T(jRTN)p04*~mO$XRBgP7fbo^tqgga+7^pnYympQ_!{K9iL1LChiM_H8Ep z1nujVX_9|CTSp`@Hv+oKn*~>YjrV}%W2)I-uM_;0*A03^tffT2x~W|X*w^hoHs!nD zAmFIHGSDGJkAPJp>nTvtX*-muDvZ~!MS#O|Wnh$9Pd*PBpwhsmRMp+gKleC@*a`v5 zxsz>B3sTI>9d^zWfm;!x-Z}r8AYd_*G7laRq=e69Gj^}_kvrthR87bZ!3Y8KiZzEq zCkIYASUXV<+iP0lWr(>!Fhsy?jGC!DsTnfhgmF%nwCs28p4rz8MueEI1@sXx-380n z&;9}P07_8Rs(&}Y5vI60Ffcjf1g$O~bP+H?z`miJ4SBW_nCL7`Bqm9fBj6Hs*`Q~V z6JdB{1Z?L`j=@s^lm%@H&v|X@M>Y^+yE-sMz-C5eK0G1^N_dhwd=8s2(wicF$VQ06 zvsLEdXZLt%Cj&|tIAbC|=-DGPuL(>%4q{e{+{4@Kq&4?j82Pv0=*yRT67jpvX&s=e_%HNN-|pa1`ceC14~(uD}PW)}hj zH8G~pm5nu%O;~|8H2m{dCVu!Jq;BmTGko$6KRgZb7ak|6?F z;bdq-t*<_Yd)Ti6%ng`R zT%j*8ibue0KIIAgD*&RiJY@0eA_P3+pH)~9SFm0_SjSeHbBXSV9QfeEYbA`Z*0$In ztH&W0b?C}r``Kq&6T2Gbfe+?mDtFiK_*~t>t{5ys)nbvG484O%1Oo1JICoeEc<8hX z*c7SC&WY#^Obysx{8oSEU>j1>HNBY(WIzYwoH1e0UduzS4D9qChXkly1As-?wLo~? z_>(ga2$1}UQ<=)oD}~sw{=pL0Re~)7##aOm(NiXSgUq1AJgdVPbDoWU^%UV=qV45@ z4Fb*w?A57}-2@r1q5YVusg~W&-7ofXz*b~0!Qo!Ptb1@kz-wmZW)R;|=s<>UlerTP zSN$~k`5pv!T9U0T_p-q*b;Q}j8{10AfDEl}N=J2|UQ<#p3+!`k*j}=&wP&zEzzG4L z8dW(#rvMl_%ltNldzB&JmwsWZ#~~q@`3k`i0gthIO=<@jaG}*rn{cpw&R^p4!3lE< zk?rM|6pa-f3aNlcxn z53*)Gvp(#ChtEx09At6b~A=)lD6Hskhul(6z0V4ca} z@L*HUZuZ$}4yQUO4?yU&9VV}?yP0$K$-!n(x5SMRFv{Ekcp%_gJiW87%@NNA9=IdU zkYd=ij${o8o|(D$&ZNqUKEN@jR|uWH@}kox=s<&RljRC`zwZSib9x6S1+GqRvMB>! zH*1hJN6qEPgBZY|)B4&}cI5k7b86P>1}cf_WS%|tUchttiK|c5vB}ec1x`Ct!l-jy zS-xlRvJ*XXNHx(%_69Bpco$+vOq;xGcM2Uy&{^hlDAMywzZ@~E^a`$#dfYRI^khhh z7y!Hy4z`X_v)Pi5k)&b$KYn5Ir;kF8(hu&Z?r%RK#P|PysPg)=kfhdEpMazG*PlX? zvddf}aUi?d_NNa8hj1 z^xwY_!W%bc`s{s5-@6ZTf24JO{Sh1u{``%JA3gxNk9*JX@jFPO9e=o9$8Yt?jM-L? zNJ`BG zz2q_Nhi|apuRkF2OIh&e@0sqyHz9FvQ@{Tjvwr<4{nMUVB zj>t`yF!EeC+WvI6;2x1es7a-vOJV<%<-l1TMf;yQ`ap-RetWt5OuE(W!xHt9X=Gmj2c{PSKLi4= z8`@Q@`m%rx&4*<*rmc@ge;}zz6T>T)nDP{+@y7DCpc_ zlf{d73vT>e{XY2UA@M{e>3R?_iO3(29n^L=XZNq%C7|=z@p=}4?2+og) za(PU0>0|O(6$sd--ecR{u48b;YQb`}Em{^YUR}Mjbqv3SOtmYl9!!(PuvxS1(fvSB z+g+VKutNIpF#~KREicMhIv4FFxYM>Lh_+{2i91NWxiR{iy4>8{y~D9p)S|H&vvD&pFfblt!8NVwCC0wzvt^~I9I1G z{d0sa5tgXOb(h&NC?ms`=m&zk^v@ArtSf8ggg-h3z0&eA(`a6y|ZYeNXmR98dH0OfUY4p?tp7eEmQUTftuNBhAur` zuW0w*y{|PZ?)^R^)tD$QW!Nm+p5G1l_6?z3LFl!dt7GBre9XnsLG3hUim|(ku5F)P z&es=&{dB%PTW=F4C5B>a1YA}*W0$TVeE78vRO%KTFThqYlvmA*Pu?XOZ0#f7E@xaN zWNQWfs?-=(MSN^>ZxGx?v&Xf@lHt;)+GuuQ(PWl zQfGwbMjBJ^;-u%(?CBiiiq3wq-b3aw)J(Isq0IeEIOnU~oMV#ad>h|IsxdM*6qghl zYJ)O+>)7NTAt*SmD~KAV*PcnMt1i;XqNU(PEWS1uDcQ4XkhS)A1u=2K|DPPYb+*!= zmLO$*U7Iy3vj742EKIzCefAiu=Z%Wh!lyrmR!b>Gr`5KZ!=_v!9Hsj9^_{Di=6om3 ziY~~EvWmGRg<9k$+C!4NhQPicJkGm^wZ=}^on5se?|cl4mO?8_o8w(-tTl6Yz19S8 zTvChV5PM=SaSw{Q^a@P8i{2ol2MCMv?(^k2>u^;iRjU;1{t>p&QDYU=s4e{i1=5vTE9`?qa*3X{dsGlu8G9))|;eTFiy|RgU!9++RoL(tBG4Cc==UQ(<2g7tOgHm zMS~%-h$zTU&gua|lFq5|dav_bJr1(>s?lrSFW&QOl`hemi$7f(VToZl9X^PDyKr$f zhQZZ3&F4@fp<0!wMEBRPKG5E4uH#%iH}q=Gne<#)$@vAMD`C=V5pN7x;x3-E2MBF1 z6fS$8Y0p1+uYF_Je0H`9RV`n$&}p(iqlfE z`|G&rXM^?Hzw2{MzN~$wdoiVb%NBRBM2In-ctx*8t|HK$o|>H1qUAD4aT@Q21MD+x zCv%TF`G_?yj6Y{?vF^|1le%h&c(K>(tcz*qBjINggcEgbKIV@)oT-k@;#;mcr6aD8 z)RpM|{65Dy`Htrv{U2}Z)x;$&)ABj9l2g;|fr?zB72Os#mp%%!$6^8k{?C=POq@pJ z^}cO?oXL;9>wQi==-z9G=yhPExv!0zdR919TCDqB$?mU{R{btl^{=9yL9!};@xjAId1jSk>|epiU-+y-L7B$KlOY1<4k#5uUDgqTPEqb zmdBJ>%paw9*+f?~T&QvO$OvP4YR)PNT|>}w^$KC_le&j+%!45?0AcO*ckF;{0mS6)go5 zW)Bq#@%DlctdzBUi}yR{&-TX=H6UkHQZEp(qDD-tc-Kjf`IJ9<&i8Avd=^#qjQq)A}e9}TixjWTDA-hZjVK9aLbnOx6!RGpOAGI z$vM&av0?W3!0ad`rV#l-AQ)VFWt1W^Gs12xhz-e4jb0)rIXQV1EU->FOGKyUhr|{b z?Gc%giYTStWgu3-AmB=&R(&o4#)|C3z|8mv1Z?egqtO@^hWYIFts~+y0~50qDY<%U zD6U4JfJ4BQa8{Sqs#oUbrbI<4a1j-ilAEj4TdgjfS6Cxb0A$4-_1J7SgTWAr3xmO6 av)Metv*NtqDhb=+Lyd(A3w%FH*5m-*`x?Li literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..78587a1 --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #E6F1E4 + \ No newline at end of file From 7881e61a2ee562822604cfbb928845365a1385f3 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 17:57:20 +0100 Subject: [PATCH 89/95] Updated android icon --- .../app/src/main/ic_launcher-playstore.png | Bin 6223 -> 8160 bytes .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 700 -> 828 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 276 -> 448 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2164 -> 2346 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 588 -> 656 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 210 -> 348 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1400 -> 1546 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 904 -> 1082 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 334 -> 508 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3060 -> 3276 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 1228 -> 1456 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 430 -> 704 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 4656 -> 5036 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 1656 -> 1982 bytes .../ic_launcher_foreground.webp | Bin 520 -> 824 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 6724 -> 7024 bytes 16 files changed, 0 insertions(+), 0 deletions(-) diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png index e7244c6c07cf5749b8e233a7f2291a14fdbbce1f..a18a1cd6aad41e029f362c6632ef15a7dc081199 100644 GIT binary patch literal 8160 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 literal 6223 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 G{-y$;Ig}&- literal 700 zcmV;t0z>^$Nk&Gr0ssJ4MM6+kP&iDd0ssInN5ByfKjgTPBt^>M`R(qJOXt6Io-j@x zIN>&uBt^=*duGtytoL6>oqwm@Mv|mRHM7qW+VB>=|0)@9yi;&%+g4SreR20f+*=^l zBFN~1R;&ghkDvxZ_b;ao03aYBAP@xsARr*%KnVDOfY-PO|GJYGPJYEAtp3i|%g(=?ek-uma=f@vY5P(>PRS1Y2PA^{{G1?uUr)hI- z)$zd(6uPj`;h>$1&N4p?QrB9s&syly%4m<+Ne3he%}NAN%98-fT+|Zfzz{#%X33Z@Tow6 z4vCQFKO diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp index a3131a17d34303d3abda58d4d5a158bbbf437bf2..9ba77a1cdeda8bdeffbb27fc4cfcdfc27076e1c5 100644 GIT binary patch literal 448 zcmV;x0YCmyNk&Gv0RRA3MM6+kP&iDi0RR9mp+G1Qui`3_Y**<$?>%x3D$s*6RH27& z?y&(Ok|f1ec0VQ;&%d9igX#Zq5J{3EWzQZx$aw#*UIFv4jci-V`R*Uwd@QayGx~MI2vG$1VHgEBUiqQZ$$qmz*zs=Q%92@tcyw`y$;@aVJzbOlJ&YMj8!z6tka<| zx}kJ!A6l;|HZG3*2I%r~Ul?mRyI_xWOc>>TPggN7jNpK)VR~?*NG8h`64Gd|fZzEN z#t4pY*z3wxOBUm}_w)(gr18L=;(5@Wy~<;qy~UjA;I7UpI;Mmx$5sz_pZ>l{gt(nF qp8@z_XnLCnamoDsvC-)%cJ^IWb2FY*XH;J%rr3pI`ya6Ww~YnFd*2xV literal 276 zcmV+v0qg!!Nk&Et0RRA3MM6+kP&iBg0RR9mp+G1QXMw1Vq;&5E{$*eajIcHkM6!+j zC}0#&N`FC&$Eb}YN8X5INgaO(_sn(4<85GsHK=s)ZqnJ<+e_Jw0%}bC(Yp9T^lnmcqsuD?*&GV+z3=8hXRM& aA7Cq4pz}J(>m;C_j(ko7y6ighzd;&|czuQd diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 358841497c4bcbb8692e6c7a010b20ea44b26573..c3f9a6838627d00b8df6f9085ab2fe754b5d97a8 100644 GIT binary patch literal 2346 zcmV+_3Dx#eNk&E@2><|BMM6+kP&iB$2><{uN5ByfHHU(>Z5W3?>>UgdF#$3wd#Hle zAGW4hc~klSf03>Hv3e8k?(XjH?(XjH?(SaQ-QC@;yUV$22l|ygXV2~o58#(*-rRNa zsYrD1gG19~jfZfT7f-;Q^LlNArfCwRBRNb) zo8F6C`nSd1Iq{Dz0g@!iahv-88+DfV6{wy8Kt@Q6BuSB^&|kOH;F%dZG8<-R%_PXS zZQ4~O&+o;yZQHhO+qRu9Sl_nIF2uHtd)}G=IMuc-Z`6euHfBy%+L@WkotQ>uX70oc zI-Zc2%BX~DsFMh(oPUpxnyQ3ItAy-v`pa=8NwR6}e@pk-w#{SSp1J>yBiUA}{FA}m zSFm!vyWn<(1o}S#;OImkASVJrKV=z2EGGh5BA}~aqpGN~C_IV>tP?$6v3zHw484`j zYiPdu(n2?ZwMX#?O;rHE_kT#5y6(Qi8jDU^^tVOlExKpcbF*G=zP(xZ-dAGrWPaUAIZ zb8mIuIUaI~Oh-HG43R23f`_b9B3>8(7DuT?FR9LSbfC_3wCL4w02?}FLoo7J)MQ>! zG^V%ca3@mp^!1{!p#wIAar|p!(Ix7XP=JyX&AMdtuQ6_NOQ5+cxmZ4D*qY?`n+GRs9SRalI1#0COihf6T(;K^3aLVDsKr01Vn*^ zwuCy-p4$Mf_;9`t?&l6!->1X0x6I|CDZTYc5V}?sC^jVm8pQ3bX4VUo?9v-1<+bkU z7(FcwW_q?z|34}q@z}}55bJ1zYMU|Mr%tqMR~U8YVxTUK6awsTssi;p0#pEm~r8{Ng)ojC_i3;5v(sL{&~P)K2RnWh7rvd&Et;>ackIc)WxFnxQZbrfy6QCuS{kzvpCHY9lQVUzyJhghBQ|XCPxpw$Y@- zmdA=I;`85vLp9NG%5!0?4T(oicIp8o@OYrN-fG73*a0~|)=KosNgMmpwd!@KW7G@O z#ijG;=|x*JCRRNTb;k=@x29Iz_SP`O6O_aySrpOYX%qW00FMk!g1k>>TYQl!JC%bP z$TDOM!tu>4rWIFO9wuD~1ri|3G67!f_zRi17#%JCCm^yR{iHa;}=H7bs?5qH&1n9l&8Awqm_ssKvsl4Ll5}ufmi^o#5yI0Jw{{YDD3PCl2 zZlENL9m235xSxmkb-}FPvolV}Xb2CJZj}T&ZW#dnAs`wteThsMD*&r$Jk#hGJ6DO* zw(rb<85M)~{6IkL9{>P_0C(UuOE9fKET-{K!WMR~Vu$VCsvVe>yw}aX>b4yj7*;X? zMg^cBud;)Zc)UDJcdvLbyIi({9)U?fV)V)+GVuf@H0ZbbaILAqPj%aNmO+zIT`WfB z^Jobg-79L_fBb@afJHzqwE2uoEDQ(h{v;qjKKT0&0pT|H6`ddf)x{Caqb6u#U&*I+ z0Sy13dSDY!7%_S7a*D&0-cHtU&o|3V05c0lG>*lYGZTqx|_b*5|2hgZ6o7>6y>G_#}W{74RJVrn~ zD;}m}C-dk;L?1=FpWjgZ^8>1VzK6dG@iFUW4|E1J>P8{Hb*PZvxF3Fo;;fdxIncCDEdGmYU{80^ARgHG*;!%r7{7K2 z!}T>CGdXGS?A*X2K*A07b(e&a1k@|JpCKdFTu5eC;uV5-ZJz|gb{EQn@Gb9lgT39Q zF(e46h63yUYkr84fTs+0RB%duc3;&`p}^*Yo84f4tz-}-ph&=|<9&6HRXs}1QwBRC zI3-U$X7#B3b@~Vxf4zYQj?@YQZgwC~K&Ke+?W5>{`Lcp-@`;k6C_xu0JxJ8LcDQ|Ee`a;O@`cGI$1nL^aP_<*ZN)a-OY>L6Zq?E zD}Z2E2+12{cOam$e?C6{@*yjLpxrqGYpzezyg)$j@QlDbBd~s=Fm9jXuG|0DAD+kv ztX4Vv*cZ}#I@dLB5WYJvV1}%kFUVv9a(ktj|APj QSj7W-o&C4o%B_Ke0cqZTg8%>k literal 2164 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 literal 588 zcmV-S0<-;6Nk&FQ0ssJ4MM6+kP&iCC0ssInFTe{BXX7@KWLKGe@1reELga4|36{K@ zG~7m#q)63s&k~bY@4o{tzjVCiMv`q+nS0+C$cS7P#r{)rB7HJ&Yuh%p&Cho9h04^% zms|C?qraX%vi<+>-2eayAPHoE1ds$$Kuf&_0uWxmXQWaAG4QNJU=2uvNd!s-vI(R@ zDNrC4N`=xvWgB{~YH3`%|M*kWPc0SV*T3~rDSG+)^&d4?HGX~mFSbDk*8yact?RYx zrLlE2SI_IccmJGVMsv3kycnbR{*x1d4P zP#Uua7Q2CYz6Lel&4`eKh%q6#A1tzu?%K#X%Am>c|Bu;=dQ9YOU&dmw*ja>}y$qV( z`ny@ZsDq%xbM(|f&PfK1yZg$@a@AtJ a#JrxXNk&Fg0RRA3MM6+kP&iCS0RR9mYrq;17s58Sttj_Cz@-TyTeAPe*a}$M zVA@EMqndq7E&KdKe0IM~8%c6hvu~k=pMQ#v?$=Qy+fLQwoLM0N6yRS42Cxu*SP=k% zm+*Vv^xpS{MMs+b%eHh=hU0{k^(v|JU^y-7oP7OyO{6>3Nzgb)H@A_+o7 zDij8Up;oC?O2zm__K(rb{|{p9(HE1Pu4t$G!m+F3_|7rb^Ld`ighBN4pdj?)P^uV` zaJaT@+iYsvj8nHsZT|mnov7!rxF{m}KLNhCYr_evVeFQzZV%>^A(zo_R&|P8wquF% zhgXxCVKIc=xGq?2n#fIs^VQ;@fLuhl5R1d&LzY#oVR2m!BA3vPC-@*mk<0aoRb~FL uW6|F1EviXF+I?aECo!3S;jL)i9gCML77HVPs#<|biU;zf0+Td*%M<`dsj46V literal 210 zcmV;@04@JgNk&G>00012MM6+kP&iD!0000lYrq;1_n@F{BgelNe-cl!G!(RL4X8ezHQmMgTeo MXhJ!asU7T diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 0d0468ecc8eae0b6ededfdd8ee53faf2ddad7fa4..2ee59a056d1bb6d5c4f8b7e0672432397213a300 100644 GIT binary patch literal 1546 zcmV+l2KD(;Nk&Ej1^@t8MM6+kP&iEX1pojqFTe{B6^DVeZBqZT_j`zl3Gf!GU}{df z^8cH*r1cW5XRz@Oop#voU^8OeW81bP+vYX*-mTc1Pw>lTGOs$}n2l9+!$vsRV>F}j z%Eq?h>HpaF3Qsl+S3b4JJkeBZhr@kOS2=RqMw0aZug+{Lw;;5L#ZNh$w*L&S=c z*y`R7h=6Qc+wo?_wr!7Uw=>(et(naWu-#j*k))VMtww|+hcqs!QjSOOUQ8QQ^L3kJ^^GtHi<4%6IN`tOX(Cf$Lm`I|5j}alfD|4mU{HsTHJUY< z3K{7iC&xVGM20ax4JcO%7^L-<{u4KnfAVlHr`e{X9#Ae6V9glO6h`D}6cUxs7z31} z7_f9%^9*^O;Z$l?7$Be=2*4Oeb%v9VJVOPO@vkB5B+MCgnsaivglKqOpcAm#MAVG9 zrBJzo97iLh%~X_ic+aK`Ib0^B`F;p?HQ}}bnWw$NG-`n|x-|JeaP`O3F~mT5G*d`U z=Mf0*S9w8z>Rq3E=(Iv1<+N4?NW`N_lqO}GM~>zJbSApA%zy(kt04izC;(!4w9#Dh zkLK!}#J)065d#thHb-*`0$o~~fW2)IBm_`m)+r1|NQb3c4?!p#eul=-H{!wIgA^3# z+vYbWsHw=+A{|jM$Wr*R-6oZos*oQi$xtXL8AHK-agG5(;p@V9Xm`+9SLo2Xng-B0 zIy9azKm*&o9&)idY!(qP?pOSxz%L7sva2P=Ugz|Y>gxOcFn}GpM5W2K%`5=!aVIN^ z-k~~-Ir~l*j^25**lszn%{i)aeYBZ{)lp6mA?h&Ejr` z2-b*Tvv%xSVt|oTzuuv%nxe=nd*8DBzGz*A-jyJATMG`oLy{=66fQ}?diOScJyl_` z-`A~7Ux<5U?pw;&Lr~MDB?)vJChhNI3lT|V7%0ir*Iv)h7ux|^fC2fiW`~UQ&nxq< z^%YfA@6e|kwBI)n((cVZ0~!S6Uz?s)Gb@UMU?@CD;o=0W{G6u0BoL!U);>Rd3G!V`O3S8j!wR5O8BO`H^V7(Wcs~A*YLYzvv3>N1Z+jSyE1l5&wtlb z!iA>7PNIe36#e_I$$9I$vnuZV3GT(3fMvj7U|42%_r8feMM=U9g=~3}Q0nZZclR(b zK69Lar+1BIz^?*s1Mh9#zTH2UW>aJ5E$?n z=vtl=wb--UHt$;7H8CI1Iu1{+j_kbu>Bj>b$F7~`m#+GZfxhyiyLtl>KE^W5fLi(P w1vU73G>4ED|9|M@d!M@C-OA}{8N+9h-Jdu1xb#O1NDL^io$^uoxT#qI*VCr~Bme*a literal 1400 zcmV-;1&8`lNk&F+1pok7MM6+kP&iCu1pojqFTe{B*XOFXZB<$O3QqqvBp``?y@%d= zHl9IpB*~2=c@?nyzxs=@imVo#9|4e%BimN3tiA93ifjI1kOV0(XXbv-*-&g-Teh70 zejo1c?heW6ZkYj)83cnNQOP9t=I-wBegFSI>i_@(0uBf`pg%x75D*Y>!2b+@2LuG< zkk8+OcL5}g4qJJvgk)(lKLWc2&6afdNtB40aNCE-^asn`5hCxQ+ z5P*OLj4vaJ#1k-HDIUSA7?N$L6(QY84n-%(cY+g0h$J8hj7UYM3SupsfDo(jF5(oi z3le;q{_l_2g%c3kpKqK%f)jiS0zO5YhJ?NrK1KWa!oa6Nh*fwmVl8eTuUtP~A;G8d zX%J!+2(gRpxX6A?4fWk_d3P3+%k8BuHfs}1lri@E_vg#{{=PVuz8NEns|B<5*@Vd^1=)XC*GKF#gRbUK0dXX1W3vD_m)wcbn5v~n>sRG^xdPFBF;t$n|0 zRzQR@g0IGbPp>T0%k6)j8g%GO?H9FkRFL^yLxI|VL@`jMB3YEA7Eq!Jsbmqp)QYKO z!a3;x$`=a3oYNV3&L+h%4VuM;DoA6b2}wo@@I**M0Kr4uKI$s%L<))O)t&_c(xD;d z46k^I(^lqm?frH7^8aHcow4*-wcK}a(zP{iug`Cuw0k6eXcyH>;_E#AU4K9335>ih=&NZKmyZ zQtw7=dl1{1Z_S>)4rAL+>QmchQY&@4YumPB?UmH+u45;^F0Y9GPXIzo-kHOfcjgi? z3kLU3UAAxU@)O)l#32}fZ9RMS8!&Ltpn(JW_3F6|027gQ27c<+XRxB6gZp&*0q7i! zur``#bXS0f;z0Kyq;nKuPScRh01v@|EsBgq zky?gmEWiT^ptsUCMQ&>vmjVwUficR~6r;B3%M_#^!zQIQ#j1x^e-7r;85@R>O+go* z>v{T|0A#E0|3GIC0gNI;fls^4^MZvz$k0b=2p7XfrLFj^p=*8z10e9@NXk)slvAJV zh5#6TR+^GPLsvfs1__KOlMF+zry)6hsU)2Y?M(XzyfZ#i7A1p*t$r4rIldq}DR%ku zDUQ@ve#6&$u8o0S%>aLW<9E_|p6AYtPW^g4@YuJ0XI|!c;j9=wyb<_!M3ScO0scYw^J9 zt$B(uzjfk&E%r%2h-~WzCpuNpRXG#yZcEtpb7j-u*GZ$G^9ShHFsL@i5onzrA+V$~Bbz4`w8 zZ@&EGsulY?G_6y*1~CejZP>I`$D;=i95{HiW2>eOvrHgnS@?5jwobhU4eHg&?(AHU G2n_&;$GCI= diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 8aa87cc45b48978136c981269ed293717498fb24..635adb5a294434fb7a2a313502de52f2b585a326 100644 GIT binary patch literal 1082 zcmV-A1jYMONk&F81ONb6MM6+kP&iB_1ONapU%(d-CkHo@Bu7s5%-(}DiFyG4ErA9b zAaS-k(;!a3Z6wK&l+}G){qyAgcZkVLuM=)0*{+q@bH5hmM`uC)A&>|bSWo?X2Wqx$ z%a-%`KHS}jUjhbT0V*R98HN!^GzNf3B5HEiUf*+<0OQYNWGV^!e z-?iBefuS6BqpCitQb{Vw?Dij3sibcIVcbrxAR-X~gaQF27SX`UYIGyCON=TXgsKRT z$Zkl8h;FhPC8D4df{Sx?eBax;5r~RRi3)+O8v!MhIK1tRDoR9R|GE>2`@Z^GCPuHX zwYAs#^xot2`}qGGJ;{RQuWbqIls^6Z`kJ+ezrV?qwDhqRd0V#>(zb0c#wKrV+qN;= zwrzG^G27_ecD8NXTRmI9<$1H_`84K9BKki8>=$bq+ThDUdK&AiNb($LuG1S}H#Tg+gdri|4wm^7P7<}4GWraJwn8Xl z(bdb)+XL`vbLZ}J;Im^3@#%2mVKuPo{QN&K)N0%W*f1TTwqH)223}l}fzPr7j~)Z> zUhjd=;^S4-z$aIF|9;p0=DkW_#kmFJPn1>yOVzlghs!I0_pkM;d9`00I0`+L)AQD$ zqd>YPN1Z&Pfd^mJt9i2Zv^-$$==A7*)AN8zMyHO<%>zzt7cGFuzk;;nG6j|9vw7sl zVbXD)IWPSqD}7-Fc4{<7n9I)9zb8y?ZaX z{wJ!Vb!LJ;FcVS1ittwjQoQu1qKt2Rc7W})-+{^~1%wl#5MlR{3GgEi z2web@4B)M^JDplxmH2<#`qAp^Oy8)HK4*4qVFM%6EBYOXCF~QFmphAMHrm_3@CGh(S@IQ^)BEiBDLI5AAZkJ3L@hkd zr&bn~Kqd_jBitY1l1!2*OZ-Q;B=ha&vdeb%ZO;2?3&yl>;GJP_w4mkTzl<-^W09SxF8=v z-^m-kXC=$%%d9RbMbRs2S}P%iAN1xGmLjQX)dF39NbR9^me07(G1|fF7?$=urTC zvf&RT^S6xwA2%XYf5bpfKg8%5$n5wX?+NTg1C7JRss1QzOoasp4nudA)8rus$dgpI z-GLsQrvgz0l!&O@1sWnE7g|PYsQW9(QYp7kF_&aqYsF;Qd9f7#{ZL*Z7TQ_=F8GcJ zPeiY)%dNzrmPc+zPgr7+pI5cyc2w0UPX(+ueT=#wW`B!0w_#3cCW3jRTh7LOwZ%AO zCicUP&1+UHH%e=>+PnVApANAk+F`SP;)^dozAH}r(uQ6+qSB#=l&iZfes96kc*h`<$iSsK=LE~ zqW|kx-|z2p{2GSqKR^(K0Kr1AhzM{H9{55Kz6kijz`)=S2O|Yup|UEQARDH>sc^wI zD`ke1@W`1KwWoLfrbkvL`LmLuG~*wV?6^=Ok-zGcqWs$*tFrxm8sp&KMR{+Xohnn0 z+y7t^LixVz$DX1JLRTb=hlM~2T}WX)Z+xB)^J%3DRY>7=*?C=ds?bw7AlkOADxo{< z?!pez?pAEEmH+<@Yg;s2-@Wg>9nt>@un7MP{|o^8R!K$lG`=$G7ENcEVpaq6|PhG zuY_aJ!`nyfIm1@jLFc^={g*T>vYE#ZI`q!viTr&Veh+?B%0qskDdiBqaN+Nh2SHft yj+C};FG`b!MLK3-{$XnlAh#1KY|OB{jxL!{BPw63_2m?Fe#;{KFZ?h3Us)SGNdlb! literal 334 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 KBN5AD3=|8Fl|lCa literal 3060 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 CIniPO diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 71193874ff1e2cd9bb09b8af3de77c7e3ae27d1c..65379c15970a6d9eda447b381f005a6e1e2351b2 100644 GIT binary patch literal 1456 zcmV;h1yA}?Nk&Gf1pok7MM6+kP&iDR1pojqkH8}k)rOjYX&B;PIMDGLpbQMK z`C6CCwYEKtoWBV(Gv^LcZI=sRDw#@VhKiZF->t&T%uFS7#&U82Zbx(cry?zb*Bf>Q zojF;a1ucW;jMj16HiE?dtGoj4Id*1tb^!>DBsr3TKgtK-zp7`}N^opjJNB&Kwr$%s zh7h|-WsSh8lDTB1{QI!9o&NXxe%~_zaJ6kah|q%*xCHL!={1bGNZ=t4jaPK;)1?=_GFUu;792W~XK!AN%za=@jl@kzxB!Hn1UTOW7S%6xA+44Ur1WA$9 zA}JxHghVjb=@e1|kCGV{pyXozg&-@#R5YcPp?KNC?ff^Hi;Bjsz#z$;oPZ}4Q9box z`#2sB(`dK=%Iju91XEF!6ZE)+v(qK+rJ+CtXtwlBIB_8Z(Fb?YUNi1*skh+^?r0Hf_#Brv87t&x`+lLcLWUH)0JMFd*aW zIcq-5nGK+(W|SgUBergR+7}P|rGp+c#%-j0je#oAX)&(UVYvjyl)70=89><@2N(mw zkqMMrTao~HI^ZFKLo~fk#e~$Z&f42Z%QQ>FHDi0vcnw3mk zPw&8~tI2^WiqJE{tVzkl^-S9gTT(K9J%c?ke;*@KiRT&(>sIFj4*%;lYMOuzDVeaI z!BUr#gE^5WBJoKj24r}a>%=x_S58G$x-QvxFxhCM9%XOpc z`5A0_IZ=RIxcDKGZgWFa3eYRw&&f^yTaG-tJiGp+vKBAN(fwH3(s|a!p>>8 zx#g+g`@Vyq++;cB- zAbbh{r7J8z(iX?vS#OC_2O(kv-?XrQukTiP07*yKoC3h;h8!>~-D~k~3x?D|1{Ow8 zoSuOhXRo?_xikPez;A;daIW9S&XpN>*#b#$s+176ghVhlHT881r)FT`-bc}`F#!A! z1!e;5$mZAp-<)T!9^C8uRtql@APEQzV{W&w^Bg~u@43ajk(S9Q0+ifxK-~m45NJ+j z$vKZB&UtfW=KsP>@7gSUbq1;Y)miw`Oz&vtKVly@3(s-VoNS68P?rM?PJ@{M3lIak zR(Jq^HNw@1c6py_&+i=>_`MVTzIQ_9D|&qIXa}m*i2Vcn{$NRx$*6LG2Cz#2Z4K-# z@Yf*-1gjB7F!bYBRC0->b2^Oc79a!^fF7{az*QGdBmAmgR0HSV9RD9n=ZrSj0Vcrx z1IPenKA`a>tGdsZ|1A;_IXyaO^f}i7MiRhr0=)QgRgaH%W+&z9oHK{y+%$CrO_Oux Kz#KvLsZs#_gT*BP literal 1228 zcmV;-1T*_mNk&G*1ONb6MM6+kP&iDu1ONapkH8}kcLzC=Bt=qWHSHd={<__p8M-Tv zWq{Z=k|Rk~J#*gVAKWEj-p*|o#72@7Nm(^`@c;hX_Es=Y7ZlspmM!Obe|PtvDH`;`n?U5Uh~k{TdtOp;mhg zs}(lI4hyl+L?+Q;Pxr(45uS~5A#B2qOA0_DiI84ol>`Y%gpDMC08mgw5}7>JvhGqn z?!pOW4rC)4BxDoS0D%Mo0U!{}fNUfo2}uNj1Sfyfh0~`QP5KU@12t~wmCr=zVcy0+q zqz;jQ^yrcO`?7Nko}1PYCq?D}438byiBPi~QUalP zJhd0IIG8(e%_WHv$%~f{?ln1}JNfL!QeG1A@|nG<9Ms(NBV<7$k9v4-GAyak9vGs_RlMyS0(aWx-ic9 z2VIsM%8n;LwEcX&C6Ukhu7k-FIe+>?n|QI=Q25dtcQxN$oX9!9f5)lSVw`inKIbV0ZT~t{w8v#_@{7Xc)95iWl=3WTbPVV0#x`e+_B!bND6F zWcxmYM~xqA@Z&}g>D#tcZ1G?(yG#NVSKqKQG5PXWtW~GB!DGz|1@f6?!hBs7iKwi) z`GyuQT_i3afC72pQ0At}WRM9r@4)cLf<=oNJ}V?l>g}e=NSX;34{!gV;1J7c=$6Hq qzYm(KERknS*j?Q{JWVe{H%+A6E*O&>k}-t~06Aw29hd=#e<}r4S5sX8 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp index d62b6730ab7352635ef1ebc231be8c96592e690f..8cd64ee12d161275fbc05d638e7fb51b53b6234d 100644 GIT binary patch literal 704 zcmV;x0zdsyNk&Gv0ssJ4MM6+kP&iDh0ssInL%~oG@8cknBuUBcz2otK1N7h#D`-K% zKQx!WNq_0_`ycX2WC4VZadjqiV;HK z!y16`3{0`J5m?G?Ac*!*W6* zux5}Tq7#88)(o0Bu9NdN=?ntLZL%+eh`m_g7=Sta-I(ME@s1od2Ewo&TNxo&TNxo&N(Bj!7#jRxV15 zu^qwXV;DGA9&9(7F|^rggio6?9d0v9fW510<*PlQ#AZ|i_pj>rK&8!S03P%nfClG3 z=RfDaf7g5|NY^e5wH~XD6X;L1C)tgUZp`feYBkz0wbp7Z1E&|-=RWX<*o-OIetuOy zg1HErF#|iV`X@k`^PlryKxdz+Wfj`38PT>QWf2nxa_vS`KW0wnTa9{5?Xns@;Jdul zzFCKvRhuya8?X9jK%&hUg-t^eod2Bv0z#RDttYjY1DH(`sR!=#?tnTG8HTk-y<;H7 z11tgir`m_W(5FZauz9M!3FL@K2e5mpedL4jHxE#XsZ}Bp>Bmriq=;l=YK?fn&lN)! z-bF+*XUv$%5)lb%H>5Yz3rI-w;H=u*eABypGp}<>Z>lp-zWLeGH>)=1uZ|Lr7#tU` m9rh_dxEim%DnA&U9IqYwCeHuP|IYu;|IYu;|IYt`QUUH|2AA?#p!Q$ME@s1(f{;6{ZIeX|MWlo&rTZt*;IJuH=Z7wRtB)Od3MY) zkh5pU1Y`2-=wbBeKl+dU`-No&eA1ru^TSci$d|m&j$Jc@ShhYnEIEiw@X4{tV20>F z`j5p)&Ii&%xAye#7BlkM(bJ=zK`hvx99kA46Mc4MFkSQ?{r4}*rht*!*T?7|C}KDt z1FZ#7GxjYo7W-5iqPuzwRICF~bE*CMI4_qU>j&ZufJ#I?irZ9Jdbw6d+8kkFhfN)+ YC&JPptD^tufBK*Pr~m1H`v2c@06aL?9smFU diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index ab5d33715c7cf2e26c09cc29374e35444ad5d9cb..612a77e46296447c655a0f8b7226863f49da94f4 100644 GIT binary patch literal 5036 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 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index ae5a179a7dea0e707310319706535417ab8c0c91..7f10af247a6c85eca8c571830b5f0e967c540b02 100644 GIT binary patch literal 1982 zcmV;v2SNB!Nk&Gt2LJ$9MM6+kP&iDg2LJ#szrZgL6^GikZ5a9g!_$P!5uvti8wQ%) zk}r6S7%3Dw2b@Wz!1GZ=KvkG2^F7F{9@rl^x2+v>GPZ3ywr!(MXI8fJ2h`hD(%H6E+4XqlnE<){|DS)f zn{C^+ZQHh8X#gjcZ9AFc6a2pIf02~wGe9OV%J&MHBlYi_;~rO~OMR-Yx@qp*1^}Bj z|G$(nw;PUO5Exra+8sx-tt$B^gS#(%`*C^io-a>;D+%;}0(jePt_5N(P-}r+%N54* zi@p{ZU;>N2#zcIrXkb7HEXvGWpuYI|&#$S#`Qv5zFMP_(Tr?2fS|FP;8`F5R{tuYR zu9>_TRkL|wHxrx1KW8SECZ~RW1#{D;$X;X4fBlJG^ZZ{o6Ifp(0jt;RJ^fhb*HFF2 z?5TEYtlGeM@qtPb|_Q1H`nJ2S>!7(Lh0JU0WSbzKwK|ciqu;@ftk9WJ=`(c1B1aZ z3Rq|6`WI*$}HxW)n4sXAfL=`|7;MSjwA^*H*ON%jrt0WX`J zd=B6z?}QtP{S%_h(c|c2CpEMWPXrVIVZu%%U3%F`e+fnK?c;T#=+e`!jN7~NN<^T7 z0n*shXu9-ppP9#8uQml;PnquSE$f)}e2@^x^~a1cjUfy`ZYCJM-`clbv(B7@TxCmS zi$v?h$PCSdFcTe^m8YZ#P6`~kG&n29xCjf)2pc0oZh7mp7oF}xt3KSNmyehi0t*ck zmInuy@2PUvuiEx$b5B#{@d+<3S*Wl4WzfU#xoxRh#MRrL>iB>wmlFSNRoH>qhRUi4 z@rLT586(Y$WQO2Kupn3vk={Boa${IkWugrc7Y2^d-&I?x6l%*^79Etwr(S!Z)om|I z(-UIX|3{&`7$CEJ(yxoARhu~Rz6NSiq)<)_ke%N-=jj`9+I0a`r6}>=W@GziYpPeY z`>b^S1Cb8Rh6_iA#-3qApuP0rvNh)&GbS1r)+T^n4qQo$EOcO&8X7=#cxd)FT8;iD zyS$-^v?jtpPo4~Xty_2Bh}}O2D54(_W+8V5RK`dBx=ZEtKHzJgu*83xjtXMHzoh%B zcGIP;U3X(28=y8N-Y+bN9&VrG2jarx_!2LC+w3otrf1oH_<^=&)B6ua^n=2J(Ej_% zfrr}gFsuvWACLvtRhUl|Hb8>H^m-!J?ye%f^YgUO06N|EiPoSguAoI&Q5A%BRX2tR zaxuVFXZ%1lKZXm?8x!6+9~IUn0=+!=!Wg(9@I{fV5UdFL1Y&EVZ|f?Y^BvRXlw1hU z#HQ$@eLW=1Ip^=;zBWO6rZqtPZb^C1Ody1M8?_m!gLe5Kd_U?m%1A6UIp~wws@AUe z$oM`lL=4%%1ElVs@mxLm&@?%5>YktME}`FNgMa`)rU82lz*(_+N|H4aFcQ~_35>tn z9UwX;H8_O;KxcqjapCUhC!WMbl7)I;*j)=1e{fM`g;sIIXD?i}3VMyiO^-&OOy2`t zKSy=NjLPh!808aQzADvVO=7Et+7T!xXiqHfw5+eaLqs~4uZxvRnx_`Qk61e#@O%L=x`|K4( zoKOs`04zXFiV#o&o}!291EX!oUi4^M)~>r*HI%rX8H zC;=VdJ=ncZP1^$t*T9JvU3AewKUw}k2VHd0CyQ2LqFK<_z0U(aUp4g~<0wWbb|^pt z_({acKcp@HlRcMDV1X)3)eDB%?V;V>>fhh~e1LyIs4W;+ zImZAjKpYQ!9K7C-MoR)#%RW)h|H!*P z5BTiM?fhp;G=kr+e&L(D9OEcPDK>xuC>2Y@K${$&`qA)4=2*s2W#18{+qdx&RUli*gHo z4=@033qS@a9=`(6TK*M}-}GCH^Xh(I&{e{CMcKZWo~i3U4-ka3u>6p_&-2%P<|Uu4 zgi(ufeWAyE?>zvZH)v4T!W%SbaDeywV?uqd62>l~Y}fG>EEsC>1q=Ed*H$7N07aBi QHUX7VA{=mqfV@o?0TLzB5C8xG literal 1656 zcmV-;28a1lNk&F+1^@t8MM6+kP&iCu1^@srzrZgL)rQ)(Z5aCh&r{ckP?)p(G+sUKJHZT zqfe}N2wx9u0{|@Z|4X&qId*OC3<3Zw)4$AZqriCEW`X`s0ROhluOYsM`WpIcSB&FV zM!f;!RYqA7M=Rby@G8vAGwaJ~#`z7MdH#eu{&2|5T=B?#4H+|wX^vh0XYJ(5PQHk$ z#y&CFiQeId?L_-uZlc5-$HYdNXIy_WzB>O;>;yk35;(p2e(rbXQBtGKR&~r6V<#q& zn5@(!fmWi_>rG=!syfa*N+@QYFvd>wB2ldb(r6{BNc5)panC#;I40x8x@GJ{9dAly zz3^ofugx~2V~k1K%0Vz&Sjh*PN1kzdlJi+tn4PdlK zqoP0A$v0-MeDuQCdT`TsR_mu>Xxs-4sB~q)j5j58Jb&NJvcH6cr@EQStr3YyNUz|vEa#9xwp0Dc8 zm}YlRgrjh)YYwpYcarTBgUKd}jP%X(J;oDf3To$h;PtPkCjfs>Cq*O%kw8+O?J*v^ zQxqNRNgx4)+d66Ta^z0Q2#NgSn>*?9a`>*&#rt~|38*}fZkT4s%c1vK<_2mOD3F#5$Y!&N z(caemX!zWv!eMw@<@?d#z6!bRfhp35|853#k6j^@0|5LE_N1Vg+YDYVz480*N40r~ zdjc+OF@x*ek7{Ed^F$o$U+2SrXL5~Yk`NXk#0m&Hx){Chp48das3$OV*k%O;_at2; z44=!aJxOPg2f4&}_-~e}J`HhC#D#5caGgXnysu*6cwnh~#t^9o`OMlCLAh|l7J~}e zY^4py?@L6J@8cm1I~*(>s?BwoY3*y=mtYZ_^LGVxj!mVJj};(#J!y9IJeJG_1bwWG zP-7ma-#0_`sh1G;u_A>3(evz#9sN*&NR4v01493*X}U7vWY4mR-%TF^x%N!=xA(=6$L;f zYaszU;PuMM=QR8{z=mPk2`S>pfVMO%%xp zM53fq+%*e(Jx6DCeH=%8646RnuZlbHziRfym#Rot)qWroL9zE>4^EkVtK@Yf3ao0t zVs%`A4+y(**4{Hc_@ashy&#Z-Am}YfRWWw@?)xBY;cB>+Yl;M5+;A;?Kp==X``*_d z>cJZ|SX3+)twiZN{(=R?qDb&o6}QjA@%O$1A}3;C=K@ACLG1$`SOTZj2>>C}h}>hh zt}A=JAFF?qB2`7xPT=?=K{~}J30|$@{$99nw+}n*)}y%qoaRS9ASkfvy1*8=tw7Ln zgij-K8u7cnXaAYI?O(au=bLBy|6q^)kzS^UdyEeMa4*w?vkbTQ{J&!N&!;{6!H)kq zIE@6o9N~Wew>fTCF-brG3UI)@QC*6FHHid~(}-FZB`)*WAlxFH$D?b5DEtiWNK9{R?6JKe~15j(FlrMowDXn?nY6;}oT?7E^ Czc)?* diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp index 85dcbb73f50e37f8c5dc54eaba500b6fe6acbe45..7c56c5023541dc95b2235664e808e2d6444a68f9 100644 GIT binary patch delta 817 zcmV-11J3-21h@u&Qb|TeFarPpS4BclR!}%hBLe^cFR#IC5SQdAl5JP%^X?wnXA|B3JOPxhbG z8Ml!~gI<{15-b|M35R|2BDsfo6t9Z)i|S`p@E`mK z|G|IoAByK6etq|*T=#rd{gz5dcy;Q8?(H}vzwk=)7Hi4Ik3NcbKvq`I=-p)vYkV&F zaRU*5QL^4G5E0!T7?J#pz1F*Ec5g&;;6L~e{`*g~Fm`uH)JvAmDp;?7s6JM~PFdrys3+`|0?ax@L@&Apx8Gsg z?1Z&+b<6E)VJ9q@HMDjMZo9=26~O0^$#){y9etN|#|vyI2~+_ykybCRIK+I}C&4Suo8a>$GnpxGf66E% vp--fnT_iaF%~^o?#@WrR<084oS(~%w8~ws}_lpGQrN)jKlkWi+1E045ljoUM literal 520 zcmWIYbaP{1Vqge&bqWXzu<-f9$iSe#{$RF1{-G7QDxUB1Bo6;K)tGiZ=*C=Omc<^E zM4leHJA2FAdRdFND<-PAcbP@ScqjebKdoh__VtN5v#)K<{eRa)PPlsKt-9;g?^%o! zewP-OwQ+7*c5aGMkk^)Jg;%&r3_fjl|7xOj=|yGetCfeOw=Ll2U}-dH`f$91kylND zjY09_5-F3#4W!;5>HUAa-|g4FdAvs!x-1E@kGZse zk;w{|uHQFO@=xsiV8?&$|KsA{pWp78eY}&6W0K13ZRa-U-rkn`w%+yEXVdwA{apU5 zI~@4`!Tt~b|HEk1i+2|mEX%4IDPUuZ?W<_@KHJpP9~^?dL5z+t1!HFP(HgX2yC4{RL_JZ1z6q{MY>d zf&CBuf4|SEho7`~crEQn-SH`tE6yLR^;Z8W^gZ?E(H-kO^_if?t>40Bd;Z<6N1`XB zFMauyW3YI-MZ9sv3D3j4s#TU|5~VA5E_<|Vlg~Px{0*%ue}2vRa7E?rl$eP-AGIxC z^-THcYR|&e>)tMV@=A|IInF2Skl5B8m)C8*G_|bod1Zp?8cU8J^8ecZKdeWi{>z^L E08)Y(Pyhe` diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 7da3e2afb5a3f852ab740e6d50cda6d6efc9ba9d..37629ff0cbbe1521dcd1984dc018363d0119c072 100644 GIT binary patch literal 7024 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 ON-T_6;OCLDM*{#6#_Ie4 literal 6724 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 av)Metv*NtqDhb=+Lyd(A3w%FH*5m-*`x?Li From 2616f7c113abdad9a1d732c1034b912ed5b46f92 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sun, 23 Nov 2025 18:08:03 +0100 Subject: [PATCH 90/95] Refactor FutureBuilder logic in HomeView to handle empty game lists and improve code formatting --- .../views/main_menu/home_view.dart | 138 ++++++++++-------- 1 file changed, 79 insertions(+), 59 deletions(-) diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index aa21f76..a4184bf 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -138,66 +138,86 @@ class _HomeViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 40.0), child: FutureBuilder( future: _recentGamesFuture, - builder: (context, snapshot) { - if (snapshot.hasError) { - return const Center( - heightFactor: 4, - child: Text('Error while loading recent games.'), - ); - } - if (snapshot.connectionState == - ConnectionState.done && - (!snapshot.hasData || snapshot.data!.isEmpty)) { - return const Center( - heightFactor: 4, - child: Text('No recent games available.'), - ); - } - final List games = - (isLoading ? skeletonData : (snapshot.data ?? []) - ..sort( - (a, b) => - b.createdAt.compareTo(a.createdAt), - )) - .take(2) - .toList(); - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GameTile( - gameTitle: games[0].name, - gameType: 'Winner', - ruleset: 'Ruleset', - players: _getPlayerText(games[0]), - winner: games[0].winner == null - ? 'Game in progress...' - : games[0].winner!.name, - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Divider(), - ), - if (games.length >= 2) ...[ - GameTile( - gameTitle: games[1].name, - gameType: 'Winner', - ruleset: 'Ruleset', - players: _getPlayerText(games[1]), - winner: games[1].winner == null - ? 'Game in progress...' - : games[1].winner!.name, - ), - const SizedBox(height: 8), - ] else ...[ - const Center( + builder: + ( + BuildContext context, + AsyncSnapshot> snapshot, + ) { + if (snapshot.hasError) { + return const Center( heightFactor: 4, - child: Text('No second game available.'), - ), - ], - ], - ); - }, + child: Text( + 'Error while loading recent games.', + ), + ); + } + if (snapshot.connectionState == + ConnectionState.done && + (!snapshot.hasData || + snapshot.data!.isEmpty)) { + return const Center( + heightFactor: 4, + child: Text('No recent games available.'), + ); + } + final List games = + (isLoading + ? skeletonData + : (snapshot.data ?? []) + ..sort( + (a, b) => b.createdAt.compareTo( + a.createdAt, + ), + )) + .take(2) + .toList(); + if (games.length > 0) + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GameTile( + gameTitle: games[0].name, + gameType: 'Winner', + ruleset: 'Ruleset', + players: _getPlayerText(games[0]), + winner: games[0].winner == null + ? 'Game in progress...' + : games[0].winner!.name, + ), + const Padding( + padding: EdgeInsets.symmetric( + vertical: 8.0, + ), + child: Divider(), + ), + if (games.length > 1) ...[ + GameTile( + gameTitle: games[1].name, + gameType: 'Winner', + ruleset: 'Ruleset', + players: _getPlayerText(games[1]), + winner: games[1].winner == null + ? 'Game in progress...' + : games[1].winner!.name, + ), + const SizedBox(height: 8), + ] else ...[ + const Center( + heightFactor: 4, + child: Text( + 'No second game available.', + ), + ), + ], + ], + ); + else + return const Center( + heightFactor: 4, + child: Text('No recent games available.'), + ); + }, ), ), ), From e4abf53f66c3fca02c0f31f9a6305258b4bc74ce Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sun, 23 Nov 2025 18:09:45 +0100 Subject: [PATCH 91/95] fix linter errors --- lib/presentation/views/main_menu/home_view.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index a4184bf..a8ff0d3 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -171,7 +171,7 @@ class _HomeViewState extends State { )) .take(2) .toList(); - if (games.length > 0) + if (games.isNotEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -212,11 +212,12 @@ class _HomeViewState extends State { ], ], ); - else + } else { return const Center( heightFactor: 4, child: Text('No recent games available.'), ); + } }, ), ), From 651f210ea8e4ffd0ac29cc00aca81b1cb0d1fa3b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 18:28:10 +0100 Subject: [PATCH 92/95] Added ios icon --- .../main/res/drawable/launch_background.xml | 2 +- android/app/src/main/res/values/colors.xml | 4 + .../AppIcon.appiconset/Contents.json | 120 +----------------- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 295 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 406 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 450 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 282 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 462 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 704 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 406 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 586 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 862 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 862 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 1674 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 762 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1226 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1418 -> 0 bytes .../AppIcon.appiconset/icon_x1024.png | Bin 0 -> 9002 bytes ios/Runner/Assets.xcassets/Contents.json | 6 + .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/README.md | 5 - .../Contents.json | 20 +++ .../Contents.json | 8 +- .../LauncherIcon.imageset/icon.png | Bin 0 -> 9798 bytes ios/Runner/Base.lproj/LaunchScreen.storyboard | 28 ++-- ios/Runner/Base.lproj/Main.storyboard | 13 +- 29 files changed, 65 insertions(+), 141 deletions(-) create mode 100644 android/app/src/main/res/values/colors.xml delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_x1024.png create mode 100644 ios/Runner/Assets.xcassets/Contents.json delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 ios/Runner/Assets.xcassets/LauncherBackgroundColor.colorset/Contents.json rename ios/Runner/Assets.xcassets/{LaunchImage.imageset => LauncherIcon.imageset}/Contents.json (58%) create mode 100644 ios/Runner/Assets.xcassets/LauncherIcon.imageset/icon.png diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 304732f..d3f4435 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -1,7 +1,7 @@ - + @@ -14,24 +17,27 @@ + - + + + - - - - - + - + + - + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard index f3c2851..3ff2769 100644 --- a/ios/Runner/Base.lproj/Main.storyboard +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -14,13 +16,14 @@ - + - + + From ca55b99d036476d6f25f6201ca1b5bbca3eb45d6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 18:42:59 +0100 Subject: [PATCH 93/95] Changed launcher background color --- android/app/src/main/res/values/ic_launcher_background.xml | 3 ++- android/app/src/main/res/values/styles.xml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml index 78587a1..6d0d417 100644 --- a/android/app/src/main/res/values/ic_launcher_background.xml +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,5 @@ - #E6F1E4 + + @color/launch_background \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index cb1ef88..da964ae 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -4,7 +4,7 @@ - @color/launch_background + @color/app_icon_background \ No newline at end of file From 88fa886ea2efc50fc9e0db9ebce5aa139dbe51e6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 19:25:08 +0100 Subject: [PATCH 95/95] Changed launching background on ios --- .../LauncherBackgroundColor.colorset/Contents.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Runner/Assets.xcassets/LauncherBackgroundColor.colorset/Contents.json b/ios/Runner/Assets.xcassets/LauncherBackgroundColor.colorset/Contents.json index 14fbbde..41fe6c8 100644 --- a/ios/Runner/Assets.xcassets/LauncherBackgroundColor.colorset/Contents.json +++ b/ios/Runner/Assets.xcassets/LauncherBackgroundColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.894", - "green" : "0.945", - "red" : "0.902" + "blue" : "0.043", + "green" : "0.043", + "red" : "0.043" } }, "idiom" : "universal"