233 Commits

Author SHA1 Message Date
36fda30f27 Updated licenses [skip ci] 2026-04-14 16:13:31 +00:00
1ab869ec26 Updated version number [skip ci] 2026-04-14 16:12:55 +00:00
52b78e44e4 Merge pull request 'Score implementation ergänzen' (#196) from feature/191-score-implementation-ergaenzen into development
All checks were successful
Push Pipeline / test (push) Successful in 42s
Push Pipeline / update_version (push) Successful in 5s
Push Pipeline / generate_licenses (push) Successful in 35s
Push Pipeline / format (push) Successful in 54s
Push Pipeline / build (push) Successful in 5m32s
Reviewed-on: #196
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
2026-04-14 16:12:05 +00:00
e827f4c527 Fixed references
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 42s
Pull Request Pipeline / lint (pull_request) Successful in 47s
2026-04-13 22:53:39 +02:00
73c85b1ff2 Renamed score dao
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 44s
Pull Request Pipeline / test (pull_request) Failing after 1m33s
2026-04-13 22:49:30 +02:00
c43b7b478c Updated comments
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 44s
Pull Request Pipeline / lint (pull_request) Successful in 46s
2026-04-13 22:47:29 +02:00
c9188c222a Added edge cases for games, groups, teams and matches 2026-04-13 22:16:34 +02:00
fcca74cea5 Refactoring 2026-04-13 22:16:12 +02:00
80672343b9 Refactoring
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 41s
Pull Request Pipeline / lint (pull_request) Successful in 48s
2026-04-12 02:02:27 +02:00
d903a9fd7e Refactoring 2026-04-12 02:02:16 +02:00
bed8a05057 Refactoring 2026-04-12 02:01:26 +02:00
8a312152a5 Refactoring 2026-04-12 02:01:12 +02:00
eeb68496d5 Removed tester input
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 41s
Pull Request Pipeline / lint (pull_request) Successful in 45s
2026-04-12 01:42:47 +02:00
65704b4a03 Updated schema to add scores attribute
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 49s
2026-04-12 01:42:02 +02:00
f40113ef2c Added restriction to schema 2026-04-12 01:37:31 +02:00
723699d363 Added test for json schema
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 44s
Pull Request Pipeline / lint (pull_request) Successful in 46s
2026-04-12 01:32:21 +02:00
0823a4ed41 Corrected type
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 42s
Pull Request Pipeline / lint (pull_request) Successful in 46s
2026-04-12 01:21:17 +02:00
22753d29c1 Refactored method, added tests for DTS
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 44s
Pull Request Pipeline / lint (pull_request) Successful in 45s
2026-04-12 01:20:13 +02:00
541cbe9a54 Overhauled score tests
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 40s
Pull Request Pipeline / lint (pull_request) Successful in 45s
2026-04-12 00:07:16 +02:00
26d60fc8b2 Updated score saving in match 2026-04-11 23:34:57 +02:00
520edd0ca6 Removed matchId from Score class
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 43s
Pull Request Pipeline / lint (pull_request) Successful in 49s
2026-04-09 18:00:19 +02:00
73533b8c4f Renamed ScoreEntry to Score 2026-04-08 23:55:24 +02:00
be58c9ce01 Refactoring + fixed tests 2026-04-08 23:53:27 +02:00
6a49b92310 Moved dao methods 2026-04-08 23:33:42 +02:00
855b7c8bea Moved dao methods 2026-04-08 23:22:33 +02:00
e10f05adb5 Temp fixed all database functionality 2026-04-08 22:33:02 +02:00
ad6d08374e Renamed folder to "models" 2026-04-08 22:27:50 +02:00
14d46d7e52 Seperated score entry class
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 42s
Pull Request Pipeline / lint (pull_request) Failing after 47s
2026-04-08 22:20:47 +02:00
b6fa71726e Added scores to match class 2026-04-08 22:20:07 +02:00
98c846ddc6 Added scores to match class 2026-04-08 22:19:01 +02:00
42f476919b Removed score column 2026-04-08 22:17:37 +02:00
13c88cb958 Updated licenses [skip ci] 2026-03-09 20:32:00 +00:00
6e4375e459 Updated version number [skip ci] 2026-03-09 20:31:22 +00:00
59d1efb4fb Merge pull request 'Bearbeiten und Löschen von Gruppen' (#148) from feature/118-bearbeiten-und-löschen-von-gruppen into development
All checks were successful
Push Pipeline / test (push) Successful in 38s
Push Pipeline / update_version (push) Successful in 5s
Push Pipeline / generate_licenses (push) Successful in 34s
Push Pipeline / format (push) Successful in 52s
Push Pipeline / build (push) Successful in 5m8s
Reviewed-on: #148
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2026-03-09 20:30:38 +00:00
611033b5cd Moved getGroupMatches + Tests
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 39s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-03-09 21:27:05 +01:00
4e98dcde41 remove nullable from match name
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 39s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-03-09 21:13:20 +01:00
a304d9adf7 change getGroupMatches 2026-03-09 21:13:08 +01:00
23d00c64ab fix comments
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 40s
Pull Request Pipeline / lint (pull_request) Successful in 45s
2026-03-09 21:00:13 +01:00
4726d170a1 update dependencies in pubspec.yaml for build_runner and drift_dev
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 39s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-03-09 15:29:26 +01:00
3fe421676c update onDelete behavior for groupId in match_table to set null 2026-03-09 15:29:18 +01:00
b0b039875a add callback & implement deleteObsoleteMatchGroupRelations func 2026-03-09 15:29:03 +01:00
840faab024 add statistics reload and fix wrong best player calculation 2026-03-09 15:28:37 +01:00
6c50eaefc7 Add deleteMatchGroup method & tests 2026-03-09 15:27:15 +01:00
4f91130cb5 Merge remote-tracking branch 'origin/feature/118-bearbeiten-und-löschen-von-gruppen' into feature/118-bearbeiten-und-löschen-von-gruppen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 37s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-03-08 15:10:37 +01:00
2214ea8e7d Refactor group creation and editing logic into separate methods 2026-03-08 15:10:31 +01:00
5b7ef4051d Merge remote-tracking branch 'origin/feature/118-bearbeiten-und-löschen-von-gruppen' into feature/118-bearbeiten-und-löschen-von-gruppen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 38s
Pull Request Pipeline / lint (pull_request) Successful in 45s
2026-03-08 12:22:57 +01:00
e3c39521a0 Merge remote-tracking branch 'origin/feature/118-bearbeiten-und-löschen-von-gruppen' into feature/118-bearbeiten-und-löschen-von-gruppen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 39s
Pull Request Pipeline / lint (pull_request) Successful in 45s
2026-03-08 11:27:38 +01:00
b83719f16d Refactor group saving logic into a separate method 2026-03-08 11:27:18 +01:00
de0344d63d Added game fix 2026-03-08 11:10:06 +01:00
4ae1432943 excluded license file from linter rules
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 38s
Pull Request Pipeline / lint (pull_request) Successful in 47s
2026-03-08 11:07:13 +01:00
feb2b756bd Merge branch 'development' into feature/118-bearbeiten-und-löschen-von-gruppen
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 37s
Pull Request Pipeline / lint (pull_request) Failing after 45s
2026-03-08 08:39:24 +00:00
244c1b0bb0 Updated licenses [skip ci] 2026-03-08 08:29:28 +00:00
bfad74db22 Merge remote-tracking branch 'origin/development' into feature/118-bearbeiten-und-löschen-von-gruppen
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 45s
Pull Request Pipeline / test (pull_request) Successful in 40s
# Conflicts:
#	lib/presentation/views/main_menu/group_view/create_group_view.dart
2026-03-08 09:29:06 +01:00
864fde35ef Updated version number [skip ci] 2026-03-08 08:28:54 +00:00
934c048687 Merge pull request 'Bearbeiten und Löschen von Matches' (#171) from feature/120-bearbeiten-und-loeschen-von-matches into development
All checks were successful
Push Pipeline / test (push) Successful in 38s
Push Pipeline / update_version (push) Successful in 5s
Push Pipeline / generate_licenses (push) Successful in 34s
Push Pipeline / format (push) Successful in 49s
Push Pipeline / build (push) Successful in 5m35s
Reviewed-on: #171
Reviewed-by: Mathis Kirchner <mathis.kirchner.mk@gmail.com>
2026-03-08 08:28:10 +00:00
8e20fe1034 fix dart analysis issues
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 37s
Pull Request Pipeline / lint (pull_request) Successful in 46s
2026-03-07 23:34:43 +01:00
81aad9280c Merge dev & implement db
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 39s
Pull Request Pipeline / lint (pull_request) Failing after 44s
2026-03-07 23:33:25 +01:00
73c8865eb5 Removed todo
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 38s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-03-07 22:48:57 +01:00
9d2b6a0286 Adapted getTemporaryGame method 2026-03-07 22:47:28 +01:00
588b5053e8 typo 2026-03-07 22:46:21 +01:00
0597b21ac1 Added condition 2026-03-07 22:45:55 +01:00
a846e4d7ea Removed dead code 2026-03-07 22:44:49 +01:00
16aecffdbe Change var name 2026-03-07 22:43:24 +01:00
7810443a00 Fix: SetState Error
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 39s
Pull Request Pipeline / lint (pull_request) Successful in 45s
2026-03-07 22:11:51 +01:00
27c6d8b293 Small corrections 2026-03-07 21:57:51 +01:00
0822039a5f Fix: Not updated match view after updating matches players
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 38s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-03-07 21:50:25 +01:00
0b118800e4 Fixed issue with group replacing solo players 2026-03-07 21:33:51 +01:00
90cc9587ca Removed import
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 38s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-03-07 17:43:42 +01:00
4c479676d2 Added properties
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 39s
Pull Request Pipeline / lint (pull_request) Failing after 44s
2026-03-07 17:38:32 +01:00
4bcb10df81 Fix: Winner gets resetted, if player gets removed from the game 2026-03-07 17:03:52 +01:00
664af7ffee Updated dependencies 2026-03-07 16:51:37 +01:00
2bd5c30094 Fix: Setting winner
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 39s
Pull Request Pipeline / lint (pull_request) Successful in 45s
2026-03-06 22:21:07 +01:00
e909f347e3 Removed unused function 2026-03-06 22:11:15 +01:00
8e4fe26ad9 Updated theme 2026-03-06 22:09:58 +01:00
a8ade294b5 Added common.dart
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 40s
Pull Request Pipeline / lint (pull_request) Successful in 46s
2026-03-06 21:53:46 +01:00
688a8a1706 Added comment 2026-03-06 20:59:13 +01:00
598b8d0f9e Updated player receiving logic 2026-03-06 20:59:04 +01:00
cff95aff00 Updated view aligning with new database 2026-03-06 16:51:26 +01:00
f07532c1e2 Fix: Added correct localizations 2026-03-06 16:51:01 +01:00
b68c570d47 Fixed issues with match.players including group.members, added callback 2026-03-06 16:49:56 +01:00
8bd251ac7d Fixed match result view with new db 2026-03-05 22:24:14 +01:00
89ad6824e6 Added import, removed unessecary player add 2026-03-05 22:22:13 +01:00
f9edf64e83 Small changes 2026-03-05 22:21:50 +01:00
37955c5701 test: Setting & fetching winner 2026-03-05 12:25:09 +01:00
5ed35362ac Fix: Setting & fetching winner 2026-03-05 12:25:01 +01:00
8e75f6af56 Merge branch 'development' into feature/120-bearbeiten-und-loeschen-von-matches
# Conflicts:
#	lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart
2026-03-05 11:00:43 +01:00
5b8dd6adb2 Updated version number [skip ci] 2026-03-04 19:02:47 +00:00
1273c99fdc Merge pull request 'Neue Datenbank Struktur' (#156) from feature/88-neue-datenbank-struktur into development
Some checks failed
Push Pipeline / test (push) Successful in 36s
Push Pipeline / update_version (push) Successful in 5s
Push Pipeline / generate_licenses (push) Failing after 25s
Push Pipeline / format (push) Has been skipped
Push Pipeline / build (push) Successful in 6m12s
Reviewed-on: #156
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
Reviewed-by: Mathis Kirchner <mathis.kirchner.mk@gmail.com>
2026-03-04 19:02:03 +00:00
gelbeinhalb
e5bc2076dc Merge remote-tracking branch 'origin/development' into feature/88-neue-datenbank-struktur
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 40s
Pull Request Pipeline / lint (pull_request) Successful in 46s
2026-03-04 19:34:38 +01:00
gelbeinhalb
5d45339337 set notes default
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 38s
Pull Request Pipeline / lint (pull_request) Successful in 45s
2026-03-04 13:36:07 +01:00
gelbeinhalb
f04d57382c set notes default
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 41s
Pull Request Pipeline / lint (pull_request) Successful in 47s
2026-03-04 13:35:43 +01:00
gelbeinhalb
5094554475 set description default 2026-03-04 13:35:18 +01:00
gelbeinhalb
866f79998c set description default 2026-03-04 13:35:04 +01:00
e71943f6e2 Implemented Radio Theme
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 35s
Pull Request Pipeline / lint (pull_request) Successful in 46s
2026-02-24 18:01:10 +01:00
f07103a516 Fixed theme issue 2026-02-24 17:49:24 +01:00
b84a893706 Typo
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 36s
Pull Request Pipeline / lint (pull_request) Successful in 42s
2026-02-23 21:35:26 +01:00
527ffd194f Fixed PR problems
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 37s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-02-23 21:34:29 +01:00
9e9491c98e Merge remote-tracking branch 'refs/remotes/origin/development' into feature/120-bearbeiten-und-loeschen-von-matches
# Conflicts:
#	lib/presentation/views/main_menu/custom_navigation_bar.dart
#	lib/presentation/views/main_menu/group_view/create_group_view.dart
#	lib/presentation/views/main_menu/group_view/group_detail_view.dart
#	lib/presentation/views/main_menu/group_view/group_view.dart
#	lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart
#	lib/presentation/views/main_menu/match_view/match_view.dart
#	pubspec.yaml
2026-02-23 21:23:28 +01:00
gelbeinhalb
8e4cff19d1 change comment 2026-02-23 19:29:20 +01:00
048fb0ef43 Updated licenses [skip ci] 2026-02-07 20:19:54 +00:00
a4bc03111d Updated version number [skip ci] 2026-02-07 20:19:24 +00:00
00abc4d1c0 Merge pull request 'NavBar optimieren' (#189) from enhancement/188-navbar-optimieren into development
All checks were successful
Push Pipeline / test (push) Successful in 33s
Push Pipeline / update_version (push) Successful in 5s
Push Pipeline / generate_licenses (push) Successful in 28s
Push Pipeline / format (push) Successful in 53s
Push Pipeline / build (push) Successful in 6m2s
Reviewed-on: #189
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
2026-02-07 20:18:46 +00:00
d4fcc8106f Removed spacing in navbar item
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 37s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-02-07 20:21:46 +01:00
gelbeinhalb
e4ea46c6cd Merge remote-tracking branch 'origin/development' into feature/88-neue-datenbank-struktur
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 40s
Pull Request Pipeline / test (pull_request) Successful in 36s
2026-02-07 20:11:51 +01:00
gelbeinhalb
e881cf0555 fix expected null when empty string is correct
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 40s
Pull Request Pipeline / test (pull_request) Successful in 35s
2026-02-07 18:17:08 +01:00
gelbeinhalb
278544788e fix formatting
Some checks failed
Pull Request Pipeline / lint (pull_request) Successful in 1m25s
Pull Request Pipeline / test (pull_request) Failing after 36s
2026-02-07 17:40:27 +01:00
gelbeinhalb
a12f4eb1c1 implement winner methods
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 28s
Pull Request Pipeline / lint (pull_request) Failing after 40s
2026-02-07 17:35:43 +01:00
gelbeinhalb
0eb8e2683c add replace players in a match test 2026-02-07 17:31:23 +01:00
gelbeinhalb
07b9b78252 add replace players in a match method 2026-02-07 17:31:19 +01:00
gelbeinhalb
fa9ad9dbae add test for replacing group players 2026-02-07 17:26:07 +01:00
gelbeinhalb
25699dffc0 add replaceGroupPlayers method 2026-02-07 17:25:49 +01:00
487efb4d61 Added type annotation
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 35s
Pull Request Pipeline / lint (pull_request) Successful in 46s
2026-02-06 14:07:43 +01:00
3f790cfbb1 Renamed variable 2026-02-06 14:04:16 +01:00
ee1962ef9c Implemented new nav bar 2026-02-06 14:03:57 +01:00
a4d4703069 Updated licenses [skip ci] 2026-02-06 12:32:19 +00:00
fabb7bae19 Updated version number [skip ci] 2026-02-06 12:31:47 +00:00
gelbeinhalb
70d6178829 restructure tests 2026-02-01 19:51:57 +01:00
gelbeinhalb
7aba8554c0 players cant be null
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 40s
Pull Request Pipeline / lint (pull_request) Failing after 44s
2026-02-01 18:23:58 +01:00
gelbeinhalb
dbef735a82 add color enum 2026-02-01 18:13:03 +01:00
gelbeinhalb
ccfea71a35 reduce line length 2026-02-01 17:56:24 +01:00
gelbeinhalb
415cae18cd added endedAt to matches 2026-02-01 17:55:42 +01:00
gelbeinhalb
2a3ea32193 change how optional parameters are defined 2026-02-01 17:31:40 +01:00
gelbeinhalb
2ea68dcc89 fix errors after merging 2026-02-01 17:29:19 +01:00
gelbeinhalb
acf3a7b003 Merge remote-tracking branch 'origin/development' into feature/88-neue-datenbank-struktur
# Conflicts:
#	lib/data/dao/group_match_dao.dart
#	lib/data/dao/match_dao.dart
#	lib/data/dao/player_match_dao.dart
#	lib/data/db/database.dart
#	lib/data/db/database.g.dart
#	lib/data/db/tables/group_match_table.dart
#	lib/data/db/tables/player_match_table.dart
#	lib/data/dto/match.dart
#	lib/presentation/views/main_menu/home_view.dart
#	lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart
#	lib/presentation/views/main_menu/match_view/match_view.dart
#	lib/services/data_transfer_service.dart
#	test/db_tests/game_test.dart
#	test/db_tests/group_match_test.dart
#	test/db_tests/player_match_test.dart
2026-02-01 15:55:06 +01:00
gelbeinhalb
1d352821fc all parameters are now required 2026-01-30 13:13:06 +01:00
d1458443eb Added ref again
Some checks failed
Push Pipeline / update_version (push) Successful in 6s
Push Pipeline / build (push) Failing after 31s
Push Pipeline / generate_licenses (push) Successful in 30s
Push Pipeline / test (push) Successful in 38s
Push Pipeline / format (push) Successful in 41s
2026-01-26 14:47:26 +01:00
9c00b48de5 Updated licenses [skip ci] 2026-01-26 13:38:35 +00:00
03ce304a0a Updated version number [skip ci] 2026-01-26 13:38:01 +00:00
dde617d429 Tried new workflow fix
Some checks failed
Push Pipeline / update_version (push) Successful in 4s
Push Pipeline / generate_licenses (push) Successful in 33s
Push Pipeline / test (push) Successful in 39s
Push Pipeline / format (push) Successful in 47s
Push Pipeline / build (push) Failing after 1m42s
2026-01-26 14:37:23 +01:00
03a2df4fdf Updated version number [skip ci] 2026-01-26 08:55:12 +00:00
96ef70b209 Updated version number [skip ci] 2026-01-26 08:45:49 +00:00
4ae59ec881 Merge pull request 'README vervollständigen' (#150) from enhancement/81-readme-vervollstaendigen into development
Some checks failed
Push Pipeline / update_version (push) Successful in 5s
Push Pipeline / generate_licenses (push) Failing after 30s
Push Pipeline / format (push) Has been skipped
Push Pipeline / test (push) Successful in 38s
Push Pipeline / build (push) Successful in 5m5s
Reviewed-on: #150
2026-01-26 08:45:42 +00:00
gelbeinhalb
3bd6dd4189 fix typo 2026-01-25 10:15:50 +01:00
c162e245bd Updated license name in Readme
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 37s
Pull Request Pipeline / lint (pull_request) Successful in 42s
2026-01-24 22:35:11 +01:00
481afd7533 Added example CONTRIBUTING.md
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 37s
Pull Request Pipeline / lint (pull_request) Successful in 43s
2026-01-24 19:17:33 +01:00
af42ebbf92 Merge remote-tracking branch 'origin/development' into enhancement/81-readme-vervollstaendigen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 37s
Pull Request Pipeline / lint (pull_request) Successful in 45s
2026-01-24 17:48:25 +01:00
34e442dbe9 added white space
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2026-01-24 17:47:32 +01:00
8783d0ac44 Version badges in one row
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2026-01-24 17:30:56 +01:00
7f356b0c86 Updated licenses [skip ci] 2026-01-24 13:55:49 +00:00
e4a2ac6b47 Updated version number [skip ci] 2026-01-24 13:55:18 +00:00
6065b53ce9 Updated workflow
All checks were successful
Push Pipeline / test (push) Successful in 36s
Push Pipeline / update_version (push) Successful in 5s
Push Pipeline / generate_licenses (push) Successful in 30s
Push Pipeline / format (push) Successful in 50s
Push Pipeline / build (push) Successful in 5m21s
2026-01-24 14:53:31 +01:00
c644ccfb06 Updated version number [skip ci] 2026-01-24 12:21:35 +00:00
2dd7d0285d Pipeline Push Error (again) (#187)
Some checks failed
Push Pipeline / test (push) Successful in 34s
Push Pipeline / update_version (push) Successful in 5s
Push Pipeline / generate_licenses (push) Failing after 30s
Push Pipeline / format (push) Has been skipped
Push Pipeline / build (push) Successful in 5m23s
Pipeline Push Error (again) (#187)
Co-authored-by: Gitea Actions [bot] <actions@yannick-weigert.de>
Reviewed-on: #187
2026-01-24 12:20:55 +00:00
cf5883b430 Updated version number [skip ci] 2026-01-24 12:03:27 +00:00
7af2b43193 Merge pull request 'Hotfix: Pipeline Push Error' (#186) from hotifx/pipeline-push-error into development
Some checks failed
Push Pipeline / test (push) Successful in 36s
Push Pipeline / update_version (push) Successful in 5s
Push Pipeline / generate_licenses (push) Failing after 28s
Push Pipeline / format (push) Has been skipped
Push Pipeline / build (push) Successful in 5m15s
Reviewed-on: #186
2026-01-24 12:02:46 +00:00
90cc4d4c2a Added missing flutter step
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 36s
Pull Request Pipeline / lint (pull_request) Successful in 42s
2026-01-24 12:49:40 +01:00
db2ba2571f Added pull 2026-01-24 12:49:21 +01:00
3b01c4623c Updated version number [skip ci] 2026-01-24 11:41:50 +00:00
c5c9a43459 Update licenses in push pipeline (#184)
Some checks failed
Push Pipeline / test (push) Successful in 35s
Push Pipeline / update_version (push) Successful in 4s
Push Pipeline / generate_licenses (push) Failing after 2m1s
Push Pipeline / format (push) Has been skipped
Push Pipeline / build (push) Successful in 5m22s
Update licenses in push pipeline (#184)
Co-authored-by: Gitea Actions [bot] <actions@yannick-weigert.de>
Reviewed-on: #184
2026-01-24 11:41:07 +00:00
b2e58f2539 Updated version number [skip ci] 2026-01-24 10:33:32 +00:00
58dc5d8c2e Extend workflow to include build stage (#185)
All checks were successful
Push Pipeline / test (push) Successful in 35s
Push Pipeline / format (push) Successful in 50s
Push Pipeline / update_version (push) Successful in 4s
Push Pipeline / build (push) Successful in 5m22s
Extend workflow to include build stage (#185)
Reviewed-on: #185
Reviewed-by: Mathis Kirchner <mathis.kirchner.mk@gmail.com>
2026-01-24 10:32:00 +00:00
fd7dbd1155 Added gnu license
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2026-01-23 22:30:54 +01:00
61b72cf595 Merge remote-tracking branch 'origin/enhancement/81-readme-vervollstaendigen' into enhancement/81-readme-vervollstaendigen
# Conflicts:
#	LICENSE
2026-01-23 22:15:01 +01:00
81c6c377d4 Added placeholder LICENSE and CONTRIBUTING.md 2026-01-23 22:14:51 +01:00
92f2b4db95 Added placeholder LICENSE and CONTRIBUTING.md
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2026-01-23 22:14:12 +01:00
5178adf71c Changed screenshot order
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m14s
2026-01-23 22:11:33 +01:00
4a6d639f1c Updated readme
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 1m59s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2026-01-23 22:10:30 +01:00
e9cf707fca Merge branch 'refs/heads/development' into enhancement/81-readme-vervollstaendigen 2026-01-23 20:56:56 +01:00
2b7941202a Updated version number [skip ci] 2026-01-23 12:31:09 +00:00
92317bcbdf Merge pull request 'App-Name, Icon und Design' (#179) from documentation/131-app-name-icon-und-design into development
All checks were successful
Push Pipeline / test (push) Successful in 1m56s
Push Pipeline / format (push) Successful in 2m4s
Push Pipeline / update_version (push) Successful in 4s
Reviewed-on: #179
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
Reviewed-by: Mathis Kirchner <mathis.kirchner.mk@gmail.com>
2026-01-23 12:28:56 +00:00
gelbeinhalb
b0cb385756 ruleset is now a required game enum parameter
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2026-01-23 11:54:37 +01:00
gelbeinhalb
118b316a35 description is now a required game parameter 2026-01-23 11:25:20 +01:00
gelbeinhalb
55f5aac4e2 color is now a required game parameter 2026-01-23 11:08:11 +01:00
ceade5cafd Fixed ios laucn screen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2026-01-22 22:34:36 +01:00
6060afc543 Renamed package & bundle identifier
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 1m58s
Pull Request Pipeline / lint (pull_request) Successful in 2m18s
2026-01-22 22:13:12 +01:00
ac6399d707 Merge branch 'development' into documentation/131-app-name-icon-und-design 2026-01-22 21:46:11 +01:00
gelbeinhalb
6006c6d3f7 Merge remote-tracking branch 'origin/development' into feature/88-neue-datenbank-struktur
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 1m58s
Pull Request Pipeline / lint (pull_request) Successful in 2m17s
2026-01-21 16:10:47 +01:00
gelbeinhalb
0ddb4edbc9 remove unused variable
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
2026-01-21 16:08:21 +01:00
gelbeinhalb
7339194ba0 fix data import and export
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Failing after 2m18s
2026-01-21 15:43:56 +01:00
gelbeinhalb
bd5e38a3ca save game to database on create 2026-01-21 15:42:47 +01:00
gelbeinhalb
12b713bb70 remove skip from test
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2026-01-21 12:04:13 +01:00
gelbeinhalb
e55cea0dcc fix missing await 2026-01-21 12:04:00 +01:00
gelbeinhalb
b2a3a0cf75 fix getting non-existent team throws exception test
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m19s
2026-01-21 11:55:15 +01:00
gelbeinhalb
f142169371 fix overlapping members in teams test 2026-01-21 11:54:00 +01:00
gelbeinhalb
0b778210ef add new team tests
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2026-01-21 11:36:23 +01:00
gelbeinhalb
748361d04f add new score tests 2026-01-21 11:33:37 +01:00
gelbeinhalb
2e6a2b8e55 add more player_match tests 2026-01-21 11:26:46 +01:00
gelbeinhalb
15894096f3 fix missing await error 2026-01-21 11:11:46 +01:00
gelbeinhalb
d488eac3ae add more group tests 2026-01-21 11:11:37 +01:00
gelbeinhalb
19b2685714 add more player tests 2026-01-21 11:03:09 +01:00
gelbeinhalb
068ca95afc Merge branch 'feature/88-neue-datenbank-struktur' of git.yannick-weigert.de:liquid-development/game-tracker into feature/88-neue-datenbank-struktur
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m16s
2026-01-20 16:34:43 +01:00
gelbeinhalb
0d28f4b87c remove winner test again 2026-01-20 16:34:38 +01:00
gelbeinhalb
2e454a530a remove winner test again 2026-01-20 16:33:12 +01:00
gelbeinhalb
6c39e1e574 remove winner check
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2026-01-20 16:31:52 +01:00
gelbeinhalb
b33260ec23 fix match missing game 2026-01-20 16:31:43 +01:00
8ee2b6cb06 Updated version number [skip ci] 2026-01-20 14:55:05 +00:00
b14b7733ca Merge pull request 'Push Pipeline Error' (#181) from hotfix/push-pipeline-error into development
All checks were successful
Push Pipeline / test (push) Successful in 2m7s
Push Pipeline / format (push) Successful in 2m8s
Push Pipeline / update_version (push) Successful in 5s
Reviewed-on: #181
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
2026-01-20 14:52:47 +00:00
bc51b23563 Updated ref names
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m13s
Pull Request Pipeline / test (pull_request) Successful in 2m22s
2026-01-20 12:03:31 +01:00
057f8c1d58 Changed workflow back to prod mode 2026-01-20 12:00:33 +01:00
4c1c22123e Updated version number [skip ci] 2026-01-20 10:59:27 +00:00
e9929426e0 Removed development restriction
All checks were successful
Push Pipeline / test (push) Successful in 1m57s
Push Pipeline / update_version (push) Successful in 6s
Push Pipeline / format (push) Successful in 2m6s
2026-01-20 11:57:17 +01:00
eb404f3ef2 Fixed push pipeline
All checks were successful
Push Pipeline / test (push) Successful in 1m57s
Push Pipeline / update_version (push) Has been skipped
Push Pipeline / format (push) Successful in 2m17s
2026-01-20 11:54:15 +01:00
c7b4623198 Workflows um Format Stage erweitern (#175)
Some checks failed
Push Pipeline / test (push) Successful in 1m58s
Push Pipeline / update_version (push) Failing after 4s
Push Pipeline / format (push) Successful in 2m8s
Extend workflows with format stage

Co-authored-by: Gitea Actions [bot] <>
Reviewed-on: #175
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
2026-01-20 10:48:49 +00:00
ccd0c62e3c Added launchscreen
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m3s
Pull Request Pipeline / test (pull_request) Successful in 1m56s
2026-01-19 20:04:58 +01:00
9f71c22a56 Fixed pipeline
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
Pull Request Pipeline / test (pull_request) Successful in 2m5s
2026-01-19 16:14:26 +01:00
87d7fbebcd Updated quick create button color
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m6s
Pull Request Pipeline / test (pull_request) Successful in 2m25s
2026-01-19 16:07:20 +01:00
c625174017 Updated text color 2026-01-19 16:07:12 +01:00
53a33ca2e1 Updated app name
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
Pull Request Pipeline / test (pull_request) Successful in 1m56s
2026-01-18 22:58:28 +01:00
e1dd40a1c3 first app icon and theme update 2026-01-18 22:53:54 +01:00
9a5929382b Changed button icon
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
2026-01-18 13:08:33 +01:00
cca09cc27e Renamed MatchProfileView to MatchDetailView 2026-01-18 13:01:24 +01:00
f00aa15518 Merge remote-tracking branch 'origin/feature/118-bearbeiten-und-löschen-von-gruppen' into feature/120-bearbeiten-und-loeschen-von-matches
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m15s
# Conflicts:
#	lib/l10n/generated/app_localizations.dart
2026-01-18 12:59:56 +01:00
810f635987 merge fix
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2026-01-18 12:25:47 +01:00
49a6259d8a Merge remote-tracking branch 'origin/development' into feature/118-bearbeiten-und-löschen-von-gruppen
# Conflicts:
#	lib/presentation/views/main_menu/group_view/create_group_view.dart
#	pubspec.yaml
2026-01-18 12:24:07 +01:00
e4c3bc1c5e merge & made group_detail_view.dart & group_create_view.dart work together when editing
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
2026-01-18 11:41:50 +01:00
14d30f55a7 Merge remote-tracking branch 'origin/development' into feature/118-bearbeiten-und-löschen-von-gruppen
# Conflicts:
#	lib/l10n/arb/app_de.arb
#	lib/l10n/arb/app_en.arb
#	lib/l10n/generated/app_localizations.dart
#	lib/l10n/generated/app_localizations_de.dart
#	lib/l10n/generated/app_localizations_en.dart
#	lib/presentation/views/main_menu/group_view/groups_view.dart
#	lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart
#	lib/presentation/widgets/tiles/group_tile.dart
#	pubspec.yaml
2026-01-18 11:15:04 +01:00
f1df067824 delete group view & update build nr
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m14s
2026-01-18 10:51:53 +01:00
765610b184 Enhanced editing
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m18s
2026-01-18 01:02:35 +01:00
48d99a0386 Added match editing 2026-01-18 00:46:50 +01:00
a56e738064 Implemented winner update 2026-01-18 00:42:18 +01:00
1450e9b958 Updated attributes 2026-01-18 00:36:21 +01:00
eb114f2853 Added empty winner tile 2026-01-18 00:36:15 +01:00
7faf80de03 Updated extra player count 2026-01-18 00:14:45 +01:00
8fe01c332e Added comment 2026-01-18 00:10:31 +01:00
374c9295ef Implemented MatchProfileView 2026-01-18 00:08:39 +01:00
8b7300eac3 Merge remote-tracking branch 'origin/development' into feature/118-bearbeiten-und-löschen-von-gruppen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m7s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
# Conflicts:
#	pubspec.yaml
2026-01-17 16:05:08 +01:00
919a38afe5 fix merge conflicts
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2026-01-17 10:21:55 +01:00
def31acfb1 Merge remote-tracking branch 'origin/development' into feature/118-bearbeiten-und-löschen-von-gruppen
# Conflicts:
#	lib/presentation/views/main_menu/group_view/create_group_view.dart
#	lib/presentation/views/main_menu/settings_view/settings_view.dart
#	lib/presentation/widgets/tiles/group_tile.dart
2026-01-17 10:16:41 +01:00
016c1ceb6e add context to mounted check
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2026-01-13 21:38:53 +01:00
1b297d15b0 fix snackbar showing also showing on other screens (#155)
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m10s
Pull Request Pipeline / lint (pull_request) Successful in 2m12s
2026-01-13 21:35:10 +01:00
ed642e3d4f merge dev into #118
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 1m59s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
2026-01-13 21:07:23 +01:00
7cc3873a31 Merge remote-tracking branch 'origin/development' into feature/118-bearbeiten-und-löschen-von-gruppen
# Conflicts:
#	lib/presentation/views/main_menu/settings_view.dart
2026-01-13 21:02:04 +01:00
b1e9bb3aeb update localization comments for clarity in group and match creation 2026-01-13 21:01:31 +01:00
5da12939cb Updated Dart version
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m12s
Pull Request Pipeline / lint (pull_request) Successful in 2m18s
2026-01-12 20:30:57 +01:00
0d20b5847f Updated versions
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m19s
2026-01-12 20:29:18 +01:00
973e327232 Merge remote-tracking branch 'origin/development' into enhancement/81-readme-vervollstaendigen 2026-01-12 20:29:06 +01:00
caf60d046b fix merge mistake
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 3m9s
Pull Request Pipeline / test (pull_request) Successful in 2m30s
2026-01-10 22:20:14 +01:00
38663c6b67 Merge remote-tracking branch 'origin/development' into feature/118-bearbeiten-und-löschen-von-gruppen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m33s
Pull Request Pipeline / lint (pull_request) Successful in 2m44s
# Conflicts:
#	lib/l10n/arb/app_de.arb
#	lib/l10n/arb/app_en.arb
#	lib/l10n/generated/app_localizations_de.dart
#	lib/presentation/views/main_menu/settings_view.dart
2026-01-10 22:19:19 +01:00
ee84c60ba6 remove questionmark
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2026-01-10 22:13:26 +01:00
b6dd0541ae rename CreateGroupView to GroupDetailView for clarity and consistency
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
2026-01-10 22:11:28 +01:00
525acec1d3 Merge branch 'development' into feature/118-bearbeiten-und-löschen-von-gruppen
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Failing after 2m3s
2026-01-10 20:48:25 +00:00
45a419cae7 implement group edit view
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Failing after 2m3s
2026-01-10 20:14:37 +01:00
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
153 changed files with 39426 additions and 4319 deletions

View File

@@ -10,22 +10,22 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
# Required for Flutter action
- name: Install jq - name: Install jq
run: | run: |
apt-get update apt-get update
apt-get install -y jq apt-get install -y jq
- name: Install Flutter (wget) - name: Set up Flutter
run: | uses: subosito/flutter-action@v2
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz with:
tar xf flutter_linux_3.38.2-stable.tar.xz channel: stable
# Set Git safe directory for Flutter path flutter-version: 3.38.6
git config --global --add safe.directory "$(pwd)/flutter"
# Set Flutter path
echo "$(pwd)/flutter/bin" >> $GITHUB_PATH
- name: Get dependencies - name: Get dependencies
run: flutter pub get run: |
git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64
flutter pub get
- name: Analyze Formatting - name: Analyze Formatting
run: flutter analyze lib test run: flutter analyze lib test
@@ -36,22 +36,22 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install dependencies # Required for Flutter action
- name: Install jq
run: | run: |
apt-get update apt-get update
apt-get install -y jq apt-get install -y jq
- name: Install Flutter (wget) - name: Set up Flutter
run: | uses: subosito/flutter-action@v2
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz with:
tar xf flutter_linux_3.38.2-stable.tar.xz channel: stable
# Set Git safe directory for Flutter path flutter-version: 3.38.6
git config --global --add safe.directory "$(pwd)/flutter"
# Set Flutter path
echo "$(pwd)/flutter/bin" >> $GITHUB_PATH
- name: Get dependencies - name: Get dependencies
run: flutter pub get run: |
git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64
flutter pub get
- name: Run tests - name: Run tests
run: flutter test run: flutter test

View File

@@ -7,44 +7,194 @@ on:
- "main" - "main"
jobs: jobs:
format: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: false # Needs bot user
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install dependencies - name: Setup Java (Temurin 17)
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
# Required for Flutter action
- name: Install jq
run: | run: |
apt-get update apt-get update
apt-get install -y jq apt-get install -y jq
- name: Install Flutter (wget) - name: Set up Flutter
run: | uses: subosito/flutter-action@v2
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz with:
tar xf flutter_linux_3.38.2-stable.tar.xz channel: stable
# Set Git safe directory for Flutter path flutter-version: 3.38.6
git config --global --add safe.directory "$(pwd)/flutter"
# Set Flutter path
echo "$(pwd)/flutter/bin" >> $GITHUB_PATH
- name: Get & upgrade dependencies - name: Get dependencies
run: | run: |
git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64
flutter pub get flutter pub get
flutter pub upgrade --major-versions
- name: Auto-format - name: Build APK
run: flutter build apk --release
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
# Required for Flutter action
- name: Install jq
run: | run: |
dart format lib apt-get update
apt-get install -y jq
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.38.6
- name: Get dependencies
run: |
git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64
flutter pub get
- name: Run tests
run: flutter test
update_version:
runs-on: ubuntu-latest
if: gitea.ref == 'refs/heads/development'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.BOT_TOKEN }}
ref: ${{ gitea.ref_name }}
- name: Increment version number
uses: stikkyapp/update-pubspec-version@v2
with:
strategy: 'patch'
path: './pubspec.yaml'
- name: Commit version update
env:
GITEA_TOKEN: ${{ secrets.BOT_TOKEN }}
run: |
git config --global user.name "Gitea Actions [bot]"
git config --global user.email "actions@yannick-weigert.de"
git config pull.rebase false
git pull origin ${{ gitea.ref_name }}
git add pubspec.yaml
git commit -m "Updated version number [skip ci]"
git push origin HEAD:${{ gitea.ref_name }}
generate_licenses:
runs-on: ubuntu-latest
needs: update_version
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.BOT_TOKEN }}
ref: ${{ gitea.ref_name }}
# Required for Flutter action
- name: Install jq
run: |
apt-get update
apt-get install -y jq
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.38.6
- name: Get dependencies
run: |
git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64
flutter pub get
- name: Generate oss_licenses.dart
run: flutter pub run dart_pubspec_licenses:generate -o lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart
- name: Commit license update
env:
GITEA_TOKEN: ${{ secrets.BOT_TOKEN }}
run: |
if [ -n "$(git status --porcelain lib test)" ]; then
git config --global user.name "Gitea Actions [bot]"
git config --global user.email "actions@yannick-weigert.de"
git config pull.rebase false
git pull origin ${{ gitea.ref_name }}
git add lib test
git commit -m "Updated licenses [skip ci]"
git push origin HEAD:${{ gitea.ref_name }}
else
echo "No changes to commit"
fi
format:
runs-on: ubuntu-latest
needs: [update_version, generate_licenses]
steps:
- name: Checkout code
uses: actions/checkout@v4
# Required for Flutter action
- name: Install jq
run: |
apt-get update
apt-get install -y jq
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.38.6
- name: Get dependencies
run: |
git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64
flutter pub get
- name: Check code format
id: check_format
continue-on-error: true
run: flutter analyze lib test
- name: Format code
if: steps.check_format.outcome == 'failure'
env:
GITEA_TOKEN: ${{ secrets.BOT_TOKEN }}
run: |
git fetch origin ${{ gitea.ref_name }}
git checkout ${{ gitea.ref_name }}
dart fix --apply lib dart fix --apply lib
dart fix --apply test
# Needs credentials, push access and the right files need to be staged if [ -n "$(git status --porcelain lib test)" ]; then
- name: Commit Changes git config --global user.name "Gitea Actions [bot]"
run: | git config --global user.email "actions@yannick-weigert.de"
git config --global user.name "Gitea Actions" git config pull.rebase false
git config --global user.email "actions@gitea.com" git pull origin ${{ gitea.ref_name }}
git status git add lib test
git add lib/ git commit -m "Auto-format code [skip ci]"
git status git push origin HEAD:${{ gitea.ref_name }}
git commit -m "Actions: Auto-formatting [skip ci]" else
git push echo "No changes to commit"
fi
- name: Verify format
run: flutter analyze lib test

13
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,13 @@
# Contributing
## Code of Conduct
`<insert link to code of conduct here>`
## Code Style
`<insert styling guidelines here>`
## Repository structure
`<insert folder structure and explanation here>`

165
LICENSE Normal file
View File

@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@@ -1,7 +1,63 @@
# Game Tracker <p align="center">
<img alt="Tallee Logo" src="/artefacts/app-logo.png" width="200"/>
<h2 align="center">Tallee</h2>
</p>
<p align="center">
An open-source app to track card- and board games, manage players & groups and get statistics about your played games.
</p>
<p align="center">
<a href="https://apps.apple.com/">
<img src="https://tools.applemediaservices.com/api/badges/download-on-the-app-store/black/en-US"
alt="Download on the App Store"
height="48"
/>
</a>
<a href="https://play.google.com/">
<img alt="Get it on Google Play"
title="Google Play"
src="https://raw.githubusercontent.com/pd4d10/git-touch/main/assets/google-play-badge.png"
height="48"
/>
</a>
</p>
![Version](https://img.shields.io/badge/App--Version-0.0.1_Alpha-orange)
![Flutter](https://img.shields.io/badge/Flutter-3.38.6-027DFD?logo=flutter)
![iOS26](https://img.shields.io/badge/iOS-26-white?logo=apple)
![Android16](https://img.shields.io/badge/Android-16-3DDC84?logo=android)
## Screenshots
<table align="center" cellspacing="8">
<tr>
<td><img src="/artefacts/screenshot-1.png" alt="Screenshot 1" width="240" /></td>
<td><img src="/artefacts/screenshot-2.png" alt="Screenshot 2" width="240" /></td>
<td><img src="/artefacts/screenshot-3.png" alt="Screenshot 3" width="240" /></td>
<td><img src="/artefacts/screenshot-4.png" alt="Screenshot 4" width="240" /></td>
</tr>
</table>
## Contributing
Contributions are welcome! If you find a bug or have a feature request, please open an issue on GitHub. If you'd like to
contribute code, feel free to fork the repository and submit a pull request. For contribution guidelines, please refer
to [CONTRIBUTING.md](CONTRIBUTING.md).
## License
This project is licensed under the GNU LGPLv3 License. See the [LICENSE](LICENSE) file for details.
## Contributors
<a href="https://github.com/liquiddevelopmentde/game-tracker/graphs/contributors">
<img src="https://contrib.rocks/image?repo=liquiddevelopmentde/game-tracker" />
</a>
## Credits
Tallee is developed and maintained by [Liquid Development](https://liquid-dev.de). For more information or support regarding Tallee, contact us through our website or [hello@liquid-dev.de](mailto:hello@liquid-dev.de).
![Created by Liquid Development](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/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)
A all-in-one app to track card- and board games, manage players and groups and get statistics about your played games.

View File

@@ -12,3 +12,7 @@ linter:
unnecessary_const: true unnecessary_const: true
lines_longer_than_80_chars: false lines_longer_than_80_chars: false
constant_identifier_names: false constant_identifier_names: false
analyzer:
exclude:
- lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart

View File

@@ -6,7 +6,7 @@ plugins {
} }
android { android {
namespace = "com.example.game_tracker" namespace = "de.liquid.tallee"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
@@ -21,7 +21,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.game_tracker" applicationId = "de.liquid.tallee"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion

View File

@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:label="game_tracker" android:label="Tallee"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,4 +1,4 @@
package com.example.game_tracker package de.liquid.tallee
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 B

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 B

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

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

View File

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

BIN
artefacts/app-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
artefacts/screenshot-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

BIN
artefacts/screenshot-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

BIN
artefacts/screenshot-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

BIN
artefacts/screenshot-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

View File

@@ -15,16 +15,94 @@
}, },
"name": { "name": {
"type": "string" "type": "string"
},
"description": {
"type": "string"
} }
}, },
"additionalProperties": false,
"required": [ "required": [
"id", "id",
"createdAt", "createdAt",
"name" "name",
"description"
]
}
},
"games": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
},
"ruleset": {
"type": "string"
},
"description": {
"type": "string"
},
"color": {
"type": "string"
},
"icon": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"id",
"createdAt",
"name",
"ruleset",
"description",
"color",
"icon"
] ]
} }
}, },
"groups": { "groups": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"memberIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"required": [
"id",
"name",
"createdAt",
"description",
"memberIds"
]
}
},
"teams": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
@@ -45,6 +123,7 @@
} }
} }
}, },
"additionalProperties": false,
"required": [ "required": [
"id", "id",
"name", "name",
@@ -67,11 +146,14 @@
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
"endedAt": {
"type": ["string", "null"]
},
"gameId": {
"type": "string"
},
"groupId": { "groupId": {
"anyOf": [ "type": ["string", "null"]
{"type": "string"},
{"type": "null"}
]
}, },
"playerIds": { "playerIds": {
"type": "array", "type": "array",
@@ -79,26 +161,48 @@
"type": "string" "type": "string"
} }
}, },
"winnerId": { "scores": {
"anyOf": [ "type": "object",
{"type": "string"}, "items": {
{"type": "null"} "type": "array",
] "items": {
"type": "string",
"properties": {
"roundNumber": {
"type": "number"
},
"score": {
"type": "number"
},
"change": {
"type": "number"
}
}
}
}
},
"notes": {
"type": "string"
} }
}, },
"additionalProperties": false,
"required": [ "required": [
"id", "id",
"name", "name",
"createdAt", "createdAt",
"groupId", "gameId",
"playerIds" "playerIds",
"notes"
] ]
} }
} }
}, },
"additionalProperties": false,
"required": [ "required": [
"players", "players",
"games",
"groups", "groups",
"teams",
"matches" "matches"
] ]
} }

