123 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
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
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
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
76 changed files with 35082 additions and 2760 deletions

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

@@ -20,6 +20,7 @@
"type": "string" "type": "string"
} }
}, },
"additionalProperties": false,
"required": [ "required": [
"id", "id",
"createdAt", "createdAt",
@@ -55,6 +56,7 @@
"type": "string" "type": "string"
} }
}, },
"additionalProperties": false,
"required": [ "required": [
"id", "id",
"createdAt", "createdAt",
@@ -90,6 +92,7 @@
} }
} }
}, },
"additionalProperties": false,
"required": [ "required": [
"id", "id",
"name", "name",
@@ -120,6 +123,7 @@
} }
} }
}, },
"additionalProperties": false,
"required": [ "required": [
"id", "id",
"name", "name",
@@ -157,10 +161,31 @@
"type": "string" "type": "string"
} }
}, },
"scores": {
"type": "object",
"items": {
"type": "array",
"items": {
"type": "string",
"properties": {
"roundNumber": {
"type": "number"
},
"score": {
"type": "number"
},
"change": {
"type": "number"
}
}
}
}
},
"notes": { "notes": {
"type": "string" "type": "string"
} }
}, },
"additionalProperties": false,
"required": [ "required": [
"id", "id",
"name", "name",
@@ -172,6 +197,7 @@
} }
} }
}, },
"additionalProperties": false,
"required": [ "required": [
"players", "players",
"games", "games",

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

@@ -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)
}
} }

View File

@@ -31,6 +31,27 @@
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<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> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>

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

@@ -27,6 +27,9 @@ class CustomTheme {
/// Text color used throughout the app /// Text color used throughout the app
static const Color textColor = Color(0xFFFFFFFF); static const Color textColor = Color(0xFFFFFFFF);
/// Text color used throughout the app
static const Color hintColor = Color(0xFF888888);
/// Background color for the navigation bar /// Background color for the navigation bar
static const Color navBarBackgroundColor = Color(0xFF131313); static const Color navBarBackgroundColor = Color(0xFF131313);
@@ -65,7 +68,7 @@ class CustomTheme {
boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)], boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)],
); );
// ==================== App Bar Theme ==================== // ==================== Component Themes ====================
static const AppBarTheme appBarTheme = AppBarTheme( static const AppBarTheme appBarTheme = AppBarTheme(
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
foregroundColor: textColor, foregroundColor: textColor,
@@ -80,4 +83,23 @@ class CustomTheme {
), ),
iconTheme: 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:tallee/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.
@@ -35,7 +32,13 @@ enum ExportResult { success, canceled, unknownException }
/// - [Ruleset.singleWinner]: The match is won by a single player. /// - [Ruleset.singleWinner]: The match is won by a single player.
/// - [Ruleset.singleLoser]: The match has a single loser. /// - [Ruleset.singleLoser]: The match has a single loser.
/// - [Ruleset.multipleWinners]: Multiple players can be winners. /// - [Ruleset.multipleWinners]: Multiple players can be winners.
enum Ruleset { highestScore, lowestScore, singleWinner, singleLoser, multipleWinners } enum Ruleset {
highestScore,
lowestScore,
singleWinner,
singleLoser,
multipleWinners,
}
/// Different colors available for games /// Different colors available for games
/// - [GameColor.red]: Red color /// - [GameColor.red]: Red color
@@ -47,20 +50,3 @@ enum Ruleset { highestScore, lowestScore, singleWinner, singleLoser, multipleWin
/// - [GameColor.pink]: Pink color /// - [GameColor.pink]: Pink color
/// - [GameColor.teal]: Teal color /// - [GameColor.teal]: Teal color
enum GameColor { red, blue, green, yellow, purple, orange, pink, teal } enum GameColor { red, blue, green, yellow, purple, orange, pink, teal }
/// 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;
}
}

View File

@@ -1,8 +1,8 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/game_table.dart'; import 'package:tallee/data/db/tables/game_table.dart';
import 'package:tallee/data/dto/game.dart'; import 'package:tallee/data/models/game.dart';
import 'package:tallee/core/enums.dart';
part 'game_dao.g.dart'; part 'game_dao.g.dart';
@@ -111,14 +111,20 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
} }
/// Updates the name of the game with the given [gameId] to [newName]. /// Updates the name of the game with the given [gameId] to [newName].
Future<void> updateGameName({required String gameId, required String newName}) async { Future<void> updateGameName({
await (update( required String gameId,
gameTable, required String newName,
)..where((g) => g.id.equals(gameId))).write(GameTableCompanion(name: Value(newName))); }) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(name: Value(newName)),
);
} }
/// Updates the ruleset of the game with the given [gameId]. /// Updates the ruleset of the game with the given [gameId].
Future<void> updateGameRuleset({required String gameId, required Ruleset newRuleset}) async { Future<void> updateGameRuleset({
required String gameId,
required Ruleset newRuleset,
}) 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.name)), GameTableCompanion(ruleset: Value(newRuleset.name)),
); );
@@ -135,24 +141,31 @@ 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({required String gameId, required GameColor newColor}) async { Future<void> updateGameColor({
await (update( required String gameId,
gameTable, required GameColor newColor,
)..where((g) => g.id.equals(gameId))).write(GameTableCompanion(color: Value(newColor.name))); }) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(color: Value(newColor.name)),
);
} }
/// Updates the icon of the game with the given [gameId]. /// Updates the icon of the game with the given [gameId].
Future<void> updateGameIcon({required String gameId, required String newIcon}) async { Future<void> updateGameIcon({
await (update( required String gameId,
gameTable, required String newIcon,
)..where((g) => g.id.equals(gameId))).write(GameTableCompanion(icon: Value(newIcon))); }) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(icon: Value(newIcon)),
);
} }
/// Retrieves the total count of games in the database. /// Retrieves the total count of games in the database.
Future<int> getGameCount() async { Future<int> getGameCount() async {
final count = await (selectOnly( final count =
gameTable, await (selectOnly(gameTable)..addColumns([gameTable.id.count()]))
)..addColumns([gameTable.id.count()])).map((row) => row.read(gameTable.id.count())).getSingle(); .map((row) => row.read(gameTable.id.count()))
.getSingle();
return count ?? 0; return count ?? 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:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/group_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_group_table.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/player.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);
@@ -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 =
@@ -235,10 +234,13 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
/// Replaces all players in a group with the provided list of players. /// Replaces all players in a group with the provided list of players.
/// Removes all existing players from the group and adds the new 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. /// Also adds any new players to the player table if they don't exist.
Future<void> replaceGroupPlayers({ /// Returns `true` if the group exists and players were replaced, `false` otherwise.
Future<bool> replaceGroupPlayers({
required String groupId, required String groupId,
required List<Player> newPlayers, required List<Player> newPlayers,
}) async { }) async {
if (!await groupExists(groupId: groupId)) return false;
await db.transaction(() async { await db.transaction(() async {
// Remove all existing players from the group // Remove all existing players from the group
final deleteQuery = delete(db.playerGroupTable) final deleteQuery = delete(db.playerGroupTable)
@@ -270,5 +272,6 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
), ),
); );
}); });
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

@@ -4,10 +4,10 @@ import 'package:tallee/data/db/tables/game_table.dart';
import 'package:tallee/data/db/tables/group_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/match_table.dart';
import 'package:tallee/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:tallee/data/dto/game.dart'; import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
part 'match_dao.g.dart'; part 'match_dao.g.dart';
@@ -27,18 +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, endedAt: row.endedAt,
scores: scores,
winner: winner,
); );
}), }),
); );
@@ -56,17 +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!);
} }
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; final 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, endedAt: result.endedAt,
scores: scores,
winner: winner,
); );
} }
@@ -80,7 +94,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
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), endedAt: Value(match.endedAt),
@@ -94,6 +108,22 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
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,
);
}
}); });
} }
@@ -112,20 +142,20 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
if (uniqueGames.isNotEmpty) { if (uniqueGames.isNotEmpty) {
await db.batch( await db.batch(
(b) => b.insertAll( (b) => b.insertAll(
db.gameTable, db.gameTable,
uniqueGames.values uniqueGames.values
.map( .map(
(game) => GameTableCompanion.insert( (game) => GameTableCompanion.insert(
id: game.id, id: game.id,
name: game.name, name: game.name,
ruleset: game.ruleset.name, ruleset: game.ruleset.name,
description: game.description, description: game.description,
color: game.color.name, color: game.color.name,
icon: game.icon, icon: game.icon,
createdAt: game.createdAt, createdAt: game.createdAt,
), ),
) )
.toList(), .toList(),
mode: InsertMode.insertOrIgnore, mode: InsertMode.insertOrIgnore,
), ),
@@ -134,18 +164,18 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
// Add all groups of the matches in batch // Add all groups of the matches in batch
await db.batch( await db.batch(
(b) => b.insertAll( (b) => b.insertAll(
db.groupTable, db.groupTable,
matches matches
.where((match) => match.group != null) .where((match) => match.group != null)
.map( .map(
(match) => GroupTableCompanion.insert( (match) => GroupTableCompanion.insert(
id: match.group!.id, id: match.group!.id,
name: match.group!.name, name: match.group!.name,
description: match.group!.description, description: match.group!.description,
createdAt: match.group!.createdAt, createdAt: match.group!.createdAt,
), ),
) )
.toList(), .toList(),
mode: InsertMode.insertOrIgnore, mode: InsertMode.insertOrIgnore,
), ),
@@ -153,20 +183,20 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
// Add all matches in batch // Add all matches in batch
await db.batch( await db.batch(
(b) => b.insertAll( (b) => b.insertAll(
matchTable, matchTable,
matches matches
.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), endedAt: Value(match.endedAt),
), ),
) )
.toList(), .toList(),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
), ),
@@ -188,17 +218,17 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
if (uniquePlayers.isNotEmpty) { if (uniquePlayers.isNotEmpty) {
await db.batch( await db.batch(
(b) => b.insertAll( (b) => b.insertAll(
db.playerTable, db.playerTable,
uniquePlayers.values uniquePlayers.values
.map( .map(
(p) => PlayerTableCompanion.insert( (p) => PlayerTableCompanion.insert(
id: p.id, id: p.id,
name: p.name, name: p.name,
description: p.description, description: p.description,
createdAt: p.createdAt, createdAt: p.createdAt,
), ),
) )
.toList(), .toList(),
mode: InsertMode.insertOrIgnore, mode: InsertMode.insertOrIgnore,
), ),
@@ -214,7 +244,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
PlayerMatchTableCompanion.insert( PlayerMatchTableCompanion.insert(
matchId: match.id, matchId: match.id,
playerId: p.id, playerId: p.id,
score: 0,
), ),
mode: InsertMode.insertOrIgnore, mode: InsertMode.insertOrIgnore,
); );
@@ -253,12 +282,40 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Retrieves the number of matches in the database. /// Retrieves the number of matches in the database.
Future<int> getMatchCount() async { Future<int> getMatchCount() async {
final count = final count =
await (selectOnly(matchTable)..addColumns([matchTable.id.count()])) await (selectOnly(matchTable)..addColumns([matchTable.id.count()]))
.map((row) => row.read(matchTable.id.count())) .map((row) => row.read(matchTable.id.count()))
.getSingle(); .getSingle();
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 {
@@ -315,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;
} }
@@ -379,95 +448,13 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
// Add the new players to the match // Add the new players to the match
await Future.wait( await Future.wait(
newPlayers.map((player) => db.playerMatchDao.addPlayerToMatch( newPlayers.map(
matchId: matchId, (player) => db.playerMatchDao.addPlayerToMatch(
playerId: player.id, matchId: matchId,
)), playerId: player.id,
),
),
); );
}); });
} }
// ============================================================
// Winner methods - handle winner logic via player scores
// ============================================================
/// Checks if a match has a winner.
/// Returns true if any player in the match has their score set to 1.
Future<bool> hasWinner({required String matchId}) async {
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
for (final player in players) {
final score = await db.playerMatchDao.getPlayerScore(
matchId: matchId,
playerId: player.id,
);
if (score == 1) {
return true;
}
}
return false;
}
/// Gets the winner of a match.
/// Returns the player with score 1, or null if no winner is set.
Future<Player?> getWinner({required String matchId}) async {
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
for (final player in players) {
final score = await db.playerMatchDao.getPlayerScore(
matchId: matchId,
playerId: player.id,
);
if (score == 1) {
return player;
}
}
return null;
}
/// Sets the winner of a match.
/// Sets all players' scores to 0, then sets the specified player's score to 1.
/// Returns `true` if the operation was successful, otherwise `false`.
Future<bool> setWinner({
required String matchId,
required String winnerId,
}) async {
await db.transaction(() async {
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
// Set all players' scores to 0
for (final player in players) {
await db.playerMatchDao.updatePlayerScore(
matchId: matchId,
playerId: player.id,
newScore: 0,
);
}
// Set the winner's score to 1
await db.playerMatchDao.updatePlayerScore(
matchId: matchId,
playerId: winnerId,
newScore: 1,
);
});
return true;
}
/// Removes the winner of a match.
/// Sets the current winner's score to 0 (no winner).
/// Returns `true` if a winner was removed, otherwise `false`.
Future<bool> removeWinner({required String matchId}) async {
final winner = await getWinner(matchId: matchId);
if (winner == null) {
return false;
}
final success = await db.playerMatchDao.updatePlayerScore(
matchId: matchId,
playerId: winner.id,
newScore: 0,
);
return success;
}
} }

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:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/player_table.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
part 'player_dao.g.dart'; part 'player_dao.g.dart';

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

@@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/player_group_table.dart'; import 'package:tallee/data/db/tables/player_group_table.dart';
import 'package:tallee/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/player_table.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
part 'player_group_dao.g.dart'; part 'player_group_dao.g.dart';

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

@@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:tallee/data/db/tables/team_table.dart'; import 'package:tallee/data/db/tables/team_table.dart';
import 'package:tallee/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,
); );
@@ -40,41 +38,12 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
if (result.isEmpty) return null; if (result.isEmpty) return null;
final futures = result.map( final futures = result.map(
(row) => db.playerDao.getPlayerById(playerId: row.playerId), (row) => db.playerDao.getPlayerById(playerId: row.playerId),
); );
final players = await Future.wait(futures); final players = await Future.wait(futures);
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;
} }
@@ -94,11 +63,11 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
/// Returns `true` if there are players, otherwise `false`. /// Returns `true` if there are players, otherwise `false`.
Future<bool> matchHasPlayers({required String matchId}) async { Future<bool> matchHasPlayers({required String matchId}) async {
final count = final count =
await (selectOnly(playerMatchTable) await (selectOnly(playerMatchTable)
..where(playerMatchTable.matchId.equals(matchId)) ..where(playerMatchTable.matchId.equals(matchId))
..addColumns([playerMatchTable.playerId.count()])) ..addColumns([playerMatchTable.playerId.count()]))
.map((row) => row.read(playerMatchTable.playerId.count())) .map((row) => row.read(playerMatchTable.playerId.count()))
.getSingle(); .getSingle();
return (count ?? 0) > 0; return (count ?? 0) > 0;
} }
@@ -109,12 +78,12 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
required String playerId, required String playerId,
}) async { }) async {
final count = final count =
await (selectOnly(playerMatchTable) await (selectOnly(playerMatchTable)
..where(playerMatchTable.matchId.equals(matchId)) ..where(playerMatchTable.matchId.equals(matchId))
..where(playerMatchTable.playerId.equals(playerId)) ..where(playerMatchTable.playerId.equals(playerId))
..addColumns([playerMatchTable.playerId.count()])) ..addColumns([playerMatchTable.playerId.count()]))
.map((row) => row.read(playerMatchTable.playerId.count())) .map((row) => row.read(playerMatchTable.playerId.count()))
.getSingle(); .getSingle();
return (count ?? 0) > 0; return (count ?? 0) > 0;
} }
@@ -153,9 +122,9 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
if (playersToRemove.isNotEmpty) { if (playersToRemove.isNotEmpty) {
await (delete(playerMatchTable)..where( await (delete(playerMatchTable)..where(
(pg) => (pg) =>
pg.matchId.equals(matchId) & pg.matchId.equals(matchId) &
pg.playerId.isIn(playersToRemove.toList()), pg.playerId.isIn(playersToRemove.toList()),
)) ))
.go(); .go();
} }
@@ -164,15 +133,14 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
final inserts = playersToAdd final inserts = playersToAdd
.map( .map(
(id) => PlayerMatchTableCompanion.insert( (id) => PlayerMatchTableCompanion.insert(
playerId: id, playerId: id,
matchId: matchId, matchId: matchId,
score: 0, ),
), )
)
.toList(); .toList();
await Future.wait( await Future.wait(
inserts.map( inserts.map(
(c) => into( (c) => into(
playerMatchTable, playerMatchTable,
).insert(c, mode: InsertMode.insertOrIgnore), ).insert(c, mode: InsertMode.insertOrIgnore),
), ),
@@ -186,16 +154,14 @@ 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 [];
final futures = result.map( final futures = result.map(
(row) => db.playerDao.getPlayerById(playerId: row.playerId), (row) => db.playerDao.getPlayerById(playerId: row.playerId),
); );
return Future.wait(futures); return Future.wait(futures);
} }

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:tallee/data/db/database.dart';
import 'package:tallee/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:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/team_table.dart'; import 'package:tallee/data/db/tables/team_table.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/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

