111 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
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
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
37 changed files with 1686 additions and 598 deletions

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.

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,

View File

@@ -22,3 +22,10 @@ enum ImportResult {
/// - [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

@@ -78,7 +78,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
);
if (game.players != null) {
await db.playerDao.addPlayers(players: game.players!);
await db.playerDao.addPlayersAsList(players: game.players!);
for (final p in game.players ?? []) {
await db.playerGameDao.addPlayerToGame(
gameId: game.id,
@@ -89,12 +89,18 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
if (game.group != null) {
await db.groupDao.addGroup(group: game.group!);
await db.groupGameDao.addGroupToGame(game.id, game.group!.id);
await db.groupGameDao.addGroupToGame(
gameId: game.id,
groupId: game.group!.id,
);
}
});
}
Future<void> addGames({required List<Game> games}) async {
/// 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
@@ -253,4 +259,62 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
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

@@ -84,7 +84,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
/// Adds multiple groups to the database.
/// Also adds the group's members to the [PlayerGroupTable].
Future<void> addGroups({required List<Group> groups}) async {
Future<void> addGroupsAsList({required List<Group> groups}) async {
if (groups.isEmpty) return;
await db.transaction(() async {
// Deduplicate groups by id - keep first occurrence

View File

@@ -12,7 +12,13 @@ class GroupGameDao extends DatabaseAccessor<AppDatabase>
/// Associates a group with a game by inserting a record into the
/// [GroupGameTable].
Future<void> addGroupToGame(String gameId, String groupId) async {
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,
@@ -76,4 +82,17 @@ class GroupGameDao extends DatabaseAccessor<AppDatabase>
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

@@ -50,7 +50,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
}
/// Adds multiple [players] to the database in a batch operation.
Future<bool> addPlayers({required List<Player> players}) async {
Future<bool> addPlayersAsList({required List<Player> players}) async {
if (players.isEmpty) return false;
await db.batch(

View File

@@ -79,4 +79,50 @@ class PlayerGameDao extends DatabaseAccessor<AppDatabase>
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

@@ -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,17 +1,13 @@
import 'package:flutter/material.dart' hide ButtonStyle;
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/custom_search_bar.dart';
import 'package:game_tracker/presentation/widgets/text_input_field.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:game_tracker/presentation/widgets/player_selection.dart';
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
import 'package:provider/provider.dart';
import 'package:skeletonizer/skeletonizer.dart';
class CreateGroupView extends StatefulWidget {
const CreateGroupView({super.key});
@@ -21,53 +17,25 @@ class CreateGroupView extends StatefulWidget {
}
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(name: 'Player 0'),
);
final _groupNameController = TextEditingController();
final _searchBarController = TextEditingController();
late final AppDatabase db;
List<Player> selectedPlayers = [];
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_searchBarController.addListener(() {
setState(() {});
});
_groupNameController.addListener(() {
setState(() {});
});
loadPlayerList();
}
@override
void dispose() {
_groupNameController.dispose();
_searchBarController.dispose();
super.dispose();
}
void loadPlayerList() {
_allPlayersFuture = Future.delayed(
const Duration(milliseconds: 250),
() => db.playerDao.getAllPlayers(),
);
suggestedPlayers = skeletonData;
_allPlayersFuture.then((loadedPlayers) {
setState(() {
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...loadedPlayers];
suggestedPlayers = [...loadedPlayers];
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -96,203 +64,10 @@ class _CreateGroupViewState extends State<CreateGroupView> {
),
),
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: [
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(() {
if (value.isEmpty) {
suggestedPlayers = allPlayers.where((player) {
return !selectedPlayers.contains(player);
}).toList();
} else {
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(
'Ausgewählte Spieler: (${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>[
for (var player in selectedPlayers)
TextIconTile(
text: player.name,
onIconTap: () {
setState(() {
final currentSearch = _searchBarController.text
.toLowerCase();
selectedPlayers.remove(player);
if (currentSearch.isEmpty ||
player.name.toLowerCase().contains(
currentSearch,
)) {
suggestedPlayers.add(player);
suggestedPlayers.sort(
(a, b) => a.name.compareTo(b.name),
);
}
});
},
),
],
),
const SizedBox(height: 10),
const Text(
'Alle Spieler:',
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.',
),
);
}
if (snapshot.connectionState ==
ConnectionState.done &&
(!snapshot.hasData ||
snapshot.data!.isEmpty ||
(selectedPlayers.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: 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],
);
selectedPlayers.sort(
(a, b) => a.name.compareTo(
b.name,
),
);
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.',
),
),
),
);
},
),
],
),
child: PlayerSelection(
onChanged: (value) {
selectedPlayers = [...value];
},
),
),
CustomWidthButton(
@@ -311,9 +86,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
);
if (!context.mounted) return;
if (success) {
_groupNameController.clear();
_searchBarController.clear();
selectedPlayers.clear();
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
@@ -337,47 +109,4 @@ class _CreateGroupViewState extends State<CreateGroupView> {
),
);
}
/// Adds a new player to the database from the search bar input.
/// Shows a snackbar indicating success or failure.
/// [context] - BuildContext to show the snackbar.
void addNewPlayerFromSearch({required BuildContext context}) async {
String playerName = _searchBarController.text.trim();
Player createdPlayer = Player(name: playerName);
bool success = await db.playerDao.addPlayer(player: createdPlayer);
if (!context.mounted) return;
if (success) {
selectedPlayers.add(createdPlayer);
allPlayers.add(createdPlayer);
setState(() {
_searchBarController.clear();
suggestedPlayers = allPlayers.where((player) {
return !selectedPlayers.contains(player);
}).toList();
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: CustomTheme.boxColor,
content: Center(
child: Text(
'Successfully added player $playerName.',
style: const TextStyle(color: Colors.white),
),
),
),
);
} 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

@@ -1,11 +1,17 @@
import 'package:flutter/cupertino.dart';
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/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';
import 'package:skeletonizer/skeletonizer.dart';
class GameHistoryView extends StatefulWidget {
const GameHistoryView({super.key});
@@ -19,17 +25,21 @@ class _GameHistoryViewState extends State<GameHistoryView> {
late final AppDatabase db;
late final List<Game> skeletonData = List.filled(
2,
4,
Game(
name: 'Skeleton Game',
group: Group(
name: 'Skeleton Group',
members: [
Player(name: 'Skeleton Player 1'),
Player(name: 'Skeleton Player 2'),
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')],
),
);
@@ -39,68 +49,102 @@ class _GameHistoryViewState extends State<GameHistoryView> {
db = Provider.of<AppDatabase>(context, listen: false);
_gameListFuture = Future.delayed(
const Duration(milliseconds: 250),
() => db.gameDao.getAllGames(),
() => db.gameDao.getAllGames(),
);
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Game>>(
future: _gameListFuture,
builder: (BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
if (snapshot.hasError) {
return const Center(
heightFactor: 4,
child: Text(
'Error while loading recent games.',
),
);
}
if (snapshot.connectionState == ConnectionState.done &&
(!snapshot.hasData || snapshot.data!.isEmpty)) {
return const Center(
heightFactor: 4,
child: Text('No recent games available.'),
);
}
final bool isLoading = snapshot.connectionState == ConnectionState.waiting;
final List<Game> games = (isLoading
? skeletonData
: (snapshot.data ?? [])
..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2)
.toList();
return Skeletonizer(
effect: PulseEffect(
from: Colors.grey[800]!,
to: Colors.grey[600]!,
duration: const Duration(milliseconds: 800),
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
body: Stack(
alignment: Alignment.center,
children: [
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],
);
},
),
);
},
),
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: games.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == games.length) {
return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 20,
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();
},
),
);
}
return GameHistoryTile(game: games[index]);
},
setState(() {
_gameListFuture = db.gameDao.getAllGames();
});
},
),
),
);
},
],
),
);
}
}
}

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

