345 Commits

Author SHA1 Message Date
0f79495775 Implemented badges
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m25s
Pull Request Pipeline / test (pull_request) Successful in 2m27s
2025-12-06 15:42:02 +01:00
dcd8b460c1 Merge pull request 'GameResultView erstellen' (#62) from feature/48-game-result-view-erstellen into development
Reviewed-on: #62
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-12-06 14:06:29 +00:00
dbbe04d4cc add braces to if/else statement in game_result_view.dart
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m18s
Pull Request Pipeline / lint (pull_request) Successful in 2m20s
2025-12-06 14:21:17 +01:00
1ed6290628 Refactor winner selection and persistence logic in GameResultView
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m25s
Pull Request Pipeline / lint (pull_request) Failing after 2m25s
2025-12-06 14:20:37 +01:00
91a7273964 madio radio list tile toggleable 2025-12-06 14:12:34 +01:00
b1bb8b919f changed MaterialPageRoute to CupertinoPageRoute for ios animation
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m21s
2025-12-06 13:34:11 +01:00
697767f0de made winner deselectable and added auto save on select
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
2025-12-06 13:31:08 +01:00
306a783d67 removed dots after TopCenteredMessage text
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m17s
Pull Request Pipeline / lint (pull_request) Successful in 2m20s
2025-12-06 11:22:34 +01:00
03035138ac refactor game result view variable naming and layout
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m12s
Pull Request Pipeline / lint (pull_request) Successful in 2m12s
2025-12-06 10:00:59 +01:00
7323f52153 add delay and change empty games message
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m11s
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
2025-12-05 20:33:27 +01:00
f5842f9c4a remove print
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m12s
Pull Request Pipeline / test (pull_request) Successful in 2m14s
2025-12-05 19:44:36 +01:00
e3ac91bf48 remove todo
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m9s
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
2025-12-05 19:41:01 +01:00
dba448b9c1 added const 2025-12-05 19:39:35 +01:00
d8551b3a27 add db functionality 2025-12-05 19:35:14 +01:00
8f2c7493d0 re-set gameListFuture to reload the games after changing the winner 2025-12-05 19:35:04 +01:00
f7f97fcdcb made tapping on game tile redirect to GameResultView 2025-12-05 19:26:47 +01:00
9ac6b6e04c added ontap feature & argument 2025-12-05 19:25:52 +01:00
e77896c1d4 removed settings icon leading to game result view 2025-12-05 19:25:31 +01:00
dd2024e96e Merge branch 'development' into feature/48-game-result-view-erstellen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2025-12-05 17:55:01 +00:00
cd9780871f Merge pull request 'Fehlende Methoden für Games Datenbank inplementieren' (#76) from feature/74-fehlende-methoden-für-games-datenbank-inplementieren into development
Reviewed-on: #76
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-12-05 17:54:13 +00:00
3169eebd14 Import formatting
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m16s
Pull Request Pipeline / lint (pull_request) Successful in 2m18s
2025-12-05 18:24:06 +01:00
ec902c6196 Removed print 2025-12-05 18:23:58 +01:00
b719a6662b Merge branch 'development' into feature/74-fehlende-methoden-für-games-datenbank-inplementieren
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m19s
Pull Request Pipeline / lint (pull_request) Failing after 2m22s
# Conflicts:
#	lib/presentation/views/main_menu/game_history_view.dart
2025-12-05 18:22:25 +01:00
09b407eba8 Merge branch 'development' into feature/48-game-result-view-erstellen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-12-02 19:48:36 +00:00
877c2921d9 Merge pull request 'GameHistoryView anpassen' (#20) from feature/2-gamehistoryview-anpassen into development
Reviewed-on: #20
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-30 15:59:25 +00:00
gelbeinhalb
5ce4964c32 deleted double_row_info_tile
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-29 20:04:49 +01:00
gelbeinhalb
fb28de5772 add create game button
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2025-11-28 14:44:24 +01:00
gelbeinhalb
f713bd6fb7 use custom app skeleton 2025-11-28 14:35:20 +01:00
71b2f30d29 removed comment
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m23s
Pull Request Pipeline / lint (pull_request) Successful in 2m31s
2025-11-28 14:00:36 +01:00
d2d6852f31 removed comment
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m29s
Pull Request Pipeline / lint (pull_request) Successful in 2m51s
2025-11-28 14:00:26 +01:00
126dc7ed97 Added exception 2025-11-28 14:00:04 +01:00
40a3c1b82e Removed comment 2025-11-28 13:56:24 +01:00
gelbeinhalb
da722c5277 Merge remote-tracking branch 'origin/development' into feature/2-gamehistoryview-anpassen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m14s
Pull Request Pipeline / lint (pull_request) Successful in 2m17s
2025-11-28 12:15:14 +01:00
gelbeinhalb
516c2afd1e remove colon behind players
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m16s
Pull Request Pipeline / lint (pull_request) Successful in 2m22s
2025-11-28 12:14:22 +01:00
gelbeinhalb
9ee9da2ac8 Made space at the bottom of the list smaller
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-27 22:59:09 +01:00
gelbeinhalb
aa208bb2ef use standardized TopCenteredMessage 2025-11-27 22:56:22 +01:00
gelbeinhalb
a29123c964 fix error messages
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m15s
Pull Request Pipeline / test (pull_request) Successful in 2m14s
2025-11-27 17:25:58 +01:00
gelbeinhalb
8c005d6e5e fix possible render overflow
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m9s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
2025-11-27 17:06:32 +01:00
gelbeinhalb
cc50e497c9 merge duplicate if statements for group and winner sections 2025-11-27 17:04:08 +01:00
gelbeinhalb
ae348499d4 moved functionality methods to the bottom of the file 2025-11-27 17:00:13 +01:00
gelbeinhalb
b443230285 fixed loading too fast
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m10s
Pull Request Pipeline / test (pull_request) Successful in 5m6s
2025-11-27 16:58:02 +01:00
gelbeinhalb
099e587d45 remove useless skeleton data
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Failing after 2m17s
2025-11-27 16:45:47 +01:00
dc0e536221 Implemented updateGameName() and tests for it
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
Pull Request Pipeline / test (pull_request) Successful in 3m30s
2025-11-26 14:44:41 +01:00
2a34243e69 Renamed methods for better distinction 2025-11-26 14:42:17 +01:00
499415e0c5 Added updatePlayersFromGame(), added docs & tests 2025-11-26 14:39:38 +01:00
397c5c1550 Added updateGroupOfGame(), added docc & tests 2025-11-26 14:17:11 +01:00
738f242eee Implemented methods and test for winner 2025-11-26 13:48:53 +01:00
745aaef978 Implemented ChooseTile
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-26 13:12:15 +01:00
b5234c765c Changed create game button size
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2025-11-26 12:40:06 +01:00
919c9f57ac Fixed button state 2025-11-26 12:35:34 +01:00
27424694ce Removed unnecessary prints 2025-11-26 12:31:33 +01:00
84338f8f66 Changed title 2025-11-26 12:28:11 +01:00
733df2dcb5 Changed highlight color 2025-11-26 12:27:07 +01:00
9ba3dd7909 added missing consts
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m7s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2025-11-25 23:22:02 +01:00
2838376434 Implemented player selection
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m12s
2025-11-25 22:38:54 +01:00
86ec4de5c0 add textoverflow behaviour
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Failing after 2m8s
2025-11-25 22:03:38 +01:00
479e9a2575 add spacing between title and list and rename appbar title to game name 2025-11-25 22:01:49 +01:00
d97871d15b fix lint
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-11-25 17:29:21 +01:00
00fd6880e9 add todo comment
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m14s
Pull Request Pipeline / lint (pull_request) Failing after 2m17s
2025-11-25 17:24:35 +01:00
649330f358 Merge remote-tracking branch 'origin/feature/48-game-result-view-erstellen' into feature/48-game-result-view-erstellen
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m18s
Pull Request Pipeline / lint (pull_request) Failing after 2m23s
2025-11-25 17:23:19 +01:00
07d81d687b Implement CustomRadioListTile and update GameResultView to select a winner currently without saving to the db 2025-11-25 17:23:03 +01:00
b291673899 Merge branch 'development' into feature/48-game-result-view-erstellen
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Failing after 2m9s
2025-11-25 10:28:28 +00:00
5fbf2ccb45 Implemented app skeleton
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-24 22:21:27 +01:00
e489d16c51 Removed imports 2025-11-24 22:20:53 +01:00
7cfffadb86 Corrected import 2025-11-24 22:20:44 +01:00
ae529effd2 Merge branch 'development' into feature/3-creategameview-erstellen
# Conflicts:
#	lib/presentation/views/main_menu/create_group_view.dart
2025-11-24 22:19:32 +01:00
4c3b2152eb Merge pull request 'PlayerSelection Widget implementiert' (#72) from feature/65-spieler-suche-in-eigenes-widget-umwandeln into development
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m27s
Pull Request Pipeline / test (pull_request) Successful in 2m38s
Reviewed-on: #72
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-24 21:10:54 +00:00
51e3c04e72 Refactor PlayerSelection to use AppSkeleton widget
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-24 22:08:42 +01:00
2b9f038b0d Merge remote-tracking branch 'origin/development' into feature/65-spieler-suche-in-eigenes-widget-umwandeln
# Conflicts:
#	lib/presentation/views/main_menu/create_group_view.dart
2025-11-24 22:01:26 +01:00
0653700f9c Added comments to explain player filtering and selection logic in PlayerSelection widget
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
2025-11-24 21:56:57 +01:00
7be80e6f91 Translate player selection UI text to English
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Successful in 2m2s
2025-11-24 21:44:41 +01:00
a4b934388d Merge pull request 'Skeletonizer auslagern' (#71) from enhancement/69-skeletonizer-auslagern into development
Reviewed-on: #71
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-11-24 20:35:54 +00:00
f8c0dbba5a Remove unused TextEditingController from PlayerSelection widget
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m13s
Pull Request Pipeline / lint (pull_request) Successful in 2m18s
2025-11-24 21:33:53 +01:00
ebb531d825 initialize _searchBarController internally instead of using widget controller
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m24s
Pull Request Pipeline / test (pull_request) Successful in 2m24s
2025-11-24 21:33:28 +01:00
fc9779153d Remove unused _searchBarController from CreateGroupView 2025-11-24 21:33:19 +01:00
a2522cef13 rename searchBarController to controller in PlayerSelection widget
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-24 21:26:55 +01:00
442e1d64a3 remove uneccessary groupNameController
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Successful in 2m17s
2025-11-24 21:24:51 +01:00
54b54796e8 remove print
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
2025-11-24 21:19:05 +01:00
686463720a refactor for new name and remove hide in material import 2025-11-24 21:17:27 +01:00
6a77028171 rename to PlayerSelection 2025-11-24 21:17:01 +01:00
f1bd9c18e0 Made selectedPlayers local to SelectPlayerWidget because its not needed in CreateGroupView 2025-11-24 21:15:18 +01:00
6c9b742bdf Refactor CreateGroupView to use SelectPlayerWidget 2025-11-24 20:59:23 +01:00
744a402602 put player selection from creategroupview into own widget 2025-11-24 20:58:56 +01:00
2d9148788e Refactored skeleton widgets to own
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m19s
2025-11-24 20:05:18 +01:00
18f635e6ef Implemented custom skeleton widget 2025-11-24 20:05:07 +01:00
9efbc12909 moved input widgets to new folder
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-11-24 16:23:29 +01:00
7c7676abee Implemented CustomTextInputField 2025-11-24 16:17:15 +01:00
1faa74f026 Removed comment 2025-11-24 15:17:55 +01:00
3afae89234 Added Skeleton Loading 2025-11-24 15:17:46 +01:00
093c527591 Implemented TabView 2025-11-24 15:17:29 +01:00
2ba710ca2d Replaced unique box decorations with standardBoxDecoration
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-11-24 13:53:28 +01:00
9054b163ce Added BoxDecorations to Custom Theme 2025-11-24 13:50:23 +01:00
e182c815a1 Implemented ruleset list tile with highlighting 2025-11-24 13:50:02 +01:00
c284d10943 Refactoring 2025-11-24 13:49:25 +01:00
72e48ada94 Added seletion highlighting for selected group 2025-11-24 13:31:42 +01:00
Yannick
4591a6857d change skeleton names
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Failing after 2m6s
2025-11-24 11:39:56 +01:00
Yannick
44279bc148 Merge remote-tracking branch 'origin/development' into feature/2-gamehistoryview-anpassen
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m15s
Pull Request Pipeline / lint (pull_request) Failing after 2m15s
2025-11-24 11:34:59 +01:00
Yannick
32c7d45809 made game_history_tile prettier :)
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m13s
Pull Request Pipeline / lint (pull_request) Failing after 2m17s
2025-11-24 11:34:22 +01:00
Yannick
4341c2509e fix bug where only last 2 games were shown 2025-11-24 11:07:40 +01:00
a3fa499662 Merge pull request 'Gelbes Aufblinken der Recent Games auf der Homepage behoben & Anpassung des Recent-Games-Containers vorgenommen.' (#64) from bug/63-übergang-auf-home-seite-blitz-kurz-gelb-auf into development
Reviewed-on: #64
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-23 21:51:04 +00:00
f25737cdc5 Merge branch 'development' into bug/63-übergang-auf-home-seite-blitz-kurz-gelb-auf
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m9s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-11-23 21:48:48 +00:00
0b500b5248 Merge pull request 'Bugfix bei Spieleranzeige und Vereinfachung der Keine-Player-Logik' (#66) from bug/60-CreateGroupView-erstellter-Spieler-erscheint-nicht-in-alle-Spieler into development
Reviewed-on: #66
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-23 21:41:08 +00:00
e71cb11295 Implemented View for choosing group and ruleset
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m13s
Pull Request Pipeline / test (pull_request) Successful in 2m15s
2025-11-23 22:36:51 +01:00
b102ec4c1c Implemented first structure of CreateGameView 2025-11-23 22:36:35 +01:00
974d6e9b56 refactor empty state logic in CreateGroupView
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m22s
Pull Request Pipeline / lint (pull_request) Successful in 2m24s
The diff introduces boolean variables `doneLoading` and `snapshotDataEmpty` to simplify the conditional check for displaying the empty state message. It specifically fixes the logic to correctly show the "No players found" message when both the snapshot and the local `allPlayers` list are empty, removing the dependency on `selectedPlayers.isEmpty`.
2025-11-23 22:14:13 +01:00
Yannick
290948e50d add intl for date formatting
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m21s
Pull Request Pipeline / lint (pull_request) Failing after 2m28s
2025-11-23 22:05:25 +01:00
Yannick
bbd200e245 use Skeletonizer for Layout 2025-11-23 22:02:16 +01:00
694cac7f26 Fix bug where the skeleton was edited while it was visible and
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
match the “no games available” container size to the size used when games are available.
2025-11-23 21:16:13 +01:00
bd616c510a update GameResultView to accept and use a given Game instance 2025-11-23 20:22:26 +01:00
424a258df1 Update GameResultView with dummy Game data in CustomNavigationBar 2025-11-23 20:22:03 +01:00
6dc74ca82e Implement basic logic and UI for selecting game winners in GameResultView 2025-11-23 20:18:26 +01:00
Yannick
ac6af803fd Merge remote-tracking branch 'origin/development' into feature/2-gamehistoryview-anpassen 2025-11-23 20:07:10 +01:00
Yannick
f21d0ba4e8 move game_history_tile.dart 2025-11-23 20:04:56 +01:00
8307488f28 Merge pull request 'Provisorisches App Icon implementieren' (#59) from setup/57-provisorisches-app-icon-implementiere into development
Reviewed-on: #59
2025-11-23 18:45:44 +00:00
937f1e3ac8 made settingsbutton redirect to game result view
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Failing after 2m14s
2025-11-23 19:42:31 +01:00
46d1c25bb5 create GameResultView with basic structure and styling 2025-11-23 19:41:57 +01:00
cdba6e264a Merge branch 'development' into setup/57-provisorisches-app-icon-implementiere
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m31s
Pull Request Pipeline / lint (pull_request) Successful in 2m33s
2025-11-23 19:26:05 +01:00
88fa886ea2 Changed launching background on ios
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m39s
Pull Request Pipeline / test (pull_request) Successful in 2m38s
2025-11-23 19:25:08 +01:00
cc2f87396f Changed Launcher Background to app bg
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m14s
Pull Request Pipeline / lint (pull_request) Successful in 2m15s
2025-11-23 19:21:19 +01:00
692a3ef3a4 Merge pull request 'HomeView Mock-Daten entfernen' (#51) from enhancement/46-homeview-mockdaten-entfernen into development
Reviewed-on: #51
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-23 18:19:54 +00:00
22d95b0015 Merge remote-tracking branch 'origin/development' into enhancement/46-homeview-mockdaten-entfernen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m9s
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
# Conflicts:
#	lib/presentation/views/main_menu/home_view.dart
2025-11-23 19:04:02 +01:00
1a84d2572a Merge branch 'development' into setup/57-provisorisches-app-icon-implementiere
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m30s
Pull Request Pipeline / lint (pull_request) Successful in 2m38s
2025-11-23 18:57:26 +01:00
ca55b99d03 Changed launcher background color
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m45s
Pull Request Pipeline / lint (pull_request) Successful in 2m45s
2025-11-23 18:42:59 +01:00
651f210ea8 Added ios icon 2025-11-23 18:28:10 +01:00
0153df6195 Merge pull request 'Skeleton Delay für group- und group create view' (#58) from enhancement/54-delay-für-groups-view-skeleton into development
Reviewed-on: #58
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-23 17:10:37 +00:00
e4abf53f66 fix linter errors
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m27s
Pull Request Pipeline / lint (pull_request) Successful in 2m31s
2025-11-23 18:09:45 +01:00
2616f7c113 Refactor FutureBuilder logic in HomeView to handle empty game lists and improve code formatting
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m11s
Pull Request Pipeline / lint (pull_request) Failing after 2m12s
2025-11-23 18:08:03 +01:00
7881e61a2e Updated android icon 2025-11-23 17:57:20 +01:00
73c5586874 Added icons for android 2025-11-23 17:54:33 +01:00
963edaf1d1 Update game status text and player count label in HomeView 2025-11-23 17:51:55 +01:00
604a541392 set delay in all future builders to 250ms
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2025-11-23 17:09:52 +01:00
26fadf5093 add artificial delay to loadPlayerList for skeleton loading
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-11-23 15:22:59 +01:00
9e8bab1a60 add artificial delay to group list loading 2025-11-23 15:21:40 +01:00
fc51b30491 Merge pull request '1 wird immer als 1.00 angezeigt #55' (#56) from bug/55-1-wird-falsch-angezeigt into development
Reviewed-on: #56
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-11-23 13:58:26 +00:00
def37aa640 Refactor Recent Games tile in HomeView
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m7s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
- 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
2025-11-23 14:56:53 +01:00
8ae85c925d Fixed double
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-11-23 14:29:23 +01:00
b744b04648 Merge remote-tracking branch 'origin/development' into enhancement/46-homeview-mockdaten-entfernen 2025-11-23 14:02:55 +01:00
17e882986d Merge pull request 'StatisticsView erstellen' (#30) from feature/6-statisticsview-erstellen into development
Reviewed-on: #30
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-11-23 12:59:40 +00:00
7cda25a380 Changed item count back to normal
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m19s
Pull Request Pipeline / lint (pull_request) Successful in 2m23s
2025-11-23 12:34:42 +01:00
e9b041e43a Changed double depiction
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m20s
Pull Request Pipeline / lint (pull_request) Successful in 2m23s
2025-11-23 12:33:13 +01:00
c38c731b41 Merge branch 'development' into feature/6-statisticsview-erstellen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m25s
Pull Request Pipeline / lint (pull_request) Successful in 2m25s
# Conflicts:
#	lib/presentation/views/main_menu/statistics_view.dart
2025-11-23 12:19:03 +01:00
d411f58134 Changed icon for second statistics tile
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m21s
Pull Request Pipeline / lint (pull_request) Successful in 2m22s
2025-11-23 12:18:05 +01:00
fee5c57207 Added comments for return value -1 2025-11-23 12:13:30 +01:00
de60c942ea Merge pull request 'Winner als Player-Objekte' (#52) from feature/50-winner-soll-player-objekt-sein into development
Reviewed-on: #52
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-11-23 10:48:45 +00:00
acc5b0a3e9 Merge branch 'development' into feature/6-statisticsview-erstellen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m33s
Pull Request Pipeline / lint (pull_request) Successful in 2m33s
2025-11-23 00:36:33 +01:00
24babe06d2 Merge branch 'development' into feature/50-winner-soll-player-objekt-sein
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m33s
Pull Request Pipeline / lint (pull_request) Successful in 2m34s
2025-11-23 00:36:08 +01:00
50dd05ecc5 Merge pull request 'Erstelle Spieler direkt zu ausgewählten Spielern, Gruppen Sortierung nach Timestamp + Bugfixes' (#49) from feature/47-spieler-zur-gruppe-und-gruppen-sortierung into development
Reviewed-on: #49
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-22 23:34:47 +00:00
4ff131770e Adjust tests
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-23 00:22:53 +01:00
82b344a145 Changed winner access in statistics view
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m9s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
2025-11-23 00:17:48 +01:00
338f4294dc Updated json schema 2025-11-23 00:17:31 +01:00
cfed05595c Updated methods in gameDao 2025-11-23 00:17:27 +01:00
fa841e328e Altered game class 2025-11-23 00:17:11 +01:00
f658a88849 Implemented displaying real recent games in home view
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m13s
Pull Request Pipeline / lint (pull_request) Failing after 2m13s
2025-11-22 23:52:00 +01:00
feb5fa0615 Docs, small changes
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m13s
Pull Request Pipeline / lint (pull_request) Successful in 2m15s
2025-11-22 23:30:24 +01:00
fba35521cb changed skeletonizer transition duration back to normal 2025-11-22 23:23:02 +01:00
e60961730f Added new metric & changed layout builder of Skeletonizer 2025-11-22 23:20:58 +01:00
59c041699d Changed values attribute & maxBarWidth 2025-11-22 23:20:31 +01:00
c170aa1775 sort groups by creation date in GroupsView
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m12s
Pull Request Pipeline / lint (pull_request) Successful in 2m12s
2025-11-22 22:55:19 +01:00
692b412fe2 Fix black bars on the screens bottom and top by not wrapping scaffold in safearea, but scaffolds body children 2025-11-22 22:52:09 +01:00
cc04e05557 Adjust bottom padding in GroupsView list based on media query padding 2025-11-22 22:49:28 +01:00
546a3e3717 implemented feature to automatically add newly created player to selected players 2025-11-22 22:49:17 +01:00
b2036e4e68 Implemented first version of statistics view
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2025-11-22 22:10:16 +01:00
310b9aa43b Implemented StatisticsWidget tile 2025-11-22 22:10:02 +01:00
e464ddb466 Merge pull request 'Navbar Position fix' (#21) from enhancement/18-navbar-alignment into development
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
Reviewed-on: #21
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-22 17:06:04 +00:00
c3deaa3974 Merge branch 'development' into enhancement/18-navbar-alignment
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m9s
Pull Request Pipeline / lint (pull_request) Successful in 2m12s
2025-11-22 17:03:48 +00:00
fd13fe6e90 made CustomWidthButton's position adaptive to bottom padding of safearea
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-22 17:58:33 +01:00
5062196463 Adjust safeareas minimum padding for custom navigation bar 2025-11-22 17:57:58 +01:00
6af1df5fbc Merge pull request 'JSON Import für Testdaten & Funktion zum Löschen aller Daten' (#33) from feature/31-json-import-fuer-testdaten into development
Reviewed-on: #33
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
2025-11-22 16:47:15 +00:00
df3215ef76 Made bottom padding adaptive to screen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2025-11-22 17:40:24 +01:00
28ed22ce73 wrap navbar in SafeArea and replaced Material with Container 2025-11-22 17:40:00 +01:00
0593297fc3 Merge branch 'development' into feature/31-json-import-fuer-testdaten
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m16s
Pull Request Pipeline / test (pull_request) Successful in 2m16s
2025-11-22 16:44:09 +01:00
b668f6b9ae fix black top/bottom bar when wrapping scaffold in safearea
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m10s
Pull Request Pipeline / lint (pull_request) Failing after 2m19s
2025-11-22 16:31:15 +01:00
78b511b207 Merge pull request 'getAllGames returnt nicht alle Attribute von der game Klasse' (#45) from bug/37-getallgames-returnt-nicht-alle-attribute-von-der-game-klasse into development
Reviewed-on: #45
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-11-22 15:22:11 +00:00
63d9ed400d Adjust padding in CustomNavigationBar
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Failing after 2m10s
- 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
2025-11-22 16:18:37 +01:00
d809d80506 Merge remote-tracking branch 'origin/development' into enhancement/18-navbar-alignment 2025-11-22 15:41:33 +01:00
30645f06f8 Renamed variable
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2025-11-22 14:21:55 +01:00
9346f61d14 Renamed variable 2025-11-22 14:21:28 +01:00
bef812502c Renamed variable and added null checks 2025-11-22 14:20:51 +01:00
24f18f5c65 Removed false comparison 2025-11-22 14:13:15 +01:00
62eea08614 Renamed variable 2025-11-22 14:12:41 +01:00
893eb91143 Schema corrected
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m10s
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
2025-11-22 01:12:39 +01:00
2ebd4274f0 Moved method validateJsonSchema()
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-22 00:47:44 +01:00
70307016b3 Fixed addGames method 2025-11-22 00:47:32 +01:00
1b3334f3e0 Fixed addGroups method 2025-11-22 00:47:24 +01:00
Yannick
6a985b0b1e Merge branch 'development' into feature/2-gamehistoryview-anpassen
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m7s
Pull Request Pipeline / lint (pull_request) Failing after 2m8s
# Conflicts:
#	lib/presentation/views/main_menu/game_history_view.dart
2025-11-21 15:45:51 +01:00
Yannick
95f0861a79 add basic came history tile 2025-11-21 15:44:27 +01:00
7cd53aa695 Merge branch 'bug/37-getallgames-returnt-nicht-alle-attribute-von-der-game-klasse' into feature/31-json-import-fuer-testdaten
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m21s
Pull Request Pipeline / lint (pull_request) Successful in 2m21s
# Conflicts:
#	test/db_tests/player_test.dart
2025-11-21 14:26:18 +01:00
ab250e2df4 Typo
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m25s
Pull Request Pipeline / test (pull_request) Successful in 2m25s
2025-11-21 14:24:57 +01:00
dbb52cfc48 Added missing awaits
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2025-11-21 14:20:42 +01:00
c56663d15e Added missing await
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2025-11-21 14:17:38 +01:00
51722eb7fd Added batch insert methods to tests
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m9s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2025-11-21 14:01:45 +01:00
78d530ddd5 Merge branch 'bug/37-getallgames-returnt-nicht-alle-attribute-von-der-game-klasse' into feature/31-json-import-fuer-testdaten
# Conflicts:
#	lib/data/dao/player_group_dao.dart
#	test/db_tests/game_test.dart
#	test/db_tests/group_test.dart
#	test/db_tests/player_test.dart
2025-11-21 14:00:10 +01:00
8c05385203 Renamed variables to be consistent
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-21 13:47:27 +01:00
d948f2f13d Added iteration for multiple items test
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-21 13:42:27 +01:00
e15f5d163d Added missing methods
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2025-11-21 13:12:36 +01:00
32f3f68da9 Annotation for missing test & method
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
2025-11-21 12:46:18 +01:00
229750ffcf Fixed test 2025-11-21 12:46:04 +01:00
fe9239ee02 Added missing test 2025-11-21 12:45:48 +01:00
961c6bb679 Refactored tests in own files
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2025-11-21 01:05:35 +01:00
b21ca54672 Refactoring 2025-11-21 00:10:29 +01:00
6055eb63a8 Refactoring 2025-11-21 00:07:34 +01:00
31589855f2 Added methods of todos 2025-11-21 00:07:29 +01:00
8e63a01705 Added methods for multiple inserts to tests
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-20 23:49:57 +01:00
e512cbbf95 Merge branch 'bug/37-getallgames-returnt-nicht-alle-attribute-von-der-game-klasse' into feature/31-json-import-fuer-testdaten 2025-11-20 23:45:18 +01:00
89b3f1ff69 Overhauled tests
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2025-11-20 23:40:46 +01:00
72067863c2 Updated insert modes 2025-11-20 23:14:44 +01:00
29a3e77fc4 Merge branch 'bug/37-getallgames-returnt-nicht-alle-attribute-von-der-game-klasse' into feature/31-json-import-fuer-testdaten
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-20 22:41:14 +01:00
a61818dd77 Fixed error in getAllGames method 2025-11-20 22:40:56 +01:00
e364e15d0a Merge pull request 'Spieler erstellen in CreateGroupView' (#43) from feature/42-spieler-erstellen-in-create-group-view-implementieren into development
Reviewed-on: #43
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-20 21:18:21 +00:00
195ebf569a Added icon as parameter for custom search bar
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2025-11-20 22:17:20 +01:00
eb7b247cae Fixed error adding player with empty name 2025-11-20 22:11:23 +01:00
01fede2951 Added Visibility Widget 2025-11-20 22:09:08 +01:00
b67f321276 Added name parameters and function doc 2025-11-20 22:05:44 +01:00
d16beed490 felix mach jetzt
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
2025-11-20 21:54:43 +01:00
0111774308 Trim whitespace from group and player names in CreateGroupView
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m7s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2025-11-20 21:26:59 +01:00
8ff3c01435 added missing consts & mounted check
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
2025-11-20 16:59:49 +01:00
bce4cdcb2d Enable player creation via search bar in CreateGroupView
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Failing after 2m5s
2025-11-20 16:53:38 +01:00
fa0e9a5dfd add trailing button functionality to CustomSearchBar 2025-11-20 16:53:14 +01:00
ec9e34305a Merge branch 'development' into enhancement/18-navbar-alignment
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2025-11-20 10:50:10 +01:00
45650133a7 Corrected schema again
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m14s
2025-11-19 21:51:13 +01:00
f40a9ad09b Updated schema
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2025-11-19 21:42:07 +01:00
822bc03c83 Added dialog 2025-11-19 21:41:30 +01:00
a8d4e640cf Tabs update themselves after settings view 2025-11-19 21:39:01 +01:00
cf71b40718 changed export file name 2025-11-19 21:19:54 +01:00
f7c1d6e975 Added missing await
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-19 21:13:29 +01:00
17dabb773d Merge branch 'development' into feature/31-json-import-fuer-testdaten
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m13s
2025-11-19 20:35:34 +01:00
e14984d4a9 Merge pull request 'Actions aufsetzen' (#38) from setup/17-actions-aufsetzen into development
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 1m59s
Pull Request Pipeline / lint (pull_request) Successful in 2m1s
Reviewed-on: #38
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-11-19 19:35:07 +00:00
682c1d269d Merge branch 'development' into feature/31-json-import-fuer-testdaten 2025-11-19 20:27:21 +01:00
d264b3fa1b Merge branch 'development' into setup/17-actions-aufsetzen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 1m59s
Pull Request Pipeline / lint (pull_request) Successful in 2m1s
2025-11-19 20:26:39 +01:00
b684ebd4f6 Renamed workflow according to file name 2025-11-19 20:25:37 +01:00
2cae66a8ae Merge pull request 'Timestamp zu allen Objekten hinzufügen' (#36) from feature/29-timestamp-zu-allen-objekten-hinzufügen into development
Reviewed-on: #36
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-19 19:23:31 +00:00
0967abe10f Merge branch 'development' into feature/29-timestamp-zu-allen-objekten-hinzufügen 2025-11-19 20:23:04 +01:00
061a927324 Merge branch 'development' into feature/31-json-import-fuer-testdaten 2025-11-19 20:22:19 +01:00
412cfff9f5 Added methods for inserting list of players, groups, games 2025-11-19 20:21:55 +01:00
f7073a83a4 Added insert mode 2025-11-19 20:21:30 +01:00
9434282ed1 Added createdAt attribute in dto classes and json schema 2025-11-19 20:20:21 +01:00
d145b18891 Merge pull request 'Button ist transparent' (#41) from bug/40-button-ist-transparent into development
Reviewed-on: #41
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-11-19 18:35:41 +00:00
3e3def0bde Merge remote-tracking branch 'origin/feature/29-timestamp-zu-allen-objekten-hinzufügen' into feature/31-json-import-fuer-testdaten
# Conflicts:
#	lib/data/dao/game_dao.dart
#	lib/data/db/database.g.dart
#	lib/data/db/tables/game_table.dart
#	lib/data/dto/game.dart
#	pubspec.yaml
2025-11-19 19:30:56 +01:00
3408930524 Merge remote-tracking branch 'origin/development' into setup/17-actions-aufsetzen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m3s
2025-11-19 19:27:02 +01:00
87b1a7d57f Moved ImportStatus & ExportStatus to enums.dart 2025-11-19 19:26:35 +01:00
cf834e636e Merge remote-tracking branch 'origin/development' into feature/31-json-import-fuer-testdaten
# Conflicts:
#	lib/presentation/views/main_menu/home_view.dart
2025-11-19 19:22:34 +01:00
8e2befaf3d Fixed button color problem 2025-11-19 19:19:04 +01:00
186d8cf5c9 Merge branch 'development' into feature/29-timestamp-zu-allen-objekten-hinzufügen 2025-11-19 17:34:07 +00:00
6f42b36587 Merge pull request 'CreateGroupView erstellt' (#28) from feature/5-creategroupview-erstellen into development
Reviewed-on: #28
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-19 17:32:44 +00:00
248d652e06 Made onPressed not required 2025-11-19 18:32:25 +01:00
e71e65b197 Corrected button color behaviour and added tertiary button 2025-11-19 18:27:27 +01:00
3f79a7b898 sourcing enums out to enums.dart 2025-11-19 18:26:51 +01:00
1232cb8f0d Fix GestureDetector child ordering in TextIconListTile 2025-11-19 16:50:02 +01:00
018332d8e6 Refactor widget directory structure by organizing tiles and buttons
- Move `GameTile` and `DoubleRowInfoTile` to `presentation/widgets/tiles/`
- Move `CustomWidthButton` and `QuickCreateButton` to `presentation/widgets/buttons/`
- Update import paths in `HomeView`, `GroupsView`, `GameHistoryView`, and `CreateGroupView`
2025-11-19 16:48:43 +01:00
201fd70685 Update TextIconListTile padding and replace IconButton with GestureDetector 2025-11-19 16:44:46 +01:00
9365313c92 button not working 2025-11-19 16:39:32 +01:00
54e1756e79 moved create_group_view from subfolder to root 2025-11-19 16:39:05 +01:00
3b6a914022 removed uneccessary withClock 2025-11-19 15:52:04 +01:00
98b02adc85 Formatted files so that pipeline doesnt fail
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Successful in 2m3s
2025-11-19 15:43:49 +01:00
b82261317c move CreateGroupView to main_menu directory 2025-11-19 15:09:40 +01:00
c76e193b4d created all objects in setup() funktion to avoid redundant withClock 2025-11-19 15:02:32 +01:00
0ac8c21052 Formatted files so that pipeline doesnt fail
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 1m58s
Pull Request Pipeline / lint (pull_request) Successful in 2m1s
2025-11-19 11:32:22 +01:00
74fffa95e2 Added push workflow (not active)
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 1m58s
Pull Request Pipeline / lint (pull_request) Failing after 2m1s
2025-11-19 11:28:00 +01:00
ca4bf03bab Finalized pull request workflow 2025-11-19 11:27:48 +01:00
aade42c0a6 Tried sth
Some checks failed
Pull Request Pipeline / lint (pull_request) Has been skipped
Pull Request Pipeline / test (pull_request) Has been skipped
Pull Request Pipeline / format (pull_request) Failing after 1m54s
2025-11-19 11:21:43 +01:00
0659d202b3 Remove if clause
Some checks failed
Pull Request Pipeline / lint (pull_request) Has been skipped
Pull Request Pipeline / test (pull_request) Has been skipped
Pull Request Pipeline / format (pull_request) Failing after 2m42s
2025-11-19 11:18:23 +01:00
81cdeb7ed6 Skipped other runs for testing
All checks were successful
Pull Request Pipeline / lint (pull_request) Has been skipped
Pull Request Pipeline / test (pull_request) Has been skipped
Pull Request Pipeline / format (pull_request) Successful in 2m5s
2025-11-19 11:15:30 +01:00
89d7bb54a1 Removed false
Some checks failed
Pull Request Pipeline / format (pull_request) Failing after 1m59s
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Failing after 2m12s
2025-11-19 11:12:46 +01:00
5d8047b3ba Updated directorys
Some checks failed
Pull Request Pipeline / format (pull_request) Has been skipped
Pull Request Pipeline / test (pull_request) Has been cancelled
Pull Request Pipeline / lint (pull_request) Has been cancelled
2025-11-19 11:12:02 +01:00
dd8af42a47 corrected workflow
Some checks failed
Pull Request Pipeline / format (pull_request) Failing after 2m9s
Pull Request Pipeline / test (pull_request) Successful in 2m10s
Pull Request Pipeline / lint (pull_request) Failing after 2m12s
2025-11-19 11:08:30 +01:00
346dddcf62 testing formatting 2025-11-19 11:08:01 +01:00
9f4fc3a3b0 Merge remote-tracking branch 'origin/setup/17-actions-aufsetzen' into setup/17-actions-aufsetzen
# Conflicts:
#	.gitea/workflows/pull_request.yaml
2025-11-19 11:07:25 +01:00
594ea947c2 Tested sth 2025-11-19 11:06:40 +01:00
e5268ebc12 Tested sth 2025-11-19 11:06:37 +01:00
1732878c7f Tested sth 2025-11-19 11:03:10 +01:00
f136400c7e Final changes?
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 1m56s
Pull Request Pipeline / lint (pull_request) Failing after 2m1s
2025-11-19 11:01:33 +01:00
c89243f886 Tried sth
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 1m59s
Pull Request Pipeline / lint (pull_request) Failing after 2m2s
2025-11-19 10:57:53 +01:00
a3b45053e7 Finalized workflow
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m5s
Pull Request Pipeline / test (pull_request) Successful in 2m6s
2025-11-19 10:54:58 +01:00
91b68eac3e Implemented test workflow
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Failing after 2m7s
2025-11-19 10:50:56 +01:00
6ae1ce9bc7 Updated analyzing
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m1s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:47:14 +01:00
7ac5986588 Updated whole workflow
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 3m52s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:41:12 +01:00
601b7d0a4f Changed flutter installation
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m10s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:36:48 +01:00
e852a4d539 Added git safe directory
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 3m47s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:26:34 +01:00
e108bb41f6 Corrected installation
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 1m33s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:22:55 +01:00
17c14dd230 Added container
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 1m32s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:19:45 +01:00
7123d36cd8 Updated flutter installation way
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 1m32s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:15:12 +01:00
7cc72015d3 Upgraded to flutter 3.35.6
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 10s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:11:22 +01:00
10e56a7241 Downgraded flutter version
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 1m17s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:09:15 +01:00
6638c2deee Removed cache clearing
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 10s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:08:04 +01:00
974f06b6b8 Back again
Some checks failed
Pull Request Pipeline / test (pull_request) Has been cancelled
Pull Request Pipeline / lint (pull_request) Has been cancelled
2025-11-19 10:06:46 +01:00
63d2117a6a Tried sth
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 6s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:05:55 +01:00
6ae39717fd Cleaned flutter cache
Some checks failed
Pull Request Pipeline / test (pull_request) Has been cancelled
Pull Request Pipeline / lint (pull_request) Has been cancelled
2025-11-19 10:02:39 +01:00
003835472d Added flutter version again
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 9s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 10:00:09 +01:00
8d91eb3780 Added update
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 50s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 09:58:03 +01:00
ddc8d93592 Removed sudo
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 47s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 09:56:05 +01:00
eeec92181a Added jq installation
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 47s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 09:53:54 +01:00
a8962e68b6 Added first workflow
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 1m9s
Pull Request Pipeline / test (pull_request) Has been skipped
2025-11-19 09:51:37 +01:00
322c51a764 Added documentation and feedback in snackbar 2025-11-19 09:44:48 +01:00
Yannick
69e13e877e add game_history_tile 2025-11-19 08:58:47 +01:00
Yannick
f388c442d2 Merge branch 'development' into feature/2-gamehistoryview-anpassen
# Conflicts:
#	lib/presentation/views/main_menu/game_history_view.dart
2025-11-19 08:48:00 +01:00
82e28b7509 Refactored whole export/import methods in DataTransferService 2025-11-19 00:32:16 +01:00
8150b42dba add clock dependency to pubspec.yaml 2025-11-19 00:27:47 +01:00
75c6f4e01c verify createdAt timestamps in database tests using mocked clock 2025-11-19 00:27:40 +01:00
19c99eef9c use clock.now() instead of DateTime.now() for DTO creation timestamps 2025-11-19 00:27:08 +01:00
f2a749cb0f First version of settings view 2025-11-19 00:24:08 +01:00
5dcd0826bd Adjusted attributes to table definition 2025-11-19 00:23:53 +01:00
f6ebda7984 Changed table column because of importing issues 2025-11-19 00:22:35 +01:00
69c95ca672 Added custom statement for cascade deleting 2025-11-19 00:22:13 +01:00
42ce69f4d3 Added schema.json 2025-11-18 23:59:28 +01:00
08fcaa35ee Added methods for deleting all entities 2025-11-18 23:59:18 +01:00
2ee8edcf9b add createdAt column to game, group and player tables and DAOs 2025-11-18 23:47:45 +01:00
fd86f5193f Fixed toJson methods 2025-11-18 23:46:32 +01:00
8cc898cad6 regenerated database.g.dart 2025-11-18 23:38:43 +01:00
67c8a7e181 added createdAt timestamp to Group, Game, and Player DTOs 2025-11-18 23:21:46 +01:00
2da2e28cb6 Added json schema 2025-11-18 23:20:26 +01:00
d86de09042 Added fromJson, toJson 2025-11-18 23:16:57 +01:00
07d623d963 Merge remote-tracking branch 'refs/remotes/origin/development' into feature/31-json-import-fuer-testdaten 2025-11-18 22:47:52 +01:00
a8a81c2151 Fixed gesture detector area 2025-11-18 22:33:46 +01:00
d341634885 Disable icon for members in group tile 2025-11-18 21:56:31 +01:00
d3a63bd299 renamed IconListTile to TextIconListTile and replaced the icon parameter with iconEnabled in both TextIconListTile and TextIconTile 2025-11-18 21:56:20 +01:00
e0c8398873 remove custom colors from Create Group button in GroupsView 2025-11-18 21:43:38 +01:00
d65dd3d983 Refactor CustomWidthButton to use ButtonStyle enum and CustomTheme
- Replaced `borderColor` and `infillColor` parameters with a `buttonStyle` parameter.
- Introduced `ButtonStyle` enum (primary/secondary) to control styling.
- Updated `CustomWidthButton` to derive colors from `CustomTheme` based on the selected `ButtonStyle`.
2025-11-18 21:42:40 +01:00
51a8c4ea58 Replace MaterialStateProperty with WidgetStateProperty in CustomSearchBar 2025-11-18 21:42:17 +01:00
c67f688a77 Refactor CreateGroupView: remove UUID generation, update tiles & fix async gaps 2025-11-18 21:42:03 +01:00
1d9945c525 Merge branch 'refs/heads/development' into feature/5-creategroupview-erstellen 2025-11-18 21:25:58 +01:00
0202d09812 Merge pull request 'UUID-Generierung für Objekte' (#35) from feature/34-uuid-generierung-fuer-objekte into development
Reviewed-on: #35
2025-11-18 19:45:49 +00:00
73d8e7522c Added fifth player 2025-11-18 20:44:55 +01:00
80290efa0b rename FullWidthButton to CustomWidthButton 2025-11-18 20:37:41 +01:00
f4ed122220 Removed print 2025-11-18 20:15:10 +01:00
2f260d7cbc Add uuid dependency 2025-11-18 20:14:18 +01:00
7781284289 changed to use standardized tiles and fixed search bug 2025-11-18 20:10:48 +01:00
c0ff2bf677 Removed unnecessary id declarations in tests 2025-11-18 20:10:29 +01:00
8f9289617f changed group tile to use standardized text icon tile 2025-11-18 20:10:26 +01:00
1882d0007b created widgets for search bar list tile, selected tile and text input field in create groups view 2025-11-18 20:09:57 +01:00
282841ecf1 Implemented uuid for all dtos 2025-11-18 20:07:53 +01:00
05c41707ca Refactor: Remove sample player generation code 2025-11-18 17:34:09 +01:00
a5e508dbda Refresh group list after adding a new group 2025-11-18 17:29:24 +01:00
412d1fd334 fixed search bugs where duplicates where created or search results were wrong 2025-11-18 17:08:07 +01:00
c31d757615 fixed renderoverflow for long player & group names in create group view and group view 2025-11-18 17:00:09 +01:00
178aaa9643 Added function headers 2025-11-17 23:25:12 +01:00
2076e45fd5 Created new layout for settings view 2025-11-17 23:23:52 +01:00
62acc87e0e Moved game tile in tiles folder 2025-11-17 23:23:41 +01:00
3ca081419b Implemented new SettingsListTile 2025-11-17 23:23:04 +01:00
3e89bfd641 added info message for when all players are selected 2025-11-17 21:34:51 +01:00
c3a2ac77b0 fixed color change in appbar when scrolling 2025-11-17 21:19:17 +01:00
6b2fb18ec0 fixed groups not getting added & added feature to remove player from all players when selected 2025-11-17 20:20:56 +01:00
36aa4722a3 wrapped custom_navigation_bar in safearea 2025-11-15 17:11:48 +01:00
Yannick
1536e2b2af move navbar up 2025-11-14 17:31:50 +01:00
Yannick
0f13de30c4 Merge branch 'feature/2-gamehistoryview-anpassen' of https://git.yannick-weigert.de/liquid-development/game-tracker into feature/2-gamehistoryview-anpassen 2025-11-14 17:20:38 +01:00
Yannick
3f0adb4c05 use GameTile to display game history 2025-11-14 17:20:27 +01:00
116 changed files with 5048 additions and 1563 deletions

View File

@@ -0,0 +1,57 @@
name: Pull Request Pipeline
on:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install jq
run: |
apt-get update
apt-get install -y jq
- name: Install Flutter (wget)
run: |
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz
tar xf flutter_linux_3.38.2-stable.tar.xz
# Set Git safe directory for Flutter path
git config --global --add safe.directory "$(pwd)/flutter"
# Set Flutter path
echo "$(pwd)/flutter/bin" >> $GITHUB_PATH
- name: Get dependencies
run: flutter pub get
- name: Analyze Formatting
run: flutter analyze lib test
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
apt-get update
apt-get install -y jq
- name: Install Flutter (wget)
run: |
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz
tar xf flutter_linux_3.38.2-stable.tar.xz
# Set Git safe directory for Flutter path
git config --global --add safe.directory "$(pwd)/flutter"
# Set Flutter path
echo "$(pwd)/flutter/bin" >> $GITHUB_PATH
- name: Get dependencies
run: flutter pub get
- name: Run tests
run: flutter test

View File

@@ -0,0 +1,50 @@
name: Push Pipeline
on:
push:
branches:
- "development"
- "main"
jobs:
format:
runs-on: ubuntu-latest
if: false # Needs bot user
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
apt-get update
apt-get install -y jq
- name: Install Flutter (wget)
run: |
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz
tar xf flutter_linux_3.38.2-stable.tar.xz
# Set Git safe directory for Flutter path
git config --global --add safe.directory "$(pwd)/flutter"
# Set Flutter path
echo "$(pwd)/flutter/bin" >> $GITHUB_PATH
- name: Get & upgrade dependencies
run: |
flutter pub get
flutter pub upgrade --major-versions
- name: Auto-format
run: |
dart format lib
dart fix --apply lib
# Needs credentials, push access and the right files need to be staged
- name: Commit Changes
run: |
git config --global user.name "Gitea Actions"
git config --global user.email "actions@gitea.com"
git status
git add lib/
git status
git commit -m "Actions: Auto-formatting [skip ci]"
git push

View File

@@ -1,7 +1,15 @@
# Game Tracker
![Version](https://img.shields.io/badge/Version-0.3.0-orange)
![Flutter](https://img.shields.io/badge/Flutter-3.32.1-blue?logo=flutter)
![Dart](https://img.shields.io/badge/Dart-3.8.1-blue?logo=dart)
![Flutter](https://img.shields.io/badge/Created_by-Liquid_Development-027DFD?logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iRWJlbmVfMSIgZGF0YS1uYW1lPSJFYmVuZSAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3MjUuNDggODk3LjMiPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuY2xzLTEgewogICAgICAgIGZpbGw6ICNmZmY7CiAgICAgIH0KICAgIDwvc3R5bGU+CiAgPC9kZWZzPgogIDxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTcwNS4yNiw3MDEuOTJsNi40LDExLjA4Yy0xLjk1LTMuODEtNC4wOS03LjUxLTYuNC0xMS4wOFoiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik02MDIuMzksODk3LjI1aC03LjIxYzEuMi4wMywyLjQuMDUsMy42MS4wNXMyLjQxLS4wMiwzLjYxLS4wNVoiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0wLDY5NS4zOGwyLjY4LTQuNjRjLS45MywxLjUyLTEuODIsMy4wNy0yLjY4LDQuNjRaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNjgyLjU1LDcyMy40NWw2LjA1LDEwLjQ5Yy0xLjc5LTMuNjQtMy44MS03LjE1LTYuMDUtMTAuNDlaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzcuNzIsNzMzLjI4bDUuMy05LjE4Yy0xLjk0LDIuOTQtMy43MSw2LjAxLTUuMyw5LjE4WiIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTcxMS42Niw3MTMuMDFsLTYuNC0xMS4wOC0yMjAuNDYtMzgxLjg0aDBWMTAxLjg0YzIwLjY3LTYuOTgsMzUuNTYtMjYuNTIsMzUuNTYtNDkuNTQsMC0yOC44OC0yMy40MS01Mi4zLTUyLjMtNTIuM2gtMjA5LjQ4Yy0yOC44OCwwLTUyLjMsMjMuNDEtNTIuMyw1Mi4zLDAsMjIuNzEsMTQuNDgsNDIuMDMsMzQuNyw0OS4yNXYyMTguNTRsLS4zMy41OEwxOC44OSw3MDQuNzlsLTIuNjgsNC42NGMtOS45OSwxOC4xMi0xNS42OCwzOC45Ni0xNS42OCw2MS4xMiwwLDY5Ljk3LDU2LjY0LDEyNi43LDEyNi41MSwxMjYuN2g0NzUuMzVjNjguMy0xLjkxLDEyMy4wOS01Ny44OCwxMjMuMDktMTI2LjY0LDAtMjAuNzQtNC45OS00MC4zMi0xMy44Mi01Ny42Wk02MDguNTYsODYyLjUzSDExNy40M2MtNDkuMzcsMC04OS4zOS00MC4wMi04OS4zOS04OS4zOSwwLTE0LjM2LDMuMzktMjcuOTMsOS40MS0zOS45Nmw1LjMtOS4xOCwyMzMuMi00MDMuOTJoLS4wOFYxMDQuNTloMTcuODFjOS40NywwLDE3LjE1LTcuNjgsMTcuMTUtMTcuMTVzLTcuNjgtMTcuMTUtMTcuMTUtMTcuMTVoLTM1LjU5di0uMDJjLTkuNzItLjI2LTE3LjUyLTguMi0xNy41Mi0xNy45OHM3LjgtMTcuNzIsMTcuNTItMTcuOTh2LS4wMmgyMDkuMjZjOS45NCwwLDE4LDguMDYsMTgsMThzLTguMDYsMTgtMTgsMThoLTM0LjQ4Yy05LjQ3LDAtMTcuMTUsNy42OC0xNy4xNSwxNy4xNXM3LjY4LDE3LjE1LDE3LjE1LDE3LjE1aDE3LjA0djIxNS40OWguMDdsMjMyLjgyLDQwMy4yNiw2LjA2LDEwLjVjNS44MiwxMS44Niw5LjA5LDI1LjIsOS4wOSwzOS4zLDAsNDkuMzctNDAuMDIsODkuMzktODkuMzksODkuMzlaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzgxLjY4LDU0NS4zOGMtMy4wOCwxLjY4LTYuMTgsMy4zLTkuMzIsNC44NiwzLjA3LTEuNjcsNi4xOC0zLjI5LDkuMzItNC44NloiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik01ODMuNDIsNTUxLjE5bC0yMC42Ny0zNS44Yy0xMy42OS0xLjg0LTI3LjY3LTIuNzktNDEuODYtMi43OS0xNy45OSwwLTM1LjYyLDEuNTMtNTIuNzgsNC40Ni0zMC41Niw1LjIxLTU5LjYsMTQuODktODYuNDIsMjguMzMtMy4wOCwxLjY4LTYuMTgsMy4zLTkuMzIsNC44Ni00MS44OCwyMC45OS04OS4xNiwzMi43OS0xMzkuMTksMzIuNzktMzQuODUsMC02OC4zNS01Ljc0LTk5LjYzLTE2LjMxLDAsMCwwLC4wMiwwLC4wMmwtMTYuNTIsMjguNjFjMzcuMDEsMTUuNTMsNzcuNjUsMjQuMTIsMTIwLjMsMjQuMTIsMTcuOTgsMCwzNS42MS0xLjUzLDUyLjc2LTQuNDYsMzIuNzctNS41OSw2My43OC0xNi4zMSw5Mi4yLTMxLjI5Ljg3LS40NiwxLjczLS45MiwyLjYtMS40LDQzLjI5LTIyLjgyLDkyLjYyLTM1Ljc0LDE0NC45Ni0zNS43NCwxOC4yOCwwLDM2LjE4LDEuNTksNTMuNTksNC42MWwtLjAyLS4wMloiLz4KICA8Zz4KICAgIDxjaXJjbGUgY2xhc3M9ImNscy0xIiBjeD0iNTg3LjY0IiBjeT0iODAzLjQiIHI9IjE4Ljk2Ii8+CiAgICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik01MTUuNTIsNzg0LjQzSDEwMy41NWMtMTAuOTIsMC0xOS43Niw4LjQ5LTE5Ljc2LDE4Ljk2czguODUsMTguOTYsMTkuNzYsMTguOTZoNDExLjk3YzEwLjkyLDAsMTkuNzYtOC40OSwxOS43Ni0xOC45NnMtOC44NS0xOC45Ni0xOS43Ni0xOC45NloiLz4KICA8L2c+CiAgPGNpcmNsZSBjbGFzcz0iY2xzLTEiIGN4PSIyODMuMzIiIGN5PSI0NjcuNTkiIHI9IjE4Ljk2Ii8+CiAgPGNpcmNsZSBjbGFzcz0iY2xzLTEiIGN4PSIzMjYuMjMiIGN5PSIzNjYuMjUiIHI9IjE4Ljk2Ii8+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDA2LjU2LDM4NS4yMmMtMjQuMDYsMC00My41NiwxOS41LTQzLjU2LDQzLjU2czE5LjUsNDMuNTYsNDMuNTYsNDMuNTYsNDMuNTYtMTkuNSw0My41Ni00My41Ni0xOS41LTQzLjU2LTQzLjU2LTQzLjU2Wk00MDYuNTYsNDQ3Ljc0Yy0xMC40NywwLTE4Ljk2LTguNDktMTguOTYtMTguOTZzOC40OS0xOC45NiwxOC45Ni0xOC45NiwxOC45Niw4LjQ5LDE4Ljk2LDE4Ljk2LTguNDksMTguOTYtMTguOTYsMTguOTZaIi8+Cjwvc3ZnPg==)
![Version](https://img.shields.io/badge/App--Version-MVP-orange)
![Flutter](https://img.shields.io/badge/Flutter-3.35.6-027DFD?logo=flutter)
![Dart](https://img.shields.io/badge/Dart-3.9.2-027DFD?logo=dart)
### Versions Supported
![iOS18](https://img.shields.io/badge/iOS-18.7.1-white?logo=apple)
![iOS26](https://img.shields.io/badge/iOS-26.1-white?logo=apple)
![Android16](https://img.shields.io/badge/Android-16-3DDC84?logo=android)
A all-in-one app to track card- and board games, manage players and groups and get statistics about your played games.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<item android:drawable="@color/launch_background" />
<!-- You can insert your own image assets here -->
<!-- <item>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="app_icon_background">#E6F1E4</color>
<color name="launch_background">#0B0B0B</color>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Referenz unbedingt als @color/launch_background (nicht @colors/...) -->
<color name="ic_launcher_background">@color/app_icon_background</color>
</resources>

View File

@@ -4,7 +4,7 @@
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowBackground">@color/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

179
assets/schema.json Normal file
View File

@@ -0,0 +1,179 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"games": {
"type": "array",
"items": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
},
"players": {
"type": [
"array",
"null"
],
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"createdAt",
"name"
]
}
},
"group": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
},
"members": {
"type": "array",
"items": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"createdAt",
"name"
]
}
]
}
},
"required": [
"id",
"createdAt",
"name",
"members"
]
},
"winner": {
"type": ["object","null"]
},
"required": [
"id",
"createdAt",
"name"
]
}
]
},
"groups": {
"type": "array",
"items": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
},
"members": {
"type": "array",
"items": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"createdAt",
"name"
]
}
]
}
},
"required": [
"id",
"createdAt",
"name",
"members"
]
}
]
},
"players": {
"type": "array",
"items": [
{
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"createdAt",
"name"
]
}
]
}
}
}

View File

@@ -1,122 +1,14 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
"filename" : "icon_x1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

View File

@@ -1,5 +0,0 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.043",
"green" : "0.043",
"red" : "0.043"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,23 +1,21 @@
{
"images" : [
{
"filename" : "icon.png",
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24412" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24405"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
@@ -14,24 +17,27 @@
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="LauncherIcon" translatesAutoresizingMaskIntoConstraints="NO" id="ygV-Op-Bu5">
<rect key="frame" x="46" y="334" width="301" height="184"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
<color key="backgroundColor" name="LauncherBackgroundColor"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
<point key="canvasLocation" x="80.152671755725194" y="264.08450704225356"/>
</scene>
</scenes>
<color key="tintColor" red="0.90196078431372551" green="0.94509803921568625" blue="0.89411764705882346" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<resources>
<image name="LaunchImage" width="168" height="185"/>
<image name="LauncherIcon" width="1000" height="1000"/>
<namedColor name="LauncherBackgroundColor">
<color red="0.90196078431372551" green="0.94509803921568625" blue="0.89411764705882346" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24412" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24405"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
@@ -14,13 +16,14 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="141" y="131"/>
</scene>
</scenes>
</document>

View File

@@ -8,6 +8,19 @@ class CustomTheme {
static Color onBoxColor = const Color(0xFF181818);
static Color boxBorder = const Color(0xFF272727);
static BoxDecoration standardBoxDecoration = BoxDecoration(
color: boxColor,
border: Border.all(color: boxBorder),
borderRadius: BorderRadius.circular(12),
);
static BoxDecoration highlightedBoxDecoration = BoxDecoration(
color: boxColor,
border: Border.all(color: primaryColor),
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)],
);
static AppBarTheme appBarTheme = AppBarTheme(
backgroundColor: backgroundColor,
foregroundColor: Colors.white,

31
lib/core/enums.dart Normal file
View File

@@ -0,0 +1,31 @@
/// 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 }
/// Different rulesets available for games
/// - [Ruleset.singleWinner]: The game is won by a single player
/// - [Ruleset.singleLoser]: The game is lost by a single player
/// - [Ruleset.mostPoints]: The player with the most points wins.
/// - [Ruleset.lastPoints]: The player with the fewest points wins.
enum Ruleset { singleWinner, singleLoser, mostPoints, lastPoints }

View File

@@ -15,7 +15,24 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
Future<List<Game>> getAllGames() async {
final query = select(gameTable);
final result = await query.get();
return result.map((row) => Game(id: row.id, name: row.name)).toList();
return Future.wait(
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: winner,
);
}),
);
}
/// Retrieves a [Game] by its [gameId].
@@ -25,11 +42,15 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
List<Player>? 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.hasGameGroup(gameId: gameId)) {
group = await db.groupGameDao.getGroupByGameId(gameId: gameId);
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(
@@ -37,7 +58,8 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
name: result.name,
players: players,
group: group,
winner: result.winnerId,
winner: winner,
createdAt: result.createdAt,
);
}
@@ -45,22 +67,163 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Also adds associated players and group if they exist.
Future<void> addGame({required Game game}) async {
await db.transaction(() async {
for (final p in game.players ?? []) {
await db.playerDao.addPlayer(player: p);
await db.playerGameDao.addPlayerToGame(gameId: game.id, playerId: p.id);
}
if (game.group != null) {
await db.groupDao.addGroup(group: game.group!);
await db.groupGameDao.addGroupToGame(game.id, game.group!.id);
}
await into(gameTable).insert(
GameTableCompanion.insert(
id: game.id,
name: game.name,
winnerId: game.winner,
winnerId: Value(game.winner?.id),
createdAt: game.createdAt,
),
mode: InsertMode.insertOrReplace,
);
if (game.players != null) {
await db.playerDao.addPlayersAsList(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(
gameId: game.id,
groupId: game.group!.id,
);
}
});
}
/// Adds multiple [Game]s to the database in a batch operation.
/// Also adds associated players and groups if they exist.
/// If the [games] list is empty, the method returns immediately.
Future<void> addGamesAsList({required List<Game> 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?.id),
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
// Add all groups of the games in batch
await db.batch(
(b) => b.insertAll(
db.groupTable,
games
.where((game) => game.group != null)
.map(
(game) => GroupTableCompanion.insert(
id: game.group!.id,
name: game.group!.name,
createdAt: game.group!.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
// Add all players of the games in batch (unique)
final uniquePlayers = <String, Player>{};
for (final game in games) {
if (game.players != null) {
for (final p in game.players!) {
uniquePlayers[p.id] = p;
}
}
// Also include members of groups
if (game.group != null) {
for (final m in game.group!.members) {
uniquePlayers[m.id] = m;
}
}
}
if (uniquePlayers.isNotEmpty) {
await db.batch(
(b) => b.insertAll(
db.playerTable,
uniquePlayers.values
.map(
(p) => PlayerTableCompanion.insert(
id: p.id,
name: p.name,
createdAt: p.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
}
// Add all player-game associations in batch
await db.batch((b) {
for (final game in games) {
if (game.players != null) {
for (final p in game.players ?? []) {
b.insert(
db.playerGameTable,
PlayerGameTableCompanion.insert(
gameId: game.id,
playerId: p.id,
),
mode: InsertMode.insertOrReplace,
);
}
}
}
});
// Add all player-group associations in batch
await db.batch((b) {
for (final game in games) {
if (game.group != null) {
for (final m in game.group!.members) {
b.insert(
db.playerGroupTable,
PlayerGroupTableCompanion.insert(
playerId: m.id,
groupId: game.group!.id,
),
mode: InsertMode.insertOrReplace,
);
}
}
}
});
// Add all group-game associations in batch
await db.batch((b) {
for (final game in games) {
if (game.group != null) {
b.insert(
db.groupGameTable,
GroupGameTableCompanion.insert(
gameId: game.id,
groupId: game.group!.id,
),
mode: InsertMode.insertOrReplace,
);
}
}
});
});
}
@@ -88,4 +251,70 @@ class GameDao extends DatabaseAccessor<AppDatabase> 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<bool> deleteAllGames() async {
final query = delete(gameTable);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Sets the winner of the game with the given [gameId] to the player with
/// the given [winnerId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> setWinner({
required String gameId,
required String winnerId,
}) async {
final query = update(gameTable)..where((g) => g.id.equals(gameId));
final rowsAffected = await query.write(
GameTableCompanion(winnerId: Value(winnerId)),
);
return rowsAffected > 0;
}
/// Retrieves the winner of the game with the given [gameId].
/// Returns the [Player] who won the game, or `null` if no winner is set.
Future<Player?> getWinner({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId));
final result = await query.getSingleOrNull();
if (result == null || result.winnerId == null) {
return null;
}
final winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
return winner;
}
/// Removes the winner of the game with the given [gameId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeWinner({required String gameId}) async {
final query = update(gameTable)..where((g) => g.id.equals(gameId));
final rowsAffected = await query.write(
const GameTableCompanion(winnerId: Value(null)),
);
return rowsAffected > 0;
}
/// Checks if the game with the given [gameId] has a winner set.
/// Returns `true` if a winner is set, otherwise `false`.
Future<bool> hasWinner({required String gameId}) async {
final query = select(gameTable)
..where((g) => g.id.equals(gameId) & g.winnerId.isNotNull());
final result = await query.getSingleOrNull();
return result != null;
}
/// Changes the title of the game with the given [gameId] to [newName].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGameName({
required String gameId,
required String newName,
}) async {
final query = update(gameTable)..where((g) => g.id.equals(gameId));
final rowsAffected = await query.write(
GameTableCompanion(name: Value(newName)),
);
return rowsAffected > 0;
}
}

View File

@@ -4,6 +4,5 @@ part of 'game_dao.dart';
// ignore_for_file: type=lint
mixin _$GameDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
}

View File

@@ -16,10 +16,15 @@ class GroupDao extends DatabaseAccessor<AppDatabase> 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(id: groupData.id, name: groupData.name, members: members);
return Group(
id: groupData.id,
name: groupData.name,
members: members,
createdAt: groupData.createdAt,
);
}),
);
}
@@ -29,11 +34,16 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
final query = select(groupTable)..where((g) => g.id.equals(groupId));
final result = await query.getSingle();
List<Player> members = await db.playerGroupDao.getPlayersOfGroupById(
List<Player> members = await db.playerGroupDao.getPlayersOfGroup(
groupId: groupId,
);
return Group(id: result.id, name: result.name, members: members);
return Group(
id: result.id,
name: result.name,
members: members,
createdAt: result.createdAt,
);
}
/// Adds a new group with the given [id] and [name] to the database.
@@ -41,9 +51,17 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
Future<bool> addGroup({required Group group}) async {
if (!await groupExists(groupId: group.id)) {
await db.transaction(() async {
await into(
groupTable,
).insert(GroupTableCompanion.insert(id: group.id, name: group.name));
await into(groupTable).insert(
GroupTableCompanion.insert(
id: group.id,
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(
db.playerGroupTable,
@@ -55,17 +73,94 @@ class GroupDao extends DatabaseAccessor<AppDatabase> 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<void> addGroupsAsList({required List<Group> groups}) async {
if (groups.isEmpty) return;
await db.transaction(() async {
// Deduplicate groups by id - keep first occurrence
final Map<String, Group> uniqueGroups = {};
for (final g in groups) {
uniqueGroups.putIfAbsent(g.id, () => g);
}
// Insert unique groups in batch
await db.batch(
(b) => b.insertAll(
groupTable,
uniqueGroups.values
.map(
(group) => GroupTableCompanion.insert(
id: group.id,
name: group.name,
createdAt: group.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
// Collect unique players from all groups
final uniquePlayers = <String, Player>{};
for (final g in uniqueGroups.values) {
for (final m in g.members) {
uniquePlayers[m.id] = m;
}
}
if (uniquePlayers.isNotEmpty) {
await db.batch(
(b) => b.insertAll(
db.playerTable,
uniquePlayers.values
.map(
(p) => PlayerTableCompanion.insert(
id: p.id,
name: p.name,
createdAt: p.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
}
// Prepare all player-group associations in one list (unique pairs)
final Set<String> seenPairs = {};
final List<PlayerGroupTableCompanion> pgRows = [];
for (final g in uniqueGroups.values) {
for (final m in g.members) {
final key = '${m.id}|${g.id}';
if (!seenPairs.contains(key)) {
seenPairs.add(key);
pgRows.add(
PlayerGroupTableCompanion.insert(playerId: m.id, groupId: g.id),
);
}
}
}
if (pgRows.isNotEmpty) {
await db.batch((b) {
for (final pg in pgRows) {
b.insert(db.playerGroupTable, pg, mode: InsertMode.insertOrReplace);
}
});
}
});
}
/// Deletes the group with the given [id] from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteGroup({required String groupId}) async {
@@ -103,4 +198,12 @@ class GroupDao extends DatabaseAccessor<AppDatabase> 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<bool> deleteAllGroups() async {
final query = delete(groupTable);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
}

View File

@@ -10,9 +10,39 @@ class GroupGameDao extends DatabaseAccessor<AppDatabase>
with _$GroupGameDaoMixin {
GroupGameDao(super.db);
/// Associates a group with a game by inserting a record into the
/// [GroupGameTable].
Future<void> addGroupToGame({
required String gameId,
required String groupId,
}) async {
if (await gameHasGroup(gameId: gameId)) {
throw Exception('Game already has a group');
}
await into(groupGameTable).insert(
GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId),
mode: InsertMode.insertOrReplace,
);
}
/// Retrieves the [Group] associated with the given [gameId].
/// Returns `null` if no group is found.
Future<Group?> getGroupOfGame({required String gameId}) async {
final result = await (select(
groupGameTable,
)..where((g) => g.gameId.equals(gameId))).getSingleOrNull();
if (result == null) {
return null;
}
final group = await db.groupDao.getGroupById(groupId: result.groupId);
return group;
}
/// Checks if there is a group associated with the given [gameId].
/// Returns `true` if there is a group, otherwise `false`.
Future<bool> hasGameGroup({required String gameId}) async {
Future<bool> gameHasGroup({required String gameId}) async {
final count =
await (selectOnly(groupGameTable)
..where(groupGameTable.gameId.equals(gameId))
@@ -22,21 +52,47 @@ class GroupGameDao extends DatabaseAccessor<AppDatabase>
return (count ?? 0) > 0;
}
/// Retrieves the [Group] associated with the given [gameId].
Future<Group> getGroupByGameId({required String gameId}) async {
final result = await (select(
groupGameTable,
)..where((g) => g.gameId.equals(gameId))).getSingle();
final group = await db.groupDao.getGroupById(groupId: result.groupId);
return group;
/// Checks if a specific group is associated with a specific game.
/// Returns `true` if the group is in the game, otherwise `false`.
Future<bool> 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;
}
/// Associates a group with a game by inserting a record into the
/// [GroupGameTable].
Future<void> addGroupToGame(String gameId, String groupId) async {
await into(
groupGameTable,
).insert(GroupGameTableCompanion.insert(groupId: groupId, gameId: 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<bool> 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;
}
/// Updates the group associated with a game to [newGroupId] based on
/// [gameId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGroupOfGame({
required String gameId,
required String newGroupId,
}) async {
final updatedRows =
await (update(groupGameTable)..where((g) => g.gameId.equals(gameId)))
.write(GroupGameTableCompanion(groupId: Value(newGroupId)));
return updatedRows > 0;
}
}

View File

@@ -5,7 +5,6 @@ part of 'group_game_dao.dart';
// ignore_for_file: type=lint
mixin _$GroupGameDaoMixin on DatabaseAccessor<AppDatabase> {
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupGameTableTable get groupGameTable => attachedDatabase.groupGameTable;
}

View File

@@ -13,14 +13,22 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
Future<List<Player>> getAllPlayers() async {
final query = select(playerTable);
final result = await query.get();
return result.map((row) => Player(id: row.id, name: row.name)).toList();
return result
.map(
(row) => Player(id: row.id, name: row.name, createdAt: row.createdAt),
)
.toList();
}
/// Retrieves a [Player] by their [id].
Future<Player> getPlayerById({required String playerId}) async {
final query = select(playerTable)..where((p) => p.id.equals(playerId));
final result = await query.getSingle();
return Player(id: result.id, name: result.name);
return Player(
id: result.id,
name: result.name,
createdAt: result.createdAt,
);
}
/// Adds a new [player] to the database.
@@ -28,14 +36,42 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// the new one.
Future<bool> addPlayer({required Player player}) async {
if (!await playerExists(playerId: player.id)) {
await into(
playerTable,
).insert(PlayerTableCompanion.insert(id: player.id, name: player.name));
await into(playerTable).insert(
PlayerTableCompanion.insert(
id: player.id,
name: player.name,
createdAt: player.createdAt,
),
mode: InsertMode.insertOrReplace,
);
return true;
}
return false;
}
/// Adds multiple [players] to the database in a batch operation.
Future<bool> addPlayersAsList({required List<Player> 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<bool> deletePlayer({required String playerId}) async {
@@ -70,4 +106,12 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
.getSingle();
return count ?? 0;
}
/// Deletes all players from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteAllPlayers() async {
final query = delete(playerTable);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
}

View File

@@ -10,6 +10,34 @@ class PlayerGameDao extends DatabaseAccessor<AppDatabase>
with _$PlayerGameDaoMixin {
PlayerGameDao(super.db);
/// Associates a player with a game by inserting a record into the
/// [PlayerGameTable].
Future<void> 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].
/// Returns null if no players are found.
Future<List<Player>?> getPlayersOfGame({required String gameId}) async {
final result = await (select(
playerGameTable,
)..where((p) => p.gameId.equals(gameId))).get();
if (result.isEmpty) return null;
final futures = result.map(
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
);
final players = await Future.wait(futures);
return players;
}
/// Checks if there are any players associated with the given [gameId].
/// Returns `true` if there are players, otherwise `false`.
Future<bool> gameHasPlayers({required String gameId}) async {
@@ -22,30 +50,79 @@ class PlayerGameDao extends DatabaseAccessor<AppDatabase>
return (count ?? 0) > 0;
}
/// Retrieves a list of [Player]s associated with the given [gameId].
/// Returns an empty list if no players are found.
Future<List<Player>> getPlayersByGameId({required String gameId}) async {
final result = await (select(
playerGameTable,
)..where((p) => p.gameId.equals(gameId))).get();
if (result.isEmpty) return <Player>[];
final futures = result.map(
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
);
final players = await Future.wait(futures);
return players.whereType<Player>().toList();
}
/// Associates a player with a game by inserting a record into the
/// [PlayerGameTable].
Future<void> addPlayerToGame({
/// Checks if a specific player is associated with a specific game.
/// Returns `true` if the player is in the game, otherwise `false`.
Future<bool> isPlayerInGame({
required String gameId,
required String playerId,
}) async {
await into(playerGameTable).insert(
PlayerGameTableCompanion.insert(playerId: playerId, gameId: gameId),
);
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<bool> 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;
}
/// Updates the players associated with a game based on the provided
/// [newPlayer] list. It adds new players and removes players that are no
/// longer associated with the game.
Future<void> updatePlayersFromGame({
required String gameId,
required List<Player> newPlayer,
}) async {
final currentPlayers = await getPlayersOfGame(gameId: gameId);
// Create sets of player IDs for easy comparison
final currentPlayerIds = currentPlayers?.map((p) => p.id).toSet() ?? {};
final newPlayerIdsSet = newPlayer.map((p) => p.id).toSet();
// Determine players to add and remove
final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds);
final playersToRemove = currentPlayerIds.difference(newPlayerIdsSet);
db.transaction(() async {
// Remove old players
if (playersToRemove.isNotEmpty) {
await (delete(playerGameTable)..where(
(pg) =>
pg.gameId.equals(gameId) &
pg.playerId.isIn(playersToRemove.toList()),
))
.go();
}
// Add new players
if (playersToAdd.isNotEmpty) {
final inserts = playersToAdd
.map(
(id) =>
PlayerGameTableCompanion.insert(playerId: id, gameId: gameId),
)
.toList();
await Future.wait(
inserts.map(
(c) => into(
playerGameTable,
).insert(c, mode: InsertMode.insertOrReplace),
),
);
}
});
}
}

View File

@@ -10,8 +10,34 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
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.
/// Returns `true` if the player was added, otherwise `false`.
Future<bool> 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)) {
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<List<Player>> getPlayersOfGroupById({required String groupId}) async {
Future<List<Player>> getPlayersOfGroup({required String groupId}) async {
final query = select(playerGroupTable)
..where((pG) => pG.groupId.equals(groupId));
final result = await query.get();
@@ -38,29 +64,6 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
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<bool> 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<bool> isPlayerInGroup({

View File

@@ -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',

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,10 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/player_table.dart';
class GameTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get winnerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
late final winnerId = text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override
Set<Column<Object>> get primaryKey => {id};

View File

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

View File

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

View File

@@ -1,23 +1,51 @@
import 'package:clock/clock.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:uuid/uuid.dart';
class Game {
final String id;
final DateTime createdAt;
final String name;
final List<Player>? players;
final Group? group;
final String winner;
final Player? winner;
Game({
String? id,
DateTime? createdAt,
required this.name,
this.players,
this.group,
this.winner = '',
required this.id,
required this.name,
});
this.winner,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@override
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<String, dynamic> json)
: id = json['id'],
name = json['name'],
createdAt = DateTime.parse(json['createdAt']),
players = json['players'] != null
? (json['players'] as List)
.map((playerJson) => Player.fromJson(playerJson))
.toList()
: null,
group = json['group'] != null ? Group.fromJson(json['group']) : null,
winner = json['winner'] != null ? Player.fromJson(json['winner']) : null;
/// Converts the Game instance to a JSON object.
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'name': name,
'players': players?.map((player) => player.toJson()).toList(),
'group': group?.toJson(),
'winner': winner?.toJson(),
};
}

View File

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

View File

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

View File

@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
class ChooseGroupView extends StatefulWidget {
final List<Group> groups;
final int initialGroupIndex;
const ChooseGroupView({
super.key,
required this.groups,
required this.initialGroupIndex,
});
@override
State<ChooseGroupView> createState() => _ChooseGroupViewState();
}
class _ChooseGroupViewState extends State<ChooseGroupView> {
late int selectedGroupIndex;
@override
void initState() {
selectedGroupIndex = widget.initialGroupIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
title: const Text(
'Choose Group',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: widget.groups.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
setState(() {
selectedGroupIndex = index;
});
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
Navigator.of(context).pop(widget.groups[index]);
});
},
child: GroupTile(
group: widget.groups[index],
isHighlighted: selectedGroupIndex == index,
),
);
},
),
);
}
}

View File

@@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/presentation/widgets/tiles/ruleset_list_tile.dart';
class ChooseRulesetView extends StatefulWidget {
final List<(Ruleset, String, String)> rulesets;
final int initialRulesetIndex;
const ChooseRulesetView({
super.key,
required this.rulesets,
required this.initialRulesetIndex,
});
@override
State<ChooseRulesetView> createState() => _ChooseRulesetViewState();
}
class _ChooseRulesetViewState extends State<ChooseRulesetView> {
late int selectedRulesetIndex;
@override
void initState() {
selectedRulesetIndex = widget.initialRulesetIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
initialIndex: 0,
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
title: const Text(
'Choose Ruleset',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: Column(
children: [
Container(
color: CustomTheme.backgroundColor,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: TabBar(
padding: const EdgeInsets.symmetric(horizontal: 5),
// Label Settings
labelStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
labelColor: Colors.white,
unselectedLabelStyle: const TextStyle(fontSize: 14),
unselectedLabelColor: Colors.white70,
// Indicator Settings
indicator: CustomTheme.standardBoxDecoration,
indicatorSize: TabBarIndicatorSize.tab,
indicatorWeight: 1,
indicatorPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 0,
),
// Divider Settings
dividerHeight: 0,
tabs: const [
Tab(text: 'Rulesets'),
Tab(text: 'Gametypes'),
],
),
),
const Divider(
indent: 30,
endIndent: 30,
thickness: 3,
radius: BorderRadius.all(Radius.circular(12)),
),
Expanded(
child: TabBarView(
children: [
ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: widget.rulesets.length,
itemBuilder: (BuildContext context, int index) {
return RulesetListTile(
onPressed: () async {
setState(() {
selectedRulesetIndex = index;
});
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
Navigator.of(
context,
).pop(widget.rulesets[index].$1);
});
},
title: widget.rulesets[index].$2,
description: widget.rulesets[index].$3,
isHighlighted: selectedRulesetIndex == index,
);
},
),
const Center(
child: Text(
'No gametypes available',
style: TextStyle(color: Colors.white70),
),
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,232 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.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/create_game/choose_group_view.dart';
import 'package:game_tracker/presentation/views/main_menu/create_game/choose_ruleset_view.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/player_selection.dart';
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart';
import 'package:provider/provider.dart';
class CreateGameView extends StatefulWidget {
const CreateGameView({super.key});
@override
State<CreateGameView> createState() => _CreateGameViewState();
}
class _CreateGameViewState extends State<CreateGameView> {
/// Reference to the app database
late final AppDatabase db;
/// Futures to load all groups and players from the database
late Future<List<Group>> _allGroupsFuture;
/// Future to load all players from the database
late Future<List<Player>> _allPlayersFuture;
/// Controller for the game name input field
final TextEditingController _gameNameController = TextEditingController();
/// List of all groups from the database
List<Group> groupsList = [];
/// List of all players from the database
List<Player> playerList = [];
/// The currently selected group
Group? selectedGroup;
/// The index of the currently selected group in [groupsList] to mark it in
/// the [ChooseGroupView]
int selectedGroupIndex = -1;
/// The currently selected ruleset
Ruleset? selectedRuleset;
/// The index of the currently selected ruleset in [rulesets] to mark it in
/// the [ChooseRulesetView]
int selectedRulesetIndex = -1;
/// The currently selected players
List<Player>? selectedPlayers;
/// List of available rulesets with their display names and descriptions
/// as tuples of (Ruleset, String, String)
List<(Ruleset, String, String)> rulesets = [
(
Ruleset.singleWinner,
'Single Winner',
'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.',
),
(
Ruleset.singleLoser,
'Single Loser',
'Exactly one loser is determined; last place receives the penalty or consequence.',
),
(
Ruleset.mostPoints,
'Most Points',
'Traditional ruleset: the player with the most points wins.',
),
(
Ruleset.lastPoints,
'Least Points',
'Inverse scoring: the player with the fewest points wins.',
),
];
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_allGroupsFuture = db.groupDao.getAllGroups();
_allPlayersFuture = db.playerDao.getAllPlayers();
Future.wait([_allGroupsFuture, _allPlayersFuture]).then((result) async {
groupsList = result[0] as List<Group>;
playerList = result[1] as List<Player>;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
title: const Text(
'Create new game',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: TextInputField(
controller: _gameNameController,
hintText: 'Game name',
onChanged: (value) {
setState(() {});
},
),
),
ChooseTile(
title: 'Ruleset',
trailingText: selectedRuleset == null
? 'None'
: translateRulesetToString(selectedRuleset!),
onPressed: () async {
selectedRuleset = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChooseRulesetView(
rulesets: rulesets,
initialRulesetIndex: selectedRulesetIndex,
),
),
);
selectedRulesetIndex = rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
setState(() {});
},
),
ChooseTile(
title: 'Group',
trailingText: selectedGroup == null
? 'None'
: selectedGroup!.name,
onPressed: () async {
selectedGroup = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChooseGroupView(
groups: groupsList,
initialGroupIndex: selectedGroupIndex,
),
),
);
selectedGroupIndex = groupsList.indexWhere(
(g) => g.id == selectedGroup?.id,
);
setState(() {});
},
),
Expanded(
child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialPlayers: selectedGroup == null
? playerList
: playerList
.where(
(p) => !selectedGroup!.members.any(
(m) => m.id == p.id,
),
)
.toList(),
onChanged: (value) {
setState(() {
selectedPlayers = value;
});
},
),
),
CustomWidthButton(
text: 'Create game',
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary,
onPressed: _enableCreateGameButton()
? () async {
Game game = Game(
name: _gameNameController.text.trim(),
createdAt: DateTime.now(),
group: selectedGroup!,
players: selectedPlayers,
);
// TODO: Replace with navigation to GameResultView()
print('Created game: $game');
Navigator.pop(context);
}
: null,
),
const SizedBox(height: 20),
],
),
),
);
}
/// Translates a [Ruleset] enum value to its corresponding string representation.
String translateRulesetToString(Ruleset ruleset) {
switch (ruleset) {
case Ruleset.singleWinner:
return 'Single Winner';
case Ruleset.singleLoser:
return 'Single Loser';
case Ruleset.mostPoints:
return 'Most Points';
case Ruleset.lastPoints:
return 'Least Points';
}
}
/// Determines whether the "Create Game" button should be enabled based on
/// the current state of the input fields.
bool _enableCreateGameButton() {
return _gameNameController.text.isNotEmpty &&
(selectedGroup != null ||
(selectedPlayers != null && selectedPlayers!.isNotEmpty)) &&
selectedRuleset != null;
}
}

View File

@@ -1,395 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.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';
import 'package:game_tracker/presentation/widgets/full_width_button.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart';
import 'package:skeletonizer/skeletonizer.dart';
class CreateGroupView extends StatefulWidget {
const CreateGroupView({super.key});
@override
State<CreateGroupView> createState() => _CreateGroupViewState();
}
class _CreateGroupViewState extends State<CreateGroupView> {
List<Player> selectedPlayers = [];
List<Player> suggestedPlayers = [];
List<Player> allPlayers = [];
late final AppDatabase db;
late Future<List<Player>> _allPlayersFuture;
late final List<Player> skeletonData = List.filled(
7,
Player(id: '0', name: 'Player 0'),
);
final _groupNameController = TextEditingController();
final _searchBarController = TextEditingController();
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_allPlayersFuture = db.playerDao.getAllPlayers();
_allPlayersFuture.then((loadedPlayers) {
setState(() {
allPlayers = loadedPlayers;
suggestedPlayers = loadedPlayers;
});
});
}
@override
Widget build(BuildContext context) {
addSamplePlayers(context);
return SafeArea(
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
title: const Text(
"Create new group",
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: TextField(
controller: _groupNameController,
onChanged: (value) {
setState(() {});
},
decoration: InputDecoration(
filled: true,
fillColor: CustomTheme.boxColor,
hint: Text("Group name", style: TextStyle(fontSize: 18)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorder),
),
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
),
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 10,
),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SearchBar(
controller: _searchBarController,
constraints: BoxConstraints(maxHeight: 45, minHeight: 45),
hintText: "Search for players",
hintStyle: WidgetStateProperty.all(
TextStyle(fontSize: 16),
),
leading: Icon(Icons.search),
backgroundColor: WidgetStateProperty.all(
CustomTheme.boxColor,
),
side: WidgetStateProperty.all(
BorderSide(color: CustomTheme.boxBorder),
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onChanged: (value) {
setState(() {
if (value.isEmpty) {
suggestedPlayers = allPlayers;
} else {
suggestedPlayers = allPlayers.where((player) {
return player.name.toLowerCase().contains(
value.toLowerCase(),
);
}).toList();
}
});
},
),
SizedBox(height: 10),
Text(
"Ausgewählte Spieler: (${selectedPlayers.length})",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 10),
Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 8.0,
runSpacing: 8.0,
children: <Widget>[
for (var selectedPlayer in selectedPlayers)
Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
color: CustomTheme.onBoxColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(width: 12),
Text(
selectedPlayer.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 3),
GestureDetector(
child: const Icon(Icons.close, size: 20),
onTap: () {
setState(() {
selectedPlayers.remove(selectedPlayer);
});
},
),
],
),
),
],
),
SizedBox(height: 10),
Text(
"Alle Spieler:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 10),
FutureBuilder(
future: _allPlayersFuture,
builder:
(
BuildContext context,
AsyncSnapshot<List<Player>> snapshot,
) {
if (snapshot.hasError) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'Player data couldn\'t\nbe loaded.',
),
);
}
if (snapshot.connectionState ==
ConnectionState.done &&
(!snapshot.hasData ||
snapshot.data!.isEmpty ||
(suggestedPlayers.isEmpty &&
allPlayers.isEmpty))) {
return const Center(
child: TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'No players created yet.',
),
);
}
final bool isLoading =
snapshot.connectionState ==
ConnectionState.waiting;
return Expanded(
child: 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:
(suggestedPlayers.isEmpty &&
!allPlayers.isEmpty)
? TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message:
'No players found with that name.',
)
: ListView.builder(
itemCount: suggestedPlayers.length,
itemBuilder: (BuildContext context, int index) {
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 5,
vertical: 5,
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(
color: CustomTheme.boxBorder,
),
borderRadius:
BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Text(
suggestedPlayers[index].name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
IconButton(
icon: Icon(
Icons.add,
size: 20,
),
onPressed: () {
setState(() {
if (!selectedPlayers.contains(
suggestedPlayers[index],
)) {
selectedPlayers.add(
suggestedPlayers[index],
);
}
});
},
),
],
),
);
},
),
),
);
},
),
],
),
),
),
FullWidthButton(
text: "Create group",
infillColor: CustomTheme.primaryColor,
borderColor: CustomTheme.primaryColor,
disabledInfillColor: CustomTheme.boxColor,
sizeRelativeToWidth: 0.95,
onPressed:
(_groupNameController.text.isEmpty || selectedPlayers.isEmpty)
? null
: () {
String id = "ID_" + _groupNameController.text;
String name = _groupNameController.text;
List<Player> members = selectedPlayers;
db.groupDao.addGroup(
group: Group(id: id, name: name, members: members),
);
print(name);
print(id);
for (int i = 0; i < members.length; i++) {
print(members[i].name);
print(members[i].id);
}
if (true) {
//eigentlich wenn create group erfolgreich
_groupNameController.clear();
_searchBarController.clear();
selectedPlayers.clear();
}
setState(() {});
},
),
SizedBox(height: 10),
FullWidthButton(
text: "Cancel",
infillColor: CustomTheme.boxColor,
borderColor: CustomTheme.primaryColor,
disabledInfillColor: CustomTheme.boxColor,
sizeRelativeToWidth: 0.95,
onPressed: () {
Navigator.pop(context);
},
),
SizedBox(height: 20),
],
),
),
);
}
Future<void> addSamplePlayers(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false);
/*await db.groupDao.addGroup(
group: Group(
id: "dg1",
name: "Debug Gruppe 1",
members: [
Player(id: '1', name: 'Spieler 1'),
Player(id: '2', name: 'Spieler 2'),
Player(id: '3', name: 'Spieler 3'),
],
),
);
final group = await db.groupDao.getGroupById(groupId: "dg1");
print(group.name);
print(group.id);
print(group.members.length);
*/
final playerCount = await db.playerDao.getPlayerCount();
if (playerCount == 0) {
for (int i = 1; i <= 10; i++) {
final player = Player(id: '$i', name: 'Spieler $i');
await db.playerDao.addPlayer(player: player);
}
print("10 Beispiel-Spieler wurden zur Datenbank hinzugefügt.");
final players = await db.playerDao.getAllPlayers();
for (int i = 0; i < players.length; i++) {
print(players[i]);
}
}
}
}