View File

@@ -20,7 +20,5 @@
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict> </dict>
</plist> </plist>

View File

@@ -152,7 +152,6 @@
B68CF4A64F0B5E45B43D6900 /* Pods-RunnerTests.release.xcconfig */, B68CF4A64F0B5E45B43D6900 /* Pods-RunnerTests.release.xcconfig */,
E754D1191B3E54E52B6DCC49 /* Pods-RunnerTests.profile.xcconfig */, E754D1191B3E54E52B6DCC49 /* Pods-RunnerTests.profile.xcconfig */,
); );
name = Pods;
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -478,7 +477,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.gameTracker; PRODUCT_BUNDLE_IDENTIFIER = de.liquid.tallee;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -661,7 +660,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.gameTracker; PRODUCT_BUNDLE_IDENTIFIER = de.liquid.tallee;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -684,7 +683,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.gameTracker; PRODUCT_BUNDLE_IDENTIFIER = de.liquid.tallee;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@@ -2,12 +2,15 @@ import Flutter
import UIKit import UIKit
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,14 +1 @@
{ {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]}
"images" : [
{
"filename" : "icon_x1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -5,9 +5,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "0.043", "blue" : "0.122",
"green" : "0.043", "green" : "0.408",
"red" : "0.043" "red" : "0.937"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

@@ -1,17 +1,8 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "icon.png", "filename" : "icon-transparent.png",
"idiom" : "universal", "idiom" : "universal"
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24412" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/> <device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24405"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@@ -20,12 +20,19 @@
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Tallee" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="m4u-iU-Cmv">
<rect key="frame" x="153" y="747" width="87" height="37"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="Futura-Bold" family="Futura" pointSize="28"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="LauncherIcon" translatesAutoresizingMaskIntoConstraints="NO" id="ygV-Op-Bu5"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="LauncherIcon" translatesAutoresizingMaskIntoConstraints="NO" id="ygV-Op-Bu5">
<rect key="frame" x="46" y="334" width="301" height="184"/> <rect key="frame" x="46" y="334" width="301" height="184"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView> </imageView>
</subviews> </subviews>
<color key="backgroundColor" name="LauncherBackgroundColor"/> <color key="backgroundColor" name="LauncherColor"/>
</view> </view>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
@@ -36,8 +43,8 @@
<color key="tintColor" red="0.90196078431372551" green="0.94509803921568625" blue="0.89411764705882346" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="tintColor" red="0.90196078431372551" green="0.94509803921568625" blue="0.89411764705882346" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<resources> <resources>
<image name="LauncherIcon" width="1000" height="1000"/> <image name="LauncherIcon" width="1000" height="1000"/>
<namedColor name="LauncherBackgroundColor"> <namedColor name="LauncherColor">
<color red="0.90196078431372551" green="0.94509803921568625" blue="0.89411764705882346" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.93699997663497925" green="0.40799999237060547" blue="0.12200000137090683" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
</resources> </resources>
</document> </document>

View File

@@ -2,10 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Game Tracker</string> <string>Tallee</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@@ -13,7 +15,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>game_tracker</string> <string>tallee</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
@@ -22,13 +24,36 @@
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
</array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSApplicationQueriesSchemes</key> <key>UIApplicationSceneManifest</key>
<array> <dict>
<string>https</string> <key>UIApplicationSupportsMultipleScenes</key>
<string>http</string> <false/>
</array> <key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@@ -44,9 +69,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>

45
lib/core/common.dart Normal file
View File

@@ -0,0 +1,45 @@
import 'package:flutter/cupertino.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
/// Translates a [Ruleset] enum value to its corresponding localized string.
String translateRulesetToString(Ruleset ruleset, BuildContext context) {
final loc = AppLocalizations.of(context);
switch (ruleset) {
case Ruleset.highestScore:
return loc.highest_score;
case Ruleset.lowestScore:
return loc.lowest_score;
case Ruleset.singleWinner:
return loc.single_winner;
case Ruleset.singleLoser:
return loc.single_loser;
case Ruleset.multipleWinners:
return loc.multiple_winners;
}
}
/// Counts how many players in the match are not part of the group
/// Returns the count as a string, or an empty string if there is no group
String getExtraPlayerCount(Match match) {
int count = 0;
if (match.group == null) {
return '';
}
final groupMembers = match.group!.members;
final players = match.players;
for (var player in players) {
if (!groupMembers.any((member) => member.id == player.id)) {
count++;
}
}
if (count == 0) {
return '';
}
return ' + ${count.toString()}';
}

View File

@@ -7,25 +7,31 @@ class CustomTheme {
// ==================== Colors ==================== // ==================== Colors ====================
/// Primary color of the app theme /// Primary color of the app theme
static Color primaryColor = const Color(0xFF7505E4); static const Color primaryColor = Color(0xFFef681f);
/// Secondary color of the app theme /// Secondary color of the app theme
static Color secondaryColor = const Color(0xFFAFA2FF); static const Color secondaryColor = Color(0xFFf2a981);
/// Background color of the app theme /// Background color of the app theme
static Color backgroundColor = const Color(0xFF0B0B0B); static const Color backgroundColor = Color(0xFF0B0B0B);
/// Default color for boxes and containers /// Default color for boxes and containers
static Color boxColor = const Color(0xFF101010); static const Color boxColor = Color(0xFF101010);
/// Default border color for boxes and containers /// Default border color for boxes and containers
static Color boxBorder = const Color(0xFF272727); static const Color boxBorderColor = Color(0xFF272727);
/// Color for boxes and containers displayed on boxes /// Color for boxes and containers displayed on boxes
static Color onBoxColor = const Color(0xFF181818); static const Color onBoxColor = Color(0xFF181818);
/// Text color used throughout the app /// Text color used throughout the app
static const Color textColor = Colors.white; static const Color textColor = Color(0xFFFFFFFF);
/// Text color used throughout the app
static const Color hintColor = Color(0xFF888888);
/// Background color for the navigation bar
static const Color navBarBackgroundColor = Color(0xFF131313);
/// Selected color for the [NavbarItem] /// Selected color for the [NavbarItem]
static Color navBarItemSelectedColor = primaryColor.withGreen(100); static Color navBarItemSelectedColor = primaryColor.withGreen(100);
@@ -51,7 +57,7 @@ class CustomTheme {
// ==================== Decorations ==================== // ==================== Decorations ====================
static BoxDecoration standardBoxDecoration = BoxDecoration( static BoxDecoration standardBoxDecoration = BoxDecoration(
color: boxColor, color: boxColor,
border: Border.all(color: boxBorder), border: Border.all(color: boxBorderColor),
borderRadius: standardBorderRadiusAll, borderRadius: standardBorderRadiusAll,
); );
@@ -62,19 +68,38 @@ class CustomTheme {
boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)], boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)],
); );
// ==================== App Bar Theme ==================== // ==================== Component Themes ====================
static AppBarTheme appBarTheme = AppBarTheme( static const AppBarTheme appBarTheme = AppBarTheme(
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
foregroundColor: textColor, foregroundColor: textColor,
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
centerTitle: true, centerTitle: true,
titleTextStyle: const TextStyle( titleTextStyle: TextStyle(
color: textColor, color: textColor,
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
iconTheme: const IconThemeData(color: textColor), iconTheme: IconThemeData(color: textColor),
);
static const SearchBarThemeData searchBarTheme = SearchBarThemeData(
textStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.textColor)),
hintStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.hintColor)),
);
static final RadioThemeData radioTheme = RadioThemeData(
fillColor: WidgetStateProperty.resolveWith<Color>((states) {
if (states.contains(WidgetState.selected)) {
return CustomTheme.primaryColor;
}
return CustomTheme.textColor;
}),
);
static const InputDecorationTheme inputDecorationTheme = InputDecorationTheme(
labelStyle: TextStyle(color: CustomTheme.textColor),
hintStyle: TextStyle(color: CustomTheme.hintColor),
); );
} }