@@ -4,11 +4,11 @@ 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_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});
@@ -56,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',
),
);
}
@@ -66,7 +66,7 @@ class _GroupsViewState extends State<GroupsView> {
child: TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'No groups created yet.',
message: 'No groups created yet',
),
);
}
@@ -75,22 +75,8 @@ class _GroupsViewState extends State<GroupsView> {
final List<Group> groups =
isLoading ? skeletonData : (snapshot.data ?? [])
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
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: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: groups.length + 1,
@@ -106,7 +92,6 @@ class _GroupsViewState extends State<GroupsView> {
);
},
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: CustomWidthButton(

View File

@@ -3,12 +3,12 @@ import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/widgets/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});
@@ -62,30 +62,8 @@ class _HomeViewState extends State<HomeView> {
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: SwitchAnimationConfig(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.linear,
switchOutCurve: Curves.linear,
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
layoutBuilder:
(Widget? currentChild, List<Widget> previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
@@ -151,15 +129,6 @@ class _HomeViewState extends State<HomeView> {
),
);
}
if (snapshot.connectionState ==
ConnectionState.done &&
(!snapshot.hasData ||
snapshot.data!.isEmpty)) {
return const Center(
heightFactor: 4,
child: Text('No recent games available.'),
);
}
final List<Game> games =
(isLoading
? skeletonData
@@ -214,7 +183,7 @@ class _HomeViewState extends State<HomeView> {
);
} else {
return const Center(
heightFactor: 4,
heightFactor: 12,
child: Text('No recent games available.'),
);
}

View File

@@ -2,9 +2,9 @@ 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';
import 'package:skeletonizer/skeletonizer.dart';
class StatisticsView extends StatefulWidget {
const StatisticsView({super.key});
@@ -48,30 +48,9 @@ class _StatisticsViewState extends State<StatisticsView> {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
child: Skeletonizer(
effect: PulseEffect(
from: Colors.grey[800]!,
to: Colors.grey[600]!,
duration: const Duration(milliseconds: 800),
),
child: AppSkeleton(
enabled: isLoading,
enableSwitchAnimation: true,
switchAnimationConfig: SwitchAnimationConfig(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.linear,
switchOutCurve: Curves.linear,
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
layoutBuilder:
(Widget? currentChild, List<Widget> previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
),
fixLayoutBuilder: true,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: constraints.maxWidth),
child: Column(

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

@@ -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: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
padding: const 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: const TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
const Spacer(),
Expanded(
flex: 3,
child: Text(
titleUpperRight,
style: const TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.end,
),
),
],
),
Row(
children: [
Expanded(
flex: 10,
child: Text(
titleLowerLeft,
style: const TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
const Spacer(),
Expanded(
flex: 4,
child: Text(
titleLowerRight,
style: const TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.end,
),
),
],
),
],
),
);
}

View File

@@ -1,77 +1,180 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:skeletonizer/skeletonizer.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 String gameTitle;
final String gameType;
final String date;
final String groupName;
final String winner;
final Game game;
final VoidCallback onTap;
const GameHistoryTile({
super.key,
required this.gameTitle,
required this.gameType,
required this.date,
required this.groupName,
required this.winner,
});
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) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
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: [
Text(
widget.gameTitle,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
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,
),
],
),
Row(
children: [
Text(
widget.date,
style: const TextStyle(fontSize: 14, color: Colors.grey),
textAlign: TextAlign.left,
),
const SizedBox(width: 5),
const Text('·'),
const SizedBox(width: 5),
Text(
widget.gameType,
style: const TextStyle(fontSize: 14, color: Colors.grey),
textAlign: TextAlign.left,
),
],
),
const SizedBox(height: 15),
]
)
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;
}
}