View File

@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.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';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/player_selection.dart';
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
import 'package:provider/provider.dart';
class CreateGroupView extends StatefulWidget {
const CreateGroupView({super.key});
@override
State<CreateGroupView> createState() => _CreateGroupViewState();
}
class _CreateGroupViewState extends State<CreateGroupView> {
final _groupNameController = TextEditingController();
late final AppDatabase db;
List<Player> selectedPlayers = [];
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_groupNameController.addListener(() {
setState(() {});
});
}
@override
void dispose() {
_groupNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
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,
),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: TextInputField(
controller: _groupNameController,
hintText: 'Group name',
onChanged: (value) {
setState(() {});
},
),
),
Expanded(
child: PlayerSelection(
onChanged: (value) {
selectedPlayers = [...value];
},
),
),
CustomWidthButton(
text: 'Create group',
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary,
onPressed:
(_groupNameController.text.isEmpty || selectedPlayers.isEmpty)
? null
: () async {
bool success = await db.groupDao.addGroup(
group: Group(
name: _groupNameController.text.trim(),
members: selectedPlayers,
),
);
if (!context.mounted) return;
if (success) {
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: CustomTheme.boxColor,
content: const Center(
child: Text(
'Error while creating group, please try again',
style: TextStyle(color: Colors.white),
),
),
),
);
}
setState(() {});
},
),
const SizedBox(height: 20),
],
),
),
);
}
}