View File

@@ -1,6 +1,3 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart';
/// Button types used for styling the [CustomWidthButton] /// Button types used for styling the [CustomWidthButton]
/// - [ButtonType.primary]: Primary button style. /// - [ButtonType.primary]: Primary button style.
/// - [ButtonType.secondary]: Secondary button style. /// - [ButtonType.secondary]: Secondary button style.
@@ -29,24 +26,27 @@ enum ImportResult {
/// - [ExportResult.unknownException]: An exception occurred during export. /// - [ExportResult.unknownException]: An exception occurred during export.
enum ExportResult { success, canceled, unknownException } enum ExportResult { success, canceled, unknownException }
/// Different rulesets available for matches /// Different rulesets available for games
/// - [Ruleset.singleWinner]: The match is won by a single player /// - [Ruleset.highestScore]: The player with the highest score wins.
/// - [Ruleset.singleLoser]: The match is lost by a single player /// - [Ruleset.lowestScore]: The player with the lowest score wins.
/// - [Ruleset.mostPoints]: The player with the most points wins. /// - [Ruleset.singleWinner]: The match is won by a single player.
/// - [Ruleset.leastPoints]: The player with the fewest points wins. /// - [Ruleset.singleLoser]: The match has a single loser.
enum Ruleset { singleWinner, singleLoser, mostPoints, leastPoints } /// - [Ruleset.multipleWinners]: Multiple players can be winners.
enum Ruleset {
/// Translates a [Ruleset] enum value to its corresponding localized string. highestScore,
String translateRulesetToString(Ruleset ruleset, BuildContext context) { lowestScore,
final loc = AppLocalizations.of(context); singleWinner,
switch (ruleset) { singleLoser,
case Ruleset.singleWinner: multipleWinners,
return loc.single_winner;
case Ruleset.singleLoser:
return loc.single_loser;
case Ruleset.mostPoints:
return loc.most_points;
case Ruleset.leastPoints:
return loc.least_points;
}
} }
/// Different colors available for games
/// - [GameColor.red]: Red color
/// - [GameColor.blue]: Blue color
/// - [GameColor.green]: Green color
/// - [GameColor.yellow]: Yellow color
/// - [GameColor.purple]: Purple color
/// - [GameColor.orange]: Orange color
/// - [GameColor.pink]: Pink color
/// - [GameColor.teal]: Teal color
enum GameColor { red, blue, green, yellow, purple, orange, pink, teal }

View File

@@ -1,7 +1,8 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:tallee/core/enums.dart';
import 'package:game_tracker/data/db/tables/game_table.dart'; import 'package:tallee/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart'; import 'package:tallee/data/db/tables/game_table.dart';
import 'package:tallee/data/models/game.dart';
part 'game_dao.g.dart'; part 'game_dao.g.dart';
@@ -18,9 +19,9 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
(row) => Game( (row) => Game(
id: row.id, id: row.id,
name: row.name, name: row.name,
ruleset: row.ruleset, ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset),
description: row.description, description: row.description,
color: row.color != null ? int.tryParse(row.color!) : null, color: GameColor.values.firstWhere((e) => e.name == row.color),
icon: row.icon, icon: row.icon,
createdAt: row.createdAt, createdAt: row.createdAt,
), ),
@@ -35,9 +36,9 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
return Game( return Game(
id: result.id, id: result.id,
name: result.name, name: result.name,
ruleset: result.ruleset, ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset),
description: result.description, description: result.description,
color: result.color != null ? int.tryParse(result.color!) : null, color: GameColor.values.firstWhere((e) => e.name == result.color),
icon: result.icon, icon: result.icon,
createdAt: result.createdAt, createdAt: result.createdAt,
); );
@@ -52,10 +53,10 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
GameTableCompanion.insert( GameTableCompanion.insert(
id: game.id, id: game.id,
name: game.name, name: game.name,
ruleset: game.ruleset ?? '', ruleset: game.ruleset.name,
description: Value(game.description), description: game.description,
color: Value(game.color?.toString()), color: game.color.name,
icon: Value(game.icon), icon: game.icon,
createdAt: game.createdAt, createdAt: game.createdAt,
), ),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
@@ -78,10 +79,10 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
(game) => GameTableCompanion.insert( (game) => GameTableCompanion.insert(
id: game.id, id: game.id,
name: game.name, name: game.name,
ruleset: game.ruleset ?? '', ruleset: game.ruleset.name,
description: Value(game.description), description: game.description,
color: Value(game.color?.toString()), color: game.color.name,
icon: Value(game.icon), icon: game.icon,
createdAt: game.createdAt, createdAt: game.createdAt,
), ),
) )
@@ -122,17 +123,17 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Updates the ruleset of the game with the given [gameId]. /// Updates the ruleset of the game with the given [gameId].
Future<void> updateGameRuleset({ Future<void> updateGameRuleset({
required String gameId, required String gameId,
required String newRuleset, required Ruleset newRuleset,
}) async { }) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write( await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(ruleset: Value(newRuleset)), GameTableCompanion(ruleset: Value(newRuleset.name)),
); );
} }
/// Updates the description of the game with the given [gameId]. /// Updates the description of the game with the given [gameId].
Future<void> updateGameDescription({ Future<void> updateGameDescription({
required String gameId, required String gameId,
required String? newDescription, required String newDescription,
}) async { }) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write( await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(description: Value(newDescription)), GameTableCompanion(description: Value(newDescription)),
@@ -142,17 +143,17 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Updates the color of the game with the given [gameId]. /// Updates the color of the game with the given [gameId].
Future<void> updateGameColor({ Future<void> updateGameColor({
required String gameId, required String gameId,
required int? newColor, required GameColor newColor,
}) async { }) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write( await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(color: Value(newColor?.toString())), GameTableCompanion(color: Value(newColor.name)),
); );
} }
/// Updates the icon of the game with the given [gameId]. /// Updates the icon of the game with the given [gameId].
Future<void> updateGameIcon({ Future<void> updateGameIcon({
required String gameId, required String gameId,
required String? newIcon, required String newIcon,
}) async { }) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write( await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(icon: Value(newIcon)), GameTableCompanion(icon: Value(newIcon)),
@@ -176,4 +177,3 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
return rowsAffected > 0; return rowsAffected > 0;
} }
} }