View File

@@ -4,20 +4,20 @@ import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
class GroupTile extends StatelessWidget {
const GroupTile({super.key, required this.group});
const GroupTile({super.key, required this.group, this.isHighlighted = false});
final Group group;
final bool isHighlighted;
@override
Widget build(BuildContext context) {
return Container(
return AnimatedContainer(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
decoration: isHighlighted
? CustomTheme.highlightedBoxDecoration
: CustomTheme.standardBoxDecoration,
duration: const Duration(milliseconds: 150),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -29,11 +29,7 @@ class _InfoTileState extends State<InfoTile> {
padding: widget.padding ?? const EdgeInsets.all(12),
height: widget.height,
width: widget.width ?? 380,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
),
decoration: CustomTheme.standardBoxDecoration,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,

View File

@@ -29,11 +29,7 @@ class _QuickInfoTileState extends State<QuickInfoTile> {
padding: widget.padding ?? const EdgeInsets.all(12),
height: widget.height ?? 110,
width: widget.width ?? 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
),
decoration: CustomTheme.standardBoxDecoration,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [

View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class RulesetListTile extends StatelessWidget {
final String title;
final String description;
final VoidCallback? onPressed;
final bool isHighlighted;
const RulesetListTile({
super.key,
required this.title,
required this.description,
this.onPressed,
this.isHighlighted = false,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
child: AnimatedContainer(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
decoration: isHighlighted
? CustomTheme.highlightedBoxDecoration
: CustomTheme.standardBoxDecoration,
duration: const Duration(milliseconds: 200),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
title,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
],
),
const SizedBox(height: 5),
Text(description, style: const TextStyle(fontSize: 14)),
const SizedBox(height: 2.5),
],
),
),
);
}
}

View File

@@ -26,11 +26,7 @@ class SettingsListTile extends StatelessWidget {
child: Container(
margin: EdgeInsets.zero,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
decoration: CustomTheme.standardBoxDecoration,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@@ -18,11 +18,7 @@ class TextIconListTile extends StatelessWidget {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
decoration: CustomTheme.standardBoxDecoration,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,

View File

@@ -110,9 +110,9 @@ class DataTransferService {
.toList() ??
[];
await db.playerDao.addPlayers(players: importedPlayers);
await db.groupDao.addGroups(groups: importedGroups);
await db.gameDao.addGames(games: importedGames);
await db.playerDao.addPlayersAsList(players: importedPlayers);
await db.groupDao.addGroupsAsList(groups: importedGroups);
await db.gameDao.addGamesAsList(games: importedGames);
} else {
return ImportResult.invalidSchema;
}

View File

@@ -112,7 +112,7 @@ void main() {
});
test('Adding and fetching multiple games works correctly', () async {
await database.gameDao.addGames(
await database.gameDao.addGamesAsList(
games: [testGame1, testGame2, testGameOnlyGroup, testGameOnlyPlayers],
);
@@ -234,5 +234,97 @@ void main() {
gameCount = await database.gameDao.getGameCount();
expect(gameCount, 0);
});
test('Checking if game has winner works correclty', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.addGame(game: testGameOnlyGroup);
var hasWinner = await database.gameDao.hasWinner(gameId: testGame1.id);
expect(hasWinner, true);
hasWinner = await database.gameDao.hasWinner(
gameId: testGameOnlyGroup.id,
);
expect(hasWinner, false);
});
test('Fetching the winner of a game works correctly', () async {
await database.gameDao.addGame(game: testGame1);
final winner = await database.gameDao.getWinner(gameId: testGame1.id);
if (winner == null) {
fail('Winner is null');
} else {
expect(winner.id, testGame1.winner!.id);
expect(winner.name, testGame1.winner!.name);
expect(winner.createdAt, testGame1.winner!.createdAt);
}
});
test('Updating the winner of a game works correctly', () async {
await database.gameDao.addGame(game: testGame1);
final winner = await database.gameDao.getWinner(gameId: testGame1.id);
if (winner == null) {
fail('Winner is null');
} else {
expect(winner.id, testGame1.winner!.id);
expect(winner.name, testGame1.winner!.name);
expect(winner.createdAt, testGame1.winner!.createdAt);
expect(winner.id, testPlayer4.id);
expect(winner.id != testPlayer5.id, true);
}
await database.gameDao.setWinner(
gameId: testGame1.id,
winnerId: testPlayer5.id,
);
final newWinner = await database.gameDao.getWinner(gameId: testGame1.id);
if (newWinner == null) {
fail('New winner is null');
} else {
expect(newWinner.id, testPlayer5.id);
expect(newWinner.name, testPlayer5.name);
expect(newWinner.createdAt, testPlayer5.createdAt);
}
});
test('Removing a winner works correctly', () async {
await database.gameDao.addGame(game: testGame2);
var hasWinner = await database.gameDao.hasWinner(gameId: testGame2.id);
expect(hasWinner, true);
await database.gameDao.removeWinner(gameId: testGame2.id);
hasWinner = await database.gameDao.hasWinner(gameId: testGame2.id);
expect(hasWinner, false);
final removedWinner = await database.gameDao.getWinner(
gameId: testGame2.id,
);
expect(removedWinner, null);
});
test('Renaming a game works correctly', () async {
await database.gameDao.addGame(game: testGame1);
var fetchedGame = await database.gameDao.getGameById(
gameId: testGame1.id,
);
expect(fetchedGame.name, testGame1.name);
const newName = 'Updated Game Name';
await database.gameDao.updateGameName(
gameId: testGame1.id,
newName: newName,
);
fetchedGame = await database.gameDao.getGameById(gameId: testGame1.id);
expect(fetchedGame.name, newName);
});
});
}

View File

@@ -14,7 +14,8 @@ void main() {
late Player testPlayer3;
late Player testPlayer4;
late Player testPlayer5;
late Group testgroup;
late Group testGroup1;
late Group testGroup2;
late Game testgameWithGroup;
late Game testgameWithPlayers;
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
@@ -35,15 +36,19 @@ void main() {
testPlayer3 = Player(name: 'Charlie');
testPlayer4 = Player(name: 'Diana');
testPlayer5 = Player(name: 'Eve');
testgroup = Group(
testGroup1 = Group(
name: 'Test Group',
members: [testPlayer1, testPlayer2, testPlayer3],
);
testGroup2 = Group(
name: 'Test Group',
members: [testPlayer3, testPlayer2],
);
testgameWithPlayers = Game(
name: 'Test Game with Players',
players: [testPlayer4, testPlayer5],
);
testgameWithGroup = Game(name: 'Test Game with Group', group: testgroup);
testgameWithGroup = Game(name: 'Test Game with Group', group: testGroup1);
});
});
tearDown(() async {
@@ -52,7 +57,7 @@ void main() {
group('Group-Game Tests', () {
test('Game has group works correctly', () async {
await database.gameDao.addGame(game: testgameWithPlayers);
await database.groupDao.addGroup(group: testgroup);
await database.groupDao.addGroup(group: testGroup1);
var gameHasGroup = await database.groupGameDao.gameHasGroup(
gameId: testgameWithPlayers.id,
@@ -61,8 +66,8 @@ void main() {
expect(gameHasGroup, false);
await database.groupGameDao.addGroupToGame(
testgameWithPlayers.id,
testgroup.id,
gameId: testgameWithPlayers.id,
groupId: testGroup1.id,
);
gameHasGroup = await database.groupGameDao.gameHasGroup(
@@ -74,15 +79,15 @@ void main() {
test('Adding a group to a game works correctly', () async {
await database.gameDao.addGame(game: testgameWithPlayers);
await database.groupDao.addGroup(group: testgroup);
await database.groupDao.addGroup(group: testGroup1);
await database.groupGameDao.addGroupToGame(
testgameWithPlayers.id,
testgroup.id,
gameId: testgameWithPlayers.id,
groupId: testGroup1.id,
);
var groupAdded = await database.groupGameDao.isGroupInGame(
gameId: testgameWithPlayers.id,
groupId: testgroup.id,
groupId: testGroup1.id,
);
expect(groupAdded, true);
@@ -120,14 +125,55 @@ void main() {
fail('Group should not be null');
}
expect(group.id, testgroup.id);
expect(group.name, testgroup.name);
expect(group.createdAt, testgroup.createdAt);
expect(group.members.length, testgroup.members.length);
expect(group.id, testGroup1.id);
expect(group.name, testGroup1.name);
expect(group.createdAt, testGroup1.createdAt);
expect(group.members.length, testGroup1.members.length);
for (int i = 0; i < group.members.length; i++) {
expect(group.members[i].id, testgroup.members[i].id);
expect(group.members[i].name, testgroup.members[i].name);
expect(group.members[i].createdAt, testgroup.members[i].createdAt);
expect(group.members[i].id, testGroup1.members[i].id);
expect(group.members[i].name, testGroup1.members[i].name);
expect(group.members[i].createdAt, testGroup1.members[i].createdAt);
}
});
test('Updating the group of a game works correctly', () async {
await database.gameDao.addGame(game: testgameWithGroup);
var group = await database.groupGameDao.getGroupOfGame(
gameId: testgameWithGroup.id,
);
if (group == null) {
fail('Initial group should not be null');
} else {
expect(group.id, testGroup1.id);
expect(group.name, testGroup1.name);
expect(group.createdAt, testGroup1.createdAt);
expect(group.members.length, testGroup1.members.length);
}
await database.groupDao.addGroup(group: testGroup2);
await database.groupGameDao.updateGroupOfGame(
gameId: testgameWithGroup.id,
newGroupId: testGroup2.id,
);
group = await database.groupGameDao.getGroupOfGame(
gameId: testgameWithGroup.id,
);
if (group == null) {
fail('Updated group should not be null');
} else {
expect(group.id, testGroup2.id);
expect(group.name, testGroup2.name);
expect(group.createdAt, testGroup2.createdAt);
expect(group.members.length, testGroup2.members.length);
for (int i = 0; i < group.members.length; i++) {
expect(group.members[i].id, testGroup2.members[i].id);
expect(group.members[i].name, testGroup2.members[i].name);
expect(group.members[i].createdAt, testGroup2.members[i].createdAt);
}
}
});
});

View File

@@ -81,7 +81,7 @@ void main() {
});
test('Adding and fetching multiple groups works correctly', () async {
await database.groupDao.addGroups(
await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup2, testGroup3, testGroup4],
);

View File

@@ -136,5 +136,48 @@ void main() {
expect(players[i].createdAt, testGameOnlyPlayers.players![i].createdAt);
}
});
test('Updating the games players works coreclty', () async {
await database.gameDao.addGame(game: testGameOnlyPlayers);
final newPlayers = [testPlayer1, testPlayer2, testPlayer4];
await database.playerDao.addPlayersAsList(players: newPlayers);
// First, remove all existing players
final existingPlayers = await database.playerGameDao.getPlayersOfGame(
gameId: testGameOnlyPlayers.id,
);
if (existingPlayers == null || existingPlayers.isEmpty) {
fail('Existing players should not be null or empty');
}
await database.playerGameDao.updatePlayersFromGame(
gameId: testGameOnlyPlayers.id,
newPlayer: newPlayers,
);
final updatedPlayers = await database.playerGameDao.getPlayersOfGame(
gameId: testGameOnlyPlayers.id,
);
if (updatedPlayers == null) {
fail('Updated players should not be null');
}
expect(updatedPlayers.length, newPlayers.length);
/// Create a map of new players for easy lookup
final testPlayers = {for (var p in newPlayers) p.id: p};
/// Verify each updated player matches the new players
for (final player in updatedPlayers) {
final testPlayer = testPlayers[player.id]!;
expect(player.id, testPlayer.id);
expect(player.name, testPlayer.name);
expect(player.createdAt, testPlayer.createdAt);
}
});
});
}

View File

@@ -56,7 +56,7 @@ void main() {
});
test('Adding and fetching multiple players works correctly', () async {
await database.playerDao.addPlayers(
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4],
);