View File

@@ -17,12 +17,7 @@ class CustomNavigationBar extends StatefulWidget {
class _CustomNavigationBarState extends State<CustomNavigationBar>
with SingleTickerProviderStateMixin {
int currentIndex = 0;
final List<Widget> tabs = [
const HomeView(),
const GameHistoryView(),
const GroupsView(),
const StatisticsView(),
];
int tabKeyCount = 0;
@override
void initState() {
@@ -31,6 +26,22 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
@override
Widget build(BuildContext context) {
// Pretty ugly but works
final List<Widget> 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<CustomNavigationBar>
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),
),
],
@@ -54,12 +70,14 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
backgroundColor: CustomTheme.backgroundColor,
body: tabs[currentIndex],
extendBody: true,
bottomNavigationBar: Padding(
padding: const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 8.0),
child: Material(
elevation: 10,
borderRadius: BorderRadius.circular(24),
color: CustomTheme.primaryColor,
bottomNavigationBar: SafeArea(
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(

View File

@@ -1,7 +1,17 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/presentation/widgets/double_row_info_tile.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/create_group_view.dart';
import 'package:game_tracker/presentation/views/main_menu/game_result_view.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/game_history_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart';
class GameHistoryView extends StatefulWidget {
const GameHistoryView({super.key});
@@ -11,119 +21,36 @@ class GameHistoryView extends StatefulWidget {
}
class _GameHistoryViewState extends State<GameHistoryView> {
final allGameData = [
{
'game': 'Schach',
'title': 'Abendpartie',
'players': 2,
'group': 'Familie',
'date': '01.06.2024',
},
{
'game': 'Monopoly',
'title': 'Wochenendspaß mit Gras du Saas',
'players': 4,
'group': 'Freunde',
'date': '28.05.2024',
},
{
'game': 'Catan',
'title': 'Strategieabend',
'players': 3,
'group': 'Brettspieler',
'date': '25.05.2024',
},
{
'game': 'Uno',
'title': 'Schnelle Runde',
'players': 5,
'group': 'Kollegen',
'date': '22.05.2024',
},
{
'game': 'Poker',
'title': 'Freitagspoker',
'players': 6,
'group': 'Pokerclub',
'date': '20.05.2024',
},
{
'game': 'Scrabble',
'title': 'Wortschlacht',
'players': 4,
'group': 'Familie',
'date': '18.05.2024',
},
{
'game': 'Risiko',
'title': 'Weltherrschaft',
'players': 5,
'group': 'Strategiegruppe',
'date': '15.05.2024',
},
{
'game': 'Zug um Zug',
'title': 'Zug-Abenteuer',
'players': 4,
'group': 'Reisende',
'date': '12.05.2024',
},
{
'game': 'Carcassonne',
'title': 'Plättchenlegen',
'players': 3,
'group': 'Brettspieler',
'date': '10.05.2024',
},
{
'game': 'Pandemie',
'title': 'Welt retten',
'players': 4,
'group': 'Koop-Team',
'date': '08.05.2024',
},
{
'game': 'Cluedo',
'title': 'Krimiabend',
'players': 6,
'group': 'Detektive',
'date': '05.05.2024',
},
{
'game': 'Dixit',
'title': 'Fantasiespiel',
'players': 5,
'group': 'Künstler',
'date': '02.05.2024',
},
{
'game': 'Azul',
'title': 'Plättchenmeister',
'players': 4,
'group': 'Familie',
'date': '30.04.2024',
},
{
'game': 'Splendor',
'title': 'Edelsteinhändler',
'players': 3,
'group': 'Freunde',
'date': '28.04.2024',
},
{
'game': '7 Wonders',
'title': 'Antike Reiche',
'players': 7,
'group': 'Geschichtsfreunde',
'date': '25.04.2024',
},
];
late List<Map<String, dynamic>> suggestedGameData;
late Future<List<Game>> _gameListFuture;
late final AppDatabase db;
late final List<Game> skeletonData = List.filled(
4,
Game(
name: 'Skeleton Game',
group: Group(
name: 'Skeleton Group',
members: [
Player(name: 'Player 1'),
Player(name: 'Player 2'),
Player(name: 'Player 3'),
Player(name: 'Long Name Player 4'),
Player(name: 'Player 5'),
],
),
winner: Player(name: 'Skeleton Player 1'),
players: [Player(name: 'Skeleton Player 6')],
),
);
@override
void initState() {
super.initState();
suggestedGameData = List.from(allGameData);
db = Provider.of<AppDatabase>(context, listen: false);
_gameListFuture = Future.delayed(
const Duration(milliseconds: 250),
() => db.gameDao.getAllGames(),
);
}
@override
@@ -131,41 +58,87 @@ class _GameHistoryViewState extends State<GameHistoryView> {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
body: Stack(
alignment: Alignment.center,
children: [
Column(
children: [
Container(margin: EdgeInsets.only(bottom: 75)),
Expanded(
child: gameHistoryListView(allGameData, suggestedGameData),
),
],
FutureBuilder<List<Game>>(
future: _gameListFuture,
builder:
(BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
if (snapshot.hasError) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'Game data could not be loaded',
),
);
}
if (snapshot.connectionState == ConnectionState.done &&
(!snapshot.hasData || snapshot.data!.isEmpty)) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Info',
message: 'No games created yet',
),
);
}
final bool isLoading =
snapshot.connectionState == ConnectionState.waiting;
final List<Game> games =
(isLoading ? skeletonData : (snapshot.data ?? [])
..sort(
(a, b) => b.createdAt.compareTo(a.createdAt),
))
.toList();
return AppSkeleton(
enabled: isLoading,
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: games.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == games.length) {
return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 80,
);
}
return GameHistoryTile(
onTap: () async {
await Navigator.push(
context,
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) =>
GameResultView(game: games[index]),
),
);
setState(() {
_gameListFuture = db.gameDao.getAllGames();
});
},
game: games[index],
);
},
),
);
},
),
Container(
margin: EdgeInsets.only(top: 10, bottom: 10, left: 10, right: 10),
child: SearchBar(
leading: Icon(Icons.search),
onChanged: (value) {
if (value.isEmpty) {
setState(() {
suggestedGameData.clear();
suggestedGameData.addAll(allGameData);
});
return;
}
final suggestions = allGameData.where((currentGame) {
return currentGame['game'].toString().toLowerCase().contains(
value.toLowerCase(),
) ||
currentGame['title'].toString().toLowerCase().contains(
value.toLowerCase(),
) ||
currentGame['group'].toString().toLowerCase().contains(
value.toLowerCase(),
);
});
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: CustomWidthButton(
text: 'Create Game',
sizeRelativeToWidth: 0.90,
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const CreateGroupView();
},
),
);
setState(() {
suggestedGameData.clear();
suggestedGameData.addAll(suggestions);
_gameListFuture = db.gameDao.getAllGames();
});
},
),
@@ -175,32 +148,3 @@ class _GameHistoryViewState extends State<GameHistoryView> {
);
}
}
Widget gameHistoryListView(allGameData, suggestedGameData) {
if (suggestedGameData.isEmpty && allGameData.isEmpty) {
return TopCenteredMessage(
icon: Icons.info,
title: "Info",
message: "Keine Spiele erstellt",
);
} else if (suggestedGameData.isEmpty) {
return TopCenteredMessage(
icon: Icons.search,
title: "Info",
message: "Kein Spiel mit den Suchparametern gefunden.",
);
}
return ListView.builder(
itemCount: suggestedGameData.length,
itemBuilder: (context, index) {
final currentGame = suggestedGameData[index];
return doubleRowInfoTile(
currentGame['game'] + ": ",
currentGame['title'],
currentGame['players'].toString() + " Spieler",
currentGame['group'],
currentGame['date'],
);
},
);
}