View File

@@ -5,4 +5,12 @@ part of 'game_dao.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$GameDaoMixin on DatabaseAccessor<AppDatabase> { mixin _$GameDaoMixin on DatabaseAccessor<AppDatabase> {
$GameTableTable get gameTable => attachedDatabase.gameTable; $GameTableTable get gameTable => attachedDatabase.gameTable;
GameDaoManager get managers => GameDaoManager(this);
}
class GameDaoManager {
final _$GameDaoMixin _db;
GameDaoManager(this._db);
$$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
} }

View File

@@ -1,13 +1,14 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:game_tracker/data/db/tables/group_table.dart'; import 'package:tallee/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/player_group_table.dart'; import 'package:tallee/data/db/tables/match_table.dart';
import 'package:game_tracker/data/dto/group.dart'; import 'package:tallee/data/db/tables/player_group_table.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
part 'group_dao.g.dart'; part 'group_dao.g.dart';
@DriftAccessor(tables: [GroupTable, PlayerGroupTable]) @DriftAccessor(tables: [GroupTable, PlayerGroupTable, MatchTable])
class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin { class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
GroupDao(super.db); GroupDao(super.db);
@@ -58,7 +59,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
GroupTableCompanion.insert( GroupTableCompanion.insert(
id: group.id, id: group.id,
name: group.name, name: group.name,
description: Value(group.description), description: group.description,
createdAt: group.createdAt, createdAt: group.createdAt,
), ),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
@@ -108,7 +109,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
(group) => GroupTableCompanion.insert( (group) => GroupTableCompanion.insert(
id: group.id, id: group.id,
name: group.name, name: group.name,
description: Value(group.description), description: group.description,
createdAt: group.createdAt, createdAt: group.createdAt,
), ),
) )
@@ -136,7 +137,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
(p) => PlayerTableCompanion.insert( (p) => PlayerTableCompanion.insert(
id: p.id, id: p.id,
name: p.name, name: p.name,
description: Value(p.description), description: p.description,
createdAt: p.createdAt, createdAt: p.createdAt,
), ),
) )
@@ -196,7 +197,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGroupDescription({ Future<bool> updateGroupDescription({
required String groupId, required String groupId,
required String? newDescription, required String newDescription,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(groupTable)..where((g) => g.id.equals(groupId))).write( await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
@@ -205,8 +206,6 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
return rowsAffected > 0; return rowsAffected > 0;
} }
/// Retrieves the number of groups in the database. /// Retrieves the number of groups in the database.
Future<int> getGroupCount() async { Future<int> getGroupCount() async {
final count = final count =
@@ -231,4 +230,48 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
/// Replaces all players in a group with the provided list of players.
/// Removes all existing players from the group and adds the new players.
/// Also adds any new players to the player table if they don't exist.
/// Returns `true` if the group exists and players were replaced, `false` otherwise.
Future<bool> replaceGroupPlayers({
required String groupId,
required List<Player> newPlayers,
}) async {
if (!await groupExists(groupId: groupId)) return false;
await db.transaction(() async {
// Remove all existing players from the group
final deleteQuery = delete(db.playerGroupTable)
..where((p) => p.groupId.equals(groupId));
await deleteQuery.go();
// Add new players to the player table if they don't exist
await Future.wait(
newPlayers.map((player) async {
if (!await db.playerDao.playerExists(playerId: player.id)) {
await db.playerDao.addPlayer(player: player);
}
}),
);
// Add the new players to the group
await db.batch(
(b) => b.insertAll(
db.playerGroupTable,
newPlayers
.map(
(player) => PlayerGroupTableCompanion.insert(
playerId: player.id,
groupId: groupId,
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
});
return true;
}
} }

View File

@@ -8,4 +8,25 @@ mixin _$GroupDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable; $PlayerTableTable get playerTable => attachedDatabase.playerTable;
$PlayerGroupTableTable get playerGroupTable => $PlayerGroupTableTable get playerGroupTable =>
attachedDatabase.playerGroupTable; attachedDatabase.playerGroupTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
GroupDaoManager get managers => GroupDaoManager(this);
}
class GroupDaoManager {
final _$GroupDaoMixin _db;
GroupDaoManager(this._db);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
$$PlayerGroupTableTableTableManager get playerGroupTable =>
$$PlayerGroupTableTableTableManager(
_db.attachedDatabase,
_db.playerGroupTable,
);
$$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
$$MatchTableTableTableManager get matchTable =>
$$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable);
} }

View File

@@ -1,13 +1,13 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:game_tracker/data/db/tables/game_table.dart'; import 'package:tallee/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart'; import 'package:tallee/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/match_table.dart'; import 'package:tallee/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:game_tracker/data/dto/game.dart'; import 'package:tallee/data/models/game.dart';
import 'package:game_tracker/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:game_tracker/data/dto/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
part 'match_dao.g.dart'; part 'match_dao.g.dart';
@@ -27,17 +27,25 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
if (row.groupId != null) { if (row.groupId != null) {
group = await db.groupDao.getGroupById(groupId: row.groupId!); group = await db.groupDao.getGroupById(groupId: row.groupId!);
} }
final players = await db.playerMatchDao.getPlayersOfMatch( final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? [];
final scores = await db.scoreEntryDao.getAllMatchScores(
matchId: row.id, matchId: row.id,
); );
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
return Match( return Match(
id: row.id, id: row.id,
name: row.name ?? '', name: row.name,
game: game, game: game,
group: group, group: group,
players: players, players: players,
notes: row.notes, notes: row.notes ?? '',
createdAt: row.createdAt, createdAt: row.createdAt,
endedAt: row.endedAt,
scores: scores,
winner: winner,
); );
}), }),
); );
@@ -55,19 +63,24 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
group = await db.groupDao.getGroupById(groupId: result.groupId!); group = await db.groupDao.getGroupById(groupId: result.groupId!);
} }
List<Player>? players; final players =
if (await db.playerMatchDao.matchHasPlayers(matchId: matchId)) { await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
} final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId);
final winner = await db.scoreEntryDao.getWinner(matchId: matchId);
return Match( return Match(
id: result.id, id: result.id,
name: result.name ?? '', name: result.name,
game: game, game: game,
group: group, group: group,
players: players, players: players,
notes: result.notes, notes: result.notes ?? '',
createdAt: result.createdAt, createdAt: result.createdAt,
endedAt: result.endedAt,
scores: scores,
winner: winner,
); );
} }
@@ -75,30 +88,41 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// This method assumes that the game and group (if any) are already present /// This method assumes that the game and group (if any) are already present
/// in the database. /// in the database.
Future<void> addMatch({required Match match}) async { Future<void> addMatch({required Match match}) async {
if (match.game == null) {
throw ArgumentError('Match must have a game associated with it');
}
await db.transaction(() async { await db.transaction(() async {
await into(matchTable).insert( await into(matchTable).insert(
MatchTableCompanion.insert( MatchTableCompanion.insert(
id: match.id, id: match.id,
gameId: match.game!.id, gameId: match.game.id,
groupId: Value(match.group?.id), groupId: Value(match.group?.id),
name: Value(match.name), name: match.name,
notes: Value(match.notes), notes: Value(match.notes),
createdAt: match.createdAt, createdAt: match.createdAt,
endedAt: Value(match.endedAt),
), ),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
); );
if (match.players != null) { for (final p in match.players) {
for (final p in match.players!) { await db.playerMatchDao.addPlayerToMatch(
await db.playerMatchDao.addPlayerToMatch( matchId: match.id,
matchId: match.id, playerId: p.id,
playerId: p.id, );
); }
}
for (final pid in match.scores.keys) {
final playerScores = match.scores[pid]!;
await db.scoreEntryDao.addScoresAsList(
entrys: playerScores,
playerId: pid,
matchId: match.id,
);
}
if (match.winner != null) {
await db.scoreEntryDao.setWinner(
matchId: match.id,
playerId: match.winner!.id,
);
} }
}); });
} }
@@ -113,9 +137,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
// Add all games first (deduplicated) // Add all games first (deduplicated)
final uniqueGames = <String, Game>{}; final uniqueGames = <String, Game>{};
for (final match in matches) { for (final match in matches) {
if (match.game != null) { uniqueGames[match.game.id] = match.game;
uniqueGames[match.game!.id] = match.game!;
}
} }
if (uniqueGames.isNotEmpty) { if (uniqueGames.isNotEmpty) {
@@ -127,10 +149,10 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
(game) => GameTableCompanion.insert( (game) => GameTableCompanion.insert(
id: game.id, id: game.id,
name: game.name, name: game.name,
ruleset: game.ruleset ?? '', ruleset: game.ruleset.name,
description: Value(game.description), description: game.description,
color: Value(game.color?.toString()), color: game.color.name,
icon: Value(game.icon), icon: game.icon,
createdAt: game.createdAt, createdAt: game.createdAt,
), ),
) )
@@ -150,7 +172,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
(match) => GroupTableCompanion.insert( (match) => GroupTableCompanion.insert(
id: match.group!.id, id: match.group!.id,
name: match.group!.name, name: match.group!.name,
description: Value(match.group!.description), description: match.group!.description,
createdAt: match.group!.createdAt, createdAt: match.group!.createdAt,
), ),
) )
@@ -164,15 +186,15 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
(b) => b.insertAll( (b) => b.insertAll(
matchTable, matchTable,
matches matches
.where((match) => match.game != null)
.map( .map(
(match) => MatchTableCompanion.insert( (match) => MatchTableCompanion.insert(
id: match.id, id: match.id,
gameId: match.game!.id, gameId: match.game.id,
groupId: Value(match.group?.id), groupId: Value(match.group?.id),
name: Value(match.name), name: match.name,
notes: Value(match.notes), notes: Value(match.notes),
createdAt: match.createdAt, createdAt: match.createdAt,
endedAt: Value(match.endedAt),
), ),
) )
.toList(), .toList(),
@@ -183,10 +205,8 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
// Add all players of the matches in batch (unique) // Add all players of the matches in batch (unique)
final uniquePlayers = <String, Player>{}; final uniquePlayers = <String, Player>{};
for (final match in matches) { for (final match in matches) {
if (match.players != null) { for (final p in match.players) {
for (final p in match.players!) { uniquePlayers[p.id] = p;
uniquePlayers[p.id] = p;
}
} }
// Also include members of groups // Also include members of groups
if (match.group != null) { if (match.group != null) {
@@ -205,7 +225,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
(p) => PlayerTableCompanion.insert( (p) => PlayerTableCompanion.insert(
id: p.id, id: p.id,
name: p.name, name: p.name,
description: Value(p.description), description: p.description,
createdAt: p.createdAt, createdAt: p.createdAt,
), ),
) )
@@ -218,18 +238,15 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
// Add all player-match associations in batch // Add all player-match associations in batch
await db.batch((b) { await db.batch((b) {
for (final match in matches) { for (final match in matches) {
if (match.players != null) { for (final p in match.players) {
for (final p in match.players!) { b.insert(
b.insert( db.playerMatchTable,
db.playerMatchTable, PlayerMatchTableCompanion.insert(
PlayerMatchTableCompanion.insert( matchId: match.id,
matchId: match.id, playerId: p.id,
playerId: p.id, ),
score: 0, mode: InsertMode.insertOrIgnore,
), );
mode: InsertMode.insertOrIgnore,
);
}
} }
} }
}); });
@@ -271,6 +288,34 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return count ?? 0; return count ?? 0;
} }
/// Retrieves all matches associated with the given [groupId].
/// Queries the database directly, filtering by [groupId].
Future<List<Match>> getGroupMatches({required String groupId}) async {
final query = select(matchTable)..where((m) => m.groupId.equals(groupId));
final rows = await query.get();
return Future.wait(
rows.map((row) async {
final game = await db.gameDao.getGameById(gameId: row.gameId);
final group = await db.groupDao.getGroupById(groupId: groupId);
final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? [];
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
return Match(
id: row.id,
name: row.name,
game: game,
group: group,
players: players,
notes: row.notes ?? '',
createdAt: row.createdAt,
endedAt: row.endedAt,
winner: winner,
);
}),
);
}
/// Checks if a match with the given [matchId] exists in the database. /// Checks if a match with the given [matchId] exists in the database.
/// Returns `true` if the match exists, otherwise `false`. /// Returns `true` if the match exists, otherwise `false`.
Future<bool> matchExists({required String matchId}) async { Future<bool> matchExists({required String matchId}) async {
@@ -327,15 +372,27 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
} }
/// Updates the group of the match with the given [matchId]. /// Updates the group of the match with the given [matchId].
/// Replaces the existing group association with the new group specified by [newGroupId].
/// Pass null to remove the group association. /// Pass null to remove the group association.
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchGroup({ Future<bool> updateMatchGroup({
required String matchId, required String matchId,
required String? groupId, required String? newGroupId,
}) async { }) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId)); final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
MatchTableCompanion(groupId: Value(groupId)), MatchTableCompanion(groupId: Value(newGroupId)),
);
return rowsAffected > 0;
}
/// Removes the group association of the match with the given [matchId].
/// Sets the groupId to null.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeMatchGroup({required String matchId}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
const MatchTableCompanion(groupId: Value(null)),
); );
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -353,39 +410,51 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return rowsAffected > 0; return rowsAffected > 0;
} }
// ============================================================ /// Updates the endedAt timestamp of the match with the given [matchId].
// TEMPORARY: Winner methods - these are stubs and do not persist data /// Pass null to remove the ended time (mark match as ongoing).
// TODO: Implement proper winner handling /// Returns `true` if more than 0 rows were affected, otherwise `false`.
// ============================================================ Future<bool> updateMatchEndedAt({
/// TEMPORARY: Checks if a match has a winner.
/// Currently returns true if the match has any players.
Future<bool> hasWinner({required String matchId}) async {
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
return players?.isNotEmpty ?? false;
}
/// TEMPORARY: Gets the winner of a match.
/// Currently returns the first player in the match's player list.
Future<Player?> getWinner({required String matchId}) async {
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
return (players?.isNotEmpty ?? false) ? players!.first : null;
}
/// TEMPORARY: Sets the winner of a match.
/// Currently does nothing - winner is not persisted.
Future<bool> setWinner({
required String matchId, required String matchId,
required String winnerId, required DateTime? endedAt,
}) async { }) async {
// TODO: Implement winner persistence final query = update(matchTable)..where((g) => g.id.equals(matchId));
return true; final rowsAffected = await query.write(
MatchTableCompanion(endedAt: Value(endedAt)),
);
return rowsAffected > 0;
} }
/// TEMPORARY: Removes the winner of a match. /// Replaces all players in a match with the provided list of players.
/// Currently does nothing - winner is not persisted. /// Removes all existing players from the match and adds the new players.
Future<bool> removeWinner({required String matchId}) async { /// Also adds any new players to the player table if they don't exist.
// TODO: Implement winner persistence Future<void> replaceMatchPlayers({
return true; required String matchId,
required List<Player> newPlayers,
}) async {
await db.transaction(() async {
// Remove all existing players from the match
final deleteQuery = delete(db.playerMatchTable)
..where((p) => p.matchId.equals(matchId));
await deleteQuery.go();
// Add new players to the player table if they don't exist
await Future.wait(
newPlayers.map((player) async {
if (!await db.playerDao.playerExists(playerId: player.id)) {
await db.playerDao.addPlayer(player: player);
}
}),
);
// Add the new players to the match
await Future.wait(
newPlayers.map(
(player) => db.playerMatchDao.addPlayerToMatch(
matchId: matchId,
playerId: player.id,
),
),
);
});
} }
} }