@@ -7,7 +7,7 @@ import 'package:tallee/data/dao/match_dao.dart';
import 'package:tallee/data/dao/player_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_group_dao.dart';
import 'package:tallee/data/dao/player_match_dao.dart'; import 'package:tallee/data/dao/player_match_dao.dart';
import 'package:tallee/data/dao/score_dao.dart'; import 'package:tallee/data/dao/score_entry_dao.dart';
import 'package:tallee/data/dao/team_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/game_table.dart';
import 'package:tallee/data/db/tables/group_table.dart'; import 'package:tallee/data/db/tables/group_table.dart';
@@ -15,7 +15,7 @@ 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_group_table.dart';
import 'package:tallee/data/db/tables/player_match_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/player_table.dart';
import 'package:tallee/data/db/tables/score_table.dart'; import 'package:tallee/data/db/tables/score_entry_table.dart';
import 'package:tallee/data/db/tables/team_table.dart'; import 'package:tallee/data/db/tables/team_table.dart';
part 'database.g.dart'; part 'database.g.dart';
@@ -29,7 +29,7 @@ part 'database.g.dart';
PlayerMatchTable, PlayerMatchTable,
GameTable, GameTable,
TeamTable, TeamTable,
ScoreTable, ScoreEntryTable,
], ],
daos: [ daos: [
PlayerDao, PlayerDao,
@@ -38,8 +38,8 @@ part 'database.g.dart';
PlayerGroupDao, PlayerGroupDao,
PlayerMatchDao, PlayerMatchDao,
GameDao, GameDao,
ScoreDao, ScoreEntryDao,
TeamDao TeamDao,
], ],
) )
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {
@@ -60,7 +60,9 @@ class AppDatabase extends _$AppDatabase {
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
name: 'gametracker_db', name: 'gametracker_db',
native: const DriftNativeOptions(databaseDirectory: getApplicationSupportDirectory), native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
); );
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,11 @@ class MatchTable extends Table {
TextColumn get gameId => TextColumn get gameId =>
text().references(GameTable, #id, onDelete: KeyAction.cascade)(); text().references(GameTable, #id, onDelete: KeyAction.cascade)();
// Nullable if there is no group associated with the match // Nullable if there is no group associated with the match
TextColumn get groupId => // onDelete: If a group gets deleted, groupId in the match gets set to null
text().references(GroupTable, #id, onDelete: KeyAction.cascade).nullable()(); TextColumn get groupId => text()
TextColumn get name => text().nullable()(); .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()(); DateTimeColumn get endedAt => dateTime().nullable()();

View File

@@ -8,9 +8,7 @@ class PlayerMatchTable extends Table {
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

@@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
import 'package:tallee/data/db/tables/match_table.dart'; import 'package:tallee/data/db/tables/match_table.dart';
import 'package:tallee/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,61 +0,0 @@
import 'package:clock/clock.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/player.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;
Player? winner;
Match({
String? id,
DateTime? createdAt,
this.endedAt,
required this.name,
required this.game,
this.group,
this.players = const [],
String? notes,
this.winner,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(),
notes = notes ?? '';
@override
String toString() {
return 'Match{id: $id, name: $name, game: $game, group: $group, players: $players, notes: $notes, endedAt: $endedAt}';
}
/// Creates a Match instance from a JSON object (ID references format).
/// Related objects are reconstructed from IDs by the DataTransferService.
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: ''), // Populated during import via DataTransferService
group = null, // Populated during import via DataTransferService
players = [], // Populated during import via DataTransferService
notes = json['notes'] ?? '';
/// Converts the Match instance to a JSON object using normalized format (ID references only).
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(),
'notes': notes,
};
}

View File

@@ -1,5 +1,5 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:tallee/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 {
@@ -24,16 +24,17 @@ class Group {
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 (memberIds format). /// Creates a Group instance from a JSON object where the related [Player]
/// Player objects are reconstructed from memberIds by the DataTransferService. /// 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 = []; // Populated during import via DataTransferService members = [];
/// Converts the Group instance to a JSON object using normalized format (memberIds only). /// 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(),

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

@@ -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:tallee/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 {
@@ -29,7 +29,8 @@ class Team {
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
members = []; // Populated during import via DataTransferService members = []; // Populated during import via DataTransferService
/// Converts the Team instance to a JSON object using normalized format (memberIds only). /// 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,
@@ -37,4 +38,3 @@ class Team {
'memberIds': members.map((member) => member.id).toList(), 'memberIds': members.map((member) => member.id).toList(),
}; };
} }

View File

@@ -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:",

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"
}, },
@@ -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:",

View File

@@ -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:

View File

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

View File

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

View File

@@ -29,19 +29,31 @@ class GameTracker extends StatelessWidget {
return supportedLocale; return supportedLocale;
} }
} }
return supportedLocales.firstWhere((locale) => locale.languageCode == 'en'); return supportedLocales.firstWhere(
(locale) => locale.languageCode == 'en',
);
}, },
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

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/group_view/groups_view.dart'; import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart';
import 'package:tallee/presentation/views/main_menu/home_view.dart'; import 'package:tallee/presentation/views/main_menu/home_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart';
import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart';
@@ -38,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'),
@@ -141,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

@@ -4,16 +4,20 @@ import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/player_selection.dart'; import 'package:tallee/presentation/widgets/player_selection.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.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,40 +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(),
description: '',
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),
], ],
@@ -115,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,
),
);
}
}
} }

View File