View File

@@ -0,0 +1,144 @@
import 'package:flutter/material.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/player.dart';
import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart';
import 'package:provider/provider.dart';
class GameResultView extends StatefulWidget {
final Game game;
const GameResultView({super.key, required this.game});
@override
State<GameResultView> createState() => _GameResultViewState();
}
class _GameResultViewState extends State<GameResultView> {
late final List<Player> allPlayers;
late final AppDatabase db;
Player? _selectedPlayer;
@override
void initState() {
db = Provider.of<AppDatabase>(context, listen: false);
allPlayers = getAllPlayers(widget.game);
if (widget.game.winner != null) {
_selectedPlayer = allPlayers.firstWhere(
(p) => p.id == widget.game.winner!.id,
);
}
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
title: Text(
widget.game.name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
),
),
centerTitle: true,
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 10,
),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Select Winner:',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Expanded(
child: RadioGroup<Player>(
groupValue: _selectedPlayer,
onChanged: (Player? value) async {
setState(() {
_selectedPlayer = value;
});
await _handleWinnerSaving();
},
child: ListView.builder(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return CustomRadioListTile(
text: allPlayers[index].name,
value: allPlayers[index],
onContainerTap: (value) async {
setState(() {
// Check if the already selected player is the same as the newly tapped player.
if (_selectedPlayer == value) {
// If yes deselected the player by setting it to null.
_selectedPlayer = null;
} else {
// If no assign the newly tapped player to the selected player.
(_selectedPlayer = value);
}
});
await _handleWinnerSaving();
},
);
},
),
),
),
],
),
),
),
],
),
),
);
}
Future<void> _handleWinnerSaving() async {
if (_selectedPlayer == null) {
await db.gameDao.removeWinner(gameId: widget.game.id);
} else {
await db.gameDao.setWinner(
gameId: widget.game.id,
winnerId: _selectedPlayer!.id,
);
}
}
List<Player> getAllPlayers(Game game) {
if (game.group == null && game.players != null) {
return [...game.players!];
} else if (game.group != null && game.players != null) {
return [...game.players!, ...game.group!.members];
}
return [...game.group!.members];
}
}