View File

@@ -11,4 +11,25 @@ mixin _$MatchDaoMixin on DatabaseAccessor<AppDatabase> {
$TeamTableTable get teamTable => attachedDatabase.teamTable; $TeamTableTable get teamTable => attachedDatabase.teamTable;
$PlayerMatchTableTable get playerMatchTable => $PlayerMatchTableTable get playerMatchTable =>
attachedDatabase.playerMatchTable; attachedDatabase.playerMatchTable;
MatchDaoManager get managers => MatchDaoManager(this);
}
class MatchDaoManager {
final _$MatchDaoMixin _db;
MatchDaoManager(this._db);
$$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$MatchTableTableTableManager get matchTable =>
$$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
$$TeamTableTableTableManager get teamTable =>
$$TeamTableTableTableManager(_db.attachedDatabase, _db.teamTable);
$$PlayerMatchTableTableTableManager get playerMatchTable =>
$$PlayerMatchTableTableTableManager(
_db.attachedDatabase,
_db.playerMatchTable,
);
} }

View File

@@ -1,7 +1,7 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:game_tracker/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/player_table.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
part 'player_dao.g.dart'; part 'player_dao.g.dart';
@@ -46,7 +46,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
PlayerTableCompanion.insert( PlayerTableCompanion.insert(
id: player.id, id: player.id,
name: player.name, name: player.name,
description: Value(player.description), description: player.description,
createdAt: player.createdAt, createdAt: player.createdAt,
), ),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
@@ -70,7 +70,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
(player) => PlayerTableCompanion.insert( (player) => PlayerTableCompanion.insert(
id: player.id, id: player.id,
name: player.name, name: player.name,
description: Value(player.description), description: player.description,
createdAt: player.createdAt, createdAt: player.createdAt,
), ),
) )

View File

@@ -5,4 +5,12 @@ part of 'player_dao.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$PlayerDaoMixin on DatabaseAccessor<AppDatabase> { mixin _$PlayerDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable; $PlayerTableTable get playerTable => attachedDatabase.playerTable;
PlayerDaoManager get managers => PlayerDaoManager(this);
}
class PlayerDaoManager {
final _$PlayerDaoMixin _db;
PlayerDaoManager(this._db);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
} }

View File

@@ -1,8 +1,8 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:game_tracker/data/db/tables/player_group_table.dart'; import 'package:tallee/data/db/tables/player_group_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/player_table.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
part 'player_group_dao.g.dart'; part 'player_group_dao.g.dart';
@@ -27,7 +27,7 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
} }
if (!await db.playerDao.playerExists(playerId: player.id)) { if (!await db.playerDao.playerExists(playerId: player.id)) {
db.playerDao.addPlayer(player: player); await db.playerDao.addPlayer(player: player);
} }
await into(playerGroupTable).insert( await into(playerGroupTable).insert(

View File

@@ -8,4 +8,19 @@ mixin _$PlayerGroupDaoMixin on DatabaseAccessor<AppDatabase> {
$GroupTableTable get groupTable => attachedDatabase.groupTable; $GroupTableTable get groupTable => attachedDatabase.groupTable;
$PlayerGroupTableTable get playerGroupTable => $PlayerGroupTableTable get playerGroupTable =>
attachedDatabase.playerGroupTable; attachedDatabase.playerGroupTable;
PlayerGroupDaoManager get managers => PlayerGroupDaoManager(this);
}
class PlayerGroupDaoManager {
final _$PlayerGroupDaoMixin _db;
PlayerGroupDaoManager(this._db);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$PlayerGroupTableTableTableManager get playerGroupTable =>
$$PlayerGroupTableTableTableManager(
_db.attachedDatabase,
_db.playerGroupTable,
);
} }

View File

@@ -1,8 +1,8 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:game_tracker/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:game_tracker/data/db/tables/team_table.dart'; import 'package:tallee/data/db/tables/team_table.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
part 'player_match_dao.g.dart'; part 'player_match_dao.g.dart';
@@ -17,14 +17,12 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
required String matchId, required String matchId,
required String playerId, required String playerId,
String? teamId, String? teamId,
int score = 0,
}) async { }) async {
await into(playerMatchTable).insert( await into(playerMatchTable).insert(
PlayerMatchTableCompanion.insert( PlayerMatchTableCompanion.insert(
playerId: playerId, playerId: playerId,
matchId: matchId, matchId: matchId,
teamId: Value(teamId), teamId: Value(teamId),
score: score,
), ),
mode: InsertMode.insertOrIgnore, mode: InsertMode.insertOrIgnore,
); );
@@ -46,35 +44,6 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
return players; return players;
} }
/// Retrieves a player's score for a specific match.
/// Returns null if the player is not in the match.
Future<int?> getPlayerScore({
required String matchId,
required String playerId,
}) async {
final result = await (select(playerMatchTable)
..where(
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
))
.getSingleOrNull();
return result?.score;
}
/// Updates the score for a player in a match.
/// Returns `true` if the update was successful, otherwise `false`.
Future<bool> updatePlayerScore({
required String matchId,
required String playerId,
required int newScore,
}) async {
final rowsAffected = await (update(playerMatchTable)
..where(
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
))
.write(PlayerMatchTableCompanion(score: Value(newScore)));
return rowsAffected > 0;
}
/// Updates the team for a player in a match. /// Updates the team for a player in a match.
/// Returns `true` if the update was successful, otherwise `false`. /// Returns `true` if the update was successful, otherwise `false`.
Future<bool> updatePlayerTeam({ Future<bool> updatePlayerTeam({
@@ -82,11 +51,11 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
required String playerId, required String playerId,
required String? teamId, required String? teamId,
}) async { }) async {
final rowsAffected = await (update(playerMatchTable) final rowsAffected =
..where( await (update(playerMatchTable)..where(
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId), (p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
)) ))
.write(PlayerMatchTableCompanion(teamId: Value(teamId))); .write(PlayerMatchTableCompanion(teamId: Value(teamId)));
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -148,7 +117,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds); final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds);
final playersToRemove = currentPlayerIds.difference(newPlayerIdsSet); final playersToRemove = currentPlayerIds.difference(newPlayerIdsSet);
db.transaction(() async { await db.transaction(() async {
// Remove old players // Remove old players
if (playersToRemove.isNotEmpty) { if (playersToRemove.isNotEmpty) {
await (delete(playerMatchTable)..where( await (delete(playerMatchTable)..where(
@@ -166,7 +135,6 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
(id) => PlayerMatchTableCompanion.insert( (id) => PlayerMatchTableCompanion.insert(
playerId: id, playerId: id,
matchId: matchId, matchId: matchId,
score: 0,
), ),
) )
.toList(); .toList();
@@ -186,11 +154,9 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
required String matchId, required String matchId,
required String teamId, required String teamId,
}) async { }) async {
final result = await (select(playerMatchTable) final result = await (select(
..where( playerMatchTable,
(p) => p.matchId.equals(matchId) & p.teamId.equals(teamId), )..where((p) => p.matchId.equals(matchId) & p.teamId.equals(teamId))).get();
))
.get();
if (result.isEmpty) return []; if (result.isEmpty) return [];

View File

@@ -11,4 +11,25 @@ mixin _$PlayerMatchDaoMixin on DatabaseAccessor<AppDatabase> {
$TeamTableTable get teamTable => attachedDatabase.teamTable; $TeamTableTable get teamTable => attachedDatabase.teamTable;
$PlayerMatchTableTable get playerMatchTable => $PlayerMatchTableTable get playerMatchTable =>
attachedDatabase.playerMatchTable; attachedDatabase.playerMatchTable;
PlayerMatchDaoManager get managers => PlayerMatchDaoManager(this);
}
class PlayerMatchDaoManager {
final _$PlayerMatchDaoMixin _db;
PlayerMatchDaoManager(this._db);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
$$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$MatchTableTableTableManager get matchTable =>
$$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable);
$$TeamTableTableTableManager get teamTable =>
$$TeamTableTableTableManager(_db.attachedDatabase, _db.teamTable);
$$PlayerMatchTableTableTableManager get playerMatchTable =>
$$PlayerMatchTableTableTableManager(
_db.attachedDatabase,
_db.playerMatchTable,
);
} }

View File

@@ -1,191 +0,0 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/score_table.dart';
part 'score_dao.g.dart';
/// A data class representing a score entry.
class ScoreEntry {
final String playerId;
final String matchId;
final int roundNumber;
final int score;
final int change;
ScoreEntry({
required this.playerId,
required this.matchId,
required this.roundNumber,
required this.score,
required this.change,
});
}
@DriftAccessor(tables: [ScoreTable])
class ScoreDao extends DatabaseAccessor<AppDatabase> with _$ScoreDaoMixin {
ScoreDao(super.db);
/// Adds a score entry to the database.
Future<void> addScore({
required String playerId,
required String matchId,
required int roundNumber,
required int score,
required int change,
}) async {
await into(scoreTable).insert(
ScoreTableCompanion.insert(
playerId: playerId,
matchId: matchId,
roundNumber: roundNumber,
score: score,
change: change,
),
mode: InsertMode.insertOrReplace,
);
}
/// Retrieves all scores for a specific match.
Future<List<ScoreEntry>> getScoresForMatch({required String matchId}) async {
final query = select(scoreTable)..where((s) => s.matchId.equals(matchId));
final result = await query.get();
return result
.map(
(row) => ScoreEntry(
playerId: row.playerId,
matchId: row.matchId,
roundNumber: row.roundNumber,
score: row.score,
change: row.change,
),
)
.toList();
}
/// Retrieves all scores for a specific player in a match.
Future<List<ScoreEntry>> getPlayerScoresInMatch({
required String playerId,
required String matchId,
}) async {
final query = select(scoreTable)
..where(
(s) => s.playerId.equals(playerId) & s.matchId.equals(matchId),
)
..orderBy([(s) => OrderingTerm.asc(s.roundNumber)]);
final result = await query.get();
return result
.map(
(row) => ScoreEntry(
playerId: row.playerId,
matchId: row.matchId,
roundNumber: row.roundNumber,
score: row.score,
change: row.change,
),
)
.toList();
}
/// Retrieves the score for a specific round.
Future<ScoreEntry?> getScoreForRound({
required String playerId,
required String matchId,
required int roundNumber,
}) async {
final query = select(scoreTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
);
final result = await query.getSingleOrNull();
if (result == null) return null;
return ScoreEntry(
playerId: result.playerId,
matchId: result.matchId,
roundNumber: result.roundNumber,
score: result.score,
change: result.change,
);
}
/// Updates a score entry.
Future<bool> updateScore({
required String playerId,
required String matchId,
required int roundNumber,
required int newScore,
required int newChange,
}) async {
final rowsAffected = await (update(scoreTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
))
.write(
ScoreTableCompanion(
score: Value(newScore),
change: Value(newChange),
),
);
return rowsAffected > 0;
}
/// Deletes a score entry.
Future<bool> deleteScore({
required String playerId,
required String matchId,
required int roundNumber,
}) async {
final query = delete(scoreTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Deletes all scores for a specific match.
Future<bool> deleteScoresForMatch({required String matchId}) async {
final query = delete(scoreTable)..where((s) => s.matchId.equals(matchId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Deletes all scores for a specific player.
Future<bool> deleteScoresForPlayer({required String playerId}) async {
final query = delete(scoreTable)..where((s) => s.playerId.equals(playerId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Gets the latest round number for a match.
Future<int> getLatestRoundNumber({required String matchId}) async {
final query = selectOnly(scoreTable)
..where(scoreTable.matchId.equals(matchId))
..addColumns([scoreTable.roundNumber.max()]);
final result = await query.getSingle();
return result.read(scoreTable.roundNumber.max()) ?? 0;
}
/// Gets the total score for a player in a match (sum of all changes).
Future<int> getTotalScoreForPlayer({
required String playerId,
required String matchId,
}) async {
final scores = await getPlayerScoresInMatch(
playerId: playerId,
matchId: matchId,
);
if (scores.isEmpty) return 0;
// Return the score from the latest round
return scores.last.score;
}
}

View File

@@ -1,12 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'score_dao.dart';
// ignore_for_file: type=lint
mixin _$ScoreDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$ScoreTableTable get scoreTable => attachedDatabase.scoreTable;
}

View File

@@ -0,0 +1,328 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/score_entry_table.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
part 'score_entry_dao.g.dart';
@DriftAccessor(tables: [ScoreEntryTable])
class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
with _$ScoreEntryDaoMixin {
ScoreEntryDao(super.db);
/// Adds a score entry to the database.
Future<void> addScore({
required String playerId,
required String matchId,
required ScoreEntry entry,
}) async {
await into(scoreEntryTable).insert(
ScoreEntryTableCompanion.insert(
playerId: playerId,
matchId: matchId,
roundNumber: entry.roundNumber,
score: entry.score,
change: entry.change,
),
mode: InsertMode.insertOrReplace,
);
}
Future<void> addScoresAsList({
required List<ScoreEntry> entrys,
required String playerId,
required String matchId,
}) async {
if (entrys.isEmpty) return;
final entries = entrys
.map(
(score) => ScoreEntryTableCompanion.insert(
playerId: playerId,
matchId: matchId,
roundNumber: score.roundNumber,
score: score.score,
change: score.change,
),
)
.toList();
await batch((batch) {
batch.insertAll(
scoreEntryTable,
entries,
mode: InsertMode.insertOrReplace,
);
});
}
/// Retrieves the score for a specific round.
Future<ScoreEntry?> getScore({
required String playerId,
required String matchId,
int roundNumber = 0,
}) async {
final query = select(scoreEntryTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
);
final result = await query.getSingleOrNull();
if (result == null) return null;
return ScoreEntry(
roundNumber: result.roundNumber,
score: result.score,
change: result.change,
);
}
/// Retrieves all scores for a specific match.
Future<Map<String, List<ScoreEntry>>> getAllMatchScores({
required String matchId,
}) async {
final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId));
final result = await query.get();
final Map<String, List<ScoreEntry>> scoresByPlayer = {};
for (final row in result) {
final score = ScoreEntry(
roundNumber: row.roundNumber,
score: row.score,
change: row.change,
);
scoresByPlayer.putIfAbsent(row.playerId, () => []).add(score);
}
return scoresByPlayer;
}
/// Retrieves all scores for a specific player in a match.
Future<List<ScoreEntry>> getAllPlayerScoresInMatch({
required String playerId,
required String matchId,
}) async {
final query = select(scoreEntryTable)
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId))
..orderBy([(s) => OrderingTerm.asc(s.roundNumber)]);
final result = await query.get();
return result
.map(
(row) => ScoreEntry(
roundNumber: row.roundNumber,
score: row.score,
change: row.change,
),
)
.toList()
..sort(
(scoreA, scoreB) => scoreA.roundNumber.compareTo(scoreB.roundNumber),
);
}
/// Updates a score entry.
Future<bool> updateScore({
required String playerId,
required String matchId,
required ScoreEntry newEntry,
}) async {
final rowsAffected =
await (update(scoreEntryTable)..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(newEntry.roundNumber),
))
.write(
ScoreEntryTableCompanion(
score: Value(newEntry.score),
change: Value(newEntry.change),
),
);
return rowsAffected > 0;
}
/// Deletes a score entry.
Future<bool> deleteScore({
required String playerId,
required String matchId,
int roundNumber = 0,
}) async {
final query = delete(scoreEntryTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
Future<bool> deleteAllScoresForMatch({required String matchId}) async {
final query = delete(scoreEntryTable)
..where((s) => s.matchId.equals(matchId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
Future<bool> deleteAllScoresForPlayerInMatch({
required String matchId,
required String playerId,
}) async {
final query = delete(scoreEntryTable)
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Gets the highest (latest) round number for a match.
/// Returns `null` if there are no scores for the match.
Future<int?> getLatestRoundNumber({required String matchId}) async {
final query = selectOnly(scoreEntryTable)
..where(scoreEntryTable.matchId.equals(matchId))
..addColumns([scoreEntryTable.roundNumber.max()]);
final result = await query.getSingle();
return result.read(scoreEntryTable.roundNumber.max());
}
/// Aggregates the total score for a player in a match by summing all their
/// score entry changes. Returns `0` if there are no scores for the player
/// in the match.
Future<int> getTotalScoreForPlayer({
required String playerId,
required String matchId,
}) async {
final scores = await getAllPlayerScoresInMatch(
playerId: playerId,
matchId: matchId,
);
if (scores.isEmpty) return 0;
// Return the sum of all score changes
return scores.fold<int>(0, (sum, element) => sum + element.change);
}
Future<bool> hasWinner({required String matchId}) async {
return await getWinner(matchId: matchId) != null;
}
// Setting the winner for a game and clearing previous winner if exists.
Future<bool> setWinner({
required String matchId,
required String playerId,
}) async {
// Clear previous winner if exists
deleteAllScoresForMatch(matchId: matchId);
// Set the winner's score to 1
final rowsAffected = await into(scoreEntryTable).insert(
ScoreEntryTableCompanion.insert(
playerId: playerId,
matchId: matchId,
roundNumber: 0,
score: 1,
change: 0,
),
mode: InsertMode.insertOrReplace,
);
return rowsAffected > 0;
}
// Retrieves the winner of a match based on the highest score.
Future<Player?> getWinner({required String matchId}) async {
final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId))
..orderBy([(s) => OrderingTerm.desc(s.score)])
..limit(1);
final result = await query.getSingleOrNull();
if (result == null) return null;
final player = await db.playerDao.getPlayerById(playerId: result.playerId);
return Player(
id: player.id,
name: player.name,
createdAt: player.createdAt,
description: player.description,
);
}
/// Removes the winner of a match.
///
/// Returns `true` if the winner was removed, `false` if there are multiple
/// scores or if the winner cannot be removed.
Future<bool> removeWinner({required String matchId}) async {
final scores = await getAllMatchScores(matchId: matchId);
if (scores.length > 1) {
return false;
} else {
return await deleteAllScoresForMatch(matchId: matchId);
}
}
Future<bool> hasLooser({required String matchId}) async {
return await getLooser(matchId: matchId) != null;
}
// Setting the looser for a game and clearing previous looser if exists.
Future<bool> setLooser({
required String matchId,
required String playerId,
}) async {
// Clear previous loosers if exists
deleteAllScoresForMatch(matchId: matchId);
// Set the loosers score to 0
final rowsAffected = await into(scoreEntryTable).insert(
ScoreEntryTableCompanion.insert(
playerId: playerId,
matchId: matchId,
roundNumber: 0,
score: 0,
change: 0,
),
mode: InsertMode.insertOrReplace,
);
return rowsAffected > 0;
}
/// Retrieves the looser of a match based on the score 0.
Future<Player?> getLooser({required String matchId}) async {
final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId) & s.score.equals(0));
final result = await query.getSingleOrNull();
if (result == null) return null;
final player = await db.playerDao.getPlayerById(playerId: result.playerId);
return Player(
id: player.id,
name: player.name,
createdAt: player.createdAt,
description: player.description,
);
}
/// Removes the looser of a match.
///
/// Returns `true` if the looser was removed, `false` if there are multiple
/// scores or if the looser cannot be removed.
Future<bool> removeLooser({required String matchId}) async {
final scores = await getAllMatchScores(matchId: matchId);
if (scores.length > 1) {
return false;
} else {
return await deleteAllScoresForMatch(matchId: matchId);
}
}
}

View File

@@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'score_entry_dao.dart';
// ignore_for_file: type=lint
mixin _$ScoreEntryDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$ScoreEntryTableTable get scoreEntryTable => attachedDatabase.scoreEntryTable;
ScoreEntryDaoManager get managers => ScoreEntryDaoManager(this);
}
class ScoreEntryDaoManager {
final _$ScoreEntryDaoMixin _db;
ScoreEntryDaoManager(this._db);
$$PlayerTableTableTableManager get playerTable =>
$$PlayerTableTableTableManager(_db.attachedDatabase, _db.playerTable);
$$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$MatchTableTableTableManager get matchTable =>
$$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable);
$$ScoreEntryTableTableTableManager get scoreEntryTable =>
$$ScoreEntryTableTableTableManager(
_db.attachedDatabase,
_db.scoreEntryTable,
);
}

View File

@@ -1,8 +1,8 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:game_tracker/data/db/tables/team_table.dart'; import 'package:tallee/data/db/tables/team_table.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:game_tracker/data/dto/team.dart'; import 'package:tallee/data/models/team.dart';
part 'team_dao.g.dart'; part 'team_dao.g.dart';
@@ -144,4 +144,3 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
return rowsAffected > 0; return rowsAffected > 0;
} }
} }

View File

@@ -5,4 +5,12 @@ part of 'team_dao.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$TeamDaoMixin on DatabaseAccessor<AppDatabase> { mixin _$TeamDaoMixin on DatabaseAccessor<AppDatabase> {
$TeamTableTable get teamTable => attachedDatabase.teamTable; $TeamTableTable get teamTable => attachedDatabase.teamTable;
TeamDaoManager get managers => TeamDaoManager(this);
}
class TeamDaoManager {
final _$TeamDaoMixin _db;
TeamDaoManager(this._db);
$$TeamTableTableTableManager get teamTable =>
$$TeamTableTableTableManager(_db.attachedDatabase, _db.teamTable);
} }