@@ -1,12 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
@@ -15,10 +17,10 @@ import 'package:tallee/presentation/widgets/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class GroupProfileView extends StatefulWidget { class GroupDetailView extends StatefulWidget {
/// A view that displays the profile of a group /// A view that displays the profile of a group
/// - [group]: The group to display /// - [group]: The group to display
const GroupProfileView({ const GroupDetailView({
super.key, super.key,
required this.group, required this.group,
required this.callback, required this.callback,
@@ -30,21 +32,24 @@ class GroupProfileView extends StatefulWidget {
final VoidCallback callback; final VoidCallback callback;
@override @override
State<GroupProfileView> createState() => _GroupProfileViewState(); State<GroupDetailView> createState() => _GroupDetailViewState();
} }
class _GroupProfileViewState extends State<GroupProfileView> { class _GroupDetailViewState extends State<GroupDetailView> {
late final AppDatabase db; late final AppDatabase db;
bool isLoading = true; bool isLoading = true;
late Group _group;
/// Total matches played in this group /// Total matches played in this group
int totalMatches = 0; int totalMatches = 0;
/// The best player in this group
String bestPlayer = ''; String bestPlayer = '';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_group = widget.group;
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
_loadStatistics(); _loadStatistics();
} }
@@ -87,7 +92,7 @@ class _GroupProfileViewState extends State<GroupProfileView> {
), ),
).then((confirmed) async { ).then((confirmed) async {
if (confirmed! && context.mounted) { if (confirmed! && context.mounted) {
await db.groupDao.deleteGroup(groupId: widget.group.id); await db.groupDao.deleteGroup(groupId: _group.id);
if (!context.mounted) return; if (!context.mounted) return;
Navigator.pop(context); Navigator.pop(context);
widget.callback.call(); widget.callback.call();
@@ -118,7 +123,7 @@ class _GroupProfileViewState extends State<GroupProfileView> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
widget.group.name, _group.name,
style: const TextStyle( style: const TextStyle(
fontSize: 28, fontSize: 28,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -128,7 +133,7 @@ class _GroupProfileViewState extends State<GroupProfileView> {
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
Text( Text(
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(widget.group.createdAt)}', '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(_group.createdAt)}',
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: CustomTheme.textColor, color: CustomTheme.textColor,
@@ -145,7 +150,7 @@ class _GroupProfileViewState extends State<GroupProfileView> {
crossAxisAlignment: WrapCrossAlignment.start, crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12, spacing: 12,
runSpacing: 8, runSpacing: 8,
children: widget.group.members.map((member) { children: _group.members.map((member) {
return TextIconTile( return TextIconTile(
text: member.name, text: member.name,
iconEnabled: false, iconEnabled: false,
@@ -163,7 +168,7 @@ class _GroupProfileViewState extends State<GroupProfileView> {
children: [ children: [
_buildStatRow( _buildStatRow(
loc.members, loc.members,
widget.group.members.length.toString(), _group.members.length.toString(),
), ),
_buildStatRow( _buildStatRow(
loc.played_matches, loc.played_matches,
@@ -181,19 +186,27 @@ class _GroupProfileViewState extends State<GroupProfileView> {
child: MainMenuButton( child: MainMenuButton(
text: loc.edit_group, text: loc.edit_group,
icon: Icons.edit, icon: Icons.edit,
onPressed: () { onPressed: () async {
// TODO: Uncomment when GroupDetailView is implemented final updatedGroup = await Navigator.push<Group?>(
/*
await Navigator.push(
context, context,
adaptivePageRoute( adaptivePageRoute(
builder: (context) { builder: (context) {
return CreateGroupView(
return const GroupDetailView(); groupToEdit: _group,
onMembersChanged: () {
_loadStatistics();
},
);
}, },
), ),
);*/ );
print('Edit Group pressed'); if (updatedGroup != null && mounted) {
setState(() {
_group = updatedGroup;
});
_loadStatistics();
widget.callback();
}
}, },
), ),
), ),
@@ -234,10 +247,8 @@ class _GroupProfileViewState extends State<GroupProfileView> {
/// Loads statistics for this group /// Loads statistics for this group
Future<void> _loadStatistics() async { Future<void> _loadStatistics() async {
final matches = await db.matchDao.getAllMatches(); isLoading = true;
final groupMatches = matches final groupMatches = await db.matchDao.getGroupMatches(groupId: _group.id);
.where((match) => match.group?.id == widget.group.id)
.toList();
setState(() { setState(() {
totalMatches = groupMatches.length; totalMatches = groupMatches.length;
@@ -252,7 +263,9 @@ class _GroupProfileViewState extends State<GroupProfileView> {
// Count wins for each player // Count wins for each player
for (var match in matches) { for (var match in matches) {
if (match.winner != null) { if (match.winner != null &&
_group.members.any((m) => m.id == match.winner?.id)) {
print(match.winner);
bestPlayerCounts.update( bestPlayerCounts.update(
match.winner!, match.winner!,
(value) => value + 1, (value) => value + 1,

View File

@@ -4,25 +4,25 @@ import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/constants.dart'; import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart'; import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart';
import 'package:tallee/presentation/views/main_menu/group_view/group_profile_view.dart'; import 'package:tallee/presentation/views/main_menu/group_view/group_detail_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/tiles/group_tile.dart'; import 'package:tallee/presentation/widgets/tiles/group_tile.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart'; import 'package:tallee/presentation/widgets/top_centered_message.dart';
class GroupsView extends StatefulWidget { class GroupView extends StatefulWidget {
/// A view that displays a list of groups /// A view that displays a list of groups
const GroupsView({super.key}); const GroupView({super.key});
@override @override
State<GroupsView> createState() => _GroupsViewState(); State<GroupView> createState() => _GroupViewState();
} }
class _GroupsViewState extends State<GroupsView> { class _GroupViewState extends State<GroupView> {
late final AppDatabase db; late final AppDatabase db;
/// Loaded groups from the database /// Loaded groups from the database
@@ -83,7 +83,7 @@ class _GroupsViewState extends State<GroupsView> {
context, context,
adaptivePageRoute( adaptivePageRoute(
builder: (context) { builder: (context) {
return GroupProfileView( return GroupDetailView(
group: groups[index], group: groups[index],
callback: loadGroups, callback: loadGroups,
); );

View File

@@ -4,10 +4,10 @@ import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/constants.dart'; import 'package:tallee/core/constants.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart'; import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart';
@@ -42,7 +42,13 @@ class _HomeViewState extends State<HomeView> {
2, 2,
Match( Match(
name: 'Skeleton Match', name: 'Skeleton Match',
game: Game(name: '', ruleset: Ruleset.singleWinner, description: '', color: GameColor.blue, icon: ''), game: Game(
name: '',
ruleset: Ruleset.singleWinner,
description: '',
color: GameColor.blue,
icon: '',
),
group: Group( group: Group(
name: 'Skeleton Group', name: 'Skeleton Group',
description: '', description: '',
@@ -104,7 +110,9 @@ class _HomeViewState extends State<HomeView> {
if (recentMatches.isNotEmpty) if (recentMatches.isNotEmpty)
for (Match match in recentMatches) for (Match match in recentMatches)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0), padding: const EdgeInsets.symmetric(
vertical: 6.0,
),
child: MatchTile( child: MatchTile(
compact: true, compact: true,
width: constraints.maxWidth * 0.9, width: constraints.maxWidth * 0.9,
@@ -113,7 +121,8 @@ class _HomeViewState extends State<HomeView> {
await Navigator.of(context).push( await Navigator.of(context).push(
adaptivePageRoute( adaptivePageRoute(
fullscreenDialog: true, fullscreenDialog: true,
builder: (context) => MatchResultView(match: match), builder: (context) =>
MatchResultView(match: match),
), ),
); );
await updatedWinnerInRecentMatches(match.id); await updatedWinnerInRecentMatches(match.id);
@@ -121,7 +130,10 @@ class _HomeViewState extends State<HomeView> {
), ),
) )
else else
Center(heightFactor: 5, child: Text(loc.no_recent_matches_available)), Center(
heightFactor: 5,
child: Text(loc.no_recent_matches_available),
),
], ],
), ),
), ),
@@ -137,22 +149,40 @@ class _HomeViewState extends State<HomeView> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
QuickCreateButton(text: 'Category 1', onPressed: () {}), QuickCreateButton(
QuickCreateButton(text: 'Category 2', onPressed: () {}), text: 'Category 1',
onPressed: () {},
),
QuickCreateButton(
text: 'Category 2',
onPressed: () {},
),
], ],
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
QuickCreateButton(text: 'Category 3', onPressed: () {}), QuickCreateButton(
QuickCreateButton(text: 'Category 4', onPressed: () {}), text: 'Category 3',
onPressed: () {},
),
QuickCreateButton(
text: 'Category 4',
onPressed: () {},
),
], ],
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
QuickCreateButton(text: 'Category 5', onPressed: () {}), QuickCreateButton(
QuickCreateButton(text: 'Category 6', onPressed: () {}), text: 'Category 5',
onPressed: () {},
),
QuickCreateButton(
text: 'Category 6',
onPressed: () {},
),
], ],
), ),
], ],
@@ -181,9 +211,11 @@ class _HomeViewState extends State<HomeView> {
matchCount = results[0] as int; matchCount = results[0] as int;
groupCount = results[1] as int; groupCount = results[1] as int;
loadedRecentMatches = results[2] as List<Match>; loadedRecentMatches = results[2] as List<Match>;
recentMatches = (loadedRecentMatches..sort((a, b) => b.createdAt.compareTo(a.createdAt))) recentMatches =
.take(2) (loadedRecentMatches
.toList(); ..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2)
.toList();
if (mounted) { if (mounted) {
setState(() { setState(() {
isLoading = false; isLoading = false;
@@ -195,7 +227,7 @@ class _HomeViewState extends State<HomeView> {
/// Updates the winner information for a specific match in the recent matches list. /// Updates the winner information for a specific match in the recent matches list.
Future<void> updatedWinnerInRecentMatches(String matchId) async { Future<void> updatedWinnerInRecentMatches(String matchId) async {
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
final winner = await db.matchDao.getWinner(matchId: matchId); final winner = await db.scoreEntryDao.getWinner(matchId: matchId);
final matchIndex = recentMatches.indexWhere((match) => match.id == matchId); final matchIndex = recentMatches.indexWhere((match) => match.id == matchId);
if (matchIndex != -1) { if (matchIndex != -1) {
setState(() { setState(() {

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:tallee/presentation/widgets/tiles/group_tile.dart'; import 'package:tallee/presentation/widgets/tiles/group_tile.dart';

View File

@@ -5,10 +5,10 @@ import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart'; import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_game_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_game_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart';
@@ -21,11 +21,22 @@ import 'package:tallee/presentation/widgets/tiles/choose_tile.dart';
class CreateMatchView extends StatefulWidget { class CreateMatchView extends StatefulWidget {
/// A view that allows creating a new match /// A view that allows creating a new match
/// [onWinnerChanged]: Optional callback invoked when the winner is changed /// [onWinnerChanged]: Optional callback invoked when the winner is changed
const CreateMatchView({super.key, this.onWinnerChanged}); const CreateMatchView({
super.key,
this.onWinnerChanged,
this.matchToEdit,
this.onMatchUpdated,
});
/// Optional callback invoked when the winner is changed /// Optional callback invoked when the winner is changed
final VoidCallback? onWinnerChanged; final VoidCallback? onWinnerChanged;
/// Optional callback invoked when the match is updated
final void Function(Match)? onMatchUpdated;
/// An optional match to prefill the fields
final Match? matchToEdit;
@override @override
State<CreateMatchView> createState() => _CreateMatchViewState(); State<CreateMatchView> createState() => _CreateMatchViewState();
} }
@@ -45,19 +56,9 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// List of all players from the database /// List of all players from the database
List<Player> playerList = []; List<Player> playerList = [];
/// List of players filtered based on the selected group
/// If a group is selected, this list contains all players from [playerList]
/// who are not members of the selected group. If no group is selected,
/// this list is identical to [playerList].
List<Player> filteredPlayerList = [];
/// The currently selected group /// The currently selected group
Group? selectedGroup; Group? selectedGroup;
/// The index of the currently selected group in [groupsList] to mark it in
/// the [ChooseGroupView]
String selectedGroupId = '';
/// The index of the currently selected game in [games] to mark it in /// The index of the currently selected game in [games] to mark it in
/// the [ChooseGameView] /// the [ChooseGameView]
int selectedGameIndex = -1; int selectedGameIndex = -1;
@@ -65,6 +66,9 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// The currently selected players /// The currently selected players
List<Player> selectedPlayers = []; List<Player> selectedPlayers = [];
/// GlobalKey for ScaffoldMessenger to show snackbars
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -80,9 +84,11 @@ class _CreateMatchViewState extends State<CreateMatchView> {
]).then((result) async { ]).then((result) async {
groupsList = result[0] as List<Group>; groupsList = result[0] as List<Group>;
playerList = result[1] as List<Player>; playerList = result[1] as List<Player>;
setState(() {
filteredPlayerList = List.from(playerList); // If a match is provided, prefill the fields
}); if (widget.matchToEdit != null) {
prefillMatchDetails();
}
}); });
} }
@@ -107,11 +113,19 @@ class _CreateMatchViewState extends State<CreateMatchView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
final buttonText = widget.matchToEdit != null
? loc.save_changes
: loc.create_match;
final viewTitle = widget.matchToEdit != null
? loc.edit_match
: loc.create_new_match;
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_match)), appBar: AppBar(title: Text(viewTitle)),
body: SafeArea( body: SafeArea(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@@ -153,96 +167,55 @@ class _CreateMatchViewState extends State<CreateMatchView> {
? loc.none_group ? loc.none_group
: selectedGroup!.name, : selectedGroup!.name,
onPressed: () async { onPressed: () async {
// Remove all players from the previously selected group from
// the selected players list, in case the user deselects the
// group or selects a different group.
selectedPlayers.removeWhere(
(player) =>
selectedGroup?.members.any(
(member) => member.id == player.id,
) ??
false,
);
selectedGroup = await Navigator.of(context).push( selectedGroup = await Navigator.of(context).push(
adaptivePageRoute( adaptivePageRoute(
builder: (context) => ChooseGroupView( builder: (context) => ChooseGroupView(
groups: groupsList, groups: groupsList,
initialGroupId: selectedGroupId, initialGroupId: selectedGroup?.id ?? '',
), ),
), ),
); );
selectedGroupId = selectedGroup?.id ?? '';
if (selectedGroup != null) { setState(() {
filteredPlayerList = playerList if (selectedGroup != null) {
.where( setState(() {
(p) => selectedPlayers += [...selectedGroup!.members];
!selectedGroup!.members.any((m) => m.id == p.id), });
) }
.toList(); });
} else {
filteredPlayerList = List.from(playerList);
}
setState(() {});
}, },
), ),
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'), key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialSelectedPlayers: selectedPlayers, initialSelectedPlayers: selectedPlayers,
availablePlayers: filteredPlayerList,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectedPlayers = value; selectedPlayers = value;
removeGroupWhenNoMemberLeft();
}); });
}, },
), ),
), ),
CustomWidthButton( CustomWidthButton(
text: loc.create_match, text: buttonText,
sizeRelativeToWidth: 0.95, sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary, buttonType: ButtonType.primary,
onPressed: _enableCreateGameButton() onPressed: _enableCreateGameButton()
? () async { ? () {
// Use a game from the games list buttonNavigation(context);
Game? gameToUse; }
if (selectedGameIndex == -1) {
// Use the first game as default if none selected
final selectedGame = games[0];
gameToUse = Game(
name: selectedGame.$1,
description: selectedGame.$2,
ruleset: selectedGame.$3,
color: GameColor.blue,
icon: '',
);
} else {
// Use the selected game from the list
final selectedGame = games[selectedGameIndex];
gameToUse = Game(
name: selectedGame.$1,
description: selectedGame.$2,
ruleset: selectedGame.$3,
color: GameColor.blue,
icon: '',
);
}
// Add the game to the database if it doesn't exist
await db.gameDao.addGame(game: gameToUse);
Match match = Match(
name: _matchNameController.text.isEmpty
? (hintText ?? '')
: _matchNameController.text.trim(),
createdAt: DateTime.now(),
game: gameToUse,
group: selectedGroup,
players: selectedPlayers,
notes: '',
);
await db.matchDao.addMatch(match: match);
if (context.mounted) {
Navigator.pushReplacement(
context,
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) => MatchResultView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
),
),
);
}
}
: null, : null,
), ),
], ],
@@ -259,6 +232,156 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// - Either a group is selected OR at least 2 players are selected /// - Either a group is selected OR at least 2 players are selected
bool _enableCreateGameButton() { bool _enableCreateGameButton() {
return (selectedGroup != null || return (selectedGroup != null ||
(selectedPlayers.length > 1)); (selectedPlayers.length > 1) && selectedGameIndex != -1);
}
// If a match was provided to the view, it updates the match in the database
// and navigates back to the previous screen.
// If no match was provided, it creates a new match in the database and
// navigates to the MatchResultView for the newly created match.
void buttonNavigation(BuildContext context) async {
if (widget.matchToEdit != null) {
await updateMatch();
if (context.mounted) {
Navigator.pop(context);
}
} else {
final match = await createMatch();
if (context.mounted) {
Navigator.pushReplacement(
context,
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) => MatchResultView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
),
),
);
}
}
}
/// Updates attributes of the existing match in the database based on the
/// changes made in the edit view.
Future<void> updateMatch() async {
//TODO: Remove when Games implemented
final tempGame = await getTemporaryGame();
final updatedMatch = Match(
id: widget.matchToEdit!.id,
name: _matchNameController.text.isEmpty
? (hintText ?? '')
: _matchNameController.text.trim(),
group: selectedGroup,
players: selectedPlayers,
game: tempGame,
winner: widget.matchToEdit!.winner,
createdAt: widget.matchToEdit!.createdAt,
endedAt: widget.matchToEdit!.endedAt,
notes: widget.matchToEdit!.notes,
);
if (widget.matchToEdit!.name != updatedMatch.name) {
await db.matchDao.updateMatchName(
matchId: widget.matchToEdit!.id,
newName: updatedMatch.name,
);
}
if (widget.matchToEdit!.group?.id != updatedMatch.group?.id) {
await db.matchDao.updateMatchGroup(
matchId: widget.matchToEdit!.id,
newGroupId: updatedMatch.group?.id,
);
}
// Add players who are in updatedMatch but not in the original match
for (var player in updatedMatch.players) {
if (!widget.matchToEdit!.players.any((p) => p.id == player.id)) {
await db.playerMatchDao.addPlayerToMatch(
matchId: widget.matchToEdit!.id,
playerId: player.id,
);
}
}
// Remove players who are in the original match but not in updatedMatch
for (var player in widget.matchToEdit!.players) {
if (!updatedMatch.players.any((p) => p.id == player.id)) {
await db.playerMatchDao.removePlayerFromMatch(
matchId: widget.matchToEdit!.id,
playerId: player.id,
);
if (widget.matchToEdit!.winner?.id == player.id) {
updatedMatch.winner = null;
}
}
}
widget.onMatchUpdated?.call(updatedMatch);
}
// Creates a new match and adds it to the database.
// Returns the created match.
Future<Match> createMatch() async {
final tempGame = await getTemporaryGame();
Match match = Match(
name: _matchNameController.text.isEmpty
? (hintText ?? '')
: _matchNameController.text.trim(),
createdAt: DateTime.now(),
group: selectedGroup,
players: selectedPlayers,
game: tempGame,
);
await db.matchDao.addMatch(match: match);
return match;
}
// TODO: Remove when games fully implemented
Future<Game> getTemporaryGame() async {
Game? game;
final selectedGame = games[selectedGameIndex];
game = Game(
name: selectedGame.$1,
description: selectedGame.$2,
ruleset: selectedGame.$3,
color: GameColor.blue,
icon: '',
);
await db.gameDao.addGame(game: game);
return game;
}
// If a match was provided to the view, this method prefills the input fields
void prefillMatchDetails() {
final match = widget.matchToEdit!;
_matchNameController.text = match.name;
selectedPlayers = match.players;
selectedGameIndex = 0;
if (match.group != null) {
selectedGroup = match.group;
}
}
// If none of the selected players are from the currently selected group,
// the group is also deselected.
Future<void> removeGroupWhenNoMemberLeft() async {
if (selectedGroup == null) return;
if (!selectedPlayers.any(
(player) =>
selectedGroup!.members.any((member) => member.id == player.id),
)) {
setState(() {
selectedGroup = null;
});
}
} }
} }

View File

@@ -0,0 +1,267 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class MatchDetailView extends StatefulWidget {
/// A view that displays the profile of a match
/// - [match]: The match to display
/// - [onMatchUpdate]: Callback to refresh the match list
const MatchDetailView({
super.key,
required this.match,
required this.onMatchUpdate,
});
/// The match to display
final Match match;
/// Callback to refresh the match list
final VoidCallback onMatchUpdate;
@override
State<MatchDetailView> createState() => _MatchDetailViewState();
}
class _MatchDetailViewState extends State<MatchDetailView> {
late final AppDatabase db;
late Match match;
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
match = widget.match;
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
title: Text(loc.match_profile),
actions: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
showDialog<bool>(
context: context,
builder: (context) => CustomAlertDialog(
title: '${loc.delete_match}?',
content: loc.this_cannot_be_undone,
actions: [
AnimatedDialogButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(
loc.cancel,
style: const TextStyle(color: CustomTheme.textColor),
),
),
AnimatedDialogButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(
loc.delete,
style: const TextStyle(
color: CustomTheme.secondaryColor,
),
),
),
],
),
).then((confirmed) async {
if (confirmed! && context.mounted) {
await db.matchDao.deleteMatch(matchId: match.id);
if (!context.mounted) return;
Navigator.pop(context);
widget.onMatchUpdate.call();
}
});
},
),
],
),
body: SafeArea(
child: Stack(
alignment: Alignment.center,
children: [
ListView(
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 20,
bottom: 100,
),
children: [
const Center(
child: ColoredIconContainer(
icon: Icons.sports_esports,
containerSize: 55,
iconSize: 38,
),
),
const SizedBox(height: 10),
Text(
match.name,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 5),
Text(
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}',
style: const TextStyle(
fontSize: 12,
color: CustomTheme.textColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
if (match.group != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.group),
const SizedBox(width: 8),
Text(
'${match.group!.name}${getExtraPlayerCount(match)}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 20),
],
InfoTile(
title: loc.players,
icon: Icons.people,
horizontalAlignment: CrossAxisAlignment.start,
content: Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: match.players.map((player) {
return TextIconTile(
text: player.name,
iconEnabled: false,
);
}).toList(),
),
),
const SizedBox(height: 15),
InfoTile(
title: loc.results,
icon: Icons.emoji_events,
content: Padding(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
/// TODO: Implement different ruleset results display
if (match.winner != null) ...[
Text(
loc.winner,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
Text(
match.winner!.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
] else ...[
Text(
loc.no_results_entered_yet,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
],
],
),
),
),
],
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: Row(
children: [
MainMenuButton(
icon: Icons.edit,
onPressed: () => Navigator.push(
context,
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) => CreateMatchView(
matchToEdit: match,
onMatchUpdated: onMatchUpdated,
),
),
),
),
const SizedBox(width: 15),
MainMenuButton(
text: loc.enter_results,
icon: Icons.emoji_events,
onPressed: () async {
match.winner = await Navigator.push(
context,
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) => MatchResultView(
match: match,
onWinnerChanged: () {
widget.onMatchUpdate.call();
setState(() {});
},
),
),
);
},
),
],
),
),
],
),
),
);
}
/// Callback for when the match is updated in the edit view,
/// updates the match in this view
void onMatchUpdated(Match editedMatch) {
setState(() {
match = editedMatch;
});
widget.onMatchUpdate.call();
}
}

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart';
@@ -35,7 +35,10 @@ class _MatchResultViewState extends State<MatchResultView> {
@override @override
void initState() { void initState() {
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
allPlayers = getAllPlayers(widget.match);
allPlayers = widget.match.players;
allPlayers.sort((a, b) => a.name.compareTo(b.name));
if (widget.match.winner != null) { if (widget.match.winner != null) {
_selectedPlayer = allPlayers.firstWhere( _selectedPlayer = allPlayers.firstWhere(
(p) => p.id == widget.match.winner!.id, (p) => p.id == widget.match.winner!.id,
@@ -54,7 +57,7 @@ class _MatchResultViewState extends State<MatchResultView> {
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
widget.onWinnerChanged?.call(); widget.onWinnerChanged?.call();
Navigator.of(context).pop(); Navigator.of(context).pop(_selectedPlayer);
}, },
), ),
title: Text(widget.match.name), title: Text(widget.match.name),
@@ -136,30 +139,13 @@ class _MatchResultViewState extends State<MatchResultView> {
/// based on the current selection. /// based on the current selection.
Future<void> _handleWinnerSaving() async { Future<void> _handleWinnerSaving() async {
if (_selectedPlayer == null) { if (_selectedPlayer == null) {
await db.matchDao.removeWinner(matchId: widget.match.id); await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
} else { } else {
await db.matchDao.setWinner( await db.scoreEntryDao.setWinner(
matchId: widget.match.id, matchId: widget.match.id,
winnerId: _selectedPlayer!.id, playerId: _selectedPlayer!.id,
); );
} }
widget.onWinnerChanged?.call(); widget.onWinnerChanged?.call();
} }
/// Retrieves all players associated with the given [match].
/// This includes players directly assigned to the match
/// as well as members of the group (if any).
/// The returned list is sorted alphabetically by player name.
List<Player> getAllPlayers(Match match) {
List<Player> players = [];
if (match.group == null) {
players = [...match.players];
} else {
players = [...match.players, ...match.group!.members];
}
players.sort((a, b) => a.name.compareTo(b.name));
return players;
}
} }

View File

@@ -1,5 +1,3 @@
import 'dart:core' hide Match;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:fluttericon/rpg_awesome_icons.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -8,13 +6,13 @@ import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart'; import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_detail_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/tiles/match_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_tile.dart';
@@ -38,7 +36,13 @@ class _MatchViewState extends State<MatchView> {
4, 4,
Match( Match(
name: 'Skeleton match name', name: 'Skeleton match name',
game: Game(name: '', ruleset: Ruleset.singleWinner, description: '', color: GameColor.blue, icon: ''), game: Game(
name: '',
ruleset: Ruleset.singleWinner,
description: '',
color: GameColor.blue,
icon: '',
),
group: Group( group: Group(
name: 'Group name', name: 'Group name',
description: '', description: '',
@@ -54,7 +58,7 @@ class _MatchViewState extends State<MatchView> {
void initState() { void initState() {
super.initState(); super.initState();
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
loadGames(); loadMatches();
} }
@override @override
@@ -94,10 +98,9 @@ class _MatchViewState extends State<MatchView> {
Navigator.push( Navigator.push(
context, context,
adaptivePageRoute( adaptivePageRoute(
fullscreenDialog: true, builder: (context) => MatchDetailView(
builder: (context) => MatchResultView(
match: matches[index], match: matches[index],
onWinnerChanged: loadGames, onMatchUpdate: loadMatches,
), ),
), ),
); );
@@ -120,7 +123,7 @@ class _MatchViewState extends State<MatchView> {
context, context,
adaptivePageRoute( adaptivePageRoute(
builder: (context) => builder: (context) =>
CreateMatchView(onWinnerChanged: loadGames), CreateMatchView(onWinnerChanged: loadMatches),
), ),
); );
}, },
@@ -131,8 +134,9 @@ class _MatchViewState extends State<MatchView> {
); );
} }
/// Loads the games from the database and sorts them by creation date. /// Loads the matches from the database and sorts them by creation date.
void loadGames() { void loadMatches() {
isLoading = true;
Future.wait([ Future.wait([
db.matchDao.getAllMatches(), db.matchDao.getAllMatches(),
Future.delayed(Constants.MINIMUM_SKELETON_DURATION), Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
@@ -140,7 +144,7 @@ class _MatchViewState extends State<MatchView> {
if (mounted) { if (mounted) {
setState(() { setState(() {
final loadedMatches = results[0] as List<Match>; final loadedMatches = results[0] as List<Match>;
matches = loadedMatches matches = [...loadedMatches]
..sort((a, b) => b.createdAt.compareTo(a.createdAt)); ..sort((a, b) => b.createdAt.compareTo(a.createdAt));
isLoading = false; isLoading = false;
}); });

View File

@@ -31,6 +31,7 @@ class _SettingsViewState extends State<SettingsView> {
version: 'n.A.', version: 'n.A.',
buildNumber: 'n.A.', buildNumber: 'n.A.',
); );
@override @override
void initState() { void initState() {
super.initState(); super.initState();

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/constants.dart'; import 'package:tallee/core/constants.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart';
@@ -167,7 +167,8 @@ class _StatisticsViewState extends State<StatisticsView> {
final playerId = winCounts[i].$1; final playerId = winCounts[i].$1;
final player = players.firstWhere( final player = players.firstWhere(
(p) => p.id == playerId, (p) => p.id == playerId,
orElse: () => Player(id: playerId, name: loc.not_available, description: ''), orElse: () =>
Player(id: playerId, name: loc.not_available, description: ''),
); );
winCounts[i] = (player.name, winCounts[i].$2); winCounts[i] = (player.name, winCounts[i].$2);
} }
@@ -208,11 +209,11 @@ class _StatisticsViewState extends State<StatisticsView> {
// -1 means player not found in matchCounts // -1 means player not found in matchCounts
if (index != -1) { if (index != -1) {
final current = matchCounts[index].$2; final current = matchCounts[index].$2;
matchCounts[index] = (playerId, current + 1); matchCounts[index] = (playerId, current + 1);
} else { } else {
matchCounts.add((playerId, 1)); matchCounts.add((playerId, 1));
}
} }
}
} }
// Adding all players with zero matches // Adding all players with zero matches
@@ -229,7 +230,8 @@ class _StatisticsViewState extends State<StatisticsView> {
final playerId = matchCounts[i].$1; final playerId = matchCounts[i].$1;
final player = players.firstWhere( final player = players.firstWhere(
(p) => p.id == playerId, (p) => p.id == playerId,
orElse: () => Player(id: playerId, name: loc.not_available, description: ''), orElse: () =>
Player(id: playerId, name: loc.not_available, description: ''),
); );
matchCounts[i] = (player.name, matchCounts[i].$2); matchCounts[i] = (player.name, matchCounts[i].$2);
} }

View File

@@ -47,10 +47,7 @@ class _AppSkeletonState extends State<AppSkeleton> {
: (Widget? currentChild, List<Widget> previousChildren) { : (Widget? currentChild, List<Widget> previousChildren) {
return Stack( return Stack(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
children: [ children: [...previousChildren, ?currentChild],
...previousChildren,
if (currentChild != null) currentChild,
],
); );
}, },
), ),

View File

@@ -2,25 +2,25 @@ import 'package:flutter/material.dart';
class MainMenuButton extends StatefulWidget { class MainMenuButton extends StatefulWidget {
/// A button for the main menu with an optional icon and a press animation. /// A button for the main menu with an optional icon and a press animation.
/// - [text]: The text of the button.
/// - [icon]: The icon of the button.
/// - [onPressed]: The callback to be invoked when the button is pressed. /// - [onPressed]: The callback to be invoked when the button is pressed.
/// - [icon]: The icon of the button.
/// - [text]: The text of the button.
const MainMenuButton({ const MainMenuButton({
super.key, super.key,
required this.text,
this.icon,
required this.onPressed, required this.onPressed,
required this.icon,
this.text,
}); });
/// The text of the button.
final String text;
/// The icon of the button.
final IconData? icon;
/// The callback to be invoked when the button is pressed. /// The callback to be invoked when the button is pressed.
final void Function() onPressed; final void Function() onPressed;
/// The icon of the button.
final IconData icon;
/// The text of the button.
final String? text;
@override @override
State<MainMenuButton> createState() => _MainMenuButtonState(); State<MainMenuButton> createState() => _MainMenuButtonState();
} }
@@ -71,18 +71,18 @@ class _MainMenuButtonState extends State<MainMenuButton>
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (widget.icon != null) ...[ Icon(widget.icon, size: 26, color: Colors.black),
Icon(widget.icon, size: 26, color: Colors.black), if (widget.text != null) ...[
const SizedBox(width: 7), const SizedBox(width: 7),
], Text(
Text( widget.text!,
widget.text, style: const TextStyle(
style: const TextStyle( fontSize: 18,
fontSize: 18, fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, color: Colors.black,
color: Colors.black, ),
), ),
), ],
], ],
), ),
), ),

View File

@@ -3,7 +3,7 @@ import 'package:provider/provider.dart';
import 'package:tallee/core/constants.dart'; import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart';
@@ -70,6 +70,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
super.initState(); super.initState();
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
suggestedPlayers = skeletonData; suggestedPlayers = skeletonData;
selectedPlayers = widget.initialSelectedPlayers ?? [];
loadPlayerList(); loadPlayerList();
} }
@@ -100,7 +101,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
if (value.isEmpty) { if (value.isEmpty) {
// If the search is empty, it shows all unselected players. // If the search is empty, it shows all unselected players.
suggestedPlayers = allPlayers.where((player) { suggestedPlayers = allPlayers.where((player) {
return !selectedPlayers.contains(player); return !selectedPlayers.any((p) => p.id == player.id);
}).toList(); }).toList();
} else { } else {
// If there is input, it filters by name match (case-insensitive) and ensures // If there is input, it filters by name match (case-insensitive) and ensures
@@ -109,8 +110,8 @@ class _PlayerSelectionState extends State<PlayerSelection> {
final bool nameMatches = player.name.toLowerCase().contains( final bool nameMatches = player.name.toLowerCase().contains(
value.toLowerCase(), value.toLowerCase(),
); );
final bool isNotSelected = !selectedPlayers.contains( final bool isNotSelected = !selectedPlayers.any(
player, (p) => p.id == player.id,
); );
return nameMatches && isNotSelected; return nameMatches && isNotSelected;
}).toList(); }).toList();
@@ -126,46 +127,49 @@ class _PlayerSelectionState extends State<PlayerSelection> {
const SizedBox(height: 10), const SizedBox(height: 10),
SizedBox( SizedBox(
height: 50, height: 50,
child: selectedPlayers.isEmpty child: AppSkeleton(
? Center(child: Text(loc.no_players_selected)) enabled: isLoading,
: SingleChildScrollView( child: selectedPlayers.isEmpty
scrollDirection: Axis.horizontal, ? Center(child: Text(loc.no_players_selected))
child: Row( : SingleChildScrollView(
children: [ scrollDirection: Axis.horizontal,
for (var player in selectedPlayers) child: Row(
Padding( children: [
padding: const EdgeInsets.only(right: 8.0), for (var player in selectedPlayers)
child: TextIconTile( Padding(
text: player.name, padding: const EdgeInsets.only(right: 8.0),
onIconTap: () { child: TextIconTile(
setState(() { text: player.name,
// Removes the player from the selection and notifies the parent. onIconTap: () {
selectedPlayers.remove(player); setState(() {
widget.onChanged([...selectedPlayers]); // Removes the player from the selection and notifies the parent.
selectedPlayers.remove(player);
widget.onChanged([...selectedPlayers]);
// Get the current search query // Get the current search query
final currentSearch = _searchBarController final currentSearch = _searchBarController
.text .text
.toLowerCase(); .toLowerCase();
// If the player matches the current search query (or search is empty), // If the player matches the current search query (or search is empty),
// they are added back to the `suggestedPlayers` and the list is re-sorted. // they are added back to the `suggestedPlayers` and the list is re-sorted.
if (currentSearch.isEmpty || if (currentSearch.isEmpty ||
player.name.toLowerCase().contains( player.name.toLowerCase().contains(
currentSearch, currentSearch,
)) { )) {
suggestedPlayers.add(player); suggestedPlayers.add(player);
suggestedPlayers.sort( suggestedPlayers.sort(
(a, b) => a.name.compareTo(b.name), (a, b) => a.name.compareTo(b.name),
); );
} }
}); });
}, },
),
), ),
), ],
], ),
), ),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
@@ -222,35 +226,53 @@ class _PlayerSelectionState extends State<PlayerSelection> {
db.playerDao.getAllPlayers(), db.playerDao.getAllPlayers(),
Future.delayed(Constants.MINIMUM_SKELETON_DURATION), Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
]).then((results) => results[0] as List<Player>); ]).then((results) => results[0] as List<Player>);
if (mounted) {
_allPlayersFuture.then((loadedPlayers) {
setState(() {
// If a list of available players is provided (even if empty), use that list.
if (widget.availablePlayers != null) {
widget.availablePlayers!.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...widget.availablePlayers!];
suggestedPlayers = [...allPlayers];
if (widget.initialSelectedPlayers != null) { _allPlayersFuture.then((loadedPlayers) {
// Ensures that only players available for selection are pre-selected. if (!mounted) return;
selectedPlayers = widget.initialSelectedPlayers! setState(() {
.where( // If a list of available players is provided (even if empty), use that list.
(p) => widget.availablePlayers!.any( if (widget.availablePlayers != null) {
(available) => available.id == p.id, widget.availablePlayers!.sort((a, b) => a.name.compareTo(b.name));
), allPlayers = [...widget.availablePlayers!];
) suggestedPlayers = [...allPlayers];
.toList();
} if (widget.initialSelectedPlayers != null) {
// Ensures that only players available for selection are pre-selected.
selectedPlayers = widget.initialSelectedPlayers!
.where(
(p) => widget.availablePlayers!.any(
(available) => available.id == p.id,
),
)
.toList();
}
} else {
// Otherwise, use the loaded players from the database.
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...loadedPlayers];
if (widget.initialSelectedPlayers != null) {
// Excludes already selected players from the suggested players list.
suggestedPlayers = loadedPlayers
.where(
(p) => !widget.initialSelectedPlayers!.any(
(ip) => ip.id == p.id,
),
)
.toList();
// Ensures that only players available for selection are pre-selected.
selectedPlayers = widget.initialSelectedPlayers!
.where(
(p) => allPlayers.any((available) => available.id == p.id),
)
.toList();
} else { } else {
// Otherwise, use the loaded players from the database. // If no initial selection, all loaded players are suggested.
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...loadedPlayers];
suggestedPlayers = [...loadedPlayers]; suggestedPlayers = [...loadedPlayers];
} }
isLoading = false; }
}); isLoading = false;
}); });
} });
} }
/// Adds a new player to the database from the search bar input. /// Adds a new player to the database from the search bar input.

View File

@@ -69,7 +69,6 @@ class CustomSearchBar extends StatelessWidget {
constraints ?? const BoxConstraints(maxHeight: 45, minHeight: 45), constraints ?? const BoxConstraints(maxHeight: 45, minHeight: 45),
hintText: hintText, hintText: hintText,
onChanged: onChanged, onChanged: onChanged,
hintStyle: WidgetStateProperty.all(const TextStyle(fontSize: 16)),
leading: const Icon(Icons.search), leading: const Icon(Icons.search),
trailing: [ trailing: [
Visibility( Visibility(

View File

@@ -36,11 +36,7 @@ class CustomRadioListTile<T> extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
Radio<T>( Radio<T>(value: value, toggleable: true),
value: value,
activeColor: CustomTheme.primaryColor,
toggleable: true,
),
Expanded( Expanded(
child: Text( child: Text(
text, text,

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class GroupTile extends StatefulWidget { class GroupTile extends StatefulWidget {

View File

@@ -1,8 +1,10 @@
import 'dart:core' hide Match;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -38,18 +40,13 @@ class MatchTile extends StatefulWidget {
} }
class _MatchTileState extends State<MatchTile> { class _MatchTileState extends State<MatchTile> {
late final List<Player> _allPlayers;
@override
void initState() {
super.initState();
_allPlayers = _getCombinedPlayers();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final group = widget.match.group; final match = widget.match;
final winner = widget.match.winner; final group = match.group;
final winner = match.winner;
final players = [...match.players]
..sort((a, b) => a.name.compareTo(b.name));
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return GestureDetector( return GestureDetector(
@@ -67,7 +64,7 @@ class _MatchTileState extends State<MatchTile> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
widget.match.name, match.name,
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -76,7 +73,7 @@ class _MatchTileState extends State<MatchTile> {
), ),
), ),
Text( Text(
_formatDate(widget.match.createdAt, context), _formatDate(match.createdAt, context),
style: const TextStyle(fontSize: 12, color: Colors.grey), style: const TextStyle(fontSize: 12, color: Colors.grey),
), ),
], ],
@@ -91,7 +88,7 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: Text( child: Text(
'${group.name} + ${widget.match.players.length}', '${match.group!.name}${getExtraPlayerCount(match)}',
style: const TextStyle(fontSize: 14, color: Colors.grey), style: const TextStyle(fontSize: 14, color: Colors.grey),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -106,7 +103,7 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: Text( child: Text(
'${widget.match.players.length} ${loc.players}', '${match.players.length} ${loc.players}',
style: const TextStyle(fontSize: 14, color: Colors.grey), style: const TextStyle(fontSize: 14, color: Colors.grey),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -192,7 +189,7 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
if (_allPlayers.isNotEmpty && widget.compact == false) ...[ if (players.isNotEmpty && widget.compact == false) ...[
Text( Text(
loc.players, loc.players,
style: const TextStyle( style: const TextStyle(
@@ -205,7 +202,7 @@ class _MatchTileState extends State<MatchTile> {
Wrap( Wrap(
spacing: 6, spacing: 6,
runSpacing: 6, runSpacing: 6,
children: _allPlayers.map((player) { children: players.map((player) {
return TextIconTile(text: player.name, iconEnabled: false); return TextIconTile(text: player.name, iconEnabled: false);
}).toList(), }).toList(),
), ),
@@ -233,32 +230,4 @@ class _MatchTileState extends State<MatchTile> {
return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}'; return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}';
} }
} }
/// Retrieves all unique players associated with the match,
/// combining players from both the match and its group.
List<Player> _getCombinedPlayers() {
final allPlayers = <Player>[];
final playerIds = <String>{};
// Add players from game.players
for (var player in widget.match.players) {
if (!playerIds.contains(player.id)) {
allPlayers.add(player);
playerIds.add(player.id);
}
}
// Add players from game.group.players
if (widget.match.group?.members != null) {
for (var player in widget.match.group!.members) {
if (!playerIds.contains(player.id)) {
allPlayers.add(player);
playerIds.add(player.id);
}
}
}
allPlayers.sort((a, b) => a.name.compareTo(b.name));
return allPlayers;
}
} }

View File

@@ -8,16 +8,17 @@ import 'package:json_schema/json_schema.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart'; import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/dto/team.dart'; import 'package:tallee/data/models/team.dart';
class DataTransferService { class DataTransferService {
/// Deletes all data from the database. /// Deletes all data from the database.
static Future<void> deleteAllData(BuildContext context) async { static Future<void> deleteAllData(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
await db.matchDao.deleteAllMatches(); await db.matchDao.deleteAllMatches();
await db.teamDao.deleteAllTeams(); await db.teamDao.deleteAllTeams();
await db.groupDao.deleteAllGroups(); await db.groupDao.deleteAllGroups();
@@ -40,33 +41,53 @@ class DataTransferService {
'players': players.map((p) => p.toJson()).toList(), 'players': players.map((p) => p.toJson()).toList(),
'games': games.map((g) => g.toJson()).toList(), 'games': games.map((g) => g.toJson()).toList(),
'groups': groups 'groups': groups
.map((g) => { .map(
'id': g.id, (g) => {
'name': g.name, 'id': g.id,
'description': g.description, 'name': g.name,
'createdAt': g.createdAt.toIso8601String(), 'description': g.description,
'memberIds': (g.members).map((m) => m.id).toList(), 'createdAt': g.createdAt.toIso8601String(),
}) 'memberIds': (g.members).map((m) => m.id).toList(),
},
)
.toList(), .toList(),
'teams': teams 'teams': teams
.map((t) => { .map(
'id': t.id, (t) => {
'name': t.name, 'id': t.id,
'createdAt': t.createdAt.toIso8601String(), 'name': t.name,
'memberIds': (t.members).map((m) => m.id).toList(), 'createdAt': t.createdAt.toIso8601String(),
}) 'memberIds': (t.members).map((m) => m.id).toList(),
},
)
.toList(), .toList(),
'matches': matches 'matches': matches
.map((m) => { .map(
'id': m.id, (m) => {
'name': m.name, 'id': m.id,
'createdAt': m.createdAt.toIso8601String(), 'name': m.name,
'endedAt': m.endedAt?.toIso8601String(), 'createdAt': m.createdAt.toIso8601String(),
'gameId': m.game.id, 'endedAt': m.endedAt?.toIso8601String(),
'groupId': m.group?.id, 'gameId': m.game.id,
'playerIds': m.players.map((p) => p.id).toList(), 'groupId': m.group?.id,
'notes': m.notes, 'playerIds': m.players.map((p) => p.id).toList(),
}) 'scores': m.scores.map(
(playerId, scores) => MapEntry(
playerId,
scores
.map(
(s) => {
'roundNumber': s.roundNumber,
'score': s.score,
'change': s.change,
},
)
.toList(),
),
),
'notes': m.notes,
},
)
.toList(), .toList(),
}; };
@@ -76,12 +97,12 @@ class DataTransferService {
/// Exports the given JSON string to a file with the specified name. /// Exports the given JSON string to a file with the specified name.
/// Returns an [ExportResult] indicating the outcome. /// Returns an [ExportResult] indicating the outcome.
/// ///
/// [jsonString] The JSON string to be exported. /// - [jsonString]: The JSON string to be exported.
/// [fileName] The desired name for the exported file (without extension). /// - [fileName]: The desired name for the exported file (without extension).
static Future<ExportResult> exportData( static Future<ExportResult> exportData(
String jsonString, String jsonString,
String fileName String fileName,
) async { ) async {
try { try {
final bytes = Uint8List.fromList(utf8.encode(jsonString)); final bytes = Uint8List.fromList(utf8.encode(jsonString));
final path = await FilePicker.platform.saveFile( final path = await FilePicker.platform.saveFile(
@@ -94,7 +115,6 @@ class DataTransferService {
} else { } else {
return ExportResult.success; return ExportResult.success;
} }
} catch (e, stack) { } catch (e, stack) {
print('[exportData] $e'); print('[exportData] $e');
print(stack); print(stack);
@@ -119,110 +139,12 @@ class DataTransferService {
final jsonString = await _readFileContent(path.files.single); final jsonString = await _readFileContent(path.files.single);
if (jsonString == null) return ImportResult.fileReadError; if (jsonString == null) return ImportResult.fileReadError;
final isValid = await _validateJsonSchema(jsonString); final isValid = await validateJsonSchema(jsonString);
if (!isValid) return ImportResult.invalidSchema; if (!isValid) return ImportResult.invalidSchema;
final Map<String, dynamic> decoded = json.decode(jsonString) as Map<String, dynamic>; final decoded = json.decode(jsonString) as Map<String, dynamic>;
final List<dynamic> playersJson = (decoded['players'] as List<dynamic>?) ?? []; await importDataToDatabase(db, decoded);
final List<dynamic> gamesJson = (decoded['games'] as List<dynamic>?) ?? [];
final List<dynamic> groupsJson = (decoded['groups'] as List<dynamic>?) ?? [];
final List<dynamic> teamsJson = (decoded['teams'] as List<dynamic>?) ?? [];
final List<dynamic> matchesJson = (decoded['matches'] as List<dynamic>?) ?? [];
// Import Players
final List<Player> importedPlayers = playersJson
.map((p) => Player.fromJson(p as Map<String, dynamic>))
.toList();
final Map<String, Player> playerById = {
for (final p in importedPlayers) p.id: p,
};
// Import Games
final List<Game> importedGames = gamesJson
.map((g) => Game.fromJson(g as Map<String, dynamic>))
.toList();
final Map<String, Game> gameById = {
for (final g in importedGames) g.id: g,
};
// Import Groups
final List<Group> importedGroups = groupsJson.map((g) {
final map = g as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? []).cast<String>();
final members = memberIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Group(
id: map['id'] as String,
name: map['name'] as String,
description: map['description'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
final Map<String, Group> groupById = {
for (final g in importedGroups) g.id: g,
};
// Import Teams
final List<Team> importedTeams = teamsJson.map((t) {
final map = t as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? []).cast<String>();
final members = memberIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Team(
id: map['id'] as String,
name: map['name'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
// Import Matches
final List<Match> importedMatches = matchesJson.map((m) {
final map = m as Map<String, dynamic>;
final String gameId = map['gameId'] as String;
final String? groupId = map['groupId'] as String?;
final List<String> playerIds = (map['playerIds'] as List<dynamic>? ?? []).cast<String>();
final DateTime? endedAt = map['endedAt'] != null ? DateTime.parse(map['endedAt'] as String) : null;
final game = gameById[gameId];
final group = (groupId == null) ? null : groupById[groupId];
final players = playerIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Match(
id: map['id'] as String,
name: map['name'] as String,
game: game ?? Game(name: 'Unknown', ruleset: Ruleset.singleWinner, description: '', color: GameColor.blue, icon: ''),
group: group,
players: players,
createdAt: DateTime.parse(map['createdAt'] as String),
endedAt: endedAt,
notes: map['notes'] as String? ?? '',
);
}).toList();
// Import all data into the database
await db.playerDao.addPlayersAsList(players: importedPlayers);
await db.gameDao.addGamesAsList(games: importedGames);
await db.groupDao.addGroupsAsList(groups: importedGroups);
await db.teamDao.addTeamsAsList(teams: importedTeams);
await db.matchDao.addMatchAsList(matches: importedMatches);
return ImportResult.success; return ImportResult.success;
} on FormatException catch (e, stack) { } on FormatException catch (e, stack) {
@@ -238,6 +160,167 @@ class DataTransferService {
} }
} }
/// Imports parsed JSON data into the database.
@visibleForTesting
static Future<void> importDataToDatabase(
AppDatabase db,
Map<String, dynamic> decodedJson,
) async {
// Fetch all entities first to create lookup maps for relationships
final importedPlayers = parsePlayersFromJson(decodedJson);
final playerById = {for (final p in importedPlayers) p.id: p};
final importedGames = parseGamesFromJson(decodedJson);
final gameById = {for (final g in importedGames) g.id: g};
final importedGroups = parseGroupsFromJson(decodedJson, playerById);
final groupById = {for (final g in importedGroups) g.id: g};
final importedTeams = parseTeamsFromJson(decodedJson, playerById);
final importedMatches = parseMatchesFromJson(
decodedJson,
gameById,
groupById,
playerById,
);
await db.playerDao.addPlayersAsList(players: importedPlayers);
await db.gameDao.addGamesAsList(games: importedGames);
await db.groupDao.addGroupsAsList(groups: importedGroups);
await db.teamDao.addTeamsAsList(teams: importedTeams);
await db.matchDao.addMatchAsList(matches: importedMatches);
}
/* Parsing Methods */
@visibleForTesting
static List<Player> parsePlayersFromJson(Map<String, dynamic> decodedJson) {
final playersJson = (decodedJson['players'] as List<dynamic>?) ?? [];
return playersJson
.map((p) => Player.fromJson(p as Map<String, dynamic>))
.toList();
}
@visibleForTesting
static List<Game> parseGamesFromJson(Map<String, dynamic> decodedJson) {
final gamesJson = (decodedJson['games'] as List<dynamic>?) ?? [];
return gamesJson
.map((g) => Game.fromJson(g as Map<String, dynamic>))
.toList();
}
@visibleForTesting
static List<Group> parseGroupsFromJson(
Map<String, dynamic> decodedJson,
Map<String, Player> playerById,
) {
final groupsJson = (decodedJson['groups'] as List<dynamic>?) ?? [];
return groupsJson.map((g) {
final map = g as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? [])
.cast<String>();
final members = memberIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Group(
id: map['id'] as String,
name: map['name'] as String,
description: map['description'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
}
/// Parses teams from JSON data.
@visibleForTesting
static List<Team> parseTeamsFromJson(
Map<String, dynamic> decodedJson,
Map<String, Player> playerById,
) {
final teamsJson = (decodedJson['teams'] as List<dynamic>?) ?? [];
return teamsJson.map((t) {
final map = t as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? [])
.cast<String>();
final members = memberIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Team(
id: map['id'] as String,
name: map['name'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
}
/// Parses matches from JSON data.
@visibleForTesting
static List<Match> parseMatchesFromJson(
Map<String, dynamic> decodedJson,
Map<String, Game> gamesMap,
Map<String, Group> groupsMap,
Map<String, Player> playersMap,
) {
final matchesJson = (decodedJson['matches'] as List<dynamic>?) ?? [];
return matchesJson.map((m) {
final map = m as Map<String, dynamic>;
// Extract attributes from json
final id = map['id'] as String;
final name = map['name'] as String;
final gameId = map['gameId'] as String;
final groupId = map['groupId'] as String?;
final createdAt = DateTime.parse(map['createdAt'] as String);
final endedAt = map['endedAt'] != null
? DateTime.parse(map['endedAt'] as String)
: null;
final notes = map['notes'] as String? ?? '';
// Link attributes to objects
final game = gamesMap[gameId] ?? getFallbackGame();
final group = groupId != null ? groupsMap[groupId] : null;
final playerIds = (map['playerIds'] as List<dynamic>? ?? [])
.cast<String>();
final players = playerIds
.map((id) => playersMap[id])
.whereType<Player>()
.toList();
return Match(
id: id,
name: name,
game: game,
group: group,
players: players,
createdAt: createdAt,
endedAt: endedAt,
notes: notes,
);
}).toList();
}
/// Creates a fallback game when the referenced game is not found.
@visibleForTesting
static Game getFallbackGame() {
return Game(
name: 'Unknown',
ruleset: Ruleset.singleWinner,
description: '',
color: GameColor.blue,
icon: '',
);
}
/// Helper method to read file content from either bytes or path /// Helper method to read file content from either bytes or path
static Future<String?> _readFileContent(PlatformFile file) async { static Future<String?> _readFileContent(PlatformFile file) async {
if (file.bytes != null) return utf8.decode(file.bytes!); if (file.bytes != null) return utf8.decode(file.bytes!);
@@ -245,8 +328,10 @@ class DataTransferService {
return null; return null;
} }
/// Validates the given JSON string against the predefined schema. /// Validates the given JSON string against the schema
static Future<bool> _validateJsonSchema(String jsonString) async { /// in `assets/schema.json`.
@visibleForTesting
static Future<bool> validateJsonSchema(String jsonString) async {
final String schemaString; final String schemaString;
schemaString = await rootBundle.loadString('assets/schema.json'); schemaString = await rootBundle.loadString('assets/schema.json');

View File

@@ -1,7 +1,7 @@
name: tallee name: tallee
description: "Tracking App for Card Games" description: "Tracking App for Card Games"
publish_to: 'none' publish_to: 'none'
version: 0.0.16+250 version: 0.0.20+254
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1
@@ -31,10 +31,10 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.5.4 build_runner: ^2.7.0
dart_pubspec_licenses: ^3.0.14 dart_pubspec_licenses: ^3.0.14
drift_dev: ^2.27.0 drift_dev: ^2.29.0
flutter_lints: ^5.0.0 flutter_lints: ^6.0.0
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@@ -3,8 +3,8 @@ import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -62,7 +62,6 @@ void main() {
await database.close(); await database.close();
}); });
group('Group Tests', () { group('Group Tests', () {
// Verifies that a single group can be added and retrieved with all fields and members intact. // Verifies that a single group can be added and retrieved with all fields and members intact.
test('Adding and fetching a single group works correctly', () async { test('Adding and fetching a single group works correctly', () async {
await database.groupDao.addGroup(group: testGroup1); await database.groupDao.addGroup(group: testGroup1);
@@ -277,20 +276,20 @@ void main() {
}); });
// Verifies that updateGroupDescription returns false for a non-existent group. // Verifies that updateGroupDescription returns false for a non-existent group.
test('updateGroupDescription returns false for non-existent group', test(
() async { 'updateGroupDescription returns false for non-existent group',
final updated = await database.groupDao.updateGroupDescription( () async {
groupId: 'non-existent-id', final updated = await database.groupDao.updateGroupDescription(
newDescription: 'New Description', groupId: 'non-existent-id',
); newDescription: 'New Description',
expect(updated, false); );
}); expect(updated, false);
},
);
// Verifies that deleteAllGroups removes all groups from the database. // Verifies that deleteAllGroups removes all groups from the database.
test('deleteAllGroups removes all groups', () async { test('deleteAllGroups removes all groups', () async {
await database.groupDao.addGroupsAsList( await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]);
groups: [testGroup1, testGroup2],
);
final countBefore = await database.groupDao.getGroupCount(); final countBefore = await database.groupDao.getGroupCount();
expect(countBefore, 2); expect(countBefore, 2);

View File

@@ -1,13 +1,13 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart' hide isNotNull;
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/dto/group.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -42,7 +42,7 @@ void main() {
testPlayer4 = Player(name: 'Diana', description: ''); testPlayer4 = Player(name: 'Diana', description: '');
testPlayer5 = Player(name: 'Eve', description: ''); testPlayer5 = Player(name: 'Eve', description: '');
testGroup1 = Group( testGroup1 = Group(
name: 'Test Group 2', name: 'Test Group 1',
description: '', description: '',
members: [testPlayer1, testPlayer2, testPlayer3], members: [testPlayer1, testPlayer2, testPlayer3],
); );
@@ -51,7 +51,13 @@ void main() {
description: '', description: '',
members: [testPlayer4, testPlayer5], members: [testPlayer4, testPlayer5],
); );
testGame = Game(name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', color: GameColor.blue, icon: ''); testGame = Game(
name: 'Test Game',
ruleset: Ruleset.singleWinner,
description: 'A test game',
color: GameColor.blue,
icon: '',
);
testMatch1 = Match( testMatch1 = Match(
name: 'First Test Match', name: 'First Test Match',
game: testGame, game: testGame,
@@ -99,7 +105,6 @@ void main() {
}); });
group('Match Tests', () { group('Match Tests', () {
// Verifies that a single match can be added and retrieved with all fields, group, and players intact. // Verifies that a single match can be added and retrieved with all fields, group, and players intact.
test('Adding and fetching single match works correctly', () async { test('Adding and fetching single match works correctly', () async {
await database.matchDao.addMatch(match: testMatch1); await database.matchDao.addMatch(match: testMatch1);
@@ -127,10 +132,7 @@ void main() {
for (int i = 0; i < testMatch1.players.length; i++) { for (int i = 0; i < testMatch1.players.length; i++) {
expect(result.players[i].id, testMatch1.players[i].id); expect(result.players[i].id, testMatch1.players[i].id);
expect(result.players[i].name, testMatch1.players[i].name); expect(result.players[i].name, testMatch1.players[i].name);
expect( expect(result.players[i].createdAt, testMatch1.players[i].createdAt);
result.players[i].createdAt,
testMatch1.players[i].createdAt,
);
} }
}); });
@@ -191,10 +193,7 @@ void main() {
for (int i = 0; i < testMatch.players.length; i++) { for (int i = 0; i < testMatch.players.length; i++) {
expect(match.players[i].id, testMatch.players[i].id); expect(match.players[i].id, testMatch.players[i].id);
expect(match.players[i].name, testMatch.players[i].name); expect(match.players[i].name, testMatch.players[i].name);
expect( expect(match.players[i].createdAt, testMatch.players[i].createdAt);
match.players[i].createdAt,
testMatch.players[i].createdAt,
);
} }
} }
}); });
@@ -282,5 +281,94 @@ void main() {
); );
expect(fetchedMatch.name, newName); expect(fetchedMatch.name, newName);
}); });
test('Fetching a winner works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
var fetchedMatch = await database.matchDao.getMatchById(
matchId: testMatch1.id,
);
expect(fetchedMatch.winner, isNotNull);
expect(fetchedMatch.winner!.id, testPlayer4.id);
});
test('Setting a winner works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
await database.scoreEntryDao.setWinner(
matchId: testMatch1.id,
playerId: testPlayer5.id,
);
final fetchedMatch = await database.matchDao.getMatchById(
matchId: testMatch1.id,
);
expect(fetchedMatch.winner, isNotNull);
expect(fetchedMatch.winner!.id, testPlayer5.id);
});
test(
'removeMatchGroup removes group from match with existing group',
() async {
await database.matchDao.addMatch(match: testMatch1);
final removed = await database.matchDao.removeMatchGroup(
matchId: testMatch1.id,
);
expect(removed, isTrue);
final updatedMatch = await database.matchDao.getMatchById(
matchId: testMatch1.id,
);
expect(updatedMatch.group, null);
expect(updatedMatch.game.id, testMatch1.game.id);
expect(updatedMatch.name, testMatch1.name);
expect(updatedMatch.notes, testMatch1.notes);
},
);
test(
'removeMatchGroup on match that already has no group still succeeds',
() async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final removed = await database.matchDao.removeMatchGroup(
matchId: testMatchOnlyPlayers.id,
);
expect(removed, isTrue);
final updatedMatch = await database.matchDao.getMatchById(
matchId: testMatchOnlyPlayers.id,
);
expect(updatedMatch.group, null);
},
);
test('removeMatchGroup on non-existing match returns false', () async {
final removed = await database.matchDao.removeMatchGroup(
matchId: 'non-existing-id',
);
expect(removed, isFalse);
});
test('Fetching all matches related to a group', () async {
var matches = await database.matchDao.getGroupMatches(
groupId: 'non-existing-id',
);
expect(matches, isEmpty);
await database.matchDao.addMatch(match: testMatch1);
matches = await database.matchDao.getGroupMatches(groupId: testGroup1.id);
expect(matches, isNotEmpty);
final match = matches.first;
expect(match.id, testMatch1.id);
expect(match.group, isNotNull);
expect(match.group!.id, testGroup1.id);
});
}); });
} }

View File

@@ -2,12 +2,12 @@ import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart';
import 'package:tallee/data/dto/match.dart';
import 'package:tallee/data/dto/player.dart';
import 'package:tallee/data/dto/team.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/team.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -37,20 +37,23 @@ void main() {
testPlayer2 = Player(name: 'Bob', description: ''); testPlayer2 = Player(name: 'Bob', description: '');
testPlayer3 = Player(name: 'Charlie', description: ''); testPlayer3 = Player(name: 'Charlie', description: '');
testPlayer4 = Player(name: 'Diana', description: ''); testPlayer4 = Player(name: 'Diana', description: '');
testTeam1 = Team( testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]);
name: 'Team Alpha', testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]);
members: [testPlayer1, testPlayer2], testTeam3 = Team(name: 'Team Gamma', members: [testPlayer1, testPlayer3]);
testGame1 = Game(
name: 'Game 1',
ruleset: Ruleset.singleWinner,
description: 'Test game 1',
color: GameColor.blue,
icon: '',
); );
testTeam2 = Team( testGame2 = Game(
name: 'Team Beta', name: 'Game 2',
members: [testPlayer3, testPlayer4], ruleset: Ruleset.highestScore,
description: 'Test game 2',
color: GameColor.red,
icon: '',
); );
testTeam3 = Team(
name: 'Team Gamma',
members: [testPlayer1, testPlayer3],
);
testGame1 = Game(name: 'Game 1', ruleset: Ruleset.singleWinner, description: 'Test game 1', color: GameColor.blue, icon: '');
testGame2 = Game(name: 'Game 2', ruleset: Ruleset.highestScore, description: 'Test game 2', color: GameColor.red, icon: '');
}); });
await database.playerDao.addPlayersAsList( await database.playerDao.addPlayersAsList(
@@ -65,7 +68,6 @@ void main() {
}); });
group('Team Tests', () { group('Team Tests', () {
// Verifies that a single team can be added and retrieved with all fields intact. // Verifies that a single team can be added and retrieved with all fields intact.
test('Adding and fetching a single team works correctly', () async { test('Adding and fetching a single team works correctly', () async {
final added = await database.teamDao.addTeam(team: testTeam1); final added = await database.teamDao.addTeam(team: testTeam1);
@@ -285,10 +287,7 @@ void main() {
test('Updating team name to empty string works', () async { test('Updating team name to empty string works', () async {
await database.teamDao.addTeam(team: testTeam1); await database.teamDao.addTeam(team: testTeam1);
await database.teamDao.updateTeamName( await database.teamDao.updateTeamName(teamId: testTeam1.id, newName: '');
teamId: testTeam1.id,
newName: '',
);
final updatedTeam = await database.teamDao.getTeamById( final updatedTeam = await database.teamDao.getTeamById(
teamId: testTeam1.id, teamId: testTeam1.id,
@@ -350,9 +349,7 @@ void main() {
await database.matchDao.addMatch(match: match2); await database.matchDao.addMatch(match: match2);
// Add teams to database // Add teams to database
await database.teamDao.addTeamsAsList( await database.teamDao.addTeamsAsList(teams: [testTeam1, testTeam3]);
teams: [testTeam1, testTeam3],
);
// Associate players with teams through match1 // Associate players with teams through match1
// testTeam1: player1, player2 // testTeam1: player1, player2
@@ -360,13 +357,11 @@ void main() {
playerId: testPlayer1.id, playerId: testPlayer1.id,
matchId: match1.id, matchId: match1.id,
teamId: testTeam1.id, teamId: testTeam1.id,
score: 0,
); );
await database.playerMatchDao.addPlayerToMatch( await database.playerMatchDao.addPlayerToMatch(
playerId: testPlayer2.id, playerId: testPlayer2.id,
matchId: match1.id, matchId: match1.id,
teamId: testTeam1.id, teamId: testTeam1.id,
score: 0,
); );
// Associate players with teams through match2 // Associate players with teams through match2
@@ -375,13 +370,11 @@ void main() {
playerId: testPlayer1.id, playerId: testPlayer1.id,
matchId: match2.id, matchId: match2.id,
teamId: testTeam3.id, teamId: testTeam3.id,
score: 0,
); );
await database.playerMatchDao.addPlayerToMatch( await database.playerMatchDao.addPlayerToMatch(
playerId: testPlayer3.id, playerId: testPlayer3.id,
matchId: match2.id, matchId: match2.id,
teamId: testTeam3.id, teamId: testTeam3.id,
score: 0,
); );
final team1 = await database.teamDao.getTeamById(teamId: testTeam1.id); final team1 = await database.teamDao.getTeamById(teamId: testTeam1.id);
@@ -420,10 +413,11 @@ void main() {
final allTeams = await database.teamDao.getAllTeams(); final allTeams = await database.teamDao.getAllTeams();
expect(allTeams.length, 3); expect(allTeams.length, 3);
expect( expect(allTeams.map((t) => t.id).toSet(), {
allTeams.map((t) => t.id).toSet(), testTeam1.id,
{testTeam1.id, testTeam2.id, testTeam3.id}, testTeam2.id,
); testTeam3.id,
});
}); });
// Verifies that teamExists returns false for deleted teams. // Verifies that teamExists returns false for deleted teams.
@@ -462,9 +456,7 @@ void main() {
// Verifies that addTeam after deleteAllTeams works correctly. // Verifies that addTeam after deleteAllTeams works correctly.
test('Adding team after deleteAllTeams works correctly', () async { test('Adding team after deleteAllTeams works correctly', () async {
await database.teamDao.addTeamsAsList( await database.teamDao.addTeamsAsList(teams: [testTeam1, testTeam2]);
teams: [testTeam1, testTeam2],
);
expect(await database.teamDao.getTeamCount(), 2); expect(await database.teamDao.getTeamCount(), 2);
await database.teamDao.deleteAllTeams(); await database.teamDao.deleteAllTeams();

View File

@@ -4,7 +4,7 @@ import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart'; import 'package:tallee/data/models/game.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -54,7 +54,6 @@ void main() {
}); });
group('Game Tests', () { group('Game Tests', () {
// Verifies that getAllGames returns an empty list when the database has no games. // Verifies that getAllGames returns an empty list when the database has no games.
test('getAllGames returns empty list when no games exist', () async { test('getAllGames returns empty list when no games exist', () async {
final allGames = await database.gameDao.getAllGames(); final allGames = await database.gameDao.getAllGames();
@@ -106,7 +105,7 @@ void main() {
// Verifies that getGameById throws a StateError when the game doesn't exist. // Verifies that getGameById throws a StateError when the game doesn't exist.
test('getGameById throws exception for non-existent game', () async { test('getGameById throws exception for non-existent game', () async {
expect( expect(
() => database.gameDao.getGameById(gameId: 'non-existent-id'), () => database.gameDao.getGameById(gameId: 'non-existent-id'),
throwsA(isA<StateError>()), throwsA(isA<StateError>()),
); );
}); });
@@ -134,7 +133,13 @@ void main() {
// Verifies that a game with empty optional fields can be added and retrieved. // Verifies that a game with empty optional fields can be added and retrieved.
test('addGame handles game with null optional fields', () async { test('addGame handles game with null optional fields', () async {
final gameWithNulls = Game(name: 'Simple Game', ruleset: Ruleset.lowestScore, description: 'A simple game', color: GameColor.green, icon: ''); final gameWithNulls = Game(
name: 'Simple Game',
ruleset: Ruleset.lowestScore,
description: 'A simple game',
color: GameColor.green,
icon: '',
);
final result = await database.gameDao.addGame(game: gameWithNulls); final result = await database.gameDao.addGame(game: gameWithNulls);
expect(result, true); expect(result, true);
@@ -419,9 +424,7 @@ void main() {
// Verifies that getGameCount updates correctly after deleting a game. // Verifies that getGameCount updates correctly after deleting a game.
test('getGameCount updates correctly after deletion', () async { test('getGameCount updates correctly after deletion', () async {
await database.gameDao.addGamesAsList( await database.gameDao.addGamesAsList(games: [testGame1, testGame2]);
games: [testGame1, testGame2],
);
final countBefore = await database.gameDao.getGameCount(); final countBefore = await database.gameDao.getGameCount();
expect(countBefore, 2); expect(countBefore, 2);

View File

@@ -3,7 +3,7 @@ import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -35,7 +35,6 @@ void main() {
}); });
group('Player Tests', () { group('Player Tests', () {
// Verifies that players can be added and retrieved with all fields intact. // Verifies that players can be added and retrieved with all fields intact.
test('Adding and fetching single player works correctly', () async { test('Adding and fetching single player works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
@@ -264,16 +263,22 @@ void main() {
}); });
// Verifies that a player with special characters in name is stored correctly. // Verifies that a player with special characters in name is stored correctly.
test('Player with special characters in name is stored correctly', () async { test(
final specialPlayer = Player(name: 'Test!@#\$%^&*()_+-=[]{}|;\':",.<>?/`~', description: ''); 'Player with special characters in name is stored correctly',
() async {
final specialPlayer = Player(
name: 'Test!@#\$%^&*()_+-=[]{}|;\':",.<>?/`~',
description: '',
);
await database.playerDao.addPlayer(player: specialPlayer); await database.playerDao.addPlayer(player: specialPlayer);
final fetchedPlayer = await database.playerDao.getPlayerById( final fetchedPlayer = await database.playerDao.getPlayerById(
playerId: specialPlayer.id, playerId: specialPlayer.id,
); );
expect(fetchedPlayer.name, specialPlayer.name); expect(fetchedPlayer.name, specialPlayer.name);
}); },
);
// Verifies that a player with description is stored correctly. // Verifies that a player with description is stored correctly.
test('Player with description is stored correctly', () async { test('Player with description is stored correctly', () async {
@@ -293,7 +298,10 @@ void main() {
// Verifies that a player with null description is stored correctly. // Verifies that a player with null description is stored correctly.
test('Player with null description is stored correctly', () async { test('Player with null description is stored correctly', () async {
final playerWithoutDescription = Player(name: 'No Description Player', description: ''); final playerWithoutDescription = Player(
name: 'No Description Player',
description: '',
);
await database.playerDao.addPlayer(player: playerWithoutDescription); await database.playerDao.addPlayer(player: playerWithoutDescription);

View File

@@ -3,8 +3,8 @@ import 'package:drift/drift.dart';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -42,7 +42,6 @@ void main() {
}); });
group('Player-Group Tests', () { group('Player-Group Tests', () {
// Verifies that a player can be added to an existing group and isPlayerInGroup returns true. // Verifies that a player can be added to an existing group and isPlayerInGroup returns true.
test('Adding a player to a group works correctly', () async { test('Adding a player to a group works correctly', () async {
await database.groupDao.addGroup(group: testGroup); await database.groupDao.addGroup(group: testGroup);
@@ -127,67 +126,83 @@ void main() {
}); });
// Verifies that addPlayerToGroup returns false when player already in group. // Verifies that addPlayerToGroup returns false when player already in group.
test('addPlayerToGroup returns false when player already in group', () async { test(
await database.groupDao.addGroup(group: testGroup); 'addPlayerToGroup returns false when player already in group',
() async {
await database.groupDao.addGroup(group: testGroup);
// testPlayer1 is already in testGroup via group creation // testPlayer1 is already in testGroup via group creation
final result = await database.playerGroupDao.addPlayerToGroup( final result = await database.playerGroupDao.addPlayerToGroup(
player: testPlayer1, player: testPlayer1,
groupId: testGroup.id, groupId: testGroup.id,
); );
expect(result, false); expect(result, false);
}); },
);
// Verifies that addPlayerToGroup adds player to player table if not exists. // Verifies that addPlayerToGroup adds player to player table if not exists.
test('addPlayerToGroup adds player to player table if not exists', () async { test(
await database.groupDao.addGroup(group: testGroup); 'addPlayerToGroup adds player to player table if not exists',
() async {
await database.groupDao.addGroup(group: testGroup);
// testPlayer4 is not in the database yet // testPlayer4 is not in the database yet
var playerExists = await database.playerDao.playerExists( var playerExists = await database.playerDao.playerExists(
playerId: testPlayer4.id, playerId: testPlayer4.id,
); );
expect(playerExists, false); expect(playerExists, false);
await database.playerGroupDao.addPlayerToGroup( await database.playerGroupDao.addPlayerToGroup(
player: testPlayer4, player: testPlayer4,
groupId: testGroup.id, groupId: testGroup.id,
); );
// Now player should exist in player table // Now player should exist in player table
playerExists = await database.playerDao.playerExists( playerExists = await database.playerDao.playerExists(
playerId: testPlayer4.id, playerId: testPlayer4.id,
); );
expect(playerExists, true); expect(playerExists, true);
}); },
);
// Verifies that removePlayerFromGroup returns false for non-existent player. // Verifies that removePlayerFromGroup returns false for non-existent player.
test('removePlayerFromGroup returns false for non-existent player', () async { test(
await database.groupDao.addGroup(group: testGroup); 'removePlayerFromGroup returns false for non-existent player',
() async {
await database.groupDao.addGroup(group: testGroup);
final result = await database.playerGroupDao.removePlayerFromGroup( final result = await database.playerGroupDao.removePlayerFromGroup(
playerId: 'non-existent-player-id', playerId: 'non-existent-player-id',
groupId: testGroup.id, groupId: testGroup.id,
); );
expect(result, false); expect(result, false);
}); },
);
// Verifies that removePlayerFromGroup returns false for non-existent group. // Verifies that removePlayerFromGroup returns false for non-existent group.
test('removePlayerFromGroup returns false for non-existent group', () async { test(
await database.playerDao.addPlayer(player: testPlayer1); 'removePlayerFromGroup returns false for non-existent group',
() async {
await database.playerDao.addPlayer(player: testPlayer1);
final result = await database.playerGroupDao.removePlayerFromGroup( final result = await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id, playerId: testPlayer1.id,
groupId: 'non-existent-group-id', groupId: 'non-existent-group-id',
); );
expect(result, false); expect(result, false);
}); },
);
// Verifies that getPlayersOfGroup returns empty list for group with no members. // Verifies that getPlayersOfGroup returns empty list for group with no members.
test('getPlayersOfGroup returns empty list for empty group', () async { test('getPlayersOfGroup returns empty list for empty group', () async {
final emptyGroup = Group(name: 'Empty Group', description: '', members: []); final emptyGroup = Group(
name: 'Empty Group',
description: '',
members: [],
);
await database.groupDao.addGroup(group: emptyGroup); await database.groupDao.addGroup(group: emptyGroup);
final players = await database.playerGroupDao.getPlayersOfGroup( final players = await database.playerGroupDao.getPlayersOfGroup(
@@ -198,13 +213,16 @@ void main() {
}); });
// Verifies that getPlayersOfGroup returns empty list for non-existent group. // Verifies that getPlayersOfGroup returns empty list for non-existent group.
test('getPlayersOfGroup returns empty list for non-existent group', () async { test(
final players = await database.playerGroupDao.getPlayersOfGroup( 'getPlayersOfGroup returns empty list for non-existent group',
groupId: 'non-existent-group-id', () async {
); final players = await database.playerGroupDao.getPlayersOfGroup(
groupId: 'non-existent-group-id',
);
expect(players, isEmpty); expect(players, isEmpty);
}); },
);
// Verifies that removing all players from a group leaves the group empty. // Verifies that removing all players from a group leaves the group empty.
test('Removing all players from a group leaves group empty', () async { test('Removing all players from a group leaves group empty', () async {
@@ -231,7 +249,11 @@ void main() {
// Verifies that a player can be in multiple groups. // Verifies that a player can be in multiple groups.
test('Player can be in multiple groups', () async { test('Player can be in multiple groups', () async {
final secondGroup = Group(name: 'Second Group', description: '', members: []); final secondGroup = Group(
name: 'Second Group',
description: '',
members: [],
);
await database.groupDao.addGroup(group: testGroup); await database.groupDao.addGroup(group: testGroup);
await database.groupDao.addGroup(group: secondGroup); await database.groupDao.addGroup(group: secondGroup);
@@ -255,29 +277,36 @@ void main() {
}); });
// Verifies that removing player from one group doesn't affect other groups. // Verifies that removing player from one group doesn't affect other groups.
test('Removing player from one group does not affect other groups', () async { test(
final secondGroup = Group(name: 'Second Group', description: '', members: [testPlayer1]); 'Removing player from one group does not affect other groups',
await database.groupDao.addGroup(group: testGroup); () async {
await database.groupDao.addGroup(group: secondGroup); final secondGroup = Group(
name: 'Second Group',
description: '',
members: [testPlayer1],
);
await database.groupDao.addGroup(group: testGroup);
await database.groupDao.addGroup(group: secondGroup);
// Remove testPlayer1 from testGroup // Remove testPlayer1 from testGroup
await database.playerGroupDao.removePlayerFromGroup( await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id, playerId: testPlayer1.id,
groupId: testGroup.id, groupId: testGroup.id,
); );
final inFirstGroup = await database.playerGroupDao.isPlayerInGroup( final inFirstGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id, playerId: testPlayer1.id,
groupId: testGroup.id, groupId: testGroup.id,
); );
final inSecondGroup = await database.playerGroupDao.isPlayerInGroup( final inSecondGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id, playerId: testPlayer1.id,
groupId: secondGroup.id, groupId: secondGroup.id,
); );
expect(inFirstGroup, false); expect(inFirstGroup, false);
expect(inSecondGroup, true); expect(inSecondGroup, true);
}); },
);
// Verifies that addPlayerToGroup returns true on successful addition. // Verifies that addPlayerToGroup returns true on successful addition.
test('addPlayerToGroup returns true on successful addition', () async { test('addPlayerToGroup returns true on successful addition', () async {
@@ -293,21 +322,26 @@ void main() {
}); });
// Verifies that removing the same player twice returns false on second attempt. // Verifies that removing the same player twice returns false on second attempt.
test('Removing same player twice returns false on second attempt', () async { test(
await database.groupDao.addGroup(group: testGroup); 'Removing same player twice returns false on second attempt',
() async {
await database.groupDao.addGroup(group: testGroup);
final firstRemoval = await database.playerGroupDao.removePlayerFromGroup( final firstRemoval = await database.playerGroupDao
playerId: testPlayer1.id, .removePlayerFromGroup(
groupId: testGroup.id, playerId: testPlayer1.id,
); groupId: testGroup.id,
expect(firstRemoval, true); );
expect(firstRemoval, true);
final secondRemoval = await database.playerGroupDao.removePlayerFromGroup( final secondRemoval = await database.playerGroupDao
playerId: testPlayer1.id, .removePlayerFromGroup(
groupId: testGroup.id, playerId: testPlayer1.id,
); groupId: testGroup.id,
expect(secondRemoval, false); );
}); expect(secondRemoval, false);
},
);
// Verifies that replaceGroupPlayers removes all existing players and replaces with new list. // Verifies that replaceGroupPlayers removes all existing players and replaces with new list.
test('replaceGroupPlayers replaces all group members correctly', () async { test('replaceGroupPlayers replaces all group members correctly', () async {

View File

@@ -4,11 +4,11 @@ import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/dto/game.dart'; import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/dto/team.dart'; import 'package:tallee/data/models/team.dart';
void main() { void main() {
late AppDatabase database; late AppDatabase database;
@@ -48,7 +48,13 @@ void main() {
description: '', description: '',
members: [testPlayer1, testPlayer2, testPlayer3], members: [testPlayer1, testPlayer2, testPlayer3],
); );
testGame = Game(name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', color: GameColor.blue, icon: ''); testGame = Game(
name: 'Test Game',
ruleset: Ruleset.singleWinner,
description: 'A test game',
color: GameColor.blue,
icon: '',
);
testMatchOnlyGroup = Match( testMatchOnlyGroup = Match(
name: 'Test Match with Group', name: 'Test Match with Group',
game: testGame, game: testGame,
@@ -61,14 +67,8 @@ void main() {
players: [testPlayer4, testPlayer5, testPlayer6], players: [testPlayer4, testPlayer5, testPlayer6],
notes: '', notes: '',
); );
testTeam1 = Team( testTeam1 = Team(name: 'Team Alpha', members: [testPlayer1, testPlayer2]);
name: 'Team Alpha', testTeam2 = Team(name: 'Team Beta', members: [testPlayer3, testPlayer4]);
members: [testPlayer1, testPlayer2],
);
testTeam2 = Team(
name: 'Team Beta',
members: [testPlayer3, testPlayer4],
);
}); });
await database.playerDao.addPlayersAsList( await database.playerDao.addPlayersAsList(
players: [ players: [
@@ -88,8 +88,6 @@ void main() {
}); });
group('Player-Match Tests', () { group('Player-Match Tests', () {
// Verifies that matchHasPlayers returns false initially and true after adding a player.
test('Match has player works correctly', () async { test('Match has player works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer1);
@@ -112,7 +110,6 @@ void main() {
expect(matchHasPlayers, true); expect(matchHasPlayers, true);
}); });
// Verifies that a player can be added to a match and isPlayerInMatch returns true.
test('Adding a player to a match works correctly', () async { test('Adding a player to a match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerDao.addPlayer(player: testPlayer5); await database.playerDao.addPlayer(player: testPlayer5);
@@ -136,7 +133,6 @@ void main() {
expect(playerAdded, false); expect(playerAdded, false);
}); });
// Verifies that a player can be removed from a match and the player count decreases.
test('Removing player from match works correctly', () async { test('Removing player from match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers); await database.matchDao.addMatch(match: testMatchOnlyPlayers);
@@ -153,30 +149,25 @@ void main() {
); );
expect(result.players.length, testMatchOnlyPlayers.players.length - 1); expect(result.players.length, testMatchOnlyPlayers.players.length - 1);
final playerExists = result.players.any( final playerExists = result.players.any((p) => p.id == playerToRemove.id);
(p) => p.id == playerToRemove.id,
);
expect(playerExists, false); expect(playerExists, false);
}); });
// Verifies that getPlayersOfMatch returns all players of a match with correct data.
test('Retrieving players of a match works correctly', () async { test('Retrieving players of a match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers); await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final players = await database.playerMatchDao.getPlayersOfMatch( final players =
matchId: testMatchOnlyPlayers.id, await database.playerMatchDao.getPlayersOfMatch(
) ?? []; matchId: testMatchOnlyPlayers.id,
) ??
[];
for (int i = 0; i < players.length; i++) { for (int i = 0; i < players.length; i++) {
expect(players[i].id, testMatchOnlyPlayers.players[i].id); expect(players[i].id, testMatchOnlyPlayers.players[i].id);
expect(players[i].name, testMatchOnlyPlayers.players[i].name); expect(players[i].name, testMatchOnlyPlayers.players[i].name);
expect( expect(players[i].createdAt, testMatchOnlyPlayers.players[i].createdAt);
players[i].createdAt,
testMatchOnlyPlayers.players[i].createdAt,
);
} }
}); });
// Verifies that updatePlayersFromMatch replaces all existing players with new ones.
test('Updating the match players works correctly', () async { test('Updating the match players works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers); await database.matchDao.addMatch(match: testMatchOnlyPlayers);
@@ -220,13 +211,22 @@ void main() {
} }
}); });
// Verifies that the same player can be added to multiple different matches.
test( test(
'Adding the same player to separate matches works correctly', 'Adding the same player to separate matches works correctly',
() async { () async {
final playersList = [testPlayer1, testPlayer2, testPlayer3]; final playersList = [testPlayer1, testPlayer2, testPlayer3];
final match1 = Match(name: 'Match 1', game: testGame, players: playersList, notes: ''); final match1 = Match(
final match2 = Match(name: 'Match 2', game: testGame, players: playersList, notes: ''); name: 'Match 1',
game: testGame,
players: playersList,
notes: '',
);
final match2 = Match(
name: 'Match 2',
game: testGame,
players: playersList,
notes: '',
);
await Future.wait([ await Future.wait([
database.matchDao.addMatch(match: match1), database.matchDao.addMatch(match: match1),
@@ -267,83 +267,6 @@ void main() {
expect(players, isNull); expect(players, isNull);
}); });
// Verifies that adding a player with initial score works correctly.
test('Adding player with initial score works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
score: 100,
);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
);
expect(score, 100);
});
// Verifies that getPlayerScore returns the correct score.
test('getPlayerScore returns correct score', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// Default score should be 0 when added through match
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 0);
});
// Verifies that getPlayerScore returns null for non-existent player-match combination.
test('getPlayerScore returns null for non-existent player in match', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: 'non-existent-player-id',
);
expect(score, isNull);
});
// Verifies that updatePlayerScore updates the score correctly.
test('updatePlayerScore updates score correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 50,
);
expect(updated, true);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 50);
});
// Verifies that updatePlayerScore returns false for non-existent player-match.
test('updatePlayerScore returns false for non-existent player-match', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: 'non-existent-player-id',
newScore: 50,
);
expect(updated, false);
});
// Verifies that adding a player with teamId works correctly.
test('Adding player with teamId works correctly', () async { test('Adding player with teamId works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1); await database.teamDao.addTeam(team: testTeam1);
@@ -363,7 +286,6 @@ void main() {
expect(playersInTeam[0].id, testPlayer1.id); expect(playersInTeam[0].id, testPlayer1.id);
}); });
// Verifies that updatePlayerTeam updates the team correctly.
test('updatePlayerTeam updates team correctly', () async { test('updatePlayerTeam updates team correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1); await database.teamDao.addTeam(team: testTeam1);
@@ -402,7 +324,6 @@ void main() {
expect(playersInTeam1.isEmpty, true); expect(playersInTeam1.isEmpty, true);
}); });
// Verifies that updatePlayerTeam can set team to null.
test('updatePlayerTeam can remove player from team', () async { test('updatePlayerTeam can remove player from team', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1); await database.teamDao.addTeam(team: testTeam1);
@@ -430,18 +351,20 @@ void main() {
expect(playersInTeam.isEmpty, true); expect(playersInTeam.isEmpty, true);
}); });
// Verifies that updatePlayerTeam returns false for non-existent player-match. test(
test('updatePlayerTeam returns false for non-existent player-match', () async { 'updatePlayerTeam returns false for non-existent player-match',
await database.matchDao.addMatch(match: testMatchOnlyGroup); () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
final updated = await database.playerMatchDao.updatePlayerTeam( final updated = await database.playerMatchDao.updatePlayerTeam(
matchId: testMatchOnlyGroup.id, matchId: testMatchOnlyGroup.id,
playerId: 'non-existent-player-id', playerId: 'non-existent-player-id',
teamId: testTeam1.id, teamId: testTeam1.id,
); );
expect(updated, false); expect(updated, false);
}); },
);
// Verifies that getPlayersInTeam returns empty list for non-existent team. // Verifies that getPlayersInTeam returns empty list for non-existent team.
test('getPlayersInTeam returns empty list for non-existent team', () async { test('getPlayersInTeam returns empty list for non-existent team', () async {
@@ -455,7 +378,6 @@ void main() {
expect(players.isEmpty, true); expect(players.isEmpty, true);
}); });
// Verifies that getPlayersInTeam returns all players of a team.
test('getPlayersInTeam returns all players of a team', () async { test('getPlayersInTeam returns all players of a team', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1); await database.teamDao.addTeam(team: testTeam1);
@@ -482,43 +404,34 @@ void main() {
expect(playerIds.contains(testPlayer2.id), true); expect(playerIds.contains(testPlayer2.id), true);
}); });
// Verifies that removePlayerFromMatch returns false for non-existent player. test(
test('removePlayerFromMatch returns false for non-existent player', () async { 'removePlayerFromMatch returns false for non-existent player',
await database.matchDao.addMatch(match: testMatchOnlyPlayers); () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final removed = await database.playerMatchDao.removePlayerFromMatch( final removed = await database.playerMatchDao.removePlayerFromMatch(
playerId: 'non-existent-player-id', playerId: 'non-existent-player-id',
matchId: testMatchOnlyPlayers.id, matchId: testMatchOnlyPlayers.id,
); );
expect(removed, false); expect(removed, false);
}); },
);
// Verifies that adding the same player twice to the same match is ignored.
test('Adding same player twice to same match is ignored', () async { test('Adding same player twice to same match is ignored', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerMatchDao.addPlayerToMatch( await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id, matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id, playerId: testPlayer1.id,
score: 10,
); );
// Try to add the same player again with different score // Try to add the same player again with different score
await database.playerMatchDao.addPlayerToMatch( await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id, matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id, playerId: testPlayer1.id,
score: 100,
); );
// Score should still be 10 because insert was ignored
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
);
expect(score, 10);
// Verify player count is still 1 // Verify player count is still 1
final players = await database.playerMatchDao.getPlayersOfMatch( final players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyGroup.id, matchId: testMatchOnlyGroup.id,
@@ -527,30 +440,31 @@ void main() {
expect(players?.length, 1); expect(players?.length, 1);
}); });
// Verifies that updatePlayersFromMatch with empty list removes all players. test(
test('updatePlayersFromMatch with empty list removes all players', () async { 'updatePlayersFromMatch with empty list removes all players',
await database.matchDao.addMatch(match: testMatchOnlyPlayers); () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// Verify players exist initially // Verify players exist initially
var players = await database.playerMatchDao.getPlayersOfMatch( var players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyPlayers.id, matchId: testMatchOnlyPlayers.id,
); );
expect(players?.length, 3); expect(players?.length, 3);
// Update with empty list // Update with empty list
await database.playerMatchDao.updatePlayersFromMatch( await database.playerMatchDao.updatePlayersFromMatch(
matchId: testMatchOnlyPlayers.id, matchId: testMatchOnlyPlayers.id,
newPlayer: [], newPlayer: [],
); );
// Verify all players are removed // Verify all players are removed
players = await database.playerMatchDao.getPlayersOfMatch( players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyPlayers.id, matchId: testMatchOnlyPlayers.id,
); );
expect(players, isNull); expect(players, isNull);
}); },
);
// Verifies that updatePlayersFromMatch with same players makes no changes.
test('updatePlayersFromMatch with same players makes no changes', () async { test('updatePlayersFromMatch with same players makes no changes', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers); await database.matchDao.addMatch(match: testMatchOnlyPlayers);
@@ -572,7 +486,6 @@ void main() {
} }
}); });
// Verifies that matchHasPlayers returns false for non-existent match.
test('matchHasPlayers returns false for non-existent match', () async { test('matchHasPlayers returns false for non-existent match', () async {
final hasPlayers = await database.playerMatchDao.matchHasPlayers( final hasPlayers = await database.playerMatchDao.matchHasPlayers(
matchId: 'non-existent-match-id', matchId: 'non-existent-match-id',
@@ -581,7 +494,6 @@ void main() {
expect(hasPlayers, false); expect(hasPlayers, false);
}); });
// Verifies that isPlayerInMatch returns false for non-existent match.
test('isPlayerInMatch returns false for non-existent match', () async { test('isPlayerInMatch returns false for non-existent match', () async {
final isInMatch = await database.playerMatchDao.isPlayerInMatch( final isInMatch = await database.playerMatchDao.isPlayerInMatch(
matchId: 'non-existent-match-id', matchId: 'non-existent-match-id',
@@ -591,127 +503,20 @@ void main() {
expect(isInMatch, false); expect(isInMatch, false);
}); });
// Verifies that updatePlayersFromMatch preserves scores for existing players.
test('updatePlayersFromMatch only modifies player associations', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// Update score for existing player
await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 75,
);
// Update players, keeping testPlayer4 and adding testPlayer1
await database.playerMatchDao.updatePlayersFromMatch(
matchId: testMatchOnlyPlayers.id,
newPlayer: [testPlayer4, testPlayer1],
);
// Verify testPlayer4's score is preserved
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 75);
// Verify testPlayer1 was added with default score
final newPlayerScore = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer1.id,
);
expect(newPlayerScore, 0);
});
// Verifies that adding a player with both score and teamId works correctly.
test('Adding player with score and teamId works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
teamId: testTeam1.id,
score: 150,
);
// Verify score
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
);
expect(score, 150);
// Verify team assignment
final playersInTeam = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyGroup.id,
teamId: testTeam1.id,
);
expect(playersInTeam.length, 1);
expect(playersInTeam[0].id, testPlayer1.id);
});
// Verifies that updating score with negative value works.
test('updatePlayerScore with negative score works', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: -10,
);
expect(updated, true);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, -10);
});
// Verifies that updating score with zero value works.
test('updatePlayerScore with zero score works', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// First set a non-zero score
await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 100,
);
// Then update to zero
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 0,
);
expect(updated, true);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 0);
});
// Verifies that getPlayersInTeam returns empty list for non-existent match. // Verifies that getPlayersInTeam returns empty list for non-existent match.
test('getPlayersInTeam returns empty list for non-existent match', () async { test(
await database.teamDao.addTeam(team: testTeam1); 'getPlayersInTeam returns empty list for non-existent match',
() async {
await database.teamDao.addTeam(team: testTeam1);
final players = await database.playerMatchDao.getPlayersInTeam( final players = await database.playerMatchDao.getPlayersInTeam(
matchId: 'non-existent-match-id', matchId: 'non-existent-match-id',
teamId: testTeam1.id, teamId: testTeam1.id,
); );
expect(players.isEmpty, true); expect(players.isEmpty, true);
}); },
);
// Verifies that players in different teams within the same match are returned correctly. // Verifies that players in different teams within the same match are returned correctly.
test('Players in different teams within same match are separate', () async { test('Players in different teams within same match are separate', () async {
@@ -759,8 +564,18 @@ void main() {
// Verifies that removePlayerFromMatch does not affect other matches. // Verifies that removePlayerFromMatch does not affect other matches.
test('removePlayerFromMatch does not affect other matches', () async { test('removePlayerFromMatch does not affect other matches', () async {
final playersList = [testPlayer1, testPlayer2]; final playersList = [testPlayer1, testPlayer2];
final match1 = Match(name: 'Match 1', game: testGame, players: playersList, notes: ''); final match1 = Match(
final match2 = Match(name: 'Match 2', game: testGame, players: playersList, notes: ''); name: 'Match 1',
game: testGame,
players: playersList,
notes: '',
);
final match2 = Match(
name: 'Match 2',
game: testGame,
players: playersList,
notes: '',
);
await Future.wait([ await Future.wait([
database.matchDao.addMatch(match: match1), database.matchDao.addMatch(match: match1),
@@ -789,56 +604,20 @@ void main() {
expect(isInMatch2, true); expect(isInMatch2, true);
}); });
// Verifies that updating scores for players in different matches are independent.
test('Player scores are independent across matches', () async {
final playersList = [testPlayer1];
final match1 = Match(name: 'Match 1', game: testGame, players: playersList, notes: '');
final match2 = Match(name: 'Match 2', game: testGame, players: playersList, notes: '');
await Future.wait([
database.matchDao.addMatch(match: match1),
database.matchDao.addMatch(match: match2),
]);
// Update score in match1
await database.playerMatchDao.updatePlayerScore(
matchId: match1.id,
playerId: testPlayer1.id,
newScore: 100,
);
// Update score in match2
await database.playerMatchDao.updatePlayerScore(
matchId: match2.id,
playerId: testPlayer1.id,
newScore: 50,
);
// Verify scores are independent
final scoreInMatch1 = await database.playerMatchDao.getPlayerScore(
matchId: match1.id,
playerId: testPlayer1.id,
);
final scoreInMatch2 = await database.playerMatchDao.getPlayerScore(
matchId: match2.id,
playerId: testPlayer1.id,
);
expect(scoreInMatch1, 100);
expect(scoreInMatch2, 50);
});
// Verifies that updatePlayersFromMatch on non-existent match fails with constraint error. // Verifies that updatePlayersFromMatch on non-existent match fails with constraint error.
test('updatePlayersFromMatch on non-existent match fails with foreign key constraint', () async { test(
// Should throw due to foreign key constraint - match doesn't exist 'updatePlayersFromMatch on non-existent match fails with foreign key constraint',
await expectLater( () async {
database.playerMatchDao.updatePlayersFromMatch( // Should throw due to foreign key constraint - match doesn't exist
matchId: 'non-existent-match-id', await expectLater(
newPlayer: [testPlayer1, testPlayer2], database.playerMatchDao.updatePlayersFromMatch(
), matchId: 'non-existent-match-id',
throwsA(anything), newPlayer: [testPlayer1, testPlayer2],
); ),
}); throwsA(anything),
);
},
);
// Verifies that a player can be in a match without being assigned to a team. // Verifies that a player can be in a match without being assigned to a team.
test('Player can exist in match without team assignment', () async { test('Player can exist in match without team assignment', () async {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,926 @@
import 'dart:convert';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' hide isNull, isNotNull;
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/services/data_transfer_service.dart';
void main() {
late AppDatabase database;
late Player testPlayer1;
late Player testPlayer2;
late Player testPlayer3;
late Game testGame;
late Group testGroup;
late Team testTeam;
late Match testMatch;
final fixedDate = DateTime(2025, 11, 19, 0, 11, 23);
final fakeClock = Clock(() => fixedDate);
setUp(() {
database = AppDatabase(
DatabaseConnection(
NativeDatabase.memory(),
closeStreamsSynchronously: true,
),
);
withClock(fakeClock, () {
testPlayer1 = Player(name: 'Alice', description: 'First test player');
testPlayer2 = Player(name: 'Bob', description: 'Second test player');
testPlayer3 = Player(name: 'Charlie', description: 'Third player');
testGame = Game(
name: 'Chess',
ruleset: Ruleset.singleWinner,
description: 'Strategic board game',
color: GameColor.blue,
icon: 'chess_icon',
);
testGroup = Group(
name: 'Test Group',
description: 'Group for testing',
members: [testPlayer1, testPlayer2],
);
testTeam = Team(name: 'Test Team', members: [testPlayer1, testPlayer2]);
testMatch = Match(
name: 'Test Match',
game: testGame,
group: testGroup,
players: [testPlayer1, testPlayer2],
notes: 'Test notes',
scores: {
testPlayer1.id: [
ScoreEntry(roundNumber: 1, score: 10, change: 10),
ScoreEntry(roundNumber: 2, score: 20, change: 10),
],
testPlayer2.id: [
ScoreEntry(roundNumber: 1, score: 15, change: 15),
ScoreEntry(roundNumber: 2, score: 25, change: 10),
],
},
);
});
});
tearDown(() async {
await database.close();
});
// Helper for getting BuildContext
Future<BuildContext> getContext(WidgetTester tester) async {
// Minimal widget with Provider
await tester.pumpWidget(
Provider<AppDatabase>.value(
value: database,
child: MaterialApp(
home: Builder(
builder: (context) {
return Container();
},
),
),
),
);
final BuildContext context = tester.element(find.byType(Container));
return context;
}
group('DataTransferService Tests', () {
testWidgets('deleteAllData()', (tester) async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.gameDao.addGame(game: testGame);
await database.groupDao.addGroup(group: testGroup);
await database.teamDao.addTeam(team: testTeam);
await database.matchDao.addMatch(match: testMatch);
var playerCount = await database.playerDao.getPlayerCount();
var gameCount = await database.gameDao.getGameCount();
var groupCount = await database.groupDao.getGroupCount();
var teamCount = await database.teamDao.getTeamCount();
var matchCount = await database.matchDao.getMatchCount();
expect(playerCount, greaterThan(0));
expect(gameCount, greaterThan(0));
expect(groupCount, greaterThan(0));
expect(teamCount, greaterThan(0));
expect(matchCount, greaterThan(0));
final ctx = await getContext(tester);
await DataTransferService.deleteAllData(ctx);
playerCount = await database.playerDao.getPlayerCount();
gameCount = await database.gameDao.getGameCount();
groupCount = await database.groupDao.getGroupCount();
teamCount = await database.teamDao.getTeamCount();
matchCount = await database.matchDao.getMatchCount();
expect(playerCount, 0);
expect(gameCount, 0);
expect(groupCount, 0);
expect(teamCount, 0);
expect(matchCount, 0);
});
group('getAppDataAsJson()', () {
group('Whole export', () {
testWidgets('Exporting app data works correctly', (tester) async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer2);
await database.gameDao.addGame(game: testGame);
await database.groupDao.addGroup(group: testGroup);
await database.teamDao.addTeam(team: testTeam);
await database.matchDao.addMatch(match: testMatch);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
expect(jsonString, isNotEmpty);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
expect(decoded.containsKey('players'), true);
expect(decoded.containsKey('games'), true);
expect(decoded.containsKey('groups'), true);
expect(decoded.containsKey('teams'), true);
expect(decoded.containsKey('matches'), true);
final players = decoded['players'] as List<dynamic>;
final games = decoded['games'] as List<dynamic>;
final groups = decoded['groups'] as List<dynamic>;
final teams = decoded['teams'] as List<dynamic>;
final matches = decoded['matches'] as List<dynamic>;
expect(players.length, 2);
expect(games.length, 1);
expect(groups.length, 1);
expect(teams.length, 1);
expect(matches.length, 1);
});
testWidgets('Exporting empty data works correctly', (tester) async {
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final players = decoded['players'] as List<dynamic>;
final games = decoded['games'] as List<dynamic>;
final groups = decoded['groups'] as List<dynamic>;
final teams = decoded['teams'] as List<dynamic>;
final matches = decoded['matches'] as List<dynamic>;
expect(players, isEmpty);
expect(games, isEmpty);
expect(groups, isEmpty);
expect(teams, isEmpty);
expect(matches, isEmpty);
});
});
group('Checking specific data', () {
testWidgets('Player data is correct', (tester) async {
await database.playerDao.addPlayer(player: testPlayer1);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final players = decoded['players'] as List<dynamic>;
final playerData = players[0] as Map<String, dynamic>;
expect(playerData['id'], testPlayer1.id);
expect(playerData['name'], testPlayer1.name);
expect(playerData['description'], testPlayer1.description);
expect(
playerData['createdAt'],
testPlayer1.createdAt.toIso8601String(),
);
});
testWidgets('Game data is correct', (tester) async {
await database.gameDao.addGame(game: testGame);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final games = decoded['games'] as List<dynamic>;
final gameData = games[0] as Map<String, dynamic>;
expect(gameData['id'], testGame.id);
expect(gameData['name'], testGame.name);
expect(gameData['ruleset'], testGame.ruleset.name);
expect(gameData['description'], testGame.description);
expect(gameData['color'], testGame.color.name);
expect(gameData['icon'], testGame.icon);
});
testWidgets('Group data is correct', (tester) async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer2);
await database.groupDao.addGroup(group: testGroup);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final groups = decoded['groups'] as List<dynamic>;
final groupData = groups[0] as Map<String, dynamic>;
expect(groupData['id'], testGroup.id);
expect(groupData['name'], testGroup.name);
expect(groupData['description'], testGroup.description);
expect(groupData['memberIds'], isA<List>());
final memberIds = groupData['memberIds'] as List<dynamic>;
expect(memberIds.length, 2);
expect(memberIds, containsAll([testPlayer1.id, testPlayer2.id]));
});
testWidgets('Team data is correct', (tester) async {
await database.teamDao.addTeam(team: testTeam);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final teams = decoded['teams'] as List<dynamic>;
expect(teams.length, 1);
final teamData = teams[0] as Map<String, dynamic>;
expect(teamData['id'], testTeam.id);
expect(teamData['name'], testTeam.name);
expect(teamData['memberIds'], isA<List>());
// Note: In this system, teams don't have independent members.
// Team members are only tracked through matches via PlayerMatchTable.
// Therefore, memberIds will be empty for standalone teams.
final memberIds = teamData['memberIds'] as List<dynamic>;
expect(memberIds, isEmpty);
});
testWidgets('Match data is correct', (tester) async {
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2],
);
await database.gameDao.addGame(game: testGame);
await database.groupDao.addGroup(group: testGroup);
await database.matchDao.addMatch(match: testMatch);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final matches = decoded['matches'] as List<dynamic>;
final matchData = matches[0] as Map<String, dynamic>;
expect(matchData['id'], testMatch.id);
expect(matchData['name'], testMatch.name);
expect(matchData['gameId'], testGame.id);
expect(matchData['groupId'], testGroup.id);
expect(matchData['playerIds'], isA<List>());
expect(matchData['notes'], testMatch.notes);
// Check player ids
final playerIds = matchData['playerIds'] as List<dynamic>;
expect(playerIds.length, 2);
expect(playerIds, containsAll([testPlayer1.id, testPlayer2.id]));
// Check scores structure
final scoresJson = matchData['scores'] as Map<String, dynamic>;
expect(scoresJson, isA<Map<String, dynamic>>());
final scores = scoresJson.map(
(playerId, scoreList) => MapEntry(
playerId,
(scoreList as List)
.map((s) => ScoreEntry.fromJson(s as Map<String, dynamic>))
.toList(),
),
);
expect(scores, isA<Map<String, List<ScoreEntry>>>());
/* Player 1 scores */
// General structure
expect(scores[testPlayer1.id], isNotNull);
expect(scores[testPlayer1.id]!.length, 2);
// Round 1
expect(scores[testPlayer1.id]![0].roundNumber, 1);
expect(scores[testPlayer1.id]![0].score, 10);
expect(scores[testPlayer1.id]![0].change, 10);
// Round 2
expect(scores[testPlayer1.id]![1].roundNumber, 2);
expect(scores[testPlayer1.id]![1].score, 20);
expect(scores[testPlayer1.id]![1].change, 10);
/* Player 2 scores */
// General structure
expect(scores[testPlayer2.id], isNotNull);
expect(scores[testPlayer2.id]!.length, 2);
// Round 1
expect(scores[testPlayer2.id]![0].roundNumber, 1);
expect(scores[testPlayer2.id]![0].score, 15);
expect(scores[testPlayer2.id]![0].change, 15);
// Round 2
expect(scores[testPlayer2.id]![1].roundNumber, 2);
expect(scores[testPlayer2.id]![1].score, 25);
expect(scores[testPlayer2.id]![1].change, 10);
});
testWidgets('Match without group is handled correctly', (tester) async {
final matchWithoutGroup = Match(
name: 'No Group Match',
game: testGame,
group: null,
players: [testPlayer1],
notes: 'No group',
);
await database.playerDao.addPlayer(player: testPlayer1);
await database.gameDao.addGame(game: testGame);
await database.matchDao.addMatch(match: matchWithoutGroup);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final matches = decoded['matches'] as List<dynamic>;
final matchData = matches[0] as Map<String, dynamic>;
expect(matchData['groupId'], isNull);
});
testWidgets('Match with endedAt is handled correctly', (tester) async {
final endedDate = DateTime(2025, 12, 1, 10, 0, 0);
final endedMatch = Match(
name: 'Ended Match',
game: testGame,
players: [testPlayer1],
endedAt: endedDate,
notes: 'Finished',
);
await database.playerDao.addPlayer(player: testPlayer1);
await database.gameDao.addGame(game: testGame);
await database.matchDao.addMatch(match: endedMatch);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final matches = decoded['matches'] as List<dynamic>;
final matchData = matches[0] as Map<String, dynamic>;
expect(matchData['endedAt'], endedDate.toIso8601String());
});
testWidgets('Structure is consistent', (tester) async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.gameDao.addGame(game: testGame);
final ctx = await getContext(tester);
final jsonString1 = await DataTransferService.getAppDataAsJson(ctx);
final jsonString2 = await DataTransferService.getAppDataAsJson(ctx);
expect(jsonString1, equals(jsonString2));
});
testWidgets('Empty match notes is handled correctly', (tester) async {
final matchWithEmptyNotes = Match(
name: 'Empty Notes Match',
game: testGame,
players: [testPlayer1],
notes: '',
);
await database.playerDao.addPlayer(player: testPlayer1);
await database.gameDao.addGame(game: testGame);
await database.matchDao.addMatch(match: matchWithEmptyNotes);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final matches = decoded['matches'] as List<dynamic>;
final matchData = matches[0] as Map<String, dynamic>;
expect(matchData['notes'], '');
});
testWidgets('Multiple players in match is handled correctly', (
tester,
) async {
final multiPlayerMatch = Match(
name: 'Multi Player Match',
game: testGame,
players: [testPlayer1, testPlayer2, testPlayer3],
notes: 'Three players',
);
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2, testPlayer3],
);
await database.gameDao.addGame(game: testGame);
await database.matchDao.addMatch(match: multiPlayerMatch);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final matches = decoded['matches'] as List<dynamic>;
final matchData = matches[0] as Map<String, dynamic>;
final playerIds = matchData['playerIds'] as List<dynamic>;
expect(playerIds.length, 3);
expect(
playerIds,
containsAll([testPlayer1.id, testPlayer2.id, testPlayer3.id]),
);
});
testWidgets('All game colors are handled correctly', (tester) async {
final games = [
Game(
name: 'Red Game',
ruleset: Ruleset.singleWinner,
color: GameColor.red,
icon: 'icon',
),
Game(
name: 'Blue Game',
ruleset: Ruleset.singleWinner,
color: GameColor.blue,
icon: 'icon',
),
Game(
name: 'Green Game',
ruleset: Ruleset.singleWinner,
color: GameColor.green,
icon: 'icon',
),
];
await database.gameDao.addGamesAsList(games: games);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final gamesJson = decoded['games'] as List<dynamic>;
expect(gamesJson.length, 3);
expect(
gamesJson.map((g) => g['color']),
containsAll(['red', 'blue', 'green']),
);
});
testWidgets('All rulesets are handled correctly', (tester) async {
final games = [
Game(
name: 'Highest Score Game',
ruleset: Ruleset.highestScore,
color: GameColor.blue,
icon: 'icon',
),
Game(
name: 'Lowest Score Game',
ruleset: Ruleset.lowestScore,
color: GameColor.blue,
icon: 'icon',
),
Game(
name: 'Single Winner',
ruleset: Ruleset.singleWinner,
color: GameColor.blue,
icon: 'icon',
),
];
await database.gameDao.addGamesAsList(games: games);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
final decoded = json.decode(jsonString) as Map<String, dynamic>;
final gamesJson = decoded['games'] as List<dynamic>;
expect(gamesJson.length, 3);
expect(
gamesJson.map((g) => g['ruleset']),
containsAll(['highestScore', 'lowestScore', 'singleWinner']),
);
});
});
});
group('Parse Methods', () {
test('parsePlayersFromJson()', () {
final jsonMap = {
'players': [
{
'id': testPlayer1.id,
'name': testPlayer1.name,
'description': testPlayer1.description,
'createdAt': testPlayer1.createdAt.toIso8601String(),
},
{
'id': testPlayer2.id,
'name': testPlayer2.name,
'description': testPlayer2.description,
'createdAt': testPlayer2.createdAt.toIso8601String(),
},
],
};
final players = DataTransferService.parsePlayersFromJson(jsonMap);
expect(players.length, 2);
expect(players[0].id, testPlayer1.id);
expect(players[0].name, testPlayer1.name);
expect(players[1].id, testPlayer2.id);
expect(players[1].name, testPlayer2.name);
});
test('parsePlayersFromJson() empty list', () {
final jsonMap = {'players': []};
final players = DataTransferService.parsePlayersFromJson(jsonMap);
expect(players, isEmpty);
});
test('parsePlayersFromJson() missing key', () {
final jsonMap = <String, dynamic>{};
final players = DataTransferService.parsePlayersFromJson(jsonMap);
expect(players, isEmpty);
});
test('parseGamesFromJson()', () {
final jsonMap = {
'games': [
{
'id': testGame.id,
'name': testGame.name,
'ruleset': testGame.ruleset.name,
'description': testGame.description,
'color': testGame.color.name,
'icon': testGame.icon,
'createdAt': testGame.createdAt.toIso8601String(),
},
],
};
final games = DataTransferService.parseGamesFromJson(jsonMap);
expect(games.length, 1);
expect(games[0].id, testGame.id);
expect(games[0].name, testGame.name);
expect(games[0].ruleset, testGame.ruleset);
});
test('parseGamesFromJson() empty list', () {
final jsonMap = {'games': []};
final games = DataTransferService.parseGamesFromJson(jsonMap);
expect(games, isEmpty);
});
test('parseGamesFromJson() missing key', () {
final jsonMap = <String, dynamic>{};
final games = DataTransferService.parseGamesFromJson(jsonMap);
expect(games, isEmpty);
});
test('parseGroupsFromJson()', () {
final playerById = {
testPlayer1.id: testPlayer1,
testPlayer2.id: testPlayer2,
};
final jsonMap = {
'groups': [
{
'id': testGroup.id,
'name': testGroup.name,
'description': testGroup.description,
'memberIds': [testPlayer1.id, testPlayer2.id],
'createdAt': testGroup.createdAt.toIso8601String(),
},
],
};
final groups = DataTransferService.parseGroupsFromJson(
jsonMap,
playerById,
);
expect(groups.length, 1);
expect(groups[0].id, testGroup.id);
expect(groups[0].name, testGroup.name);
expect(groups[0].members.length, 2);
expect(groups[0].members[0].id, testPlayer1.id);
expect(groups[0].members[1].id, testPlayer2.id);
});
test('parseGroupsFromJson() empty list', () {
final jsonMap = {'groups': []};
final groups = DataTransferService.parseGroupsFromJson(jsonMap, {});
expect(groups, isEmpty);
});
test('parseGroupsFromJson() missing key', () {
final jsonMap = <String, dynamic>{};
final groups = DataTransferService.parseGroupsFromJson(jsonMap, {});
expect(groups, isEmpty);
});
test('parseGroupsFromJson() ignores invalid player ids', () {
final playerById = {testPlayer1.id: testPlayer1};
final jsonMap = {
'groups': [
{
'id': testGroup.id,
'name': testGroup.name,
'description': testGroup.description,
'memberIds': [testPlayer1.id, 'invalid-id'],
'createdAt': testGroup.createdAt.toIso8601String(),
},
],
};
final groups = DataTransferService.parseGroupsFromJson(
jsonMap,
playerById,
);
expect(groups.length, 1);
expect(groups[0].members.length, 1);
expect(groups[0].members[0].id, testPlayer1.id);
});
test('parseTeamsFromJson()', () {
final playerById = {testPlayer1.id: testPlayer1};
final jsonMap = {
'teams': [
{
'id': testTeam.id,
'name': testTeam.name,
'memberIds': [testPlayer1.id],
'createdAt': testTeam.createdAt.toIso8601String(),
},
],
};
final teams = DataTransferService.parseTeamsFromJson(
jsonMap,
playerById,
);
expect(teams.length, 1);
expect(teams[0].id, testTeam.id);
expect(teams[0].name, testTeam.name);
expect(teams[0].members.length, 1);
expect(teams[0].members[0].id, testPlayer1.id);
});
test('parseTeamsFromJson() empty list', () {
final jsonMap = {'teams': []};
final teams = DataTransferService.parseTeamsFromJson(jsonMap, {});
expect(teams, isEmpty);
});
test('parseTeamsFromJson() missing key', () {
final jsonMap = <String, dynamic>{};
final teams = DataTransferService.parseTeamsFromJson(jsonMap, {});
expect(teams, isEmpty);
});
test('parseMatchesFromJson()', () {
final playerById = {
testPlayer1.id: testPlayer1,
testPlayer2.id: testPlayer2,
};
final gameById = {testGame.id: testGame};
final groupById = {testGroup.id: testGroup};
final jsonMap = {
'matches': [
{
'id': testMatch.id,
'name': testMatch.name,
'gameId': testGame.id,
'groupId': testGroup.id,
'playerIds': [testPlayer1.id, testPlayer2.id],
'notes': testMatch.notes,
'createdAt': testMatch.createdAt.toIso8601String(),
},
],
};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
gameById,
groupById,
playerById,
);
expect(matches.length, 1);
expect(matches[0].id, testMatch.id);
expect(matches[0].name, testMatch.name);
expect(matches[0].game.id, testGame.id);
expect(matches[0].group?.id, testGroup.id);
expect(matches[0].players.length, 2);
});
test('parseMatchesFromJson() empty list', () {
final jsonMap = {'teams': []};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
{},
{},
{},
);
expect(matches, isEmpty);
});
test('parseMatchesFromJson() missing key', () {
final jsonMap = <String, dynamic>{};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
{},
{},
{},
);
expect(matches, isEmpty);
});
test('parseMatchesFromJson() creates unknown game for missing game', () {
final playerById = {testPlayer1.id: testPlayer1};
final gameById = <String, Game>{};
final groupById = <String, Group>{};
final jsonMap = {
'matches': [
{
'id': testMatch.id,
'name': testMatch.name,
'gameId': 'non-existent-game-id',
'playerIds': [testPlayer1.id],
'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(),
},
],
};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
gameById,
groupById,
playerById,
);
expect(matches.length, 1);
expect(matches[0].game.name, 'Unknown');
expect(matches[0].game.ruleset, Ruleset.singleWinner);
});
test('parseMatchesFromJson() handles null group', () {
final playerById = {testPlayer1.id: testPlayer1};
final gameById = {testGame.id: testGame};
final groupById = <String, Group>{};
final jsonMap = {
'matches': [
{
'id': testMatch.id,
'name': testMatch.name,
'gameId': testGame.id,
'groupId': null,
'playerIds': [testPlayer1.id],
'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(),
},
],
};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
gameById,
groupById,
playerById,
);
expect(matches.length, 1);
expect(matches[0].group, isNull);
});
test('parseMatchesFromJson() handles endedAt', () {
final playerById = {testPlayer1.id: testPlayer1};
final gameById = {testGame.id: testGame};
final groupById = <String, Group>{};
final endedDate = DateTime(2025, 12, 1, 10, 0, 0);
final jsonMap = {
'matches': [
{
'id': testMatch.id,
'name': testMatch.name,
'gameId': testGame.id,
'playerIds': [testPlayer1.id],
'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(),
'endedAt': endedDate.toIso8601String(),
},
],
};
final matches = DataTransferService.parseMatchesFromJson(
jsonMap,
gameById,
groupById,
playerById,
);
expect(matches.length, 1);
expect(matches[0].endedAt, endedDate);
});
});
test('validateJsonSchema()', () async {
final validJson = json.encode({
'players': [
{
'id': testPlayer1.id,
'name': testPlayer1.name,
'description': testPlayer1.description,
'createdAt': testPlayer1.createdAt.toIso8601String(),
},
],
'games': [
{
'id': testGame.id,
'name': testGame.name,
'ruleset': testGame.ruleset.name,
'description': testGame.description,
'color': testGame.color.name,
'icon': testGame.icon,
'createdAt': testGame.createdAt.toIso8601String(),
},
],
'groups': [
{
'id': testGroup.id,
'name': testGroup.name,
'description': testGroup.description,
'memberIds': [testPlayer1.id, testPlayer2.id],
'createdAt': testGroup.createdAt.toIso8601String(),
},
],
'teams': [
{
'id': testTeam.id,
'name': testTeam.name,
'memberIds': [testPlayer1.id, testPlayer2.id],
'createdAt': testTeam.createdAt.toIso8601String(),
},
],
'matches': [
{
'id': testMatch.id,
'name': testMatch.name,
'gameId': testGame.id,
'groupId': testGroup.id,
'playerIds': [testPlayer1.id, testPlayer2.id],
'notes': testMatch.notes,
'scores': {
testPlayer1.id: [
{'roundNumber': 1, 'score': 10, 'change': 10},
{'roundNumber': 2, 'score': 20, 'change': 10},
],
testPlayer2.id: [
{'roundNumber': 1, 'score': 15, 'change': 15},
{'roundNumber': 2, 'score': 25, 'change': 10},
],
},
'createdAt': testMatch.createdAt.toIso8601String(),
'endedAt': null,
},
],
});
final isValid = await DataTransferService.validateJsonSchema(validJson);
expect(isValid, true);
});
});
}