View File

@@ -3,12 +3,12 @@ import 'package:game_tracker/core/custom_theme.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';
import 'package:game_tracker/presentation/views/main_menu/create_group/create_group_view.dart';
import 'package:game_tracker/presentation/widgets/full_width_button.dart';
import 'package:game_tracker/presentation/views/main_menu/create_group_view.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart';
import 'package:skeletonizer/skeletonizer.dart';
class GroupsView extends StatefulWidget {
const GroupsView({super.key});
@@ -19,13 +19,13 @@ class GroupsView extends StatefulWidget {
class _GroupsViewState extends State<GroupsView> {
late Future<List<Group>> _allGroupsFuture;
late final AppDatabase db;
final player = Player(id: 'p1', name: 'Sample');
final player = Player(name: 'Skeleton Player');
late final List<Group> skeletonData = List.filled(
7,
Group(
id: '0',
name: 'Sample Game',
name: 'Skeleton Game',
members: [player, player, player, player, player, player],
),
);
@@ -33,8 +33,11 @@ class _GroupsViewState extends State<GroupsView> {
@override
void initState() {
super.initState();
final db = Provider.of<AppDatabase>(context, listen: false);
_allGroupsFuture = db.groupDao.getAllGroups();
db = Provider.of<AppDatabase>(context, listen: false);
_allGroupsFuture = Future.delayed(
const Duration(milliseconds: 250),
() => db.groupDao.getAllGroups(),
);
}
@override
@@ -53,7 +56,7 @@ class _GroupsViewState extends State<GroupsView> {
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'Group data couldn\'t\nbe loaded.',
message: 'Group data couldn\'t\nbe loaded',
),
);
}
@@ -63,37 +66,25 @@ class _GroupsViewState extends State<GroupsView> {
child: TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'No groups created yet.',
message: 'No groups created yet',
),
);
}
final bool isLoading =
snapshot.connectionState == ConnectionState.waiting;
final List<Group> groups = isLoading
? skeletonData
: (snapshot.data ?? []);
return Skeletonizer(
effect: PulseEffect(
from: Colors.grey[800]!,
to: Colors.grey[600]!,
duration: const Duration(milliseconds: 800),
),
final List<Group> groups =
isLoading ? skeletonData : (snapshot.data ?? [])
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
return AppSkeleton(
enabled: isLoading,
enableSwitchAnimation: true,
switchAnimationConfig: const SwitchAnimationConfig(
duration: Duration(milliseconds: 200),
switchInCurve: Curves.linear,
switchOutCurve: Curves.linear,
transitionBuilder:
AnimatedSwitcher.defaultTransitionBuilder,
layoutBuilder: AnimatedSwitcher.defaultLayoutBuilder,
),
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
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]);
},
@@ -101,16 +92,13 @@ class _GroupsViewState extends State<GroupsView> {
);
},
),
Positioned(
bottom: 80,
child: FullWidthButton(
bottom: MediaQuery.paddingOf(context).bottom,
child: CustomWidthButton(
text: 'Create Group',
infillColor: CustomTheme.primaryColor,
borderColor: CustomTheme.primaryColor,
sizeRelativeToWidth: 0.90,
onPressed: () {
Navigator.push(
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
@@ -118,6 +106,9 @@ class _GroupsViewState extends State<GroupsView> {
},
),
);
setState(() {
_allGroupsFuture = db.groupDao.getAllGroups();
});
},
),
),