View File

@@ -1,22 +1,22 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'; import 'package:drift_flutter/drift_flutter.dart';
import 'package:game_tracker/data/dao/game_dao.dart';
import 'package:game_tracker/data/dao/group_dao.dart';
import 'package:game_tracker/data/dao/match_dao.dart';
import 'package:game_tracker/data/dao/player_dao.dart';
import 'package:game_tracker/data/dao/player_group_dao.dart';
import 'package:game_tracker/data/dao/player_match_dao.dart';
import 'package:game_tracker/data/dao/score_dao.dart';
import 'package:game_tracker/data/dao/team_dao.dart';
import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_group_table.dart';
import 'package:game_tracker/data/db/tables/player_match_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart';
import 'package:game_tracker/data/db/tables/score_table.dart';
import 'package:game_tracker/data/db/tables/team_table.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:tallee/data/dao/game_dao.dart';
import 'package:tallee/data/dao/group_dao.dart';
import 'package:tallee/data/dao/match_dao.dart';
import 'package:tallee/data/dao/player_dao.dart';
import 'package:tallee/data/dao/player_group_dao.dart';
import 'package:tallee/data/dao/player_match_dao.dart';
import 'package:tallee/data/dao/score_entry_dao.dart';
import 'package:tallee/data/dao/team_dao.dart';
import 'package:tallee/data/db/tables/game_table.dart';
import 'package:tallee/data/db/tables/group_table.dart';
import 'package:tallee/data/db/tables/match_table.dart';
import 'package:tallee/data/db/tables/player_group_table.dart';
import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:tallee/data/db/tables/player_table.dart';
import 'package:tallee/data/db/tables/score_entry_table.dart';
import 'package:tallee/data/db/tables/team_table.dart';
part 'database.g.dart'; part 'database.g.dart';
@@ -24,29 +24,29 @@ part 'database.g.dart';
tables: [ tables: [
PlayerTable, PlayerTable,
GroupTable, GroupTable,
GameTable,
TeamTable,
MatchTable, MatchTable,
PlayerGroupTable, PlayerGroupTable,
PlayerMatchTable, PlayerMatchTable,
ScoreTable, GameTable,
TeamTable,
ScoreEntryTable,
], ],
daos: [ daos: [
PlayerDao, PlayerDao,
GroupDao, GroupDao,
GameDao,
TeamDao,
MatchDao, MatchDao,
PlayerGroupDao, PlayerGroupDao,
PlayerMatchDao, PlayerMatchDao,
ScoreDao, GameDao,
ScoreEntryDao,
TeamDao,
], ],
) )
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override @override
int get schemaVersion => 2; int get schemaVersion => 1;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {

File diff suppressed because it is too large Load Diff

View File

@@ -4,9 +4,9 @@ class GameTable extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get name => text()(); TextColumn get name => text()();
TextColumn get ruleset => text()(); TextColumn get ruleset => text()();
TextColumn get description => text().nullable()(); TextColumn get description => text()();
TextColumn get color => text().nullable()(); TextColumn get color => text()();
TextColumn get icon => text().nullable()(); TextColumn get icon => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
@override @override

View File

@@ -3,7 +3,7 @@ import 'package:drift/drift.dart';
class GroupTable extends Table { class GroupTable extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get name => text()(); TextColumn get name => text()();
TextColumn get description => text().nullable()(); TextColumn get description => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
@override @override

View File

@@ -1,16 +1,20 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/game_table.dart'; import 'package:tallee/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart'; import 'package:tallee/data/db/tables/group_table.dart';
class MatchTable extends Table { class MatchTable extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get gameId => TextColumn get gameId =>
text().references(GameTable, #id, onDelete: KeyAction.cascade)(); text().references(GameTable, #id, onDelete: KeyAction.cascade)();
TextColumn get groupId => // Nullable if there is no group associated with the match
text().references(GroupTable, #id, onDelete: KeyAction.cascade).nullable()(); // Nullable if not part of a group // onDelete: If a group gets deleted, groupId in the match gets set to null
TextColumn get name => text().nullable()(); TextColumn get groupId => text()
.references(GroupTable, #id, onDelete: KeyAction.setNull)
.nullable()();
TextColumn get name => text()();
TextColumn get notes => text().nullable()(); TextColumn get notes => text().nullable()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get endedAt => dateTime().nullable()();
@override @override
Set<Column<Object>> get primaryKey => {id}; Set<Column<Object>> get primaryKey => {id};

View File

@@ -1,6 +1,6 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/group_table.dart'; import 'package:tallee/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/player_table.dart';
class PlayerGroupTable extends Table { class PlayerGroupTable extends Table {
TextColumn get playerId => TextColumn get playerId =>

View File

@@ -1,16 +1,14 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/match_table.dart'; import 'package:tallee/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/player_table.dart';
import 'package:game_tracker/data/db/tables/team_table.dart'; import 'package:tallee/data/db/tables/team_table.dart';
class PlayerMatchTable extends Table { class PlayerMatchTable extends Table {
TextColumn get playerId => TextColumn get playerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)(); text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId => TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)(); text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
TextColumn get teamId => TextColumn get teamId => text().references(TeamTable, #id).nullable()();
text().references(TeamTable, #id).nullable()();
IntColumn get score => integer()();
@override @override
Set<Column<Object>> get primaryKey => {playerId, matchId}; Set<Column<Object>> get primaryKey => {playerId, matchId};

View File

@@ -3,7 +3,7 @@ import 'package:drift/drift.dart';
class PlayerTable extends Table { class PlayerTable extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get name => text()(); TextColumn get name => text()();
TextColumn get description => text().nullable()(); TextColumn get description => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
@override @override

View File

@@ -1,8 +1,8 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/match_table.dart'; import 'package:tallee/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/player_table.dart';
class ScoreTable extends Table { class ScoreEntryTable extends Table {
TextColumn get playerId => TextColumn get playerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)(); text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId => TextColumn get matchId =>

View File

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

View File

@@ -1,25 +1,27 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:tallee/core/enums.dart';
class Game { class Game {
final String id; final String id;
final DateTime createdAt; final DateTime createdAt;
final String name; final String name;
final String? ruleset; final Ruleset ruleset;
final String? description; final String description;
final int? color; final GameColor color;
final String? icon; final String icon;
Game({ Game({
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
required this.name, required this.name,
this.ruleset, required this.ruleset,
this.description, String? description,
this.color, required this.color,
this.icon, required this.icon,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(); createdAt = createdAt ?? clock.now(),
description = description ?? '';
@override @override
String toString() { String toString() {
@@ -31,9 +33,9 @@ class Game {
: id = json['id'], : id = json['id'],
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
name = json['name'], name = json['name'],
ruleset = json['ruleset'], ruleset = Ruleset.values.firstWhere((e) => e.name == json['ruleset']),
description = json['description'], description = json['description'],
color = json['color'], color = GameColor.values.firstWhere((e) => e.name == json['color']),
icon = json['icon']; icon = json['icon'];
/// Converts the Game instance to a JSON object. /// Converts the Game instance to a JSON object.
@@ -41,9 +43,9 @@ class Game {
'id': id, 'id': id,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'name': name, 'name': name,
'ruleset': ruleset, 'ruleset': ruleset.name,
'description': description, 'description': description,
'color': color, 'color': color.name,
'icon': icon, 'icon': icon,
}; };
} }

View File

@@ -1,11 +1,11 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class Group { class Group {
final String id; final String id;
final String name; final String name;
final String? description; final String description;
final DateTime createdAt; final DateTime createdAt;
final List<Player> members; final List<Player> members;
@@ -13,32 +13,33 @@ class Group {
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
required this.name, required this.name,
this.description, String? description,
required this.members, required this.members,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(); createdAt = createdAt ?? clock.now(),
description = description ?? '';
@override @override
String toString() { String toString() {
return 'Group{id: $id, name: $name, description: $description, members: $members}'; return 'Group{id: $id, name: $name, description: $description, members: $members}';
} }
/// Creates a Group instance from a JSON object. /// Creates a Group instance from a JSON object where the related [Player]
/// objects are represented by their IDs.
Group.fromJson(Map<String, dynamic> json) Group.fromJson(Map<String, dynamic> json)
: id = json['id'], : id = json['id'],
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
name = json['name'], name = json['name'],
description = json['description'], description = json['description'],
members = (json['members'] as List) members = [];
.map((memberJson) => Player.fromJson(memberJson))
.toList();
/// Converts the Group instance to a JSON object. /// Converts the Group instance to a JSON object. Related [Player] objects are
/// represented by their IDs.
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'name': name, 'name': name,
'description': description, 'description': description,
'members': members.map((member) => member.toJson()).toList(), 'memberIds': members.map((member) => member.id).toList(),
}; };
} }

View File

@@ -0,0 +1,80 @@
import 'package:clock/clock.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
import 'package:uuid/uuid.dart';
class Match {
final String id;
final DateTime createdAt;
final DateTime? endedAt;
final String name;
final Game game;
final Group? group;
final List<Player> players;
final String notes;
Map<String, List<ScoreEntry>> scores;
Player? winner;
Match({
String? id,
DateTime? createdAt,
this.endedAt,
required this.name,
required this.game,
this.group,
this.players = const [],
this.notes = '',
Map<String, List<ScoreEntry>>? scores,
this.winner,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(),
scores = scores ?? {for (var player in players) player.id: []};
@override
String toString() {
return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, winner: $winner}';
}
/// Creates a Match instance from a JSON object where related objects are
/// represented by their IDs. Therefore, the game, group, and players are not
/// fully constructed here.
Match.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
endedAt = json['endedAt'] != null
? DateTime.parse(json['endedAt'])
: null,
name = json['name'],
game = Game(
name: '',
ruleset: Ruleset.singleWinner,
description: '',
color: GameColor.blue,
icon: '',
),
group = null,
players = [],
scores = json['scores'],
notes = json['notes'] ?? '';
/// Converts the Match instance to a JSON object. Related objects are
/// represented by their IDs, so the game, group, and players are not fully
/// serialized here.
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'endedAt': endedAt?.toIso8601String(),
'name': name,
'gameId': game.id,
'groupId': group?.id,
'playerIds': players.map((player) => player.id).toList(),
'scores': scores.map(
(playerId, scoreList) =>
MapEntry(playerId, scoreList.map((score) => score.toJson()).toList()),
),
'notes': notes,
};
}

View File

@@ -5,15 +5,16 @@ class Player {
final String id; final String id;
final DateTime createdAt; final DateTime createdAt;
final String name; final String name;
final String? description; final String description;
Player({ Player({
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
required this.name, required this.name,
this.description, String? description,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(); createdAt = createdAt ?? clock.now(),
description = description ?? '';
@override @override
String toString() { String toString() {

View File

@@ -0,0 +1,22 @@
class ScoreEntry {
int roundNumber = 0;
final int score;
final int change;
ScoreEntry({
required this.roundNumber,
required this.score,
required this.change,
});
ScoreEntry.fromJson(Map<String, dynamic> json)
: roundNumber = json['roundNumber'],
score = json['score'],
change = json['change'];
Map<String, dynamic> toJson() => {
'roundNumber': roundNumber,
'score': score,
'change': change,
};
}

View File

@@ -1,5 +1,5 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class Team { class Team {
@@ -21,21 +21,20 @@ class Team {
return 'Team{id: $id, name: $name, members: $members}'; return 'Team{id: $id, name: $name, members: $members}';
} }
/// Creates a Team instance from a JSON object. /// Creates a Team instance from a JSON object (memberIds format).
/// Player objects are reconstructed from memberIds by the DataTransferService.
Team.fromJson(Map<String, dynamic> json) Team.fromJson(Map<String, dynamic> json)
: id = json['id'], : id = json['id'],
name = json['name'], name = json['name'],
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
members = (json['members'] as List) members = []; // Populated during import via DataTransferService
.map((memberJson) => Player.fromJson(memberJson))
.toList();
/// Converts the Team instance to a JSON object. /// Converts the Team instance to a JSON object. Related objects are
/// represented by their IDs.
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'name': name, 'name': name,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'members': members.map((member) => member.toJson()).toList(), 'memberIds': members.map((member) => member.id).toList(),
}; };
} }

View File

@@ -3,7 +3,7 @@
"all_players": "Alle Spieler:innen", "all_players": "Alle Spieler:innen",
"all_players_selected": "Alle Spieler:innen ausgewählt", "all_players_selected": "Alle Spieler:innen ausgewählt",
"amount_of_matches": "Anzahl der Spiele", "amount_of_matches": "Anzahl der Spiele",
"app_name": "Game Tracker", "app_name": "Tallee",
"best_player": "Beste:r Spieler:in", "best_player": "Beste:r Spieler:in",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"choose_game": "Spielvorlage wählen", "choose_game": "Spielvorlage wählen",
@@ -22,9 +22,14 @@
"days_ago": "vor {count} Tagen", "days_ago": "vor {count} Tagen",
"delete": "Löschen", "delete": "Löschen",
"delete_all_data": "Alle Daten löschen", "delete_all_data": "Alle Daten löschen",
"delete_group": "Gruppe löschen", "delete_group": "Diese Gruppe löschen",
"delete_match": "Spiel löschen",
"edit_group": "Gruppe bearbeiten", "edit_group": "Gruppe bearbeiten",
"edit_match": "Gruppe bearbeiten",
"enter_results": "Ergebnisse eintragen",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
"error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen",
"error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen",
"error_reading_file": "Fehler beim Lesen der Datei", "error_reading_file": "Fehler beim Lesen der Datei",
"export_canceled": "Export abgebrochen", "export_canceled": "Export abgebrochen",
"export_data": "Daten exportieren", "export_data": "Daten exportieren",
@@ -46,6 +51,7 @@
"licenses": "Lizenzen", "licenses": "Lizenzen",
"match_in_progress": "Spiel läuft...", "match_in_progress": "Spiel läuft...",
"match_name": "Spieltitel", "match_name": "Spieltitel",
"match_profile": "Spielprofil",
"matches": "Spiele", "matches": "Spiele",
"members": "Mitglieder", "members": "Mitglieder",
"most_points": "Höchste Punkte", "most_points": "Höchste Punkte",
@@ -58,6 +64,7 @@
"no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden", "no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
"no_players_selected": "Keine Spieler:innen ausgewählt", "no_players_selected": "Keine Spieler:innen ausgewählt",
"no_recent_matches_available": "Keine letzten Spiele verfügbar", "no_recent_matches_available": "Keine letzten Spiele verfügbar",
"no_results_entered_yet": "Noch keine Ergebnisse eingetragen",
"no_second_match_available": "Kein zweites Spiel verfügbar", "no_second_match_available": "Kein zweites Spiel verfügbar",
"no_statistics_available": "Keine Statistiken verfügbar", "no_statistics_available": "Keine Statistiken verfügbar",
"none": "Kein", "none": "Kein",
@@ -70,11 +77,14 @@
"privacy_policy": "Datenschutzerklärung", "privacy_policy": "Datenschutzerklärung",
"quick_create": "Schnellzugriff", "quick_create": "Schnellzugriff",
"recent_matches": "Letzte Spiele", "recent_matches": "Letzte Spiele",
"result": "Ergebnis",
"results": "Ergebnisse",
"ruleset": "Regelwerk", "ruleset": "Regelwerk",
"ruleset_least_points": "Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.", "ruleset_least_points": "Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.",
"ruleset_most_points": "Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.", "ruleset_most_points": "Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.",
"ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.", "ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.",
"ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.", "ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.",
"save_changes": "Änderungen speichern",
"search_for_groups": "Nach Gruppen suchen", "search_for_groups": "Nach Gruppen suchen",
"search_for_players": "Nach Spieler:innen suchen", "search_for_players": "Nach Spieler:innen suchen",
"select_winner": "Gewinner:in wählen:", "select_winner": "Gewinner:in wählen:",
@@ -82,6 +92,9 @@
"settings": "Einstellungen", "settings": "Einstellungen",
"single_loser": "Ein:e Verlierer:in", "single_loser": "Ein:e Verlierer:in",
"single_winner": "Ein:e Gewinner:in", "single_winner": "Ein:e Gewinner:in",
"highest_score": "Höchste Punkte",
"lowest_score": "Niedrigste Punkte",
"multiple_winners": "Mehrere Gewinner:innen",
"statistics": "Statistiken", "statistics": "Statistiken",
"stats": "Statistiken", "stats": "Statistiken",
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",

View File

@@ -37,10 +37,10 @@
"description": "Button text to create a match" "description": "Button text to create a match"
}, },
"@create_new_group": { "@create_new_group": {
"description": "Button text to create a new group" "description": "Appbar text to create a new group"
}, },
"@create_new_match": { "@create_new_match": {
"description": "Button text to create a new match" "description": "Appbar text to create a new match"
}, },
"@created_on": { "@created_on": {
"description": "Label for creation date" "description": "Label for creation date"
@@ -72,14 +72,29 @@
"description": "Confirmation dialog for deleting all data" "description": "Confirmation dialog for deleting all data"
}, },
"@delete_group": { "@delete_group": {
"description": "Button text to delete a group" "description": "Confirmation dialog for deleting a group"
},
"@delete_match": {
"description": "Button text to delete a match"
}, },
"@edit_group": { "@edit_group": {
"description": "Button text to edit a group" "description": "Button & Appbar label for editing a group"
},
"@edit_match": {
"description": "Button & Appbar label for editing a match"
},
"@enter_results": {
"description": "Button text to enter match results"
}, },
"@error_creating_group": { "@error_creating_group": {
"description": "Error message when group creation fails" "description": "Error message when group creation fails"
}, },
"@error_deleting_group": {
"description": "Error message when group deletion fails"
},
"@error_editing_group": {
"description": "Error message when group editing fails"
},
"@error_reading_file": { "@error_reading_file": {
"description": "Error message when file cannot be read" "description": "Error message when file cannot be read"
}, },
@@ -143,6 +158,9 @@
"@match_name": { "@match_name": {
"description": "Placeholder for match name input" "description": "Placeholder for match name input"
}, },
"@match_profile": {
"description": "Title for match profile view"
},
"@matches": { "@matches": {
"description": "Label for matches" "description": "Label for matches"
}, },
@@ -179,6 +197,9 @@
"@no_recent_matches_available": { "@no_recent_matches_available": {
"description": "Message when no recent matches exist" "description": "Message when no recent matches exist"
}, },
"@no_results_entered_yet": {
"description": "Message when no results have been entered yet"
},
"@no_second_match_available": { "@no_second_match_available": {
"description": "Message when no second match exists" "description": "Message when no second match exists"
}, },
@@ -220,6 +241,9 @@
"@recent_matches": { "@recent_matches": {
"description": "Title for recent matches section" "description": "Title for recent matches section"
}, },
"@results": {
"description": "Label for match results"
},
"@ruleset": { "@ruleset": {
"description": "Ruleset label" "description": "Ruleset label"
}, },
@@ -235,6 +259,9 @@
"@ruleset_single_winner": { "@ruleset_single_winner": {
"description": "Description for single winner ruleset" "description": "Description for single winner ruleset"
}, },
"@save_changes": {
"description": "Save changes button text"
},
"@search_for_groups": { "@search_for_groups": {
"description": "Hint text for group search input field" "description": "Hint text for group search input field"
}, },
@@ -301,7 +328,7 @@
"all_players": "All players", "all_players": "All players",
"all_players_selected": "All players selected", "all_players_selected": "All players selected",
"amount_of_matches": "Amount of Matches", "amount_of_matches": "Amount of Matches",
"app_name": "Game Tracker", "app_name": "Tallee",
"best_player": "Best Player", "best_player": "Best Player",
"cancel": "Cancel", "cancel": "Cancel",
"choose_game": "Choose Game", "choose_game": "Choose Game",
@@ -321,8 +348,13 @@
"delete": "Delete", "delete": "Delete",
"delete_all_data": "Delete all data", "delete_all_data": "Delete all data",
"delete_group": "Delete Group", "delete_group": "Delete Group",
"delete_match": "Delete Match",
"edit_group": "Edit Group", "edit_group": "Edit Group",
"edit_match": "Edit Match",
"enter_results": "Enter Results",
"error_creating_group": "Error while creating group, please try again", "error_creating_group": "Error while creating group, please try again",
"error_deleting_group": "Error while deleting group, please try again",
"error_editing_group": "Error while editing group, please try again",
"error_reading_file": "Error reading file", "error_reading_file": "Error reading file",
"export_canceled": "Export canceled", "export_canceled": "Export canceled",
"export_data": "Export data", "export_data": "Export data",
@@ -344,6 +376,7 @@
"licenses": "Licenses", "licenses": "Licenses",
"match_in_progress": "Match in progress...", "match_in_progress": "Match in progress...",
"match_name": "Match name", "match_name": "Match name",
"match_profile": "Match Profile",
"matches": "Matches", "matches": "Matches",
"members": "Members", "members": "Members",
"most_points": "Most Points", "most_points": "Most Points",
@@ -356,6 +389,7 @@
"no_players_found_with_that_name": "No players found with that name", "no_players_found_with_that_name": "No players found with that name",
"no_players_selected": "No players selected", "no_players_selected": "No players selected",
"no_recent_matches_available": "No recent matches available", "no_recent_matches_available": "No recent matches available",
"no_results_entered_yet": "No results entered yet",
"no_second_match_available": "No second match available", "no_second_match_available": "No second match available",
"no_statistics_available": "No statistics available", "no_statistics_available": "No statistics available",
"none": "None", "none": "None",
@@ -368,11 +402,13 @@
"privacy_policy": "Privacy Policy", "privacy_policy": "Privacy Policy",
"quick_create": "Quick Create", "quick_create": "Quick Create",
"recent_matches": "Recent Matches", "recent_matches": "Recent Matches",
"results": "Results",
"ruleset": "Ruleset", "ruleset": "Ruleset",
"ruleset_least_points": "Inverse scoring: the player with the fewest points wins.", "ruleset_least_points": "Inverse scoring: the player with the fewest points wins.",
"ruleset_most_points": "Traditional ruleset: the player with the most points wins.", "ruleset_most_points": "Traditional ruleset: the player with the most points wins.",
"ruleset_single_loser": "Exactly one loser is determined; last place receives the penalty or consequence.", "ruleset_single_loser": "Exactly one loser is determined; last place receives the penalty or consequence.",
"ruleset_single_winner": "Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.", "ruleset_single_winner": "Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.",
"save_changes": "Save Changes",
"search_for_groups": "Search for groups", "search_for_groups": "Search for groups",
"search_for_players": "Search for players", "search_for_players": "Search for players",
"select_winner": "Select Winner:", "select_winner": "Select Winner:",
@@ -380,6 +416,9 @@
"settings": "Settings", "settings": "Settings",
"single_loser": "Single Loser", "single_loser": "Single Loser",
"single_winner": "Single Winner", "single_winner": "Single Winner",
"highest_score": "Highest Score",
"lowest_score": "Lowest Score",
"multiple_winners": "Multiple Winners",
"statistics": "Statistics", "statistics": "Statistics",
"stats": "Stats", "stats": "Stats",
"successfully_added_player": "Successfully added player {playerName}", "successfully_added_player": "Successfully added player {playerName}",

View File

@@ -119,7 +119,7 @@ abstract class AppLocalizations {
/// The name of the App /// The name of the App
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Game Tracker'** /// **'Tallee'**
String get app_name; String get app_name;
/// Label for best player statistic /// Label for best player statistic
@@ -170,7 +170,7 @@ abstract class AppLocalizations {
/// **'Create match'** /// **'Create match'**
String get create_match; String get create_match;
/// Button text to create a new group /// Appbar text to create a new group
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Create new group'** /// **'Create new group'**
@@ -182,7 +182,7 @@ abstract class AppLocalizations {
/// **'Created on'** /// **'Created on'**
String get created_on; String get created_on;
/// Button text to create a new match /// Appbar text to create a new match
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Create new match'** /// **'Create new match'**
@@ -230,24 +230,54 @@ abstract class AppLocalizations {
/// **'Delete all data'** /// **'Delete all data'**
String get delete_all_data; String get delete_all_data;
/// Button text to delete a group /// Confirmation dialog for deleting a group
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Delete Group'** /// **'Delete Group'**
String get delete_group; String get delete_group;
/// Button text to edit a group /// Button text to delete a match
///
/// In en, this message translates to:
/// **'Delete Match'**
String get delete_match;
/// Button & Appbar label for editing a group
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Edit Group'** /// **'Edit Group'**
String get edit_group; String get edit_group;
/// Button & Appbar label for editing a match
///
/// In en, this message translates to:
/// **'Edit Match'**
String get edit_match;
/// Button text to enter match results
///
/// In en, this message translates to:
/// **'Enter Results'**
String get enter_results;
/// Error message when group creation fails /// Error message when group creation fails
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Error while creating group, please try again'** /// **'Error while creating group, please try again'**
String get error_creating_group; String get error_creating_group;
/// Error message when group deletion fails
///
/// In en, this message translates to:
/// **'Error while deleting group, please try again'**
String get error_deleting_group;
/// Error message when group editing fails
///
/// In en, this message translates to:
/// **'Error while editing group, please try again'**
String get error_editing_group;
/// Error message when file cannot be read /// Error message when file cannot be read
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -374,6 +404,12 @@ abstract class AppLocalizations {
/// **'Match name'** /// **'Match name'**
String get match_name; String get match_name;
/// Title for match profile view
///
/// In en, this message translates to:
/// **'Match Profile'**
String get match_profile;
/// Label for matches /// Label for matches
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -446,6 +482,12 @@ abstract class AppLocalizations {
/// **'No recent matches available'** /// **'No recent matches available'**
String get no_recent_matches_available; String get no_recent_matches_available;
/// Message when no results have been entered yet
///
/// In en, this message translates to:
/// **'No results entered yet'**
String get no_results_entered_yet;
/// Message when no second match exists /// Message when no second match exists
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -518,6 +560,12 @@ abstract class AppLocalizations {
/// **'Recent Matches'** /// **'Recent Matches'**
String get recent_matches; String get recent_matches;
/// Label for match results
///
/// In en, this message translates to:
/// **'Results'**
String get results;
/// Ruleset label /// Ruleset label
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -548,6 +596,12 @@ abstract class AppLocalizations {
/// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'** /// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'**
String get ruleset_single_winner; String get ruleset_single_winner;
/// Save changes button text
///
/// In en, this message translates to:
/// **'Save Changes'**
String get save_changes;
/// Hint text for group search input field /// Hint text for group search input field
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -590,6 +644,24 @@ abstract class AppLocalizations {
/// **'Single Winner'** /// **'Single Winner'**
String get single_winner; String get single_winner;
/// No description provided for @highest_score.
///
/// In en, this message translates to:
/// **'Highest Score'**
String get highest_score;
/// No description provided for @lowest_score.
///
/// In en, this message translates to:
/// **'Lowest Score'**
String get lowest_score;
/// No description provided for @multiple_winners.
///
/// In en, this message translates to:
/// **'Multiple Winners'**
String get multiple_winners;
/// Statistics tab label /// Statistics tab label
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -18,7 +18,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get amount_of_matches => 'Anzahl der Spiele'; String get amount_of_matches => 'Anzahl der Spiele';
@override @override
String get app_name => 'Game Tracker'; String get app_name => 'Tallee';
@override @override
String get best_player => 'Beste:r Spieler:in'; String get best_player => 'Beste:r Spieler:in';
@@ -79,15 +79,32 @@ class AppLocalizationsDe extends AppLocalizations {
String get delete_all_data => 'Alle Daten löschen'; String get delete_all_data => 'Alle Daten löschen';
@override @override
String get delete_group => 'Gruppe löschen'; String get delete_group => 'Diese Gruppe löschen';
@override
String get delete_match => 'Spiel löschen';
@override @override
String get edit_group => 'Gruppe bearbeiten'; String get edit_group => 'Gruppe bearbeiten';
@override
String get edit_match => 'Gruppe bearbeiten';
@override
String get enter_results => 'Ergebnisse eintragen';
@override @override
String get error_creating_group => String get error_creating_group =>
'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen';
@override
String get error_deleting_group =>
'Fehler beim Löschen der Gruppe, bitte erneut versuchen';
@override
String get error_editing_group =>
'Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen';
@override @override
String get error_reading_file => 'Fehler beim Lesen der Datei'; String get error_reading_file => 'Fehler beim Lesen der Datei';
@@ -151,6 +168,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get match_name => 'Spieltitel'; String get match_name => 'Spieltitel';
@override
String get match_profile => 'Spielprofil';
@override @override
String get matches => 'Spiele'; String get matches => 'Spiele';
@@ -188,6 +208,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get no_recent_matches_available => 'Keine letzten Spiele verfügbar'; String get no_recent_matches_available => 'Keine letzten Spiele verfügbar';
@override
String get no_results_entered_yet => 'Noch keine Ergebnisse eingetragen';
@override @override
String get no_second_match_available => 'Kein zweites Spiel verfügbar'; String get no_second_match_available => 'Kein zweites Spiel verfügbar';
@@ -226,6 +249,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get recent_matches => 'Letzte Spiele'; String get recent_matches => 'Letzte Spiele';
@override
String get results => 'Ergebnisse';
@override @override
String get ruleset => 'Regelwerk'; String get ruleset => 'Regelwerk';
@@ -245,6 +271,9 @@ class AppLocalizationsDe extends AppLocalizations {
String get ruleset_single_winner => String get ruleset_single_winner =>
'Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.'; 'Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.';
@override
String get save_changes => 'Änderungen speichern';
@override @override
String get search_for_groups => 'Nach Gruppen suchen'; String get search_for_groups => 'Nach Gruppen suchen';
@@ -266,6 +295,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get single_winner => 'Ein:e Gewinner:in'; String get single_winner => 'Ein:e Gewinner:in';
@override
String get highest_score => 'Höchste Punkte';
@override
String get lowest_score => 'Niedrigste Punkte';
@override
String get multiple_winners => 'Mehrere Gewinner:innen';
@override @override
String get statistics => 'Statistiken'; String get statistics => 'Statistiken';

View File

@@ -18,7 +18,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get amount_of_matches => 'Amount of Matches'; String get amount_of_matches => 'Amount of Matches';
@override @override
String get app_name => 'Game Tracker'; String get app_name => 'Tallee';
@override @override
String get best_player => 'Best Player'; String get best_player => 'Best Player';
@@ -81,13 +81,30 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get delete_group => 'Delete Group'; String get delete_group => 'Delete Group';
@override
String get delete_match => 'Delete Match';
@override @override
String get edit_group => 'Edit Group'; String get edit_group => 'Edit Group';
@override
String get edit_match => 'Edit Match';
@override
String get enter_results => 'Enter Results';
@override @override
String get error_creating_group => String get error_creating_group =>
'Error while creating group, please try again'; 'Error while creating group, please try again';
@override
String get error_deleting_group =>
'Error while deleting group, please try again';
@override
String get error_editing_group =>
'Error while editing group, please try again';
@override @override
String get error_reading_file => 'Error reading file'; String get error_reading_file => 'Error reading file';
@@ -151,6 +168,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get match_name => 'Match name'; String get match_name => 'Match name';
@override
String get match_profile => 'Match Profile';
@override @override
String get matches => 'Matches'; String get matches => 'Matches';
@@ -188,6 +208,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get no_recent_matches_available => 'No recent matches available'; String get no_recent_matches_available => 'No recent matches available';
@override
String get no_results_entered_yet => 'No results entered yet';
@override @override
String get no_second_match_available => 'No second match available'; String get no_second_match_available => 'No second match available';
@@ -226,6 +249,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get recent_matches => 'Recent Matches'; String get recent_matches => 'Recent Matches';
@override
String get results => 'Results';
@override @override
String get ruleset => 'Ruleset'; String get ruleset => 'Ruleset';
@@ -245,6 +271,9 @@ class AppLocalizationsEn extends AppLocalizations {
String get ruleset_single_winner => String get ruleset_single_winner =>
'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'; 'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.';
@override
String get save_changes => 'Save Changes';
@override @override
String get search_for_groups => 'Search for groups'; String get search_for_groups => 'Search for groups';
@@ -266,6 +295,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get single_winner => 'Single Winner'; String get single_winner => 'Single Winner';
@override
String get highest_score => 'Highest Score';
@override
String get lowest_score => 'Lowest Score';
@override
String get multiple_winners => 'Multiple Winners';
@override @override
String get statistics => 'Statistics'; String get statistics => 'Statistics';

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.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/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/custom_navigation_bar.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/custom_navigation_bar.dart';
void main() { void main() {
runApp( runApp(
@@ -35,15 +35,25 @@ class GameTracker extends StatelessWidget {
}, },
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
onGenerateTitle: (context) => AppLocalizations.of(context).app_name, onGenerateTitle: (context) => AppLocalizations.of(context).app_name,
themeMode: ThemeMode.dark, // forces dark mode themeMode: ThemeMode.dark,
theme: ThemeData( theme: ThemeData(
// main colors
primaryColor: CustomTheme.primaryColor, primaryColor: CustomTheme.primaryColor,
scaffoldBackgroundColor: CustomTheme.backgroundColor, scaffoldBackgroundColor: CustomTheme.backgroundColor,
// themes
appBarTheme: CustomTheme.appBarTheme, appBarTheme: CustomTheme.appBarTheme,
inputDecorationTheme: CustomTheme.inputDecorationTheme,
searchBarTheme: CustomTheme.searchBarTheme,
radioTheme: CustomTheme.radioTheme,
// color scheme
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: CustomTheme.primaryColor, seedColor: CustomTheme.textColor,
brightness: Brightness.dark, brightness: Brightness.dark,
).copyWith(surface: CustomTheme.backgroundColor), primary: CustomTheme.primaryColor,
onPrimary: CustomTheme.textColor,
surface: CustomTheme.backgroundColor,
onSurface: CustomTheme.textColor,
),
pageTransitionsTheme: const PageTransitionsTheme( pageTransitionsTheme: const PageTransitionsTheme(
builders: { builders: {
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),

View File

@@ -1,15 +1,13 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart'; import 'package:tallee/core/adaptive_page_route.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/group_view/groups_view.dart'; import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart';
import 'package:game_tracker/presentation/views/main_menu/home_view.dart'; import 'package:tallee/presentation/views/main_menu/home_view.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart';
import 'package:game_tracker/presentation/views/main_menu/settings_view/settings_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart';
import 'package:game_tracker/presentation/views/main_menu/statistics_view.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view.dart';
import 'package:game_tracker/presentation/widgets/navbar_item.dart'; import 'package:tallee/presentation/widgets/navbar_item.dart';
class CustomNavigationBar extends StatefulWidget { class CustomNavigationBar extends StatefulWidget {
/// A custom navigation bar widget that provides tabbed navigation /// A custom navigation bar widget that provides tabbed navigation
@@ -40,7 +38,7 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
), ),
KeyedSubtree( KeyedSubtree(
key: ValueKey('groups_$tabKeyCount'), key: ValueKey('groups_$tabKeyCount'),
child: const GroupsView(), child: const GroupView(),
), ),
KeyedSubtree( KeyedSubtree(
key: ValueKey('stats_$tabKeyCount'), key: ValueKey('stats_$tabKeyCount'),
@@ -75,103 +73,62 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
body: tabs[currentIndex], body: tabs[currentIndex],
extendBody: true, extendBody: true,
bottomNavigationBar: SizedBox( bottomNavigationBar: Container(
height: 70 + MediaQuery.of(context).padding.bottom, height: 115,
child: Stack( decoration: BoxDecoration(
children: [ color: CustomTheme.navBarBackgroundColor,
// Dynamically generated blur layers for ultra-smooth transition border: Border.all(
...List.generate(34, (index) { strokeAlign: BorderSide.strokeAlignOutside,
// Use cubic curve for an even more natural, smoother transition color: CustomTheme.boxBorderColor,
final progress = index / 34.0; // 0.0 to 1.0 width: 2,
final cubic = progress * progress * progress; // cubic curve ),
final blurStrength = borderRadius: const BorderRadius.only(
0.5 + (cubic * 50.0); // Very smooth from 0.5 to 50.5 topLeft: Radius.circular(30),
topRight: Radius.circular(30),
// Height goes completely from 100% to 0% (all the way down) ),
// With extra density at the bottom for softer transition boxShadow: [
final heightFactor = index < 25 BoxShadow(
// First 25 layers: 100% to 30% color: Colors.black.withValues(alpha: 0.1),
? 1.0 - (progress * 0.7) blurRadius: 20,
// Last 10 layers: 30% to 0% (denser) offset: const Offset(0, -5),
: 0.3 - ((index - 25) / 34.0);
return Positioned(
left: 0,
right: 0,
bottom: 0,
height:
(70 + MediaQuery.of(context).padding.bottom) *
heightFactor.clamp(0.05, 1.0),
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: blurStrength,
sigmaY: blurStrength,
),
child: Container(color: Colors.transparent),
),
),
);
}),
// Gradient overlay
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
CustomTheme.boxColor.withValues(alpha: 1),
CustomTheme.boxColor.withValues(alpha: 0.5),
CustomTheme.boxColor.withValues(alpha: 0.2),
CustomTheme.boxColor.withValues(alpha: 0.0),
],
stops: const [0.0, 0.4, 0.8, 1],
),
),
),
),
// Navbar content
SafeArea(
child: SizedBox(
height: 70,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
NavbarItem(
index: 0,
isSelected: currentIndex == 0,
icon: Icons.home_rounded,
label: loc.home,
onTabTapped: onTabTapped,
),
NavbarItem(
index: 1,
isSelected: currentIndex == 1,
icon: Icons.gamepad_rounded,
label: loc.matches,
onTabTapped: onTabTapped,
),
NavbarItem(
index: 2,
isSelected: currentIndex == 2,
icon: Icons.group_rounded,
label: loc.groups,
onTabTapped: onTabTapped,
),
NavbarItem(
index: 3,
isSelected: currentIndex == 3,
icon: Icons.bar_chart_rounded,
label: loc.statistics,
onTabTapped: onTabTapped,
),
],
),
),
), ),
], ],
), ),
child: SafeArea(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
NavbarItem(
index: 0,
isSelected: currentIndex == 0,
icon: Icons.home_rounded,
label: loc.home,
onTabTapped: onTabTapped,
),
NavbarItem(
index: 1,
isSelected: currentIndex == 1,
icon: Icons.gamepad_rounded,
label: loc.matches,
onTabTapped: onTabTapped,
),
NavbarItem(
index: 2,
isSelected: currentIndex == 2,
icon: Icons.group_rounded,
label: loc.groups,
onTabTapped: onTabTapped,
),
NavbarItem(
index: 3,
isSelected: currentIndex == 3,
icon: Icons.bar_chart_rounded,
label: loc.statistics,
onTabTapped: onTabTapped,
),
],
),
),
), ),
); );
} }
@@ -184,7 +141,7 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
} }
/// Returns the title of the current tab based on [currentIndex]. /// Returns the title of the current tab based on [currentIndex].
String _currentTabTitle(context) { String _currentTabTitle(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
switch (currentIndex) { switch (currentIndex) {
case 0: case 0:

View File

@@ -1,19 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/constants.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/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/player_selection.dart';
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/player_selection.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
class CreateGroupView extends StatefulWidget { class CreateGroupView extends StatefulWidget {
/// A view that allows the user to create a new group const CreateGroupView({super.key, this.groupToEdit, this.onMembersChanged});
const CreateGroupView({super.key});
/// The group to edit, if any
final Group? groupToEdit;
final VoidCallback? onMembersChanged;
@override @override
State<CreateGroupView> createState() => _CreateGroupViewState(); State<CreateGroupView> createState() => _CreateGroupViewState();
@@ -22,16 +26,29 @@ class CreateGroupView extends StatefulWidget {
class _CreateGroupViewState extends State<CreateGroupView> { class _CreateGroupViewState extends State<CreateGroupView> {
late final AppDatabase db; late final AppDatabase db;
/// GlobalKey for ScaffoldMessenger to show snackbars
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
/// Controller for the group name input field /// Controller for the group name input field
final _groupNameController = TextEditingController(); final _groupNameController = TextEditingController();
/// List of currently selected players /// List of currently selected players
List<Player> selectedPlayers = []; List<Player> selectedPlayers = [];
/// List of initially selected players (when editing a group)
List<Player> initialSelectedPlayers = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
if (widget.groupToEdit != null) {
_groupNameController.text = widget.groupToEdit!.name;
setState(() {
initialSelectedPlayers = widget.groupToEdit!.members;
selectedPlayers = widget.groupToEdit!.members;
});
}
_groupNameController.addListener(() { _groupNameController.addListener(() {
setState(() {}); setState(() {});
}); });
@@ -47,10 +64,15 @@ class _CreateGroupViewState extends State<CreateGroupView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return ScaffoldMessenger( return ScaffoldMessenger(
key: _scaffoldMessengerKey,
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.create_new_group)), appBar: AppBar(
title: Text(
widget.groupToEdit == null ? loc.create_new_group : loc.edit_group,
),
),
body: SafeArea( body: SafeArea(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@@ -65,6 +87,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
), ),
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
initialSelectedPlayers: initialSelectedPlayers,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectedPlayers = [...value]; selectedPlayers = [...value];
@@ -73,39 +96,16 @@ class _CreateGroupViewState extends State<CreateGroupView> {
), ),
), ),
CustomWidthButton( CustomWidthButton(
text: loc.create_group, text: widget.groupToEdit == null
? loc.create_group
: loc.edit_group,
sizeRelativeToWidth: 0.95, sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary, buttonType: ButtonType.primary,
onPressed: onPressed:
(_groupNameController.text.isEmpty || (_groupNameController.text.isEmpty ||
(selectedPlayers.length < 2)) (selectedPlayers.length < 2))
? null ? null
: () async { : _saveGroup,
bool success = await db.groupDao.addGroup(
group: Group(
name: _groupNameController.text.trim(),
members: selectedPlayers,
),
);
if (!context.mounted) return;
if (success) {
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: CustomTheme.boxColor,
content: Center(
child: Text(
AppLocalizations.of(
context,
).error_creating_group,
style: const TextStyle(color: Colors.white),
),
),
),
);
}
},
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
@@ -114,4 +114,118 @@ class _CreateGroupViewState extends State<CreateGroupView> {
), ),
); );
} }
/// Saves the group by creating a new one or updating the existing one,
/// depending on whether the widget is in edit mode.
Future<void> _saveGroup() async {
final loc = AppLocalizations.of(context);
late bool success;
Group? updatedGroup;
if (widget.groupToEdit == null) {
success = await _createGroup();
} else {
final result = await _editGroup();
success = result.$1;
updatedGroup = result.$2;
}
if (!mounted) return;
if (success) {
Navigator.pop(context, updatedGroup);
} else {
showSnackbar(
message: widget.groupToEdit == null
? loc.error_creating_group
: loc.error_editing_group,
);
}
}
/// Handles creating a new group and returns whether the operation was successful.
Future<bool> _createGroup() async {
final groupName = _groupNameController.text.trim();
final success = await db.groupDao.addGroup(
group: Group(name: groupName, description: '', members: selectedPlayers),
);
return success;
}
/// Handles editing an existing group and returns a tuple of
/// (success, updatedGroup).
Future<(bool, Group?)> _editGroup() async {
final groupName = _groupNameController.text.trim();
Group? updatedGroup = Group(
id: widget.groupToEdit!.id,
name: groupName,
description: '',
members: selectedPlayers,
);
bool successfullNameChange = true;
bool successfullMemberChange = true;
if (widget.groupToEdit!.name != groupName) {
successfullNameChange = await db.groupDao.updateGroupName(
groupId: widget.groupToEdit!.id,
newName: groupName,
);
}
if (widget.groupToEdit!.members != selectedPlayers) {
successfullMemberChange = await db.groupDao.replaceGroupPlayers(
groupId: widget.groupToEdit!.id,
newPlayers: selectedPlayers,
);
await deleteObsoleteMatchGroupRelations();
widget.onMembersChanged?.call();
}
final success = successfullNameChange && successfullMemberChange;
return (success, updatedGroup);
}
/// Removes the group association from matches that no longer belong to the edited group.
///
/// After updating the group's members, matches that were previously linked to
/// this group but don't have any of the newly selected players are considered
/// obsolete. For each such match, the group association is removed by setting
/// its [groupId] to null.
Future<void> deleteObsoleteMatchGroupRelations() async {
final groupMatches = await db.matchDao.getGroupMatches(
groupId: widget.groupToEdit!.id,
);
final selectedPlayerIds = selectedPlayers.map((p) => p.id).toSet();
final relationshipsToDelete = groupMatches.where((match) {
return !match.players.any(
(player) => selectedPlayerIds.contains(player.id),
);
}).toList();
for (var match in relationshipsToDelete) {
await db.matchDao.removeMatchGroup(matchId: match.id);
}
}
/// Displays a snackbar with the given message and optional action.
///
/// [message] The message to display in the snackbar.
void showSnackbar({required String message}) {
final messenger = _scaffoldMessengerKey.currentState;
if (messenger != null) {
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
content: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: CustomTheme.boxColor,
),
);
}
}
} }

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