Compare commits
476 Commits
e14984d4a9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f0379e97c7 | |||
| c2394c3733 | |||
| 88766652b9 | |||
| 2a1573ee2d | |||
| cfb07bfe28 | |||
| d741990f2f | |||
| 76121eb4fb | |||
| d5ee6449b0 | |||
| 7d9757abb6 | |||
| d94d51c6ea | |||
|
|
803cf8a972 | ||
|
|
13be75a65b | ||
| aef12bd65a | |||
| 02d79574dd | |||
| fdd0e7579a | |||
| cd2770be26 | |||
| e196d6e5ef | |||
| bc3beac866 | |||
| aa936a938d | |||
| 2811ea892e | |||
| 4b1d3923a0 | |||
| 0f824bb30a | |||
| 21c74b74bc | |||
| 6e45e9435b | |||
| a78614851b | |||
| 8df9a27dc7 | |||
| 52180eeb6d | |||
| 0893667e41 | |||
| 1dc5286c6b | |||
|
|
f140ebae2e | ||
| a487e4071f | |||
|
|
99044c0ea0 | ||
| 69effa2b7d | |||
| 5db008c274 | |||
| feaa0709a7 | |||
|
|
2fea3597fe | ||
|
|
7b2314a25e | ||
|
|
7f6c1cb9a6 | ||
| dcfed75a93 | |||
| 7fa434782c | |||
| bdcee85eb9 | |||
| 072dba1cde | |||
| 3c7c4598ff | |||
| 1a20d7b64c | |||
| ec94e12ed7 | |||
| 9fc308554c | |||
| 42cef337aa | |||
|
|
d9980edf0e | ||
|
|
4c084cae4a | ||
|
|
cecfd5da4e | ||
| 5289660327 | |||
| 16e1f542f5 | |||
| e4ae526d93 | |||
| 2f65316a51 | |||
| 31abb7f1e4 | |||
| 132966f3d2 | |||
| 77095725de | |||
| d081a597db | |||
| bdc3453d7f | |||
| 678ab90af3 | |||
| a038c22ba6 | |||
| 22fcff73bd | |||
|
|
4a67dae456 | ||
| 3c22b084d6 | |||
| 7f923f6dff | |||
| 87856e6548 | |||
| cb7804cf55 | |||
| 304fa82a93 | |||
| f84a645e61 | |||
| b0073addf8 | |||
| b5c17ac5a9 | |||
| 4451f9e2a9 | |||
| 1d4fdae84b | |||
| 60862de4c4 | |||
| 6899bb0c3c | |||
| 55a22b292f | |||
| db30b0fd5a | |||
| 5ee0d59377 | |||
| f22595e678 | |||
| 25fe10eb9a | |||
| 5a318f9760 | |||
| 0051990168 | |||
| 7e21c65026 | |||
| cc23c03f6b | |||
| d77b5f20f9 | |||
| 3c3bf506cb | |||
| 8afba5680b | |||
| 7103765054 | |||
| bc01a6de9a | |||
| ad87dca674 | |||
| 633a21d829 | |||
| 81f63c1c07 | |||
| fc6c74b377 | |||
| 15d09f381a | |||
| f97c341b81 | |||
| 7bf03ec388 | |||
| 0bfaba4225 | |||
| bbb7914fc5 | |||
| 86dbc9afb0 | |||
| 73aea0d0f5 | |||
| ed9e3af768 | |||
| d93ead40b6 | |||
| 3374a80c27 | |||
| 534d19efc3 | |||
| d9a26a8cf7 | |||
| 8e05e9d61b | |||
| 179ac2fe21 | |||
| 2b78617924 | |||
| 175a9cb349 | |||
| 18f0626e95 | |||
| d99a04549a | |||
| cc1ecc4c86 | |||
| 16cdf9db3e | |||
| da494ed146 | |||
| af0242c44f | |||
| cd63f5cf06 | |||
| 8c3282b954 | |||
| 1c0fb9f3ab | |||
| bfca41bd36 | |||
| 92b8ef0877 | |||
| 5428064c53 | |||
| 27916456db | |||
| 58fe38fd72 | |||
| ecd03dfd9d | |||
| 05c6625bf3 | |||
| 9821af39fe | |||
| c1789458e0 | |||
| e19f696714 | |||
| 9cb35dc4a1 | |||
| b8b65c4ca1 | |||
| 356cb1fb43 | |||
| 4c8896d2d9 | |||
| 31c8598222 | |||
| 884680d329 | |||
| 5ae569f2e7 | |||
| 37d6ad81fd | |||
| cea6c36407 | |||
| 1b300faea9 | |||
| dac5cc8a89 | |||
| bafbde7d03 | |||
| fdea594a1e | |||
| 856ce43c49 | |||
| 5c8b93072f | |||
| 94fdd7026d | |||
| 966f7caa54 | |||
| 88bea48956 | |||
| a2e66b9c80 | |||
| d7a006dd87 | |||
| 12856342a8 | |||
| b0a5145490 | |||
| 7a80c1a792 | |||
| c73f37507f | |||
| 1e730cebe6 | |||
| 7eb25221d7 | |||
| f9722bc762 | |||
| f2917a6813 | |||
| b29cd6dff4 | |||
| 1d92084da6 | |||
| 7732c6ceb9 | |||
| a747d91c5d | |||
| 06a9c0cd84 | |||
| 9ad5c4ad6f | |||
| 9b3d61e5b0 | |||
| df0a5207c2 | |||
| 4f0a1eec6d | |||
| 24b60bb18b | |||
| c8532adfde | |||
| 76186787e7 | |||
| f05114a99e | |||
| d96494f608 | |||
| 0eaf3d251b | |||
| 0567fce02b | |||
| 74ea540b39 | |||
| d578a7f9bd | |||
| 0e5ad1c3d9 | |||
| c4b249b199 | |||
| 99cea1e703 | |||
| 7b7c41b96c | |||
| d0059b44a8 | |||
| 3b3d298ff5 | |||
| e1626225ac | |||
| 93ced81e7e | |||
| 23cdddfbd9 | |||
| 5d2fed74ac | |||
| 0d0806dfbb | |||
| 27ff599a88 | |||
| c4094a547e | |||
| 701500c7e2 | |||
| 062c2681bf | |||
| 7cff48ebc0 | |||
| 708157df54 | |||
| f1f3fd7b6e | |||
| 7bc75d60a7 | |||
| a1ed17355a | |||
| 45abc79f95 | |||
| c214a26c54 | |||
| 6c0cb92e56 | |||
| b4ba4f8d74 | |||
| f60c11fc08 | |||
| d2d0a82c9b | |||
| 1044ee9bea | |||
| 32e812127d | |||
| 4500d85f78 | |||
| c8bf03f462 | |||
| 7ecccb13c2 | |||
| 4fc1959704 | |||
| 1e03507f75 | |||
| e37a98fdcc | |||
| 6ae0471fa2 | |||
| a1a995777b | |||
| 3d12f0c160 | |||
| 10aad47124 | |||
| 75b62d0854 | |||
| 527f163346 | |||
| 6ae7166d34 | |||
| dcd8b460c1 | |||
| dbbe04d4cc | |||
| 1ed6290628 | |||
| 91a7273964 | |||
| b1bb8b919f | |||
| 697767f0de | |||
| 306a783d67 | |||
| 03035138ac | |||
| 7323f52153 | |||
| f5842f9c4a | |||
| e3ac91bf48 | |||
| dba448b9c1 | |||
| d8551b3a27 | |||
| 8f2c7493d0 | |||
| f7f97fcdcb | |||
| 9ac6b6e04c | |||
| e77896c1d4 | |||
| dd2024e96e | |||
| cd9780871f | |||
| 3169eebd14 | |||
| ec902c6196 | |||
| b719a6662b | |||
| 09b407eba8 | |||
| 877c2921d9 | |||
|
|
5ce4964c32 | ||
| b4ccb567b5 | |||
| bcd7bf751b | |||
| 4dbc106e38 | |||
|
|
fb28de5772 | ||
| 236a737fd1 | |||
|
|
f713bd6fb7 | ||
| 94b113eb95 | |||
| 71b2f30d29 | |||
| d2d6852f31 | |||
| 126dc7ed97 | |||
| 40a3c1b82e | |||
|
|
da722c5277 | ||
|
|
516c2afd1e | ||
|
|
9ee9da2ac8 | ||
|
|
aa208bb2ef | ||
|
|
a29123c964 | ||
|
|
8c005d6e5e | ||
|
|
cc50e497c9 | ||
|
|
ae348499d4 | ||
|
|
b443230285 | ||
|
|
099e587d45 | ||
| dc0e536221 | |||
| 2a34243e69 | |||
| 499415e0c5 | |||
| 397c5c1550 | |||
| 738f242eee | |||
| 745aaef978 | |||
| b5234c765c | |||
| 919c9f57ac | |||
| 27424694ce | |||
| 84338f8f66 | |||
| 733df2dcb5 | |||
| 9ba3dd7909 | |||
| 2838376434 | |||
| 86ec4de5c0 | |||
| 479e9a2575 | |||
| d97871d15b | |||
| 00fd6880e9 | |||
| 649330f358 | |||
| 07d81d687b | |||
| b291673899 | |||
| 5fbf2ccb45 | |||
| e489d16c51 | |||
| 7cfffadb86 | |||
| ae529effd2 | |||
| 4c3b2152eb | |||
| 51e3c04e72 | |||
| 2b9f038b0d | |||
| 0653700f9c | |||
| 7be80e6f91 | |||
| a4b934388d | |||
| f8c0dbba5a | |||
| ebb531d825 | |||
| fc9779153d | |||
| a2522cef13 | |||
| 442e1d64a3 | |||
| 54b54796e8 | |||
| 686463720a | |||
| 6a77028171 | |||
| f1bd9c18e0 | |||
| 6c9b742bdf | |||
| 744a402602 | |||
| 2d9148788e | |||
| 18f635e6ef | |||
| 9efbc12909 | |||
| 7c7676abee | |||
| 1faa74f026 | |||
| 3afae89234 | |||
| 093c527591 | |||
| 2ba710ca2d | |||
| 9054b163ce | |||
| e182c815a1 | |||
| c284d10943 | |||
| 72e48ada94 | |||
|
|
4591a6857d | ||
|
|
44279bc148 | ||
|
|
32c7d45809 | ||
|
|
4341c2509e | ||
| a3fa499662 | |||
| f25737cdc5 | |||
| 0b500b5248 | |||
| e71cb11295 | |||
| b102ec4c1c | |||
| 974d6e9b56 | |||
|
|
290948e50d | ||
|
|
bbd200e245 | ||
| 694cac7f26 | |||
| bd616c510a | |||
| 424a258df1 | |||
| 6dc74ca82e | |||
|
|
ac6af803fd | ||
|
|
f21d0ba4e8 | ||
| 8307488f28 | |||
| 937f1e3ac8 | |||
| 46d1c25bb5 | |||
| cdba6e264a | |||
| 88fa886ea2 | |||
| cc2f87396f | |||
| 692a3ef3a4 | |||
| 22d95b0015 | |||
| 1a84d2572a | |||
| ca55b99d03 | |||
| 651f210ea8 | |||
| 0153df6195 | |||
| e4abf53f66 | |||
| 2616f7c113 | |||
| 7881e61a2e | |||
| 73c5586874 | |||
| 963edaf1d1 | |||
| 604a541392 | |||
| 26fadf5093 | |||
| 9e8bab1a60 | |||
| fc51b30491 | |||
| def37aa640 | |||
| 8ae85c925d | |||
| b744b04648 | |||
| 17e882986d | |||
| 7cda25a380 | |||
| e9b041e43a | |||
| c38c731b41 | |||
| d411f58134 | |||
| fee5c57207 | |||
| de60c942ea | |||
| acc5b0a3e9 | |||
| 24babe06d2 | |||
| 50dd05ecc5 | |||
| 4ff131770e | |||
| 82b344a145 | |||
| 338f4294dc | |||
| cfed05595c | |||
| fa841e328e | |||
| f658a88849 | |||
| feb5fa0615 | |||
| fba35521cb | |||
| e60961730f | |||
| 59c041699d | |||
| c170aa1775 | |||
| 692b412fe2 | |||
| cc04e05557 | |||
| 546a3e3717 | |||
| b2036e4e68 | |||
| 310b9aa43b | |||
| e464ddb466 | |||
| c3deaa3974 | |||
| fd13fe6e90 | |||
| 5062196463 | |||
| 6af1df5fbc | |||
| df3215ef76 | |||
| 28ed22ce73 | |||
| 0593297fc3 | |||
| b668f6b9ae | |||
| 78b511b207 | |||
| 63d9ed400d | |||
| d809d80506 | |||
| 30645f06f8 | |||
| 9346f61d14 | |||
| bef812502c | |||
| 24f18f5c65 | |||
| 62eea08614 | |||
| 893eb91143 | |||
| 2ebd4274f0 | |||
| 70307016b3 | |||
| 1b3334f3e0 | |||
|
|
6a985b0b1e | ||
|
|
95f0861a79 | ||
| 7cd53aa695 | |||
| ab250e2df4 | |||
| dbb52cfc48 | |||
| c56663d15e | |||
| 51722eb7fd | |||
| 78d530ddd5 | |||
| 8c05385203 | |||
| d948f2f13d | |||
| e15f5d163d | |||
| 32f3f68da9 | |||
| 229750ffcf | |||
| fe9239ee02 | |||
| 961c6bb679 | |||
| b21ca54672 | |||
| 6055eb63a8 | |||
| 31589855f2 | |||
| 8e63a01705 | |||
| e512cbbf95 | |||
| 89b3f1ff69 | |||
| 72067863c2 | |||
| 29a3e77fc4 | |||
| a61818dd77 | |||
| e364e15d0a | |||
| 195ebf569a | |||
| eb7b247cae | |||
| 01fede2951 | |||
| b67f321276 | |||
| d16beed490 | |||
| 0111774308 | |||
| 8ff3c01435 | |||
| bce4cdcb2d | |||
| fa0e9a5dfd | |||
| ec9e34305a | |||
| 45650133a7 | |||
| f40a9ad09b | |||
| 822bc03c83 | |||
| a8d4e640cf | |||
| cf71b40718 | |||
| f7c1d6e975 | |||
| 17dabb773d | |||
| 682c1d269d | |||
| 061a927324 | |||
| 412cfff9f5 | |||
| f7073a83a4 | |||
| 9434282ed1 | |||
| 3e3def0bde | |||
| 87b1a7d57f | |||
| cf834e636e | |||
| 322c51a764 | |||
|
|
69e13e877e | ||
|
|
f388c442d2 | ||
| 82e28b7509 | |||
| f2a749cb0f | |||
| 5dcd0826bd | |||
| f6ebda7984 | |||
| 69c95ca672 | |||
| 42ce69f4d3 | |||
| 08fcaa35ee | |||
| fd86f5193f | |||
| 2da2e28cb6 | |||
| d86de09042 | |||
| 07d623d963 | |||
| a8a81c2151 | |||
| 178aaa9643 | |||
| 2076e45fd5 | |||
| 62acc87e0e | |||
| 3ca081419b | |||
| 36aa4722a3 | |||
|
|
1536e2b2af | ||
|
|
0f13de30c4 | ||
|
|
3f0adb4c05 |
1
.gitignore
vendored
@@ -195,3 +195,4 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
/devtools_options.yaml
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
|
||||
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
<item android:drawable="@color/launch_background" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 828 B |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 448 B |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 656 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 508 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 704 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 824 B |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
5
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="app_icon_background">#E6F1E4</color>
|
||||
<color name="launch_background">#0B0B0B</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Referenz unbedingt als @color/launch_background (nicht @colors/...) -->
|
||||
<color name="ic_launcher_background">@color/app_icon_background</color>
|
||||
</resources>
|
||||
@@ -4,7 +4,7 @@
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:windowBackground">@color/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
104
assets/schema.json
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"players": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"createdAt",
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"memberIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"createdAt",
|
||||
"memberIds"
|
||||
]
|
||||
}
|
||||
},
|
||||
"matches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"groupId": {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
},
|
||||
"playerIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"winnerId": {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"createdAt",
|
||||
"groupId",
|
||||
"playerIds"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"players",
|
||||
"groups",
|
||||
"matches"
|
||||
]
|
||||
}
|
||||
@@ -1,122 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
"filename" : "icon_x1024.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 450 B |
|
Before Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 762 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_x1024.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
6
ios/Runner/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B |
@@ -1,5 +0,0 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.043",
|
||||
"green" : "0.043",
|
||||
"red" : "0.043"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon.png",
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
ios/Runner/Assets.xcassets/LauncherIcon.imageset/icon.png
vendored
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
@@ -1,8 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24412" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24405"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
@@ -14,24 +17,27 @@
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="LauncherIcon" translatesAutoresizingMaskIntoConstraints="NO" id="ygV-Op-Bu5">
|
||||
<rect key="frame" x="46" y="334" width="301" height="184"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
<color key="backgroundColor" name="LauncherBackgroundColor"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
<point key="canvasLocation" x="80.152671755725194" y="264.08450704225356"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<color key="tintColor" red="0.90196078431372551" green="0.94509803921568625" blue="0.89411764705882346" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
<image name="LauncherIcon" width="1000" height="1000"/>
|
||||
<namedColor name="LauncherBackgroundColor">
|
||||
<color red="0.90196078431372551" green="0.94509803921568625" blue="0.89411764705882346" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24412" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24405"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
@@ -14,13 +16,14 @@
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="141" y="131"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
|
||||
5
l10n.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
arb-dir: lib/l10n/arb
|
||||
template-arb-file: app_en.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
output-dir: lib/l10n/generated
|
||||
nullable-getter: false
|
||||
6
lib/core/constants.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
class Constants {
|
||||
Constants._(); // Private constructor to prevent instantiation
|
||||
|
||||
/// Minimum duration of all app skeletons
|
||||
static Duration minimumSkeletonDuration = const Duration(milliseconds: 250);
|
||||
}
|
||||
@@ -1,22 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomTheme {
|
||||
CustomTheme._(); // Private constructor to prevent instantiation
|
||||
|
||||
// ==================== Colors ====================
|
||||
static Color primaryColor = const Color(0xFF7505E4);
|
||||
static Color secondaryColor = const Color(0xFFAFA2FF);
|
||||
static Color backgroundColor = const Color(0xFF0B0B0B);
|
||||
static Color boxColor = const Color(0xFF101010);
|
||||
static Color onBoxColor = const Color(0xFF181818);
|
||||
static Color boxBorder = const Color(0xFF272727);
|
||||
static const Color textColor = Colors.white;
|
||||
|
||||
// ==================== Border Radius ====================
|
||||
static const double standardBorderRadius = 12.0;
|
||||
static BorderRadius get standardBorderRadiusAll =>
|
||||
BorderRadius.circular(standardBorderRadius);
|
||||
|
||||
// ==================== Padding & Margins ====================
|
||||
static const EdgeInsets standardMargin = EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
);
|
||||
static const EdgeInsets tileMargin = EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 5,
|
||||
);
|
||||
|
||||
// ==================== Decorations ====================
|
||||
static BoxDecoration standardBoxDecoration = BoxDecoration(
|
||||
color: boxColor,
|
||||
border: Border.all(color: boxBorder),
|
||||
borderRadius: standardBorderRadiusAll,
|
||||
);
|
||||
|
||||
static BoxDecoration highlightedBoxDecoration = BoxDecoration(
|
||||
color: boxColor,
|
||||
border: Border.all(color: primaryColor),
|
||||
borderRadius: standardBorderRadiusAll,
|
||||
boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)],
|
||||
);
|
||||
|
||||
// ==================== App Bar Theme ====================
|
||||
static AppBarTheme appBarTheme = AppBarTheme(
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: Colors.white,
|
||||
foregroundColor: textColor,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
centerTitle: true,
|
||||
titleTextStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
color: textColor,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
iconTheme: const IconThemeData(color: textColor),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,2 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/l10n/generated/app_localizations.dart';
|
||||
|
||||
/// Button types used for styling the [CustomWidthButton]
|
||||
/// - [ButtonType.primary]: Primary button style.
|
||||
/// - [ButtonType.secondary]: Secondary button style.
|
||||
/// - [ButtonType.tertiary]: Tertiary button style.
|
||||
enum ButtonType { primary, secondary, tertiary }
|
||||
|
||||
/// Result types for import operations in the [SettingsView]
|
||||
/// - [ImportResult.success]: The import operation was successful.
|
||||
/// - [ImportResult.canceled]: The import operation was canceled by the user.
|
||||
/// - [ImportResult.fileReadError]: There was an error reading the selected file.
|
||||
/// - [ImportResult.invalidSchema]: The JSON schema of the imported data is invalid.
|
||||
/// - [ImportResult.formatException]: A format exception occurred during import.
|
||||
/// - [ImportResult.unknownException]: An exception occurred during import.
|
||||
enum ImportResult {
|
||||
success,
|
||||
canceled,
|
||||
fileReadError,
|
||||
invalidSchema,
|
||||
formatException,
|
||||
unknownException,
|
||||
}
|
||||
|
||||
/// Result types for export operations in the [SettingsView]
|
||||
/// - [ExportResult.success]: The export operation was successful.
|
||||
/// - [ExportResult.canceled]: The export operation was canceled by the user.
|
||||
/// - [ExportResult.unknownException]: An exception occurred during export.
|
||||
enum ExportResult { success, canceled, unknownException }
|
||||
|
||||
/// Different rulesets available for matches
|
||||
/// - [Ruleset.singleWinner]: The match is won by a single player
|
||||
/// - [Ruleset.singleLoser]: The match is lost by a single player
|
||||
/// - [Ruleset.mostPoints]: The player with the most points wins.
|
||||
/// - [Ruleset.leastPoints]: The player with the fewest points wins.
|
||||
enum Ruleset { singleWinner, singleLoser, mostPoints, leastPoints }
|
||||
|
||||
/// 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.singleWinner:
|
||||
return loc.single_winner;
|
||||
case Ruleset.singleLoser:
|
||||
return loc.single_loser;
|
||||
case Ruleset.mostPoints:
|
||||
return loc.most_points;
|
||||
case Ruleset.leastPoints:
|
||||
return loc.least_points;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/db/tables/game_table.dart';
|
||||
import 'package:game_tracker/data/dto/game.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
|
||||
part 'game_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [GameTable])
|
||||
class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
||||
GameDao(super.db);
|
||||
|
||||
/// Retrieves all games from the database.
|
||||
Future<List<Game>> getAllGames() async {
|
||||
final query = select(gameTable);
|
||||
final result = await query.get();
|
||||
return result
|
||||
.map(
|
||||
(row) => Game(id: row.id, name: row.name, createdAt: row.createdAt),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Retrieves a [Game] by its [gameId].
|
||||
Future<Game> getGameById({required String gameId}) async {
|
||||
final query = select(gameTable)..where((g) => g.id.equals(gameId));
|
||||
final result = await query.getSingle();
|
||||
|
||||
List<Player>? players;
|
||||
if (await db.playerGameDao.gameHasPlayers(gameId: gameId)) {
|
||||
players = await db.playerGameDao.getPlayersByGameId(gameId: gameId);
|
||||
}
|
||||
Group? group;
|
||||
if (await db.groupGameDao.hasGameGroup(gameId: gameId)) {
|
||||
group = await db.groupGameDao.getGroupByGameId(gameId: gameId);
|
||||
}
|
||||
|
||||
return Game(
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
players: players,
|
||||
group: group,
|
||||
winner: result.winnerId,
|
||||
createdAt: result.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds a new [Game] to the database.
|
||||
/// Also adds associated players and group if they exist.
|
||||
Future<void> addGame({required Game game}) async {
|
||||
await db.transaction(() async {
|
||||
for (final p in game.players ?? []) {
|
||||
await db.playerDao.addPlayer(player: p);
|
||||
await db.playerGameDao.addPlayerToGame(gameId: game.id, playerId: p.id);
|
||||
}
|
||||
if (game.group != null) {
|
||||
await db.groupDao.addGroup(group: game.group!);
|
||||
await db.groupGameDao.addGroupToGame(game.id, game.group!.id);
|
||||
}
|
||||
await into(gameTable).insert(
|
||||
GameTableCompanion.insert(
|
||||
id: game.id,
|
||||
name: game.name,
|
||||
winnerId: game.winner,
|
||||
createdAt: game.createdAt,
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Deletes the game with the given [gameId] from the database.
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> deleteGame({required String gameId}) async {
|
||||
final query = delete(gameTable)..where((g) => g.id.equals(gameId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Retrieves the number of games in the database.
|
||||
Future<int> getGameCount() async {
|
||||
final count =
|
||||
await (selectOnly(gameTable)..addColumns([gameTable.id.count()]))
|
||||
.map((row) => row.read(gameTable.id.count()))
|
||||
.getSingle();
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
/// Checks if a game with the given [gameId] exists in the database.
|
||||
/// Returns `true` if the game exists, otherwise `false`.
|
||||
Future<bool> gameExists({required String gameId}) async {
|
||||
final query = select(gameTable)..where((g) => g.id.equals(gameId));
|
||||
final result = await query.getSingleOrNull();
|
||||
return result != null;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'game_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$GameDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
||||
$GameTableTable get gameTable => attachedDatabase.gameTable;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/db/tables/group_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_group_table.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
|
||||
part 'group_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [GroupTable])
|
||||
@DriftAccessor(tables: [GroupTable, PlayerGroupTable])
|
||||
class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
GroupDao(super.db);
|
||||
|
||||
@@ -16,7 +17,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
final result = await query.get();
|
||||
return Future.wait(
|
||||
result.map((groupData) async {
|
||||
final members = await db.playerGroupDao.getPlayersOfGroupById(
|
||||
final members = await db.playerGroupDao.getPlayersOfGroup(
|
||||
groupId: groupData.id,
|
||||
);
|
||||
return Group(
|
||||
@@ -34,7 +35,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
final query = select(groupTable)..where((g) => g.id.equals(groupId));
|
||||
final result = await query.getSingle();
|
||||
|
||||
List<Player> members = await db.playerGroupDao.getPlayersOfGroupById(
|
||||
List<Player> members = await db.playerGroupDao.getPlayersOfGroup(
|
||||
groupId: groupId,
|
||||
);
|
||||
|
||||
@@ -57,6 +58,10 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
name: group.name,
|
||||
createdAt: group.createdAt,
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
await Future.wait(
|
||||
group.members.map((player) => db.playerDao.addPlayer(player: player)),
|
||||
);
|
||||
await db.batch(
|
||||
(b) => b.insertAll(
|
||||
@@ -69,17 +74,98 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
),
|
||||
);
|
||||
await Future.wait(
|
||||
group.members.map((player) => db.playerDao.addPlayer(player: player)),
|
||||
);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Adds multiple groups to the database.
|
||||
/// Also adds the group's members to the [PlayerGroupTable].
|
||||
Future<void> addGroupsAsList({required List<Group> groups}) async {
|
||||
if (groups.isEmpty) return;
|
||||
await db.transaction(() async {
|
||||
// Deduplicate groups by id - keep first occurrence
|
||||
final Map<String, Group> uniqueGroups = {};
|
||||
for (final g in groups) {
|
||||
uniqueGroups.putIfAbsent(g.id, () => g);
|
||||
}
|
||||
|
||||
// Insert unique groups in batch
|
||||
// Using insertOrIgnore to avoid triggering cascade deletes on
|
||||
// player_group associations when groups already exist
|
||||
await db.batch(
|
||||
(b) => b.insertAll(
|
||||
groupTable,
|
||||
uniqueGroups.values
|
||||
.map(
|
||||
(group) => GroupTableCompanion.insert(
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
createdAt: group.createdAt,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
),
|
||||
);
|
||||
|
||||
// Collect unique players from all groups
|
||||
final uniquePlayers = <String, Player>{};
|
||||
for (final g in uniqueGroups.values) {
|
||||
for (final m in g.members) {
|
||||
uniquePlayers[m.id] = m;
|
||||
}
|
||||
}
|
||||
|
||||
if (uniquePlayers.isNotEmpty) {
|
||||
// Using insertOrIgnore to avoid triggering cascade deletes on
|
||||
// player_group associations when players already exist
|
||||
await db.batch(
|
||||
(b) => b.insertAll(
|
||||
db.playerTable,
|
||||
uniquePlayers.values
|
||||
.map(
|
||||
(p) => PlayerTableCompanion.insert(
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
createdAt: p.createdAt,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare all player-group associations in one list (unique pairs)
|
||||
final Set<String> seenPairs = {};
|
||||
final List<PlayerGroupTableCompanion> pgRows = [];
|
||||
for (final g in uniqueGroups.values) {
|
||||
for (final m in g.members) {
|
||||
final key = '${m.id}|${g.id}';
|
||||
if (!seenPairs.contains(key)) {
|
||||
seenPairs.add(key);
|
||||
pgRows.add(
|
||||
PlayerGroupTableCompanion.insert(playerId: m.id, groupId: g.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pgRows.isNotEmpty) {
|
||||
await db.batch((b) {
|
||||
for (final pg in pgRows) {
|
||||
b.insert(db.playerGroupTable, pg, mode: InsertMode.insertOrReplace);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Deletes the group with the given [id] from the database.
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> deleteGroup({required String groupId}) async {
|
||||
@@ -117,4 +203,12 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
final result = await query.getSingleOrNull();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// Deletes all groups from the database.
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> deleteAllGroups() async {
|
||||
final query = delete(groupTable);
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,7 @@ part of 'group_dao.dart';
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$GroupDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$GroupTableTable get groupTable => attachedDatabase.groupTable;
|
||||
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
||||
$PlayerGroupTableTable get playerGroupTable =>
|
||||
attachedDatabase.playerGroupTable;
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/db/tables/group_game_table.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
|
||||
part 'group_game_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [GroupGameTable])
|
||||
class GroupGameDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$GroupGameDaoMixin {
|
||||
GroupGameDao(super.db);
|
||||
|
||||
/// Checks if there is a group associated with the given [gameId].
|
||||
/// Returns `true` if there is a group, otherwise `false`.
|
||||
Future<bool> hasGameGroup({required String gameId}) async {
|
||||
final count =
|
||||
await (selectOnly(groupGameTable)
|
||||
..where(groupGameTable.gameId.equals(gameId))
|
||||
..addColumns([groupGameTable.groupId.count()]))
|
||||
.map((row) => row.read(groupGameTable.groupId.count()))
|
||||
.getSingle();
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
|
||||
/// Retrieves the [Group] associated with the given [gameId].
|
||||
Future<Group> getGroupByGameId({required String gameId}) async {
|
||||
final result = await (select(
|
||||
groupGameTable,
|
||||
)..where((g) => g.gameId.equals(gameId))).getSingle();
|
||||
|
||||
final group = await db.groupDao.getGroupById(groupId: result.groupId);
|
||||
return group;
|
||||
}
|
||||
|
||||
/// Associates a group with a game by inserting a record into the
|
||||
/// [GroupGameTable].
|
||||
Future<void> addGroupToGame(String gameId, String groupId) async {
|
||||
await into(
|
||||
groupGameTable,
|
||||
).insert(GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'group_game_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$GroupGameDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$GroupTableTable get groupTable => attachedDatabase.groupTable;
|
||||
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
||||
$GameTableTable get gameTable => attachedDatabase.gameTable;
|
||||
$GroupGameTableTable get groupGameTable => attachedDatabase.groupGameTable;
|
||||
}
|
||||
98
lib/data/dao/group_match_dao.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/db/tables/group_match_table.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
|
||||
part 'group_match_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [GroupMatchTable])
|
||||
class GroupMatchDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$GroupMatchDaoMixin {
|
||||
GroupMatchDao(super.db);
|
||||
|
||||
/// Associates a group with a match by inserting a record into the
|
||||
/// [GroupMatchTable].
|
||||
Future<void> addGroupToMatch({
|
||||
required String matchId,
|
||||
required String groupId,
|
||||
}) async {
|
||||
if (await matchHasGroup(matchId: matchId)) {
|
||||
throw Exception('Match already has a group');
|
||||
}
|
||||
await into(groupMatchTable).insert(
|
||||
GroupMatchTableCompanion.insert(groupId: groupId, matchId: matchId),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
);
|
||||
}
|
||||
|
||||
/// Retrieves the [Group] associated with the given [matchId].
|
||||
/// Returns `null` if no group is found.
|
||||
Future<Group?> getGroupOfMatch({required String matchId}) async {
|
||||
final result = await (select(
|
||||
groupMatchTable,
|
||||
)..where((g) => g.matchId.equals(matchId))).getSingleOrNull();
|
||||
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final group = await db.groupDao.getGroupById(groupId: result.groupId);
|
||||
return group;
|
||||
}
|
||||
|
||||
/// Checks if there is a group associated with the given [matchId].
|
||||
/// Returns `true` if there is a group, otherwise `false`.
|
||||
Future<bool> matchHasGroup({required String matchId}) async {
|
||||
final count =
|
||||
await (selectOnly(groupMatchTable)
|
||||
..where(groupMatchTable.matchId.equals(matchId))
|
||||
..addColumns([groupMatchTable.groupId.count()]))
|
||||
.map((row) => row.read(groupMatchTable.groupId.count()))
|
||||
.getSingle();
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
|
||||
/// Checks if a specific group is associated with a specific match.
|
||||
/// Returns `true` if the group is in the match, otherwise `false`.
|
||||
Future<bool> isGroupInMatch({
|
||||
required String matchId,
|
||||
required String groupId,
|
||||
}) async {
|
||||
final count =
|
||||
await (selectOnly(groupMatchTable)
|
||||
..where(
|
||||
groupMatchTable.matchId.equals(matchId) &
|
||||
groupMatchTable.groupId.equals(groupId),
|
||||
)
|
||||
..addColumns([groupMatchTable.groupId.count()]))
|
||||
.map((row) => row.read(groupMatchTable.groupId.count()))
|
||||
.getSingle();
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
|
||||
/// Removes the association of a group from a match based on [groupId] and
|
||||
/// [matchId].
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> removeGroupFromMatch({
|
||||
required String matchId,
|
||||
required String groupId,
|
||||
}) async {
|
||||
final query = delete(groupMatchTable)
|
||||
..where((g) => g.matchId.equals(matchId) & g.groupId.equals(groupId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Updates the group associated with a match to [newGroupId] based on
|
||||
/// [matchId].
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> updateGroupOfMatch({
|
||||
required String matchId,
|
||||
required String newGroupId,
|
||||
}) async {
|
||||
final updatedRows =
|
||||
await (update(groupMatchTable)..where((g) => g.matchId.equals(matchId)))
|
||||
.write(GroupMatchTableCompanion(groupId: Value(newGroupId)));
|
||||
return updatedRows > 0;
|
||||
}
|
||||
}
|
||||
10
lib/data/dao/group_match_dao.g.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'group_match_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$GroupMatchDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$GroupTableTable get groupTable => attachedDatabase.groupTable;
|
||||
$MatchTableTable get matchTable => attachedDatabase.matchTable;
|
||||
$GroupMatchTableTable get groupMatchTable => attachedDatabase.groupMatchTable;
|
||||
}
|
||||
326
lib/data/dao/match_dao.dart
Normal file
@@ -0,0 +1,326 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/db/tables/match_table.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/data/dto/match.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
|
||||
part 'match_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [MatchTable])
|
||||
class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
MatchDao(super.db);
|
||||
|
||||
/// Retrieves all matches from the database.
|
||||
Future<List<Match>> getAllMatches() async {
|
||||
final query = select(matchTable);
|
||||
final result = await query.get();
|
||||
|
||||
return Future.wait(
|
||||
result.map((row) async {
|
||||
final group = await db.groupMatchDao.getGroupOfMatch(matchId: row.id);
|
||||
final players = await db.playerMatchDao.getPlayersOfMatch(
|
||||
matchId: row.id,
|
||||
);
|
||||
final winner = row.winnerId != null
|
||||
? await db.playerDao.getPlayerById(playerId: row.winnerId!)
|
||||
: null;
|
||||
return Match(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
group: group,
|
||||
players: players,
|
||||
createdAt: row.createdAt,
|
||||
winner: winner,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Retrieves a [Match] by its [matchId].
|
||||
Future<Match> getMatchById({required String matchId}) async {
|
||||
final query = select(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final result = await query.getSingle();
|
||||
|
||||
List<Player>? players;
|
||||
if (await db.playerMatchDao.matchHasPlayers(matchId: matchId)) {
|
||||
players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
|
||||
}
|
||||
Group? group;
|
||||
if (await db.groupMatchDao.matchHasGroup(matchId: matchId)) {
|
||||
group = await db.groupMatchDao.getGroupOfMatch(matchId: matchId);
|
||||
}
|
||||
Player? winner;
|
||||
if (result.winnerId != null) {
|
||||
winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
|
||||
}
|
||||
|
||||
return Match(
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
players: players,
|
||||
group: group,
|
||||
winner: winner,
|
||||
createdAt: result.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds a new [Match] to the database. Also adds players and group
|
||||
/// associations. This method assumes that the players and groups added to
|
||||
/// this match are already present in the database.
|
||||
Future<void> addMatch({required Match match}) async {
|
||||
await db.transaction(() async {
|
||||
await into(matchTable).insert(
|
||||
MatchTableCompanion.insert(
|
||||
id: match.id,
|
||||
name: match.name,
|
||||
winnerId: Value(match.winner?.id),
|
||||
createdAt: match.createdAt,
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
|
||||
if (match.players != null) {
|
||||
for (final p in match.players ?? []) {
|
||||
await db.playerMatchDao.addPlayerToMatch(
|
||||
matchId: match.id,
|
||||
playerId: p.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (match.group != null) {
|
||||
await db.groupMatchDao.addGroupToMatch(
|
||||
matchId: match.id,
|
||||
groupId: match.group!.id,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds multiple [Match]s to the database in a batch operation.
|
||||
/// Also adds associated players and groups if they exist.
|
||||
/// If the [matches] list is empty, the method returns immediately.
|
||||
/// This Method should only be used to import matches from a different device.
|
||||
Future<void> addMatchAsList({required List<Match> matches}) async {
|
||||
if (matches.isEmpty) return;
|
||||
await db.transaction(() async {
|
||||
// Add all matches in batch
|
||||
await db.batch(
|
||||
(b) => b.insertAll(
|
||||
matchTable,
|
||||
matches
|
||||
.map(
|
||||
(match) => MatchTableCompanion.insert(
|
||||
id: match.id,
|
||||
name: match.name,
|
||||
createdAt: match.createdAt,
|
||||
winnerId: Value(match.winner?.id),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
),
|
||||
);
|
||||
|
||||
// Add all groups of the matches in batch
|
||||
// Using insertOrIgnore to avoid overwriting existing groups (which would
|
||||
// trigger cascade deletes on player_group associations)
|
||||
await db.batch(
|
||||
(b) => b.insertAll(
|
||||
db.groupTable,
|
||||
matches
|
||||
.where((match) => match.group != null)
|
||||
.map(
|
||||
(matches) => GroupTableCompanion.insert(
|
||||
id: matches.group!.id,
|
||||
name: matches.group!.name,
|
||||
createdAt: matches.group!.createdAt,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
),
|
||||
);
|
||||
|
||||
// Add all players of the matches in batch (unique)
|
||||
final uniquePlayers = <String, Player>{};
|
||||
for (final match in matches) {
|
||||
if (match.players != null) {
|
||||
for (final p in match.players!) {
|
||||
uniquePlayers[p.id] = p;
|
||||
}
|
||||
}
|
||||
// Also include members of groups
|
||||
if (match.group != null) {
|
||||
for (final m in match.group!.members) {
|
||||
uniquePlayers[m.id] = m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uniquePlayers.isNotEmpty) {
|
||||
// Using insertOrIgnore to avoid triggering cascade deletes on
|
||||
// player_group/player_match associations when players already exist
|
||||
await db.batch(
|
||||
(b) => b.insertAll(
|
||||
db.playerTable,
|
||||
uniquePlayers.values
|
||||
.map(
|
||||
(p) => PlayerTableCompanion.insert(
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
createdAt: p.createdAt,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Add all player-match associations in batch
|
||||
await db.batch((b) {
|
||||
for (final match in matches) {
|
||||
if (match.players != null) {
|
||||
for (final p in match.players ?? []) {
|
||||
b.insert(
|
||||
db.playerMatchTable,
|
||||
PlayerMatchTableCompanion.insert(
|
||||
matchId: match.id,
|
||||
playerId: p.id,
|
||||
),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add all player-group associations in batch
|
||||
await db.batch((b) {
|
||||
for (final match in matches) {
|
||||
if (match.group != null) {
|
||||
for (final m in match.group!.members) {
|
||||
b.insert(
|
||||
db.playerGroupTable,
|
||||
PlayerGroupTableCompanion.insert(
|
||||
playerId: m.id,
|
||||
groupId: match.group!.id,
|
||||
),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add all group-match associations in batch
|
||||
await db.batch((b) {
|
||||
for (final match in matches) {
|
||||
if (match.group != null) {
|
||||
b.insert(
|
||||
db.groupMatchTable,
|
||||
GroupMatchTableCompanion.insert(
|
||||
matchId: match.id,
|
||||
groupId: match.group!.id,
|
||||
),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Deletes the match with the given [matchId] from the database.
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> deleteMatch({required String matchId}) async {
|
||||
final query = delete(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Retrieves the number of matches in the database.
|
||||
Future<int> getMatchCount() async {
|
||||
final count =
|
||||
await (selectOnly(matchTable)..addColumns([matchTable.id.count()]))
|
||||
.map((row) => row.read(matchTable.id.count()))
|
||||
.getSingle();
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
/// Checks if a match with the given [matchId] exists in the database.
|
||||
/// Returns `true` if the match exists, otherwise `false`.
|
||||
Future<bool> matchExists({required String matchId}) async {
|
||||
final query = select(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final result = await query.getSingleOrNull();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// Deletes all matches from the database.
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> deleteAllMatches() async {
|
||||
final query = delete(matchTable);
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Sets the winner of the match with the given [matchId] to the player with
|
||||
/// the given [winnerId].
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> setWinner({
|
||||
required String matchId,
|
||||
required String winnerId,
|
||||
}) async {
|
||||
final query = update(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final rowsAffected = await query.write(
|
||||
MatchTableCompanion(winnerId: Value(winnerId)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Retrieves the winner of the match with the given [matchId].
|
||||
/// Returns the [Player] who won the match, or `null` if no winner is set.
|
||||
Future<Player?> getWinner({required String matchId}) async {
|
||||
final query = select(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final result = await query.getSingleOrNull();
|
||||
if (result == null || result.winnerId == null) {
|
||||
return null;
|
||||
}
|
||||
final winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
|
||||
return winner;
|
||||
}
|
||||
|
||||
/// Removes the winner of the match with the given [matchId].
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> removeWinner({required String matchId}) async {
|
||||
final query = update(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final rowsAffected = await query.write(
|
||||
const MatchTableCompanion(winnerId: Value(null)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Checks if the match with the given [matchId] has a winner set.
|
||||
/// Returns `true` if a winner is set, otherwise `false`.
|
||||
Future<bool> hasWinner({required String matchId}) async {
|
||||
final query = select(matchTable)
|
||||
..where((g) => g.id.equals(matchId) & g.winnerId.isNotNull());
|
||||
final result = await query.getSingleOrNull();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// Changes the title of the match with the given [matchId] to [newName].
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> updateMatchName({
|
||||
required String matchId,
|
||||
required String newName,
|
||||
}) async {
|
||||
final query = update(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final rowsAffected = await query.write(
|
||||
MatchTableCompanion(name: Value(newName)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
}
|
||||
8
lib/data/dao/match_dao.g.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'match_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$MatchDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$MatchTableTable get matchTable => attachedDatabase.matchTable;
|
||||
}
|
||||
@@ -42,12 +42,38 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
name: player.name,
|
||||
createdAt: player.createdAt,
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Adds multiple [players] to the database in a batch operation.
|
||||
/// Uses insertOrIgnore to avoid triggering cascade deletes on
|
||||
/// player_group associations when players already exist.
|
||||
Future<bool> addPlayersAsList({required List<Player> players}) async {
|
||||
if (players.isEmpty) return false;
|
||||
|
||||
await db.batch(
|
||||
(b) => b.insertAll(
|
||||
playerTable,
|
||||
players
|
||||
.map(
|
||||
(player) => PlayerTableCompanion.insert(
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
createdAt: player.createdAt,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Deletes the player with the given [id] from the database.
|
||||
/// Returns `true` if the player was deleted, `false` if the player did not exist.
|
||||
Future<bool> deletePlayer({required String playerId}) async {
|
||||
@@ -82,4 +108,12 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
.getSingle();
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
/// Deletes all players from the database.
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> deleteAllPlayers() async {
|
||||
final query = delete(playerTable);
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_game_table.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
|
||||
part 'player_game_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [PlayerGameTable])
|
||||
class PlayerGameDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$PlayerGameDaoMixin {
|
||||
PlayerGameDao(super.db);
|
||||
|
||||
/// Checks if there are any players associated with the given [gameId].
|
||||
/// Returns `true` if there are players, otherwise `false`.
|
||||
Future<bool> gameHasPlayers({required String gameId}) async {
|
||||
final count =
|
||||
await (selectOnly(playerGameTable)
|
||||
..where(playerGameTable.gameId.equals(gameId))
|
||||
..addColumns([playerGameTable.playerId.count()]))
|
||||
.map((row) => row.read(playerGameTable.playerId.count()))
|
||||
.getSingle();
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
|
||||
/// Retrieves a list of [Player]s associated with the given [gameId].
|
||||
/// Returns an empty list if no players are found.
|
||||
Future<List<Player>> getPlayersByGameId({required String gameId}) async {
|
||||
final result = await (select(
|
||||
playerGameTable,
|
||||
)..where((p) => p.gameId.equals(gameId))).get();
|
||||
|
||||
if (result.isEmpty) return <Player>[];
|
||||
|
||||
final futures = result.map(
|
||||
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
|
||||
);
|
||||
final players = await Future.wait(futures);
|
||||
return players.whereType<Player>().toList();
|
||||
}
|
||||
|
||||
/// Associates a player with a game by inserting a record into the
|
||||
/// [PlayerGameTable].
|
||||
Future<void> addPlayerToGame({
|
||||
required String gameId,
|
||||
required String playerId,
|
||||
}) async {
|
||||
await into(playerGameTable).insert(
|
||||
PlayerGameTableCompanion.insert(playerId: playerId, gameId: gameId),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'player_game_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$PlayerGameDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
||||
$GameTableTable get gameTable => attachedDatabase.gameTable;
|
||||
$PlayerGameTableTable get playerGameTable => attachedDatabase.playerGameTable;
|
||||
}
|
||||
@@ -1,17 +1,44 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_group_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_table.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
|
||||
part 'player_group_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [PlayerGroupTable])
|
||||
@DriftAccessor(tables: [PlayerGroupTable, PlayerTable])
|
||||
class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$PlayerGroupDaoMixin {
|
||||
PlayerGroupDao(super.db);
|
||||
|
||||
/// No need for a groupHasPlayers method since the members attribute is
|
||||
/// not nullable
|
||||
|
||||
/// Adds a [player] to a group with the given [groupId].
|
||||
/// If the player is already in the group, no action is taken.
|
||||
/// If the player does not exist in the player table, they are added.
|
||||
/// Returns `true` if the player was added, otherwise `false`.
|
||||
Future<bool> addPlayerToGroup({
|
||||
required Player player,
|
||||
required String groupId,
|
||||
}) async {
|
||||
if (await isPlayerInGroup(playerId: player.id, groupId: groupId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await db.playerDao.playerExists(playerId: player.id)) {
|
||||
db.playerDao.addPlayer(player: player);
|
||||
}
|
||||
|
||||
await into(playerGroupTable).insert(
|
||||
PlayerGroupTableCompanion.insert(playerId: player.id, groupId: groupId),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Retrieves all players belonging to a specific group by [groupId].
|
||||
Future<List<Player>> getPlayersOfGroupById({required String groupId}) async {
|
||||
Future<List<Player>> getPlayersOfGroup({required String groupId}) async {
|
||||
final query = select(playerGroupTable)
|
||||
..where((pG) => pG.groupId.equals(groupId));
|
||||
final result = await query.get();
|
||||
@@ -38,29 +65,6 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Adds a [player] to a group with the given [groupId].
|
||||
/// If the player is already in the group, no action is taken.
|
||||
/// If the player does not exist in the player table, they are added.
|
||||
/// Returns `true` if the player was added, otherwise `false`.
|
||||
Future<bool> addPlayerToGroup({
|
||||
required Player player,
|
||||
required String groupId,
|
||||
}) async {
|
||||
if (await isPlayerInGroup(playerId: player.id, groupId: groupId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await db.playerDao.playerExists(playerId: player.id) == false) {
|
||||
db.playerDao.addPlayer(player: player);
|
||||
}
|
||||
|
||||
await into(playerGroupTable).insert(
|
||||
PlayerGroupTableCompanion.insert(playerId: player.id, groupId: groupId),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Checks if a player with [playerId] is in the group with [groupId].
|
||||
/// Returns `true` if the player is in the group, otherwise `false`.
|
||||
Future<bool> isPlayerInGroup({
|
||||
|
||||
130
lib/data/dao/player_match_dao.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_match_table.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
|
||||
part 'player_match_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [PlayerMatchTable])
|
||||
class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$PlayerMatchDaoMixin {
|
||||
PlayerMatchDao(super.db);
|
||||
|
||||
/// Associates a player with a match by inserting a record into the
|
||||
/// [PlayerMatchTable].
|
||||
Future<void> addPlayerToMatch({
|
||||
required String matchId,
|
||||
required String playerId,
|
||||
}) async {
|
||||
await into(playerMatchTable).insert(
|
||||
PlayerMatchTableCompanion.insert(playerId: playerId, matchId: matchId),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
);
|
||||
}
|
||||
|
||||
/// Retrieves a list of [Player]s associated with the given [matchId].
|
||||
/// Returns null if no players are found.
|
||||
Future<List<Player>?> getPlayersOfMatch({required String matchId}) async {
|
||||
final result = await (select(
|
||||
playerMatchTable,
|
||||
)..where((p) => p.matchId.equals(matchId))).get();
|
||||
|
||||
if (result.isEmpty) return null;
|
||||
|
||||
final futures = result.map(
|
||||
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
|
||||
);
|
||||
final players = await Future.wait(futures);
|
||||
return players;
|
||||
}
|
||||
|
||||
/// Checks if there are any players associated with the given [matchId].
|
||||
/// Returns `true` if there are players, otherwise `false`.
|
||||
Future<bool> matchHasPlayers({required String matchId}) async {
|
||||
final count =
|
||||
await (selectOnly(playerMatchTable)
|
||||
..where(playerMatchTable.matchId.equals(matchId))
|
||||
..addColumns([playerMatchTable.playerId.count()]))
|
||||
.map((row) => row.read(playerMatchTable.playerId.count()))
|
||||
.getSingle();
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
|
||||
/// Checks if a specific player is associated with a specific match.
|
||||
/// Returns `true` if the player is in the match, otherwise `false`.
|
||||
Future<bool> isPlayerInMatch({
|
||||
required String matchId,
|
||||
required String playerId,
|
||||
}) async {
|
||||
final count =
|
||||
await (selectOnly(playerMatchTable)
|
||||
..where(playerMatchTable.matchId.equals(matchId))
|
||||
..where(playerMatchTable.playerId.equals(playerId))
|
||||
..addColumns([playerMatchTable.playerId.count()]))
|
||||
.map((row) => row.read(playerMatchTable.playerId.count()))
|
||||
.getSingle();
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
|
||||
/// Removes the association of a player with a match by deleting the record
|
||||
/// from the [PlayerMatchTable].
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> removePlayerFromMatch({
|
||||
required String matchId,
|
||||
required String playerId,
|
||||
}) async {
|
||||
final query = delete(playerMatchTable)
|
||||
..where((pg) => pg.matchId.equals(matchId))
|
||||
..where((pg) => pg.playerId.equals(playerId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Updates the players associated with a match based on the provided
|
||||
/// [newPlayer] list. It adds new players and removes players that are no
|
||||
/// longer associated with the match.
|
||||
Future<void> updatePlayersFromMatch({
|
||||
required String matchId,
|
||||
required List<Player> newPlayer,
|
||||
}) async {
|
||||
final currentPlayers = await getPlayersOfMatch(matchId: matchId);
|
||||
// Create sets of player IDs for easy comparison
|
||||
final currentPlayerIds = currentPlayers?.map((p) => p.id).toSet() ?? {};
|
||||
final newPlayerIdsSet = newPlayer.map((p) => p.id).toSet();
|
||||
|
||||
// Determine players to add and remove
|
||||
final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds);
|
||||
final playersToRemove = currentPlayerIds.difference(newPlayerIdsSet);
|
||||
|
||||
db.transaction(() async {
|
||||
// Remove old players
|
||||
if (playersToRemove.isNotEmpty) {
|
||||
await (delete(playerMatchTable)..where(
|
||||
(pg) =>
|
||||
pg.matchId.equals(matchId) &
|
||||
pg.playerId.isIn(playersToRemove.toList()),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
// Add new players
|
||||
if (playersToAdd.isNotEmpty) {
|
||||
final inserts = playersToAdd
|
||||
.map(
|
||||
(id) => PlayerMatchTableCompanion.insert(
|
||||
playerId: id,
|
||||
matchId: matchId,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
await Future.wait(
|
||||
inserts.map(
|
||||
(c) => into(
|
||||
playerMatchTable,
|
||||
).insert(c, mode: InsertMode.insertOrIgnore),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
11
lib/data/dao/player_match_dao.g.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'player_match_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$PlayerMatchDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
||||
$MatchTableTable get matchTable => attachedDatabase.matchTable;
|
||||
$PlayerMatchTableTable get playerMatchTable =>
|
||||
attachedDatabase.playerMatchTable;
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:game_tracker/data/dao/game_dao.dart';
|
||||
import 'package:game_tracker/data/dao/group_dao.dart';
|
||||
import 'package:game_tracker/data/dao/group_game_dao.dart';
|
||||
import 'package:game_tracker/data/dao/group_match_dao.dart';
|
||||
import 'package:game_tracker/data/dao/match_dao.dart';
|
||||
import 'package:game_tracker/data/dao/player_dao.dart';
|
||||
import 'package:game_tracker/data/dao/player_game_dao.dart';
|
||||
import 'package:game_tracker/data/dao/player_group_dao.dart';
|
||||
import 'package:game_tracker/data/db/tables/game_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/group_game_table.dart';
|
||||
import 'package:game_tracker/data/dao/player_match_dao.dart';
|
||||
import 'package:game_tracker/data/db/tables/group_match_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/group_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_game_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/match_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_group_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_match_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_table.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
@@ -20,18 +20,18 @@ part 'database.g.dart';
|
||||
tables: [
|
||||
PlayerTable,
|
||||
GroupTable,
|
||||
GameTable,
|
||||
MatchTable,
|
||||
PlayerGroupTable,
|
||||
PlayerGameTable,
|
||||
GroupGameTable,
|
||||
PlayerMatchTable,
|
||||
GroupMatchTable,
|
||||
],
|
||||
daos: [
|
||||
PlayerDao,
|
||||
GroupDao,
|
||||
GameDao,
|
||||
MatchDao,
|
||||
PlayerGroupDao,
|
||||
PlayerGameDao,
|
||||
GroupGameDao,
|
||||
PlayerMatchDao,
|
||||
GroupMatchDao,
|
||||
],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
@@ -40,6 +40,15 @@ class AppDatabase extends _$AppDatabase {
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
beforeOpen: (details) async {
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
name: 'gametracker_db',
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/tables/game_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/group_table.dart';
|
||||
|
||||
class GroupGameTable extends Table {
|
||||
TextColumn get groupId =>
|
||||
text().references(GroupTable, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get gameId =>
|
||||
text().references(GameTable, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {groupId, gameId};
|
||||
}
|
||||
13
lib/data/db/tables/group_match_table.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/tables/group_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/match_table.dart';
|
||||
|
||||
class GroupMatchTable extends Table {
|
||||
TextColumn get groupId =>
|
||||
text().references(GroupTable, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get matchId =>
|
||||
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {groupId, matchId};
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_table.dart';
|
||||
|
||||
class GameTable extends Table {
|
||||
class MatchTable extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get winnerId =>
|
||||
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
|
||||
late final winnerId = text().nullable()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
|
||||
@override
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/tables/game_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_table.dart';
|
||||
|
||||
class PlayerGameTable extends Table {
|
||||
TextColumn get playerId =>
|
||||
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get gameId =>
|
||||
text().references(GameTable, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {playerId, gameId};
|
||||
}
|
||||
13
lib/data/db/tables/player_match_table.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:game_tracker/data/db/tables/match_table.dart';
|
||||
import 'package:game_tracker/data/db/tables/player_table.dart';
|
||||
|
||||
class PlayerMatchTable extends Table {
|
||||
TextColumn get playerId =>
|
||||
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get matchId =>
|
||||
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {playerId, matchId};
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class Game {
|
||||
final String id;
|
||||
final String name;
|
||||
final List<Player>? players;
|
||||
final Group? group;
|
||||
final String winner;
|
||||
final DateTime createdAt;
|
||||
|
||||
Game({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
required this.name,
|
||||
this.players,
|
||||
this.group,
|
||||
this.winner = '',
|
||||
}) : id = id ?? const Uuid().v4(),
|
||||
createdAt = createdAt ?? clock.now();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Game{\n\tid: $id,\n\tname: $name,\n\tplayers: $players,\n\tgroup: $group,\n\twinner: $winner\n}';
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
class Group {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final String name;
|
||||
final List<Player> members;
|
||||
final DateTime createdAt;
|
||||
|
||||
Group({
|
||||
String? id,
|
||||
@@ -20,4 +20,21 @@ class Group {
|
||||
String toString() {
|
||||
return 'Group{id: $id, name: $name,members: $members}';
|
||||
}
|
||||
|
||||
/// Creates a Group instance from a JSON object.
|
||||
Group.fromJson(Map<String, dynamic> json)
|
||||
: id = json['id'],
|
||||
createdAt = DateTime.parse(json['createdAt']),
|
||||
name = json['name'],
|
||||
members = (json['members'] as List)
|
||||
.map((memberJson) => Player.fromJson(memberJson))
|
||||
.toList();
|
||||
|
||||
/// Converts the Group instance to a JSON object.
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'name': name,
|
||||
'members': members.map((member) => member.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
51
lib/data/dto/match.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class Match {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final String name;
|
||||
final List<Player>? players;
|
||||
final Group? group;
|
||||
final Player? winner;
|
||||
|
||||
Match({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
required this.name,
|
||||
this.players,
|
||||
this.group,
|
||||
this.winner,
|
||||
}) : id = id ?? const Uuid().v4(),
|
||||
createdAt = createdAt ?? clock.now();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Match{\n\tid: $id,\n\tname: $name,\n\tplayers: $players,\n\tgroup: $group,\n\twinner: $winner\n}';
|
||||
}
|
||||
|
||||
/// Creates a Match instance from a JSON object.
|
||||
Match.fromJson(Map<String, dynamic> json)
|
||||
: id = json['id'],
|
||||
name = json['name'],
|
||||
createdAt = DateTime.parse(json['createdAt']),
|
||||
players = json['players'] != null
|
||||
? (json['players'] as List)
|
||||
.map((playerJson) => Player.fromJson(playerJson))
|
||||
.toList()
|
||||
: null,
|
||||
group = json['group'] != null ? Group.fromJson(json['group']) : null,
|
||||
winner = json['winner'] != null ? Player.fromJson(json['winner']) : null;
|
||||
|
||||
/// Converts the Match instance to a JSON object.
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'name': name,
|
||||
'players': players?.map((player) => player.toJson()).toList(),
|
||||
'group': group?.toJson(),
|
||||
'winner': winner?.toJson(),
|
||||
};
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
class Player {
|
||||
final String id;
|
||||
final String name;
|
||||
final DateTime createdAt;
|
||||
final String name;
|
||||
|
||||
Player({String? id, DateTime? createdAt, required this.name})
|
||||
: id = id ?? const Uuid().v4(),
|
||||
@@ -14,4 +14,17 @@ class Player {
|
||||
String toString() {
|
||||
return 'Player{id: $id,name: $name}';
|
||||
}
|
||||
|
||||
/// Creates a Player instance from a JSON object.
|
||||
Player.fromJson(Map<String, dynamic> json)
|
||||
: id = json['id'],
|
||||
createdAt = DateTime.parse(json['createdAt']),
|
||||
name = json['name'];
|
||||
|
||||
/// Converts the Player instance to a JSON object.
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'name': name,
|
||||
};
|
||||
}
|
||||
|
||||
83
lib/l10n/arb/app_de.arb
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"@@locale": "de",
|
||||
"all_players": "Alle Spieler:innen",
|
||||
"all_players_selected": "Alle Spieler:innen ausgewählt",
|
||||
"amount_of_matches": "Anzahl der Spiele",
|
||||
"cancel": "Abbrechen",
|
||||
"choose_game": "Spielvorlage wählen",
|
||||
"choose_group": "Gruppe wählen",
|
||||
"choose_ruleset": "Regelwerk wählen",
|
||||
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
|
||||
"create_group": "Gruppe erstellen",
|
||||
"create_match": "Spiel erstellen",
|
||||
"create_new_group": "Neue Gruppe erstellen",
|
||||
"create_new_match": "Neues Spiel erstellen",
|
||||
"data_successfully_deleted": "Daten erfolgreich gelöscht",
|
||||
"data_successfully_exported": "Daten erfolgreich exportiert",
|
||||
"data_successfully_imported": "Daten erfolgreich importiert",
|
||||
"days_ago": "vor {count} Tagen",
|
||||
"delete": "Löschen",
|
||||
"delete_all_data": "Alle Daten löschen?",
|
||||
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
|
||||
"error_reading_file": "Fehler beim Lesen der Datei",
|
||||
"export_canceled": "Export abgebrochen",
|
||||
"export_data": "Daten exportieren",
|
||||
"format_exception": "Formatfehler (siehe Konsole)",
|
||||
"game": "Spielvorlage",
|
||||
"game_name": "Spielvorlagenname",
|
||||
"group": "Gruppe",
|
||||
"group_name": "Gruppenname",
|
||||
"groups": "Gruppen",
|
||||
"home": "Startseite",
|
||||
"import_canceled": "Import abgebrochen",
|
||||
"import_data": "Daten importieren",
|
||||
"info": "Info",
|
||||
"invalid_schema": "Ungültiges Schema",
|
||||
"least_points": "Niedrigste Punkte",
|
||||
"match_in_progress": "Spiel läuft...",
|
||||
"match_name": "Spieltitel",
|
||||
"matches": "Spiele",
|
||||
"menu": "Menü",
|
||||
"most_points": "Höchste Punkte",
|
||||
"no_data_available": "Keine Daten verfügbar",
|
||||
"no_groups_created_yet": "Noch keine Gruppen erstellt",
|
||||
"no_matches_created_yet": "Noch keine Spiele erstellt",
|
||||
"no_players_created_yet": "Noch keine Spieler:in erstellt",
|
||||
"no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
|
||||
"no_players_selected": "Keine Spieler:innen ausgewählt",
|
||||
"no_recent_matches_available": "Keine letzten Spiele verfügbar",
|
||||
"no_second_match_available": "Kein zweites Spiel verfügbar",
|
||||
"no_statistics_available": "Keine Statistiken verfügbar",
|
||||
"none": "Kein",
|
||||
"none_group": "Keine",
|
||||
"not_available": "Nicht verfügbar",
|
||||
"player_name": "Spieler:innenname",
|
||||
"players": "Spieler:innen",
|
||||
"players_count": "{count} Spieler",
|
||||
"quick_create": "Schnellzugriff",
|
||||
"recent_matches": "Letzte Spiele",
|
||||
"ruleset": "Regelwerk",
|
||||
"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_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.",
|
||||
"search_for_groups": "Nach Gruppen suchen",
|
||||
"search_for_players": "Nach Spieler:innen suchen",
|
||||
"select_winner": "Gewinner:in wählen:",
|
||||
"selected_players": "Ausgewählte Spieler:innen",
|
||||
"settings": "Einstellungen",
|
||||
"single_loser": "Ein:e Verlierer:in",
|
||||
"single_winner": "Ein:e Gewinner:in",
|
||||
"statistics": "Statistiken",
|
||||
"stats": "Statistiken",
|
||||
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
|
||||
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht",
|
||||
"this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden",
|
||||
"today_at": "Heute um",
|
||||
"undo": "Rückgängig",
|
||||
"unknown_exception": "Unbekannter Fehler (siehe Konsole)",
|
||||
"winner": "Gewinner:in",
|
||||
"winrate": "Siegquote",
|
||||
"wins": "Siege",
|
||||
"yesterday_at": "Gestern um"
|
||||
}
|
||||
343
lib/l10n/arb/app_en.arb
Normal file
@@ -0,0 +1,343 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"@all_players": {
|
||||
"description": "Label for all players list"
|
||||
},
|
||||
"@all_players_selected": {
|
||||
"description": "Message when all players are added to selection"
|
||||
},
|
||||
"@amount_of_matches": {
|
||||
"description": "Label for amount of matches statistic"
|
||||
},
|
||||
"@app_name": {
|
||||
"description": "The name of the App"
|
||||
},
|
||||
"@cancel": {
|
||||
"description": "Cancel button text"
|
||||
},
|
||||
"@choose_game": {
|
||||
"description": "Label for choosing a game"
|
||||
},
|
||||
"@choose_group": {
|
||||
"description": "Label for choosing a group"
|
||||
},
|
||||
"@choose_ruleset": {
|
||||
"description": "Label for choosing a ruleset"
|
||||
},
|
||||
"@could_not_add_player": {
|
||||
"description": "Error message when adding a player fails"
|
||||
},
|
||||
"@create_group": {
|
||||
"description": "Button text to create a group"
|
||||
},
|
||||
"@create_match": {
|
||||
"description": "Button text to create a match"
|
||||
},
|
||||
"@create_new_group": {
|
||||
"description": "Button text to create a new group"
|
||||
},
|
||||
"@create_new_match": {
|
||||
"description": "Button text to create a new match"
|
||||
},
|
||||
"@data_successfully_deleted": {
|
||||
"description": "Success message after deleting data"
|
||||
},
|
||||
"@data_successfully_exported": {
|
||||
"description": "Success message after exporting data"
|
||||
},
|
||||
"@data_successfully_imported": {
|
||||
"description": "Success message after importing data"
|
||||
},
|
||||
"@days_ago": {
|
||||
"description": "Date format for days ago",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@delete": {
|
||||
"description": "Delete button text"
|
||||
},
|
||||
"@delete_all_data": {
|
||||
"description": "Confirmation dialog for deleting all data"
|
||||
},
|
||||
"@error_creating_group": {
|
||||
"description": "Error message when group creation fails"
|
||||
},
|
||||
"@error_reading_file": {
|
||||
"description": "Error message when file cannot be read"
|
||||
},
|
||||
"@export_canceled": {
|
||||
"description": "Message when export is canceled"
|
||||
},
|
||||
"@export_data": {
|
||||
"description": "Export data menu item"
|
||||
},
|
||||
"@format_exception": {
|
||||
"description": "Error message for format exceptions"
|
||||
},
|
||||
"@game": {
|
||||
"description": "Game label"
|
||||
},
|
||||
"@game_name": {
|
||||
"description": "Placeholder for game name search"
|
||||
},
|
||||
"@group": {
|
||||
"description": "Group label"
|
||||
},
|
||||
"@group_name": {
|
||||
"description": "Placeholder for group name input"
|
||||
},
|
||||
"@groups": {
|
||||
"description": "Label for groups"
|
||||
},
|
||||
"@home": {
|
||||
"description": "Home tab label"
|
||||
},
|
||||
"@import_canceled": {
|
||||
"description": "Message when import is canceled"
|
||||
},
|
||||
"@import_data": {
|
||||
"description": "Import data menu item"
|
||||
},
|
||||
"@info": {
|
||||
"description": "Info label"
|
||||
},
|
||||
"@invalid_schema": {
|
||||
"description": "Error message for invalid schema"
|
||||
},
|
||||
"@least_points": {
|
||||
"description": "Title for least points ruleset"
|
||||
},
|
||||
"@match_in_progress": {
|
||||
"description": "Message when match is in progress"
|
||||
},
|
||||
"@match_name": {
|
||||
"description": "Placeholder for match name input"
|
||||
},
|
||||
"@matches": {
|
||||
"description": "Label for matches"
|
||||
},
|
||||
"@menu": {
|
||||
"description": "Menu label"
|
||||
},
|
||||
"@most_points": {
|
||||
"description": "Title for most points ruleset"
|
||||
},
|
||||
"@no_data_available": {
|
||||
"description": "Message when no data in the statistic tiles is given"
|
||||
},
|
||||
"@no_groups_created_yet": {
|
||||
"description": "Message when no groups exist"
|
||||
},
|
||||
"@no_matches_created_yet": {
|
||||
"description": "Message when no matches exist"
|
||||
},
|
||||
"@no_players_created_yet": {
|
||||
"description": "Message when no players exist"
|
||||
},
|
||||
"@no_players_found_with_that_name": {
|
||||
"description": "Message when search returns no results"
|
||||
},
|
||||
"@no_players_selected": {
|
||||
"description": "Message when no players are selected"
|
||||
},
|
||||
"@no_recent_matches_available": {
|
||||
"description": "Message when no recent matches exist"
|
||||
},
|
||||
"@no_second_match_available": {
|
||||
"description": "Message when no second match exists"
|
||||
},
|
||||
"@no_statistics_available": {
|
||||
"description": "Message when no statistics are available, because no matches were played yet"
|
||||
},
|
||||
"@none": {
|
||||
"description": "None option label"
|
||||
},
|
||||
"@none_group": {
|
||||
"description": "None group option label"
|
||||
},
|
||||
"@not_available": {
|
||||
"description": "Abbreviation for not available"
|
||||
},
|
||||
"@player_name": {
|
||||
"description": "Placeholder for player name input"
|
||||
},
|
||||
"@players": {
|
||||
"description": "Players label"
|
||||
},
|
||||
"@players_count": {
|
||||
"description": "Shows the number of players",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@quick_create": {
|
||||
"description": "Title for quick create section"
|
||||
},
|
||||
"@recent_matches": {
|
||||
"description": "Title for recent matches section"
|
||||
},
|
||||
"@ruleset": {
|
||||
"description": "Ruleset label"
|
||||
},
|
||||
"@ruleset_least_points": {
|
||||
"description": "Description for least points ruleset"
|
||||
},
|
||||
"@ruleset_most_points": {
|
||||
"description": "Description for most points ruleset"
|
||||
},
|
||||
"@ruleset_single_loser": {
|
||||
"description": "Description for single loser ruleset"
|
||||
},
|
||||
"@ruleset_single_winner": {
|
||||
"description": "Description for single winner ruleset"
|
||||
},
|
||||
"@search_for_groups": {
|
||||
"description": "Hint text for group search input field"
|
||||
},
|
||||
"@search_for_players": {
|
||||
"description": "Hint text for player search input field"
|
||||
},
|
||||
"@select_winner": {
|
||||
"description": "Label to select the winner"
|
||||
},
|
||||
"@selected_players": {
|
||||
"description": "Shows the number of selected players"
|
||||
},
|
||||
"@settings": {
|
||||
"description": "Settings label"
|
||||
},
|
||||
"@single_loser": {
|
||||
"description": "Title for single loser ruleset"
|
||||
},
|
||||
"@single_winner": {
|
||||
"description": "Title for single winner ruleset"
|
||||
},
|
||||
"@statistics": {
|
||||
"description": "Statistics tab label"
|
||||
},
|
||||
"@stats": {
|
||||
"description": "Stats tab label (short)"
|
||||
},
|
||||
"@successfully_added_player": {
|
||||
"description": "Success message when adding a player",
|
||||
"placeholders": {
|
||||
"playerName": {
|
||||
"type": "String",
|
||||
"example": "John"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@there_is_no_group_matching_your_search": {
|
||||
"description": "Message when search returns no groups"
|
||||
},
|
||||
"@this_cannot_be_undone": {
|
||||
"description": "Warning message for irreversible actions"
|
||||
},
|
||||
"@today_at": {
|
||||
"description": "Date format for today"
|
||||
},
|
||||
"@undo": {
|
||||
"description": "Undo button text"
|
||||
},
|
||||
"@unknown_exception": {
|
||||
"description": "Error message for unknown exceptions"
|
||||
},
|
||||
"@winner": {
|
||||
"description": "Winner label"
|
||||
},
|
||||
"@winrate": {
|
||||
"description": "Label for winrate statistic"
|
||||
},
|
||||
"@wins": {
|
||||
"description": "Label for wins statistic"
|
||||
},
|
||||
"@yesterday_at": {
|
||||
"description": "Date format for yesterday"
|
||||
},
|
||||
"all_players": "All players",
|
||||
"all_players_selected": "All players selected",
|
||||
"amount_of_matches": "Amount of Matches",
|
||||
"app_name": "Game Tracker",
|
||||
"cancel": "Cancel",
|
||||
"choose_game": "Choose Game",
|
||||
"choose_group": "Choose Group",
|
||||
"choose_ruleset": "Choose Ruleset",
|
||||
"could_not_add_player": "Could not add player",
|
||||
"create_group": "Create Group",
|
||||
"create_match": "Create match",
|
||||
"create_new_group": "Create new group",
|
||||
"create_new_match": "Create new match",
|
||||
"data_successfully_deleted": "Data successfully deleted",
|
||||
"data_successfully_exported": "Data successfully exported",
|
||||
"data_successfully_imported": "Data successfully imported",
|
||||
"days_ago": "{count} days ago",
|
||||
"delete": "Delete",
|
||||
"delete_all_data": "Delete all data?",
|
||||
"error_creating_group": "Error while creating group, please try again",
|
||||
"error_reading_file": "Error reading file",
|
||||
"export_canceled": "Export canceled",
|
||||
"export_data": "Export data",
|
||||
"format_exception": "Format Exception (see console)",
|
||||
"game": "Game",
|
||||
"game_name": "Game Name",
|
||||
"group": "Group",
|
||||
"group_name": "Group name",
|
||||
"groups": "Groups",
|
||||
"home": "Home",
|
||||
"import_canceled": "Import canceled",
|
||||
"import_data": "Import data",
|
||||
"info": "Info",
|
||||
"invalid_schema": "Invalid Schema",
|
||||
"least_points": "Least Points",
|
||||
"match_in_progress": "Match in progress...",
|
||||
"match_name": "Match name",
|
||||
"matches": "Matches",
|
||||
"menu": "Menu",
|
||||
"most_points": "Most Points",
|
||||
"no_data_available": "No data available",
|
||||
"no_groups_created_yet": "No groups created yet",
|
||||
"no_matches_created_yet": "No matches created yet",
|
||||
"no_players_created_yet": "No players created yet",
|
||||
"no_players_found_with_that_name": "No players found with that name",
|
||||
"no_players_selected": "No players selected",
|
||||
"no_recent_matches_available": "No recent matches available",
|
||||
"no_second_match_available": "No second match available",
|
||||
"no_statistics_available": "No statistics available",
|
||||
"none": "None",
|
||||
"none_group": "None",
|
||||
"not_available": "Not available",
|
||||
"player_name": "Player name",
|
||||
"players": "Players",
|
||||
"players_count": "{count} Players",
|
||||
"quick_create": "Quick Create",
|
||||
"recent_matches": "Recent Matches",
|
||||
"ruleset": "Ruleset",
|
||||
"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_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.",
|
||||
"search_for_groups": "Search for groups",
|
||||
"search_for_players": "Search for players",
|
||||
"select_winner": "Select Winner:",
|
||||
"selected_players": "Selected players",
|
||||
"settings": "Settings",
|
||||
"single_loser": "Single Loser",
|
||||
"single_winner": "Single Winner",
|
||||
"statistics": "Statistics",
|
||||
"stats": "Stats",
|
||||
"successfully_added_player": "Successfully added player {playerName}",
|
||||
"there_is_no_group_matching_your_search": "There is no group matching your search",
|
||||
"this_cannot_be_undone": "This can't be undone",
|
||||
"today_at": "Today at",
|
||||
"undo": "Undo",
|
||||
"unknown_exception": "Unknown Exception (see console)",
|
||||
"winner": "Winner",
|
||||
"winrate": "Winrate",
|
||||
"wins": "Wins",
|
||||
"yesterday_at": "Yesterday at"
|
||||
}
|
||||
620
lib/l10n/generated/app_localizations.dart
Normal file
@@ -0,0 +1,620 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_de.dart';
|
||||
import 'app_localizations_en.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||
/// returned by `AppLocalizations.of(context)`.
|
||||
///
|
||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||
/// `localizationDelegates` list, and the locales they support in the app's
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'generated/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||
/// home: MyApplicationHome(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Update pubspec.yaml
|
||||
///
|
||||
/// Please make sure to update your pubspec.yaml to include the following
|
||||
/// packages:
|
||||
///
|
||||
/// ```yaml
|
||||
/// dependencies:
|
||||
/// # Internationalization support.
|
||||
/// flutter_localizations:
|
||||
/// sdk: flutter
|
||||
/// intl: any # Use the pinned version from flutter_localizations
|
||||
///
|
||||
/// # Rest of dependencies
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS Applications
|
||||
///
|
||||
/// iOS applications define key application metadata, including supported
|
||||
/// locales, in an Info.plist file that is built into the application bundle.
|
||||
/// To configure the locales supported by your app, you’ll need to edit this
|
||||
/// file.
|
||||
///
|
||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||
/// project’s Runner folder.
|
||||
///
|
||||
/// Next, select the Information Property List item, select Add Item from the
|
||||
/// Editor menu, then select Localizations from the pop-up menu.
|
||||
///
|
||||
/// Select and expand the newly-created Localizations item then, for each
|
||||
/// locale your application supports, add a new item and select the locale
|
||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
static AppLocalizations of(BuildContext context) {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||
_AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
///
|
||||
/// Returns a list of localizations delegates containing this delegate along with
|
||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||
/// and GlobalWidgetsLocalizations.delegate.
|
||||
///
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('de'),
|
||||
Locale('en'),
|
||||
];
|
||||
|
||||
/// Label for all players list
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'All players'**
|
||||
String get all_players;
|
||||
|
||||
/// Message when all players are added to selection
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'All players selected'**
|
||||
String get all_players_selected;
|
||||
|
||||
/// Label for amount of matches statistic
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Amount of Matches'**
|
||||
String get amount_of_matches;
|
||||
|
||||
/// The name of the App
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Game Tracker'**
|
||||
String get app_name;
|
||||
|
||||
/// Cancel button text
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cancel'**
|
||||
String get cancel;
|
||||
|
||||
/// Label for choosing a game
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose Game'**
|
||||
String get choose_game;
|
||||
|
||||
/// Label for choosing a group
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose Group'**
|
||||
String get choose_group;
|
||||
|
||||
/// Label for choosing a ruleset
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose Ruleset'**
|
||||
String get choose_ruleset;
|
||||
|
||||
/// Error message when adding a player fails
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Could not add player'**
|
||||
String could_not_add_player(Object playerName);
|
||||
|
||||
/// Button text to create a group
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create Group'**
|
||||
String get create_group;
|
||||
|
||||
/// Button text to create a match
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create match'**
|
||||
String get create_match;
|
||||
|
||||
/// Button text to create a new group
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create new group'**
|
||||
String get create_new_group;
|
||||
|
||||
/// Button text to create a new match
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create new match'**
|
||||
String get create_new_match;
|
||||
|
||||
/// Success message after deleting data
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Data successfully deleted'**
|
||||
String get data_successfully_deleted;
|
||||
|
||||
/// Success message after exporting data
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Data successfully exported'**
|
||||
String get data_successfully_exported;
|
||||
|
||||
/// Success message after importing data
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Data successfully imported'**
|
||||
String get data_successfully_imported;
|
||||
|
||||
/// Date format for days ago
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count} days ago'**
|
||||
String days_ago(int count);
|
||||
|
||||
/// Delete button text
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete'**
|
||||
String get delete;
|
||||
|
||||
/// Confirmation dialog for deleting all data
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete all data?'**
|
||||
String get delete_all_data;
|
||||
|
||||
/// Error message when group creation fails
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Error while creating group, please try again'**
|
||||
String get error_creating_group;
|
||||
|
||||
/// Error message when file cannot be read
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Error reading file'**
|
||||
String get error_reading_file;
|
||||
|
||||
/// Message when export is canceled
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Export canceled'**
|
||||
String get export_canceled;
|
||||
|
||||
/// Export data menu item
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Export data'**
|
||||
String get export_data;
|
||||
|
||||
/// Error message for format exceptions
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Format Exception (see console)'**
|
||||
String get format_exception;
|
||||
|
||||
/// Game label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Game'**
|
||||
String get game;
|
||||
|
||||
/// Placeholder for game name search
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Game Name'**
|
||||
String get game_name;
|
||||
|
||||
/// Group label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Group'**
|
||||
String get group;
|
||||
|
||||
/// Placeholder for group name input
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Group name'**
|
||||
String get group_name;
|
||||
|
||||
/// Label for groups
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Groups'**
|
||||
String get groups;
|
||||
|
||||
/// Home tab label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Home'**
|
||||
String get home;
|
||||
|
||||
/// Message when import is canceled
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Import canceled'**
|
||||
String get import_canceled;
|
||||
|
||||
/// Import data menu item
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Import data'**
|
||||
String get import_data;
|
||||
|
||||
/// Info label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Info'**
|
||||
String get info;
|
||||
|
||||
/// Error message for invalid schema
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Invalid Schema'**
|
||||
String get invalid_schema;
|
||||
|
||||
/// Title for least points ruleset
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Least Points'**
|
||||
String get least_points;
|
||||
|
||||
/// Message when match is in progress
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Match in progress...'**
|
||||
String get match_in_progress;
|
||||
|
||||
/// Placeholder for match name input
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Match name'**
|
||||
String get match_name;
|
||||
|
||||
/// Label for matches
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Matches'**
|
||||
String get matches;
|
||||
|
||||
/// Menu label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Menu'**
|
||||
String get menu;
|
||||
|
||||
/// Title for most points ruleset
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Most Points'**
|
||||
String get most_points;
|
||||
|
||||
/// Message when no data in the statistic tiles is given
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No data available'**
|
||||
String get no_data_available;
|
||||
|
||||
/// Message when no groups exist
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No groups created yet'**
|
||||
String get no_groups_created_yet;
|
||||
|
||||
/// Message when no matches exist
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No matches created yet'**
|
||||
String get no_matches_created_yet;
|
||||
|
||||
/// Message when no players exist
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No players created yet'**
|
||||
String get no_players_created_yet;
|
||||
|
||||
/// Message when search returns no results
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No players found with that name'**
|
||||
String get no_players_found_with_that_name;
|
||||
|
||||
/// Message when no players are selected
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No players selected'**
|
||||
String get no_players_selected;
|
||||
|
||||
/// Message when no recent matches exist
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No recent matches available'**
|
||||
String get no_recent_matches_available;
|
||||
|
||||
/// Message when no second match exists
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No second match available'**
|
||||
String get no_second_match_available;
|
||||
|
||||
/// Message when no statistics are available, because no matches were played yet
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No statistics available'**
|
||||
String get no_statistics_available;
|
||||
|
||||
/// None option label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'None'**
|
||||
String get none;
|
||||
|
||||
/// None group option label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'None'**
|
||||
String get none_group;
|
||||
|
||||
/// Abbreviation for not available
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not available'**
|
||||
String get not_available;
|
||||
|
||||
/// Placeholder for player name input
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Player name'**
|
||||
String get player_name;
|
||||
|
||||
/// Players label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Players'**
|
||||
String get players;
|
||||
|
||||
/// Shows the number of players
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count} Players'**
|
||||
String players_count(int count);
|
||||
|
||||
/// Title for quick create section
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Quick Create'**
|
||||
String get quick_create;
|
||||
|
||||
/// Title for recent matches section
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recent Matches'**
|
||||
String get recent_matches;
|
||||
|
||||
/// Ruleset label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Ruleset'**
|
||||
String get ruleset;
|
||||
|
||||
/// Description for least points ruleset
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Inverse scoring: the player with the fewest points wins.'**
|
||||
String get ruleset_least_points;
|
||||
|
||||
/// Description for most points ruleset
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Traditional ruleset: the player with the most points wins.'**
|
||||
String get ruleset_most_points;
|
||||
|
||||
/// Description for single loser ruleset
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Exactly one loser is determined; last place receives the penalty or consequence.'**
|
||||
String get ruleset_single_loser;
|
||||
|
||||
/// Description for single winner ruleset
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'**
|
||||
String get ruleset_single_winner;
|
||||
|
||||
/// Hint text for group search input field
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search for groups'**
|
||||
String get search_for_groups;
|
||||
|
||||
/// Hint text for player search input field
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search for players'**
|
||||
String get search_for_players;
|
||||
|
||||
/// Label to select the winner
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select Winner:'**
|
||||
String get select_winner;
|
||||
|
||||
/// Shows the number of selected players
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Selected players'**
|
||||
String get selected_players;
|
||||
|
||||
/// Settings label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settings'**
|
||||
String get settings;
|
||||
|
||||
/// Title for single loser ruleset
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Single Loser'**
|
||||
String get single_loser;
|
||||
|
||||
/// Title for single winner ruleset
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Single Winner'**
|
||||
String get single_winner;
|
||||
|
||||
/// Statistics tab label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Statistics'**
|
||||
String get statistics;
|
||||
|
||||
/// Stats tab label (short)
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stats'**
|
||||
String get stats;
|
||||
|
||||
/// Success message when adding a player
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Successfully added player {playerName}'**
|
||||
String successfully_added_player(String playerName);
|
||||
|
||||
/// Message when search returns no groups
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'There is no group matching your search'**
|
||||
String get there_is_no_group_matching_your_search;
|
||||
|
||||
/// Warning message for irreversible actions
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This can\'t be undone'**
|
||||
String get this_cannot_be_undone;
|
||||
|
||||
/// Date format for today
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Today at'**
|
||||
String get today_at;
|
||||
|
||||
/// Undo button text
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Undo'**
|
||||
String get undo;
|
||||
|
||||
/// Error message for unknown exceptions
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unknown Exception (see console)'**
|
||||
String get unknown_exception;
|
||||
|
||||
/// Winner label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Winner'**
|
||||
String get winner;
|
||||
|
||||
/// Label for winrate statistic
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Winrate'**
|
||||
String get winrate;
|
||||
|
||||
/// Label for wins statistic
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Wins'**
|
||||
String get wins;
|
||||
|
||||
/// Date format for yesterday
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Yesterday at'**
|
||||
String get yesterday_at;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
<String>['de', 'en'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'de':
|
||||
return AppLocalizationsDe();
|
||||
case 'en':
|
||||
return AppLocalizationsEn();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
269
lib/l10n/generated/app_localizations_de.dart
Normal file
@@ -0,0 +1,269 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for German (`de`).
|
||||
class AppLocalizationsDe extends AppLocalizations {
|
||||
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||
|
||||
@override
|
||||
String get all_players => 'Alle Spieler:innen';
|
||||
|
||||
@override
|
||||
String get all_players_selected => 'Alle Spieler:innen ausgewählt';
|
||||
|
||||
@override
|
||||
String get amount_of_matches => 'Anzahl der Spiele';
|
||||
|
||||
@override
|
||||
String get app_name => 'Game Tracker';
|
||||
|
||||
@override
|
||||
String get cancel => 'Abbrechen';
|
||||
|
||||
@override
|
||||
String get choose_game => 'Spielvorlage wählen';
|
||||
|
||||
@override
|
||||
String get choose_group => 'Gruppe wählen';
|
||||
|
||||
@override
|
||||
String get choose_ruleset => 'Regelwerk wählen';
|
||||
|
||||
@override
|
||||
String could_not_add_player(Object playerName) {
|
||||
return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
|
||||
}
|
||||
|
||||
@override
|
||||
String get create_group => 'Gruppe erstellen';
|
||||
|
||||
@override
|
||||
String get create_match => 'Spiel erstellen';
|
||||
|
||||
@override
|
||||
String get create_new_group => 'Neue Gruppe erstellen';
|
||||
|
||||
@override
|
||||
String get create_new_match => 'Neues Spiel erstellen';
|
||||
|
||||
@override
|
||||
String get data_successfully_deleted => 'Daten erfolgreich gelöscht';
|
||||
|
||||
@override
|
||||
String get data_successfully_exported => 'Daten erfolgreich exportiert';
|
||||
|
||||
@override
|
||||
String get data_successfully_imported => 'Daten erfolgreich importiert';
|
||||
|
||||
@override
|
||||
String days_ago(int count) {
|
||||
return 'vor $count Tagen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get delete => 'Löschen';
|
||||
|
||||
@override
|
||||
String get delete_all_data => 'Alle Daten löschen?';
|
||||
|
||||
@override
|
||||
String get error_creating_group =>
|
||||
'Fehler beim Erstellen der Gruppe, bitte erneut versuchen';
|
||||
|
||||
@override
|
||||
String get error_reading_file => 'Fehler beim Lesen der Datei';
|
||||
|
||||
@override
|
||||
String get export_canceled => 'Export abgebrochen';
|
||||
|
||||
@override
|
||||
String get export_data => 'Daten exportieren';
|
||||
|
||||
@override
|
||||
String get format_exception => 'Formatfehler (siehe Konsole)';
|
||||
|
||||
@override
|
||||
String get game => 'Spielvorlage';
|
||||
|
||||
@override
|
||||
String get game_name => 'Spielvorlagenname';
|
||||
|
||||
@override
|
||||
String get group => 'Gruppe';
|
||||
|
||||
@override
|
||||
String get group_name => 'Gruppenname';
|
||||
|
||||
@override
|
||||
String get groups => 'Gruppen';
|
||||
|
||||
@override
|
||||
String get home => 'Startseite';
|
||||
|
||||
@override
|
||||
String get import_canceled => 'Import abgebrochen';
|
||||
|
||||
@override
|
||||
String get import_data => 'Daten importieren';
|
||||
|
||||
@override
|
||||
String get info => 'Info';
|
||||
|
||||
@override
|
||||
String get invalid_schema => 'Ungültiges Schema';
|
||||
|
||||
@override
|
||||
String get least_points => 'Niedrigste Punkte';
|
||||
|
||||
@override
|
||||
String get match_in_progress => 'Spiel läuft...';
|
||||
|
||||
@override
|
||||
String get match_name => 'Spieltitel';
|
||||
|
||||
@override
|
||||
String get matches => 'Spiele';
|
||||
|
||||
@override
|
||||
String get menu => 'Menü';
|
||||
|
||||
@override
|
||||
String get most_points => 'Höchste Punkte';
|
||||
|
||||
@override
|
||||
String get no_data_available => 'Keine Daten verfügbar';
|
||||
|
||||
@override
|
||||
String get no_groups_created_yet => 'Noch keine Gruppen erstellt';
|
||||
|
||||
@override
|
||||
String get no_matches_created_yet => 'Noch keine Spiele erstellt';
|
||||
|
||||
@override
|
||||
String get no_players_created_yet => 'Noch keine Spieler:in erstellt';
|
||||
|
||||
@override
|
||||
String get no_players_found_with_that_name =>
|
||||
'Keine Spieler:in mit diesem Namen gefunden';
|
||||
|
||||
@override
|
||||
String get no_players_selected => 'Keine Spieler:innen ausgewählt';
|
||||
|
||||
@override
|
||||
String get no_recent_matches_available => 'Keine letzten Spiele verfügbar';
|
||||
|
||||
@override
|
||||
String get no_second_match_available => 'Kein zweites Spiel verfügbar';
|
||||
|
||||
@override
|
||||
String get no_statistics_available => 'Keine Statistiken verfügbar';
|
||||
|
||||
@override
|
||||
String get none => 'Kein';
|
||||
|
||||
@override
|
||||
String get none_group => 'Keine';
|
||||
|
||||
@override
|
||||
String get not_available => 'Nicht verfügbar';
|
||||
|
||||
@override
|
||||
String get player_name => 'Spieler:innenname';
|
||||
|
||||
@override
|
||||
String get players => 'Spieler:innen';
|
||||
|
||||
@override
|
||||
String players_count(int count) {
|
||||
return '$count Spieler';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quick_create => 'Schnellzugriff';
|
||||
|
||||
@override
|
||||
String get recent_matches => 'Letzte Spiele';
|
||||
|
||||
@override
|
||||
String get ruleset => 'Regelwerk';
|
||||
|
||||
@override
|
||||
String get ruleset_least_points =>
|
||||
'Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.';
|
||||
|
||||
@override
|
||||
String get ruleset_most_points =>
|
||||
'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.';
|
||||
|
||||
@override
|
||||
String get ruleset_single_loser =>
|
||||
'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.';
|
||||
|
||||
@override
|
||||
String get ruleset_single_winner =>
|
||||
'Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.';
|
||||
|
||||
@override
|
||||
String get search_for_groups => 'Nach Gruppen suchen';
|
||||
|
||||
@override
|
||||
String get search_for_players => 'Nach Spieler:innen suchen';
|
||||
|
||||
@override
|
||||
String get select_winner => 'Gewinner:in wählen:';
|
||||
|
||||
@override
|
||||
String get selected_players => 'Ausgewählte Spieler:innen';
|
||||
|
||||
@override
|
||||
String get settings => 'Einstellungen';
|
||||
|
||||
@override
|
||||
String get single_loser => 'Ein:e Verlierer:in';
|
||||
|
||||
@override
|
||||
String get single_winner => 'Ein:e Gewinner:in';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistiken';
|
||||
|
||||
@override
|
||||
String get stats => 'Statistiken';
|
||||
|
||||
@override
|
||||
String successfully_added_player(String playerName) {
|
||||
return 'Spieler:in $playerName erfolgreich hinzugefügt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get there_is_no_group_matching_your_search =>
|
||||
'Es gibt keine Gruppe, die deiner Suche entspricht';
|
||||
|
||||
@override
|
||||
String get this_cannot_be_undone =>
|
||||
'Dies kann nicht rückgängig gemacht werden';
|
||||
|
||||
@override
|
||||
String get today_at => 'Heute um';
|
||||
|
||||
@override
|
||||
String get undo => 'Rückgängig';
|
||||
|
||||
@override
|
||||
String get unknown_exception => 'Unbekannter Fehler (siehe Konsole)';
|
||||
|
||||
@override
|
||||
String get winner => 'Gewinner:in';
|
||||
|
||||
@override
|
||||
String get winrate => 'Siegquote';
|
||||
|
||||
@override
|
||||
String get wins => 'Siege';
|
||||
|
||||
@override
|
||||
String get yesterday_at => 'Gestern um';
|
||||
}
|
||||
268
lib/l10n/generated/app_localizations_en.dart
Normal file
@@ -0,0 +1,268 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get all_players => 'All players';
|
||||
|
||||
@override
|
||||
String get all_players_selected => 'All players selected';
|
||||
|
||||
@override
|
||||
String get amount_of_matches => 'Amount of Matches';
|
||||
|
||||
@override
|
||||
String get app_name => 'Game Tracker';
|
||||
|
||||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get choose_game => 'Choose Game';
|
||||
|
||||
@override
|
||||
String get choose_group => 'Choose Group';
|
||||
|
||||
@override
|
||||
String get choose_ruleset => 'Choose Ruleset';
|
||||
|
||||
@override
|
||||
String could_not_add_player(Object playerName) {
|
||||
return 'Could not add player';
|
||||
}
|
||||
|
||||
@override
|
||||
String get create_group => 'Create Group';
|
||||
|
||||
@override
|
||||
String get create_match => 'Create match';
|
||||
|
||||
@override
|
||||
String get create_new_group => 'Create new group';
|
||||
|
||||
@override
|
||||
String get create_new_match => 'Create new match';
|
||||
|
||||
@override
|
||||
String get data_successfully_deleted => 'Data successfully deleted';
|
||||
|
||||
@override
|
||||
String get data_successfully_exported => 'Data successfully exported';
|
||||
|
||||
@override
|
||||
String get data_successfully_imported => 'Data successfully imported';
|
||||
|
||||
@override
|
||||
String days_ago(int count) {
|
||||
return '$count days ago';
|
||||
}
|
||||
|
||||
@override
|
||||
String get delete => 'Delete';
|
||||
|
||||
@override
|
||||
String get delete_all_data => 'Delete all data?';
|
||||
|
||||
@override
|
||||
String get error_creating_group =>
|
||||
'Error while creating group, please try again';
|
||||
|
||||
@override
|
||||
String get error_reading_file => 'Error reading file';
|
||||
|
||||
@override
|
||||
String get export_canceled => 'Export canceled';
|
||||
|
||||
@override
|
||||
String get export_data => 'Export data';
|
||||
|
||||
@override
|
||||
String get format_exception => 'Format Exception (see console)';
|
||||
|
||||
@override
|
||||
String get game => 'Game';
|
||||
|
||||
@override
|
||||
String get game_name => 'Game Name';
|
||||
|
||||
@override
|
||||
String get group => 'Group';
|
||||
|
||||
@override
|
||||
String get group_name => 'Group name';
|
||||
|
||||
@override
|
||||
String get groups => 'Groups';
|
||||
|
||||
@override
|
||||
String get home => 'Home';
|
||||
|
||||
@override
|
||||
String get import_canceled => 'Import canceled';
|
||||
|
||||
@override
|
||||
String get import_data => 'Import data';
|
||||
|
||||
@override
|
||||
String get info => 'Info';
|
||||
|
||||
@override
|
||||
String get invalid_schema => 'Invalid Schema';
|
||||
|
||||
@override
|
||||
String get least_points => 'Least Points';
|
||||
|
||||
@override
|
||||
String get match_in_progress => 'Match in progress...';
|
||||
|
||||
@override
|
||||
String get match_name => 'Match name';
|
||||
|
||||
@override
|
||||
String get matches => 'Matches';
|
||||
|
||||
@override
|
||||
String get menu => 'Menu';
|
||||
|
||||
@override
|
||||
String get most_points => 'Most Points';
|
||||
|
||||
@override
|
||||
String get no_data_available => 'No data available';
|
||||
|
||||
@override
|
||||
String get no_groups_created_yet => 'No groups created yet';
|
||||
|
||||
@override
|
||||
String get no_matches_created_yet => 'No matches created yet';
|
||||
|
||||
@override
|
||||
String get no_players_created_yet => 'No players created yet';
|
||||
|
||||
@override
|
||||
String get no_players_found_with_that_name =>
|
||||
'No players found with that name';
|
||||
|
||||
@override
|
||||
String get no_players_selected => 'No players selected';
|
||||
|
||||
@override
|
||||
String get no_recent_matches_available => 'No recent matches available';
|
||||
|
||||
@override
|
||||
String get no_second_match_available => 'No second match available';
|
||||
|
||||
@override
|
||||
String get no_statistics_available => 'No statistics available';
|
||||
|
||||
@override
|
||||
String get none => 'None';
|
||||
|
||||
@override
|
||||
String get none_group => 'None';
|
||||
|
||||
@override
|
||||
String get not_available => 'Not available';
|
||||
|
||||
@override
|
||||
String get player_name => 'Player name';
|
||||
|
||||
@override
|
||||
String get players => 'Players';
|
||||
|
||||
@override
|
||||
String players_count(int count) {
|
||||
return '$count Players';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quick_create => 'Quick Create';
|
||||
|
||||
@override
|
||||
String get recent_matches => 'Recent Matches';
|
||||
|
||||
@override
|
||||
String get ruleset => 'Ruleset';
|
||||
|
||||
@override
|
||||
String get ruleset_least_points =>
|
||||
'Inverse scoring: the player with the fewest points wins.';
|
||||
|
||||
@override
|
||||
String get ruleset_most_points =>
|
||||
'Traditional ruleset: the player with the most points wins.';
|
||||
|
||||
@override
|
||||
String get ruleset_single_loser =>
|
||||
'Exactly one loser is determined; last place receives the penalty or consequence.';
|
||||
|
||||
@override
|
||||
String get ruleset_single_winner =>
|
||||
'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.';
|
||||
|
||||
@override
|
||||
String get search_for_groups => 'Search for groups';
|
||||
|
||||
@override
|
||||
String get search_for_players => 'Search for players';
|
||||
|
||||
@override
|
||||
String get select_winner => 'Select Winner:';
|
||||
|
||||
@override
|
||||
String get selected_players => 'Selected players';
|
||||
|
||||
@override
|
||||
String get settings => 'Settings';
|
||||
|
||||
@override
|
||||
String get single_loser => 'Single Loser';
|
||||
|
||||
@override
|
||||
String get single_winner => 'Single Winner';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistics';
|
||||
|
||||
@override
|
||||
String get stats => 'Stats';
|
||||
|
||||
@override
|
||||
String successfully_added_player(String playerName) {
|
||||
return 'Successfully added player $playerName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get there_is_no_group_matching_your_search =>
|
||||
'There is no group matching your search';
|
||||
|
||||
@override
|
||||
String get this_cannot_be_undone => 'This can\'t be undone';
|
||||
|
||||
@override
|
||||
String get today_at => 'Today at';
|
||||
|
||||
@override
|
||||
String get undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get unknown_exception => 'Unknown Exception (see console)';
|
||||
|
||||
@override
|
||||
String get winner => 'Winner';
|
||||
|
||||
@override
|
||||
String get winrate => 'Winrate';
|
||||
|
||||
@override
|
||||
String get wins => 'Wins';
|
||||
|
||||
@override
|
||||
String get yesterday_at => 'Yesterday at';
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/l10n/generated/app_localizations.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/custom_navigation_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -20,22 +21,30 @@ class GameTracker extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localeResolutionCallback: (locale, supportedLocales) {
|
||||
for (final supportedLocale in supportedLocales) {
|
||||
if (supportedLocale.languageCode == locale?.languageCode) {
|
||||
return supportedLocale;
|
||||
}
|
||||
}
|
||||
return supportedLocales.firstWhere(
|
||||
(locale) => locale.languageCode == 'en',
|
||||
);
|
||||
},
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Game Tracker',
|
||||
darkTheme: ThemeData.dark(),
|
||||
|
||||
onGenerateTitle: (context) => AppLocalizations.of(context).app_name,
|
||||
themeMode: ThemeMode.dark, // forces dark mode
|
||||
theme: ThemeData(
|
||||
primaryColor: CustomTheme.primaryColor,
|
||||
scaffoldBackgroundColor: CustomTheme.backgroundColor,
|
||||
appBarTheme: CustomTheme.appBarTheme,
|
||||
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: CustomTheme.primaryColor,
|
||||
brightness: Brightness.dark,
|
||||
).copyWith(surface: CustomTheme.backgroundColor),
|
||||
),
|
||||
|
||||
home: const CustomNavigationBar(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
import 'package:flutter/material.dart' hide ButtonStyle;
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/core/enums.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
||||
import 'package:game_tracker/presentation/widgets/custom_search_bar.dart';
|
||||
import 'package:game_tracker/presentation/widgets/text_input_field.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/text_icon_list_tile.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class CreateGroupView extends StatefulWidget {
|
||||
const CreateGroupView({super.key});
|
||||
|
||||
@override
|
||||
State<CreateGroupView> createState() => _CreateGroupViewState();
|
||||
}
|
||||
|
||||
class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
List<Player> selectedPlayers = [];
|
||||
List<Player> suggestedPlayers = [];
|
||||
List<Player> allPlayers = [];
|
||||
late final AppDatabase db;
|
||||
late Future<List<Player>> _allPlayersFuture;
|
||||
late final List<Player> skeletonData = List.filled(
|
||||
7,
|
||||
Player(name: 'Player 0'),
|
||||
);
|
||||
final _groupNameController = TextEditingController();
|
||||
final _searchBarController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
_allPlayersFuture = db.playerDao.getAllPlayers();
|
||||
_allPlayersFuture.then((loadedPlayers) {
|
||||
setState(() {
|
||||
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||
allPlayers = [...loadedPlayers];
|
||||
suggestedPlayers = [...loadedPlayers];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
scrolledUnderElevation: 0,
|
||||
title: const Text(
|
||||
'Create new group',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: TextInputField(
|
||||
controller: _groupNameController,
|
||||
hintText: 'Group name',
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.boxColor,
|
||||
border: Border.all(color: CustomTheme.boxBorder),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomSearchBar(
|
||||
controller: _searchBarController,
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 45,
|
||||
minHeight: 45,
|
||||
),
|
||||
hintText: 'Search for players',
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value.isEmpty) {
|
||||
suggestedPlayers = allPlayers.where((player) {
|
||||
return !selectedPlayers.contains(player);
|
||||
}).toList();
|
||||
} else {
|
||||
suggestedPlayers = allPlayers.where((player) {
|
||||
final bool nameMatches = player.name
|
||||
.toLowerCase()
|
||||
.contains(value.toLowerCase());
|
||||
final bool isNotSelected = !selectedPlayers
|
||||
.contains(player);
|
||||
return nameMatches && isNotSelected;
|
||||
}).toList();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Ausgewählte Spieler: (${selectedPlayers.length})',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: <Widget>[
|
||||
for (var player in selectedPlayers)
|
||||
TextIconTile(
|
||||
text: player.name,
|
||||
onIconTap: () {
|
||||
setState(() {
|
||||
final currentSearch = _searchBarController.text
|
||||
.toLowerCase();
|
||||
selectedPlayers.remove(player);
|
||||
if (currentSearch.isEmpty ||
|
||||
player.name.toLowerCase().contains(
|
||||
currentSearch,
|
||||
)) {
|
||||
suggestedPlayers.add(player);
|
||||
suggestedPlayers.sort(
|
||||
(a, b) => a.name.compareTo(b.name),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Alle Spieler:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
FutureBuilder(
|
||||
future: _allPlayersFuture,
|
||||
builder:
|
||||
(
|
||||
BuildContext context,
|
||||
AsyncSnapshot<List<Player>> snapshot,
|
||||
) {
|
||||
if (snapshot.hasError) {
|
||||
return const Center(
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.report,
|
||||
title: 'Error',
|
||||
message: 'Player data couldn\'t\nbe loaded.',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.done &&
|
||||
(!snapshot.hasData ||
|
||||
snapshot.data!.isEmpty ||
|
||||
(selectedPlayers.isEmpty &&
|
||||
allPlayers.isEmpty))) {
|
||||
return const Center(
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: 'Info',
|
||||
message: 'No players created yet.',
|
||||
),
|
||||
);
|
||||
}
|
||||
final bool isLoading =
|
||||
snapshot.connectionState ==
|
||||
ConnectionState.waiting;
|
||||
return Expanded(
|
||||
child: Skeletonizer(
|
||||
effect: PulseEffect(
|
||||
from: Colors.grey[800]!,
|
||||
to: Colors.grey[600]!,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
),
|
||||
enabled: isLoading,
|
||||
enableSwitchAnimation: true,
|
||||
switchAnimationConfig:
|
||||
const SwitchAnimationConfig(
|
||||
duration: Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.linear,
|
||||
switchOutCurve: Curves.linear,
|
||||
transitionBuilder: AnimatedSwitcher
|
||||
.defaultTransitionBuilder,
|
||||
layoutBuilder:
|
||||
AnimatedSwitcher.defaultLayoutBuilder,
|
||||
),
|
||||
child:
|
||||
(suggestedPlayers.isEmpty &&
|
||||
allPlayers.isNotEmpty)
|
||||
? TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: 'Info',
|
||||
message:
|
||||
(selectedPlayers.length ==
|
||||
allPlayers.length)
|
||||
? 'No more players to add.'
|
||||
: 'No players found with that name.',
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: suggestedPlayers.length,
|
||||
itemBuilder:
|
||||
(BuildContext context, int index) {
|
||||
return TextIconListTile(
|
||||
text: suggestedPlayers[index]
|
||||
.name,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (!selectedPlayers.contains(
|
||||
suggestedPlayers[index],
|
||||
)) {
|
||||
selectedPlayers.add(
|
||||
suggestedPlayers[index],
|
||||
);
|
||||
selectedPlayers.sort(
|
||||
(a, b) => a.name
|
||||
.compareTo(b.name),
|
||||
);
|
||||
suggestedPlayers.remove(
|
||||
suggestedPlayers[index],
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomWidthButton(
|
||||
text: 'Create group',
|
||||
sizeRelativeToWidth: 0.95,
|
||||
buttonType: ButtonType.primary,
|
||||
onPressed:
|
||||
(_groupNameController.text.isEmpty || selectedPlayers.isEmpty)
|
||||
? null
|
||||
: () async {
|
||||
bool success = await db.groupDao.addGroup(
|
||||
group: Group(
|
||||
name: _groupNameController.text,
|
||||
members: selectedPlayers,
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
_groupNameController.clear();
|
||||
_searchBarController.clear();
|
||||
selectedPlayers.clear();
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: CustomTheme.boxColor,
|
||||
content: const Center(
|
||||
child: Text(
|
||||
'Error while creating group, please try again',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/game_history_view.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/groups_view.dart';
|
||||
import 'package:game_tracker/l10n/generated/app_localizations.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/group_view/groups_view.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/home_view.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/match_view/match_view.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/settings_view.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/statistics_view.dart';
|
||||
import 'package:game_tracker/presentation/widgets/navbar_item.dart';
|
||||
@@ -16,36 +17,51 @@ class CustomNavigationBar extends StatefulWidget {
|
||||
|
||||
class _CustomNavigationBarState extends State<CustomNavigationBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
/// Currently selected tab index
|
||||
int currentIndex = 0;
|
||||
final List<Widget> tabs = [
|
||||
const HomeView(),
|
||||
const GameHistoryView(),
|
||||
const GroupsView(),
|
||||
const StatisticsView(),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
/// Key count to force rebuild of tab views
|
||||
int tabKeyCount = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
// Pretty ugly but works
|
||||
final List<Widget> tabs = [
|
||||
KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()),
|
||||
KeyedSubtree(
|
||||
key: ValueKey('matches_$tabKeyCount'),
|
||||
child: const MatchView(),
|
||||
),
|
||||
KeyedSubtree(
|
||||
key: ValueKey('groups_$tabKeyCount'),
|
||||
child: const GroupsView(),
|
||||
),
|
||||
KeyedSubtree(
|
||||
key: ValueKey('stats_$tabKeyCount'),
|
||||
child: const StatisticsView(),
|
||||
),
|
||||
];
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
_currentTabTitle(),
|
||||
_currentTabTitle(context),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
scrolledUnderElevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const SettingsView()),
|
||||
),
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const SettingsView()),
|
||||
);
|
||||
setState(() {
|
||||
tabKeyCount++;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.settings),
|
||||
),
|
||||
],
|
||||
@@ -54,12 +70,14 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
body: tabs[currentIndex],
|
||||
extendBody: true,
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 8.0),
|
||||
child: Material(
|
||||
elevation: 10,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: CustomTheme.primaryColor,
|
||||
bottomNavigationBar: SafeArea(
|
||||
minimum: const EdgeInsets.only(bottom: 30),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: SizedBox(
|
||||
@@ -71,28 +89,28 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
|
||||
index: 0,
|
||||
isSelected: currentIndex == 0,
|
||||
icon: Icons.home_rounded,
|
||||
label: 'Home',
|
||||
label: loc.home,
|
||||
onTabTapped: onTabTapped,
|
||||
),
|
||||
NavbarItem(
|
||||
index: 1,
|
||||
isSelected: currentIndex == 1,
|
||||
icon: Icons.gamepad_rounded,
|
||||
label: 'Games',
|
||||
label: loc.matches,
|
||||
onTabTapped: onTabTapped,
|
||||
),
|
||||
NavbarItem(
|
||||
index: 2,
|
||||
isSelected: currentIndex == 2,
|
||||
icon: Icons.group_rounded,
|
||||
label: 'Groups',
|
||||
label: loc.groups,
|
||||
onTabTapped: onTabTapped,
|
||||
),
|
||||
NavbarItem(
|
||||
index: 3,
|
||||
isSelected: currentIndex == 3,
|
||||
icon: Icons.bar_chart_rounded,
|
||||
label: 'Stats',
|
||||
label: loc.statistics,
|
||||
onTabTapped: onTabTapped,
|
||||
),
|
||||
],
|
||||
@@ -104,22 +122,25 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles tab tap events. Updates the current [index] state.
|
||||
void onTabTapped(int index) {
|
||||
setState(() {
|
||||
currentIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
String _currentTabTitle() {
|
||||
/// Returns the title of the current tab based on [currentIndex].
|
||||
String _currentTabTitle(context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
switch (currentIndex) {
|
||||
case 0:
|
||||
return 'Home';
|
||||
return loc.home;
|
||||
case 1:
|
||||
return 'Game History';
|
||||
return loc.matches;
|
||||
case 2:
|
||||
return 'Groups';
|
||||
return loc.groups;
|
||||
case 3:
|
||||
return 'Statistics';
|
||||
return loc.statistics;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/double_row_info_tile.dart';
|
||||
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||
|
||||
class GameHistoryView extends StatefulWidget {
|
||||
const GameHistoryView({super.key});
|
||||
|
||||
@override
|
||||
State<GameHistoryView> createState() => _GameHistoryViewState();
|
||||
}
|
||||
|
||||
class _GameHistoryViewState extends State<GameHistoryView> {
|
||||
final allGameData = [
|
||||
{
|
||||
'game': 'Schach',
|
||||
'title': 'Abendpartie',
|
||||
'players': 2,
|
||||
'group': 'Familie',
|
||||
'date': '01.06.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Monopoly',
|
||||
'title': 'Wochenendspaß mit Gras du Saas',
|
||||
'players': 4,
|
||||
'group': 'Freunde',
|
||||
'date': '28.05.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Catan',
|
||||
'title': 'Strategieabend',
|
||||
'players': 3,
|
||||
'group': 'Brettspieler',
|
||||
'date': '25.05.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Uno',
|
||||
'title': 'Schnelle Runde',
|
||||
'players': 5,
|
||||
'group': 'Kollegen',
|
||||
'date': '22.05.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Poker',
|
||||
'title': 'Freitagspoker',
|
||||
'players': 6,
|
||||
'group': 'Pokerclub',
|
||||
'date': '20.05.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Scrabble',
|
||||
'title': 'Wortschlacht',
|
||||
'players': 4,
|
||||
'group': 'Familie',
|
||||
'date': '18.05.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Risiko',
|
||||
'title': 'Weltherrschaft',
|
||||
'players': 5,
|
||||
'group': 'Strategiegruppe',
|
||||
'date': '15.05.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Zug um Zug',
|
||||
'title': 'Zug-Abenteuer',
|
||||
'players': 4,
|
||||
'group': 'Reisende',
|
||||
'date': '12.05.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Carcassonne',
|
||||
'title': 'Plättchenlegen',
|
||||
'players': 3,
|
||||
'group': 'Brettspieler',
|
||||
'date': '10.05.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Pandemie',
|
||||
'title': 'Welt retten',
|
||||
'players': 4,
|
||||
'group': 'Koop-Team',
|
||||
'date': '08.05.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Cluedo',
|
||||
'title': 'Krimiabend',
|
||||
'players': 6,
|
||||
'group': 'Detektive',
|
||||
'date': '05.05.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Dixit',
|
||||
'title': 'Fantasiespiel',
|
||||
'players': 5,
|
||||
'group': 'Künstler',
|
||||
'date': '02.05.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Azul',
|
||||
'title': 'Plättchenmeister',
|
||||
'players': 4,
|
||||
'group': 'Familie',
|
||||
'date': '30.04.2024',
|
||||
},
|
||||
{
|
||||
'game': 'Splendor',
|
||||
'title': 'Edelsteinhändler',
|
||||
'players': 3,
|
||||
'group': 'Freunde',
|
||||
'date': '28.04.2024',
|
||||
},
|
||||
{
|
||||
'game': '7 Wonders',
|
||||
'title': 'Antike Reiche',
|
||||
'players': 7,
|
||||
'group': 'Geschichtsfreunde',
|
||||
'date': '25.04.2024',
|
||||
},
|
||||
];
|
||||
late List<Map<String, dynamic>> suggestedGameData;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
suggestedGameData = List.from(allGameData);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(margin: const EdgeInsets.only(bottom: 75)),
|
||||
Expanded(
|
||||
child: gameHistoryListView(allGameData, suggestedGameData),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 10, bottom: 10, left: 10, right: 10),
|
||||
child: SearchBar(
|
||||
leading: const Icon(Icons.search),
|
||||
onChanged: (value) {
|
||||
if (value.isEmpty) {
|
||||
setState(() {
|
||||
suggestedGameData.clear();
|
||||
suggestedGameData.addAll(allGameData);
|
||||
});
|
||||
return;
|
||||
}
|
||||
final suggestions = allGameData.where((currentGame) {
|
||||
return currentGame['game'].toString().toLowerCase().contains(
|
||||
value.toLowerCase(),
|
||||
) ||
|
||||
currentGame['title'].toString().toLowerCase().contains(
|
||||
value.toLowerCase(),
|
||||
) ||
|
||||
currentGame['group'].toString().toLowerCase().contains(
|
||||
value.toLowerCase(),
|
||||
);
|
||||
});
|
||||
setState(() {
|
||||
suggestedGameData.clear();
|
||||
suggestedGameData.addAll(suggestions);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget gameHistoryListView(allGameData, suggestedGameData) {
|
||||
if (suggestedGameData.isEmpty && allGameData.isEmpty) {
|
||||
return const TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: 'Info',
|
||||
message: 'Keine Spiele erstellt',
|
||||
);
|
||||
} else if (suggestedGameData.isEmpty) {
|
||||
return const TopCenteredMessage(
|
||||
icon: Icons.search,
|
||||
title: 'Info',
|
||||
message: 'Kein Spiel mit den Suchparametern gefunden.',
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: suggestedGameData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final currentGame = suggestedGameData[index];
|
||||
return doubleRowInfoTile(
|
||||
currentGame['game'] + ': ',
|
||||
currentGame['title'],
|
||||
"${currentGame['players']} Spieler",
|
||||
currentGame['group'],
|
||||
currentGame['date'],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/core/enums.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
import 'package:game_tracker/l10n/generated/app_localizations.dart';
|
||||
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
||||
import 'package:game_tracker/presentation/widgets/player_selection.dart';
|
||||
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CreateGroupView extends StatefulWidget {
|
||||
const CreateGroupView({super.key});
|
||||
|
||||
@override
|
||||
State<CreateGroupView> createState() => _CreateGroupViewState();
|
||||
}
|
||||
|
||||
class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
late final AppDatabase db;
|
||||
|
||||
/// Controller for the group name input field
|
||||
final _groupNameController = TextEditingController();
|
||||
|
||||
/// List of currently selected players
|
||||
List<Player> selectedPlayers = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
_groupNameController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_groupNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(title: Text(loc.create_new_group)),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: CustomTheme.standardMargin,
|
||||
child: TextInputField(
|
||||
controller: _groupNameController,
|
||||
hintText: loc.group_name,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PlayerSelection(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedPlayers = [...value];
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CustomWidthButton(
|
||||
text: loc.create_group,
|
||||
sizeRelativeToWidth: 0.95,
|
||||
buttonType: ButtonType.primary,
|
||||
onPressed:
|
||||
(_groupNameController.text.isEmpty ||
|
||||
(selectedPlayers.length < 2))
|
||||
? null
|
||||
: () async {
|
||||
bool success = await db.groupDao.addGroup(
|
||||
group: Group(
|
||||
name: _groupNameController.text.trim(),
|
||||
members: selectedPlayers,
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: CustomTheme.boxColor,
|
||||
content: Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
).error_creating_group,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
122
lib/presentation/views/main_menu/group_view/groups_view.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/constants.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
import 'package:game_tracker/l10n/generated/app_localizations.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart';
|
||||
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
|
||||
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class GroupsView extends StatefulWidget {
|
||||
const GroupsView({super.key});
|
||||
|
||||
@override
|
||||
State<GroupsView> createState() => _GroupsViewState();
|
||||
}
|
||||
|
||||
class _GroupsViewState extends State<GroupsView> {
|
||||
late final AppDatabase db;
|
||||
|
||||
/// Loaded groups from the database
|
||||
late List<Group> loadedGroups;
|
||||
|
||||
/// Loading state
|
||||
bool isLoading = true;
|
||||
|
||||
List<Group> groups = List.filled(
|
||||
7,
|
||||
Group(
|
||||
name: 'Skeleton Group',
|
||||
members: List.filled(6, Player(name: 'Skeleton Player')),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
body: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AppSkeleton(
|
||||
enabled: isLoading,
|
||||
child: Visibility(
|
||||
visible: groups.isNotEmpty,
|
||||
replacement: Center(
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: loc.info,
|
||||
message: loc.no_groups_created_yet,
|
||||
),
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 85),
|
||||
itemCount: groups.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == groups.length) {
|
||||
return SizedBox(
|
||||
height: MediaQuery.paddingOf(context).bottom - 20,
|
||||
);
|
||||
}
|
||||
return GroupTile(group: groups[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
child: CustomWidthButton(
|
||||
text: loc.create_group,
|
||||
sizeRelativeToWidth: 0.90,
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const CreateGroupView();
|
||||
},
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
loadGroups();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void loadGroups() {
|
||||
Future.wait([
|
||||
db.groupDao.getAllGroups(),
|
||||
Future.delayed(Constants.minimumSkeletonDuration),
|
||||
]).then((results) {
|
||||
loadedGroups = results[0] as List<Group>;
|
||||
setState(() {
|
||||
groups = loadedGroups
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||