View File

@@ -1,11 +1,14 @@
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/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/app_skeleton.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:provider/provider.dart';
import 'package:skeletonizer/skeletonizer.dart';
class HomeView extends StatefulWidget {
const HomeView({super.key});
@@ -17,44 +20,50 @@ class HomeView extends StatefulWidget {
class _HomeViewState extends State<HomeView> {
late Future<int> _gameCountFuture;
late Future<int> _groupCountFuture;
late Future<List<Game>> _recentGamesFuture;
bool isLoading = true;
late final List<Game> 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: Player(name: 'Skeleton Player 1'),
),
);
@override
initState() {
super.initState();
final db = Provider.of<AppDatabase>(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: 250));
if (mounted) {
setState(() {
isLoading = false;
});
}
},
);
}
@override
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),
),
return AppSkeleton(
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: Column(
crossAxisAlignment: CrossAxisAlignment.center,
@@ -103,32 +112,82 @@ class _HomeViewState extends State<HomeView> {
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',
),
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),
],
content: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: FutureBuilder(
future: _recentGamesFuture,
builder:
(
BuildContext context,
AsyncSnapshot<List<Game>> snapshot,
) {
if (snapshot.hasError) {
return const Center(
heightFactor: 4,
child: Text(
'Error while loading recent games.',
),
);
}
final List<Game> games =
(isLoading
? skeletonData
: (snapshot.data ?? [])
..sort(
(a, b) => b.createdAt.compareTo(
a.createdAt,
),
))
.take(2)
.toList();
if (games.isNotEmpty) {
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: 12,
child: Text('No recent games available.'),
);
}
},
),
),
),
@@ -189,4 +248,15 @@ class _HomeViewState extends State<HomeView> {
},
);
}
String _getPlayerText(Game game) {
if (game.group == null) {
final playerCount = game.players?.length ?? 0;
return '$playerCount Players';
}
if (game.players == null || game.players!.isEmpty) {
return game.group!.name;
}
return '${game.group!.name} + ${game.players!.length}';
}
}

View File

@@ -1,13 +1,191 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart';
import 'package:game_tracker/services/data_transfer_service.dart';
class SettingsView extends StatelessWidget {
class SettingsView extends StatefulWidget {
const SettingsView({super.key});
@override
State<SettingsView> createState() => _SettingsViewState();
}
class _SettingsViewState extends State<SettingsView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Einstellungen')),
body: const Center(child: Text('Settings View')),
appBar: AppBar(backgroundColor: CustomTheme.backgroundColor),
backgroundColor: CustomTheme.backgroundColor,
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) =>
SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.fromLTRB(24, 0, 24, 10),
child: Text(
textAlign: TextAlign.start,
'Menu',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child: Text(
textAlign: TextAlign.start,
'Settings',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
),
SettingsListTile(
title: 'Export data',
icon: Icons.upload_outlined,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () async {
final String json =
await DataTransferService.getAppDataAsJson(context);
final result = await DataTransferService.exportData(
json,
'game_tracker-data',
);
if (!context.mounted) return;
showExportSnackBar(context: context, result: result);
},
),
SettingsListTile(
title: 'Import data',
icon: Icons.download_outlined,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () async {
final result = await DataTransferService.importData(
context,
);
if (!context.mounted) return;
showImportSnackBar(context: context, result: result);
},
),
SettingsListTile(
title: 'Delete all data',
icon: Icons.download_outlined,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () {
showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete all data?'),
content: const Text('This can\'t be undone'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Abbrechen'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Löschen'),
),
],
),
).then((confirmed) {
if (confirmed == true && context.mounted) {
DataTransferService.deleteAllData(context);
showSnackbar(
context: context,
message: 'Daten erfolgreich gelöscht',
);
}
});
},
),
],
),
),
),
);
}
/// Displays a snackbar based on the import result.
///
/// [context] The BuildContext to show the snackbar in.
/// [result] The result of the import operation.
void showImportSnackBar({
required BuildContext context,
required ImportResult result,
}) {
switch (result) {
case ImportResult.success:
showSnackbar(context: context, message: 'Data successfully imported');
case ImportResult.invalidSchema:
showSnackbar(context: context, message: 'Invalid Schema');
case ImportResult.fileReadError:
showSnackbar(context: context, message: 'Error reading file');
case ImportResult.canceled:
showSnackbar(context: context, message: 'Import canceled');
case ImportResult.formatException:
showSnackbar(
context: context,
message: 'Format Exception (see console)',
);
case ImportResult.unknownException:
showSnackbar(
context: context,
message: 'Unknown Exception (see console)',
);
}
}
/// Displays a snackbar based on the export result.
///
/// [context] The BuildContext to show the snackbar in.
/// [result] The result of the export operation.
void showExportSnackBar({
required BuildContext context,
required ExportResult result,
}) {
switch (result) {
case ExportResult.success:
showSnackbar(context: context, message: 'Data successfully exported');
case ExportResult.canceled:
showSnackbar(context: context, message: 'Export canceled');
case ExportResult.unknownException:
showSnackbar(
context: context,
message: 'Unknown Exception (see console)',
);
}
}
/// Displays a snackbar with the given message and optional action.
///
/// [context] The BuildContext to show the snackbar in.
/// [message] The message to display in the snackbar.
/// [duration] The duration for which the snackbar is displayed.
/// [action] An optional callback function to execute when the action button is pressed.
void showSnackbar({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 3),
VoidCallback? action,
}) {
final messenger = ScaffoldMessenger.of(context);
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
content: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: CustomTheme.onBoxColor,
duration: duration,
action: action != null
? SnackBarAction(label: 'Rückgängig', onPressed: action)
: null,
),
);
}
}

View File

@@ -1,10 +1,241 @@
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/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart';
import 'package:provider/provider.dart';
class StatisticsView extends StatelessWidget {
class StatisticsView extends StatefulWidget {
const StatisticsView({super.key});
@override
State<StatisticsView> createState() => _StatisticsViewState();
}
class _StatisticsViewState extends State<StatisticsView> {
late Future<List<Game>> _gamesFuture;
late Future<List<Player>> _playersFuture;
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
void initState() {
super.initState();
final db = Provider.of<AppDatabase>(context, listen: false);
_gamesFuture = db.gameDao.getAllGames();
_playersFuture = db.playerDao.getAllPlayers();
Future.wait([_gamesFuture, _playersFuture]).then((results) async {
await Future.delayed(const Duration(milliseconds: 250));
final games = results[0] as List<Game>;
final players = results[1] as List<Player>;
winCounts = _calculateWinsForAllPlayers(games, players);
gameCounts = _calculateGameAmountsForAllPlayers(games, players);
winRates = computeWinRatePercent(wins: winCounts, games: gameCounts);
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 SingleChildScrollView(
child: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
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: 3,
barColor: Colors.blue,
),
SizedBox(height: constraints.maxHeight * 0.02),
StatisticsTile(
icon: Icons.percent,
title: 'Winrate per Player',
width: constraints.maxWidth * 0.95,
values: winRates,
itemCount: 5,
barColor: Colors.orange[700]!,
),
SizedBox(height: constraints.maxHeight * 0.02),
StatisticsTile(
icon: Icons.casino,
title: 'Games per Player',
width: constraints.maxWidth * 0.95,
values: gameCounts,
itemCount: 10,
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<Game> games,
List<Player> players,
) {
List<(String, int)> winCounts = [];
// Getting the winners
for (var game in games) {
final winner = game.winner;
if (winner != null) {
final index = winCounts.indexWhere((entry) => entry.$1 == winner.id);
// -1 means winner not found in winCounts
if (index != -1) {
final current = winCounts[index].$2;
winCounts[index] = (winner.id, current + 1);
} else {
winCounts.add((winner.id, 1));
}
}
}
// 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));
}
}
// 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<Game> games,
List<Player> 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);
// -1 means player not found in gameCounts
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);
// -1 means player not found in gameCounts
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);
// -1 means player not found in gameCounts
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;
}
// dart
List<(String, double)> computeWinRatePercent({
required List<(String, int)> wins,
required List<(String, int)> games,
}) {
final Map<String, int> winsMap = {for (var e in wins) e.$1: e.$2};
final Map<String, int> 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;
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;
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:skeletonizer/skeletonizer.dart';
class AppSkeleton extends StatefulWidget {
final Widget child;
final bool enabled;
final bool fixLayoutBuilder;
const AppSkeleton({
super.key,
required this.child,
this.enabled = true,
this.fixLayoutBuilder = false,
});
@override
State<AppSkeleton> createState() => _AppSkeletonState();
}
class _AppSkeletonState extends State<AppSkeleton> {
@override
Widget build(BuildContext context) {
return Skeletonizer(
effect: PulseEffect(
from: Colors.grey[800]!,
to: Colors.grey[600]!,
duration: const Duration(milliseconds: 800),
),
enabled: widget.enabled,
enableSwitchAnimation: true,
switchAnimationConfig: SwitchAnimationConfig(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.linear,
switchOutCurve: Curves.linear,
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
layoutBuilder: !widget.fixLayoutBuilder
? AnimatedSwitcher.defaultLayoutBuilder
: (Widget? currentChild, List<Widget> previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
),
child: widget.child,
);
}
}

View File

@@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
class CustomWidthButton extends StatelessWidget {
const CustomWidthButton({
super.key,
required this.text,
this.buttonType = ButtonType.primary,
required this.sizeRelativeToWidth,
this.onPressed,
});
final String text;
final double sizeRelativeToWidth;
final VoidCallback? onPressed;
final ButtonType buttonType;
@override
Widget build(BuildContext context) {
final Color buttonBackgroundColor;
final Color disabledBackgroundColor;
final Color borderSideColor;
final Color textcolor;
final Color disabledTextColor;
if (buttonType == ButtonType.primary) {
textcolor = Colors.white;
disabledTextColor = Color.lerp(textcolor, Colors.black, 0.5)!;
buttonBackgroundColor = CustomTheme.primaryColor;
disabledBackgroundColor = Color.lerp(
buttonBackgroundColor,
Colors.black,
0.5,
)!;
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor,
disabledBackgroundColor: disabledBackgroundColor,
animationDuration: const Duration(),
minimumSize: Size(
MediaQuery.sizeOf(context).width * sizeRelativeToWidth,
60,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
text,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 22),
),
);
} else if (buttonType == ButtonType.secondary) {
textcolor = CustomTheme.primaryColor;
disabledTextColor = Color.lerp(textcolor, Colors.black, 0.5)!;
buttonBackgroundColor = Colors.transparent;
disabledBackgroundColor = Colors.transparent;
borderSideColor = onPressed != null
? CustomTheme.primaryColor
: Color.lerp(CustomTheme.primaryColor, Colors.black, 0.5)!;
return OutlinedButton(
onPressed: onPressed,
style: OutlinedButton.styleFrom(
foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor,
disabledBackgroundColor: disabledBackgroundColor,
animationDuration: const Duration(),
minimumSize: Size(
MediaQuery.sizeOf(context).width * sizeRelativeToWidth,
60,
),
side: BorderSide(color: borderSideColor, width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
text,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 22),
),
);
} else {
textcolor = CustomTheme.primaryColor;
disabledTextColor = Color.lerp(
CustomTheme.primaryColor,
Colors.black,
0.5,
)!;
buttonBackgroundColor = Colors.transparent;
disabledBackgroundColor = Colors.transparent;
return TextButton(
onPressed: onPressed,
style: TextButton.styleFrom(
foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor,
disabledBackgroundColor: disabledBackgroundColor,
animationDuration: const Duration(),
minimumSize: Size(
MediaQuery.sizeOf(context).width * sizeRelativeToWidth,
60,
),
side: const BorderSide(style: BorderStyle.none),
),
child: Text(
text,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 22),
),
);
}
}
}

View File

@@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
Widget doubleRowInfoTile(
String titleOneUpperLeft,
String titleTwoUpperLeft,
String titleUpperRight,
String titleLowerLeft,
String titleLowerRight,
) {
return Container(
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: CustomTheme.secondaryColor,
),
child: Column(
children: [
Row(
children: [
Expanded(
flex: 10,
child: Text(
"$titleOneUpperLeft $titleTwoUpperLeft",
style: TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
Spacer(),
Expanded(
flex: 3,
child: Text(
"$titleUpperRight",
style: TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.end,
),
),
],
),
Row(
children: [
Expanded(
flex: 10,
child: Text(
"$titleLowerLeft",
style: TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
Spacer(),
Expanded(
flex: 4,
child: Text(
"$titleLowerRight",
style: TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.end,
),
),
],
),
],
),
);
}

View File

@@ -1,45 +0,0 @@
import 'package:flutter/material.dart';
class FullWidthButton extends StatelessWidget {
const FullWidthButton({
super.key,
required this.text,
required this.borderColor,
required this.infillColor,
this.disabledInfillColor,
required this.sizeRelativeToWidth,
required this.onPressed,
});
final String text;
final Color borderColor;
final Color infillColor;
final Color? disabledInfillColor;
final double sizeRelativeToWidth;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
disabledBackgroundColor: disabledInfillColor,
minimumSize: Size(
MediaQuery.sizeOf(context).width * sizeRelativeToWidth,
60,
),
backgroundColor: infillColor,
side: BorderSide(color: borderColor, width: 2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: Text(
text,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 22,
color: Colors.white,
),
),
);
}
}

View File

@@ -0,0 +1,267 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_list_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart';
class PlayerSelection extends StatefulWidget {
final Function(List<Player> value) onChanged;
final List<Player> initialPlayers;
const PlayerSelection({
super.key,
required this.onChanged,
this.initialPlayers = const [],
});
@override
State<PlayerSelection> createState() => _PlayerSelectionState();
}
class _PlayerSelectionState extends State<PlayerSelection> {
List<Player> selectedPlayers = [];
List<Player> suggestedPlayers = [];
List<Player> allPlayers = [];
late final TextEditingController _searchBarController =
TextEditingController();
late final AppDatabase db;
late Future<List<Player>> _allPlayersFuture;
late final List<Player> skeletonData = List.filled(
7,
Player(name: 'Player 0'),
);
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
loadPlayerList();
}
void loadPlayerList() {
_allPlayersFuture = Future.delayed(
const Duration(milliseconds: 250),
() => db.playerDao.getAllPlayers(),
);
suggestedPlayers = skeletonData;
_allPlayersFuture.then((loadedPlayers) {
setState(() {
if (widget.initialPlayers.isNotEmpty) {
allPlayers = [...widget.initialPlayers];
suggestedPlayers = [...widget.initialPlayers];
} else {
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...loadedPlayers];
suggestedPlayers = [...loadedPlayers];
}
});
});
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
decoration: CustomTheme.standardBoxDecoration,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomSearchBar(
controller: _searchBarController,
constraints: const BoxConstraints(maxHeight: 45, minHeight: 45),
hintText: 'Search for players',
trailingButtonShown: true,
trailingButtonicon: Icons.add_circle,
trailingButtonEnabled: _searchBarController.text.trim().isNotEmpty,
onTrailingButtonPressed: () async {
addNewPlayerFromSearch(context: context);
},
onChanged: (value) {
setState(() {
// Filters the list of suggested players based on the search input.
if (value.isEmpty) {
// If the search is empty, it shows all unselected players.
suggestedPlayers = allPlayers.where((player) {
return !selectedPlayers.contains(player);
}).toList();
} else {
// If there is input, it filters by name match (case-insensitive) and ensures
// that already selected players are excluded from the results.
suggestedPlayers = allPlayers.where((player) {
final bool nameMatches = player.name.toLowerCase().contains(
value.toLowerCase(),
);
final bool isNotSelected = !selectedPlayers.contains(
player,
);
return nameMatches && isNotSelected;
}).toList();
}
});
},
),
const SizedBox(height: 10),
Text(
'Selected players: (${selectedPlayers.length})',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 8.0,
runSpacing: 8.0,
children: <Widget>[
// Generates a TextIconTile for each selected player.
for (var player in selectedPlayers)
TextIconTile(
text: player.name,
onIconTap: () {
setState(() {
// Removes the player from the selection and notifies the parent.
final currentSearch = _searchBarController.text
.toLowerCase();
selectedPlayers.remove(player);
widget.onChanged([...selectedPlayers]);
// If the player matches the current search query (or search is empty),
// they are added back to the suggestions and the list is re-sorted.
if (currentSearch.isEmpty ||
player.name.toLowerCase().contains(currentSearch)) {
suggestedPlayers.add(player);
}
});
},
),
],
),
const SizedBox(height: 10),
const Text(
'All players:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
FutureBuilder(
future: _allPlayersFuture,
builder:
(BuildContext context, AsyncSnapshot<List<Player>> snapshot) {
if (snapshot.hasError) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'Player data couldn\'t\nbe loaded.',
),
);
}
bool doneLoading =
snapshot.connectionState == ConnectionState.done;
bool snapshotDataEmpty =
!snapshot.hasData || snapshot.data!.isEmpty;
if (doneLoading &&
(snapshotDataEmpty && allPlayers.isEmpty)) {
return const Center(
child: TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'No players created yet.',
),
);
}
final bool isLoading =
snapshot.connectionState == ConnectionState.waiting;
return Expanded(
child: AppSkeleton(
enabled: isLoading,
child: Visibility(
visible:
(suggestedPlayers.isEmpty && allPlayers.isNotEmpty),
replacement: ListView.builder(
itemCount: suggestedPlayers.length,
itemBuilder: (BuildContext context, int index) {
return TextIconListTile(
text: suggestedPlayers[index].name,
onPressed: () {
setState(() {
if (!selectedPlayers.contains(
suggestedPlayers[index],
)) {
selectedPlayers.add(
suggestedPlayers[index],
);
widget.onChanged([...selectedPlayers]);
suggestedPlayers.remove(
suggestedPlayers[index],
);
}
});
},
);
},
),
child: TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: (selectedPlayers.length == allPlayers.length)
? 'No more players to add.'
: 'No players found with that name.',
),
),
),
);
},
),
],
),
);
}
/// 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);
widget.onChanged([...selectedPlayers]);
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),
),
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: CustomTheme.boxColor,
content: Center(
child: Text(
'Could not add player $playerName.',
style: const TextStyle(color: Colors.white),
),
),
),
);
}
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class CustomSearchBar extends StatelessWidget {
final TextEditingController controller;
final String hintText;
final ValueChanged<String>? onChanged;
final BoxConstraints? constraints;
final bool trailingButtonShown;
final bool trailingButtonEnabled;
final VoidCallback? onTrailingButtonPressed;
final IconData trailingButtonicon;
const CustomSearchBar({
super.key,
required this.controller,
required this.hintText,
this.trailingButtonShown = false,
this.trailingButtonicon = Icons.clear,
this.trailingButtonEnabled = true,
this.onTrailingButtonPressed,
this.onChanged,
this.constraints,
});
@override
Widget build(BuildContext context) {
return SearchBar(
controller: controller,
constraints:
constraints ?? const BoxConstraints(maxHeight: 45, minHeight: 45),
hintText: hintText,
onChanged: trailingButtonEnabled ? onChanged : null,
hintStyle: WidgetStateProperty.all(const TextStyle(fontSize: 16)),
leading: const Icon(Icons.search),
trailing: [
Visibility(
visible: trailingButtonShown,
child: GestureDetector(
onTap: trailingButtonEnabled ? onTrailingButtonPressed : null,
child: Icon(
trailingButtonicon,
color: trailingButtonEnabled
? null
: Colors.grey.withValues(alpha: 0.2),
),
),
),
const SizedBox(width: 5),
],
backgroundColor: WidgetStateProperty.all(CustomTheme.boxColor),
side: WidgetStateProperty.all(BorderSide(color: CustomTheme.boxBorder)),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
elevation: WidgetStateProperty.all(0),
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class TextInputField extends StatelessWidget {
final TextEditingController controller;
final ValueChanged<String>? onChanged;
final String hintText;
const TextInputField({
super.key,
required this.controller,
required this.hintText,
this.onChanged,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
onChanged: onChanged,
decoration: InputDecoration(
filled: true,
fillColor: CustomTheme.boxColor,
hintText: hintText,
hintStyle: const TextStyle(fontSize: 18),
enabledBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorder),
),
floatingLabelBehavior: FloatingLabelBehavior.never,
),
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class ChooseTile extends StatefulWidget {
final String title;
final VoidCallback? onPressed;
final String? trailingText;
const ChooseTile({
super.key,
required this.title,
this.trailingText,
this.onPressed,
});
@override
State<ChooseTile> createState() => _ChooseTileState();
}
class _ChooseTileState extends State<ChooseTile> {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onPressed,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
decoration: CustomTheme.standardBoxDecoration,
child: Row(
children: [
Text(
widget.title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const Spacer(),
if (widget.trailingText != null) Text(widget.trailingText!),
const SizedBox(width: 10),
const Icon(Icons.arrow_forward_ios, size: 16),
],
),
),
);
}
}

View File

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class CustomRadioListTile<T> extends StatelessWidget {
final String text;
final T value;
final ValueChanged<T> onContainerTap;
const CustomRadioListTile({
super.key,
required this.text,
required this.value,
required this.onContainerTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onContainerTap(value),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Radio<T>(
value: value,
activeColor: CustomTheme.primaryColor,
toggleable: true,
),
Expanded(
child: Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,180 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
import 'package:intl/intl.dart';
class GameHistoryTile extends StatefulWidget {
final Game game;
final VoidCallback onTap;
const GameHistoryTile({super.key, required this.game, required this.onTap});
@override
State<GameHistoryTile> createState() => _GameHistoryTileState();
}
class _GameHistoryTileState extends State<GameHistoryTile> {
@override
Widget build(BuildContext context) {
final group = widget.game.group;
final winner = widget.game.winner;
final allPlayers = _getAllPlayers();
return GestureDetector(
onTap: widget.onTap,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
widget.game.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Text(
_formatDate(widget.game.createdAt),
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
const SizedBox(height: 8),
if (group != null) ...[
Row(
children: [
const Icon(Icons.group, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Expanded(
child: Text(
group.name,
style: const TextStyle(fontSize: 14, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 12),
],
if (winner != null) ...[
Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.green.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
const Icon(
Icons.emoji_events,
size: 20,
color: Colors.amber,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Winner: ${winner.name}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 12),
],
if (allPlayers.isNotEmpty) ...[
const Text(
'Players',
style: TextStyle(
fontSize: 13,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: allPlayers.map((player) {
return TextIconTile(text: player.name, iconEnabled: false);
}).toList(),
),
],
],
),
),
);
}
String _formatDate(DateTime dateTime) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inDays == 0) {
return 'Today at ${DateFormat('HH:mm').format(dateTime)}';
} else if (difference.inDays == 1) {
return 'Yesterday at ${DateFormat('HH:mm').format(dateTime)}';
} else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else {
return DateFormat('MMM d, yyyy').format(dateTime);
}
}
List<dynamic> _getAllPlayers() {
final allPlayers = <dynamic>[];
final playerIds = <String>{};
// Add players from game.players
if (widget.game.players != null) {
for (var player in widget.game.players!) {
if (!playerIds.contains(player.id)) {
allPlayers.add(player);
playerIds.add(player.id);
}
}
}
// Add players from game.group.players
if (widget.game.group?.members != null) {
for (var player in widget.game.group!.members) {
if (!playerIds.contains(player.id)) {
allPlayers.add(player);
playerIds.add(player.id);
}
}
}
return allPlayers;
}
}

Some files were not shown because too many files have changed in this diff Show More