Compare commits
410 Commits
a922f24150
...
enhancemen
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f79495775 | |||
| 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 | ||
|
|
fb28de5772 | ||
|
|
f713bd6fb7 | ||
| 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 | |||
| e14984d4a9 | |||
| 682c1d269d | |||
| d264b3fa1b | |||
| b684ebd4f6 | |||
| 2cae66a8ae | |||
| 0967abe10f | |||
| 061a927324 | |||
| 412cfff9f5 | |||
| f7073a83a4 | |||
| 9434282ed1 | |||
| d145b18891 | |||
| 3e3def0bde | |||
| 3408930524 | |||
| 87b1a7d57f | |||
| cf834e636e | |||
| 8e2befaf3d | |||
| 186d8cf5c9 | |||
| 6f42b36587 | |||
| 248d652e06 | |||
| e71e65b197 | |||
| 3f79a7b898 | |||
| 1232cb8f0d | |||
| 018332d8e6 | |||
| 201fd70685 | |||
| 9365313c92 | |||
| 54e1756e79 | |||
| 3b6a914022 | |||
| 98b02adc85 | |||
| b82261317c | |||
| c76e193b4d | |||
| 0ac8c21052 | |||
| 74fffa95e2 | |||
| ca4bf03bab | |||
| aade42c0a6 | |||
| 0659d202b3 | |||
| 81cdeb7ed6 | |||
| 89d7bb54a1 | |||
| 5d8047b3ba | |||
| dd8af42a47 | |||
| 346dddcf62 | |||
| 9f4fc3a3b0 | |||
| 594ea947c2 | |||
| e5268ebc12 | |||
| 1732878c7f | |||
| f136400c7e | |||
| c89243f886 | |||
| a3b45053e7 | |||
| 91b68eac3e | |||
| 6ae1ce9bc7 | |||
| 7ac5986588 | |||
| 601b7d0a4f | |||
| e852a4d539 | |||
| e108bb41f6 | |||
| 17c14dd230 | |||
| 7123d36cd8 | |||
| 7cc72015d3 | |||
| 10e56a7241 | |||
| 6638c2deee | |||
| 974f06b6b8 | |||
| 63d2117a6a | |||
| 6ae39717fd | |||
| 003835472d | |||
| 8d91eb3780 | |||
| ddc8d93592 | |||
| eeec92181a | |||
| a8962e68b6 | |||
| 322c51a764 | |||
|
|
69e13e877e | ||
|
|
f388c442d2 | ||
| 82e28b7509 | |||
| 8150b42dba | |||
| 75c6f4e01c | |||
| 19c99eef9c | |||
| f2a749cb0f | |||
| 5dcd0826bd | |||
| f6ebda7984 | |||
| 69c95ca672 | |||
| 42ce69f4d3 | |||
| 08fcaa35ee | |||
| 2ee8edcf9b | |||
| fd86f5193f | |||
| 8cc898cad6 | |||
| 67c8a7e181 | |||
| 2da2e28cb6 | |||
| d86de09042 | |||
| 07d623d963 | |||
| a8a81c2151 | |||
| d341634885 | |||
| d3a63bd299 | |||
| e0c8398873 | |||
| d65dd3d983 | |||
| 51a8c4ea58 | |||
| c67f688a77 | |||
| 1d9945c525 | |||
| 0202d09812 | |||
| 73d8e7522c | |||
| 80290efa0b | |||
| f4ed122220 | |||
| 2f260d7cbc | |||
| 7781284289 | |||
| c0ff2bf677 | |||
| 8f9289617f | |||
| 1882d0007b | |||
| 282841ecf1 | |||
| 05c41707ca | |||
| a5e508dbda | |||
| 412d1fd334 | |||
| c31d757615 | |||
| 178aaa9643 | |||
| 2076e45fd5 | |||
| 62acc87e0e | |||
| 3ca081419b | |||
| 3e89bfd641 | |||
| c3a2ac77b0 | |||
| 6b2fb18ec0 | |||
| 7a85b5c1ac | |||
| f8b6c00d5d | |||
| a7f6a53b9c | |||
| 35f2f8754a | |||
| 47bb090e72 | |||
| b2a3d7ce7f | |||
| fde5344244 | |||
| 2fc7eab1ac | |||
| e4de8fdb25 | |||
| a54495f915 | |||
| b5e7fe23ab | |||
| c195c5e2bb | |||
| 729cd5eff1 | |||
| 630836d40c | |||
| f06f333a8e | |||
| 06933a67ac | |||
| a9dd3216cc | |||
| 10a45ac1f0 | |||
| a1d57fc424 | |||
| 9ffb7d6ca3 | |||
| 4befc85c9f | |||
| accc189949 | |||
| c1e032208c | |||
| 258f610e28 | |||
| deab074885 | |||
| 38466c6056 | |||
| 8d8d4319d2 | |||
| d3de0fda49 | |||
| e709edbf7a | |||
| 74402f2e04 | |||
| 5c09dfb47d | |||
| 02735b5b1d | |||
| a6f40456d8 | |||
| d931e85e9f | |||
| a707990356 | |||
| 3bd522df6e | |||
| 2dd4f52336 | |||
| 2dad822d79 | |||
| 640830d8ab | |||
| 168d7748a9 | |||
| b475237b9e | |||
| 2a7d409df0 | |||
| 7c34e51779 | |||
| ac6f3d0f92 | |||
| 511124e323 | |||
| d07bc6e8bb | |||
| 352fdc13f1 | |||
| d36348c59c | |||
| d08c79fc26 | |||
| 36aa4722a3 | |||
| 67bbb15845 | |||
| 1081fb8be7 | |||
| 34bf6740ad | |||
| 7be86b3d9e | |||
| cbd5e1d0ba | |||
| 2ee4d8e6fa | |||
|
|
1536e2b2af | ||
|
|
0f13de30c4 | ||
|
|
3f0adb4c05 | ||
| 2179331455 | |||
| 9229f1f0a5 | |||
| 39b2068121 | |||
| 93a4ccaee0 | |||
| 3f5a840675 | |||
| 67d5f7e891 | |||
| ac99217b72 | |||
| ca40ae668d | |||
| d07943add9 | |||
| b6700bafd9 |
57
.gitea/workflows/pull_request.yaml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: Pull Request Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install jq
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y jq
|
||||||
|
|
||||||
|
- name: Install Flutter (wget)
|
||||||
|
run: |
|
||||||
|
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz
|
||||||
|
tar xf flutter_linux_3.38.2-stable.tar.xz
|
||||||
|
# Set Git safe directory for Flutter path
|
||||||
|
git config --global --add safe.directory "$(pwd)/flutter"
|
||||||
|
# Set Flutter path
|
||||||
|
echo "$(pwd)/flutter/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Analyze Formatting
|
||||||
|
run: flutter analyze lib test
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y jq
|
||||||
|
|
||||||
|
- name: Install Flutter (wget)
|
||||||
|
run: |
|
||||||
|
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz
|
||||||
|
tar xf flutter_linux_3.38.2-stable.tar.xz
|
||||||
|
# Set Git safe directory for Flutter path
|
||||||
|
git config --global --add safe.directory "$(pwd)/flutter"
|
||||||
|
# Set Flutter path
|
||||||
|
echo "$(pwd)/flutter/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: flutter test
|
||||||
50
.gitea/workflows/push.yaml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Push Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "development"
|
||||||
|
- "main"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: false # Needs bot user
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y jq
|
||||||
|
|
||||||
|
- name: Install Flutter (wget)
|
||||||
|
run: |
|
||||||
|
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz
|
||||||
|
tar xf flutter_linux_3.38.2-stable.tar.xz
|
||||||
|
# Set Git safe directory for Flutter path
|
||||||
|
git config --global --add safe.directory "$(pwd)/flutter"
|
||||||
|
# Set Flutter path
|
||||||
|
echo "$(pwd)/flutter/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Get & upgrade dependencies
|
||||||
|
run: |
|
||||||
|
flutter pub get
|
||||||
|
flutter pub upgrade --major-versions
|
||||||
|
|
||||||
|
- name: Auto-format
|
||||||
|
run: |
|
||||||
|
dart format lib
|
||||||
|
dart fix --apply lib
|
||||||
|
|
||||||
|
# Needs credentials, push access and the right files need to be staged
|
||||||
|
- name: Commit Changes
|
||||||
|
run: |
|
||||||
|
git config --global user.name "Gitea Actions"
|
||||||
|
git config --global user.email "actions@gitea.com"
|
||||||
|
git status
|
||||||
|
git add lib/
|
||||||
|
git status
|
||||||
|
git commit -m "Actions: Auto-formatting [skip ci]"
|
||||||
|
git push
|
||||||
14
README.md
@@ -1,7 +1,15 @@
|
|||||||
# Game Tracker
|
# Game Tracker
|
||||||
|
|
||||||

|

|
||||||

|
|
||||||

|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### Versions Supported
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
A all-in-one app to track card- and board games, manage players and groups and get statistics about your played games.
|
A all-in-one app to track card- and board games, manage players and groups and get statistics about your played games.
|
||||||
|
|||||||
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<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 -->
|
<!-- You can insert your own image assets here -->
|
||||||
<!-- <item>
|
<!-- <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">
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
the Flutter engine draws its first frame -->
|
the Flutter engine draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@color/launch_background</item>
|
||||||
</style>
|
</style>
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
This theme determines the color of the Android Window while your
|
This theme determines the color of the Android Window while your
|
||||||
|
|||||||
179
assets/schema.json
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"games": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"members": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"name",
|
||||||
|
"members"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"winner": {
|
||||||
|
"type": ["object","null"]
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"members": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"name",
|
||||||
|
"members"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,122 +1,14 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
"filename" : "icon_x1024.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "universal",
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
"platform" : "ios",
|
||||||
"scale" : "2x"
|
"size" : "1024x1024"
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"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" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
|
"filename" : "icon.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"filename" : "LaunchImage.png",
|
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"filename" : "LaunchImage@2x.png",
|
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"filename" : "LaunchImage@3x.png",
|
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<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">
|
<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>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<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>
|
</dependencies>
|
||||||
<scenes>
|
<scenes>
|
||||||
<!--View Controller-->
|
<!--View Controller-->
|
||||||
@@ -14,24 +17,27 @@
|
|||||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
</layoutGuides>
|
</layoutGuides>
|
||||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
<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"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<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>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" name="LauncherBackgroundColor"/>
|
||||||
<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>
|
|
||||||
</view>
|
</view>
|
||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="53" y="375"/>
|
<point key="canvasLocation" x="80.152671755725194" y="264.08450704225356"/>
|
||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
|
<color key="tintColor" red="0.90196078431372551" green="0.94509803921568625" blue="0.89411764705882346" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<resources>
|
<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>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<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">
|
<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>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<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>
|
</dependencies>
|
||||||
<scenes>
|
<scenes>
|
||||||
<!--Flutter View Controller-->
|
<!--Flutter View Controller-->
|
||||||
@@ -14,13 +16,14 @@
|
|||||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
</layoutGuides>
|
</layoutGuides>
|
||||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
<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"/>
|
<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>
|
</view>
|
||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
|
<point key="canvasLocation" x="141" y="131"/>
|
||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
</document>
|
</document>
|
||||||
|
|||||||
@@ -5,8 +5,22 @@ class CustomTheme {
|
|||||||
static Color secondaryColor = const Color(0xFFAFA2FF);
|
static Color secondaryColor = const Color(0xFFAFA2FF);
|
||||||
static Color backgroundColor = const Color(0xFF0B0B0B);
|
static Color backgroundColor = const Color(0xFF0B0B0B);
|
||||||
static Color boxColor = const Color(0xFF101010);
|
static Color boxColor = const Color(0xFF101010);
|
||||||
|
static Color onBoxColor = const Color(0xFF181818);
|
||||||
static Color boxBorder = const Color(0xFF272727);
|
static Color boxBorder = const Color(0xFF272727);
|
||||||
|
|
||||||
|
static BoxDecoration standardBoxDecoration = BoxDecoration(
|
||||||
|
color: boxColor,
|
||||||
|
border: Border.all(color: boxBorder),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
);
|
||||||
|
|
||||||
|
static BoxDecoration highlightedBoxDecoration = BoxDecoration(
|
||||||
|
color: boxColor,
|
||||||
|
border: Border.all(color: primaryColor),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)],
|
||||||
|
);
|
||||||
|
|
||||||
static AppBarTheme appBarTheme = AppBarTheme(
|
static AppBarTheme appBarTheme = AppBarTheme(
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
|||||||
31
lib/core/enums.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/// Button types used for styling the [CustomWidthButton]
|
||||||
|
enum ButtonType { primary, secondary, tertiary }
|
||||||
|
|
||||||
|
/// Result types for import operations in the [SettingsView]
|
||||||
|
/// - [ImportResult.success]: The import operation was successful.
|
||||||
|
/// - [ImportResult.canceled]: The import operation was canceled by the user.
|
||||||
|
/// - [ImportResult.fileReadError]: There was an error reading the selected file.
|
||||||
|
/// - [ImportResult.invalidSchema]: The JSON schema of the imported data is invalid.
|
||||||
|
/// - [ImportResult.formatException]: A format exception occurred during import.
|
||||||
|
/// - [ImportResult.unknownException]: An exception occurred during import.
|
||||||
|
enum ImportResult {
|
||||||
|
success,
|
||||||
|
canceled,
|
||||||
|
fileReadError,
|
||||||
|
invalidSchema,
|
||||||
|
formatException,
|
||||||
|
unknownException,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result types for export operations in the [SettingsView]
|
||||||
|
/// - [ExportResult.success]: The export operation was successful.
|
||||||
|
/// - [ExportResult.canceled]: The export operation was canceled by the user.
|
||||||
|
/// - [ExportResult.unknownException]: An exception occurred during export.
|
||||||
|
enum ExportResult { success, canceled, unknownException }
|
||||||
|
|
||||||
|
/// Different rulesets available for games
|
||||||
|
/// - [Ruleset.singleWinner]: The game is won by a single player
|
||||||
|
/// - [Ruleset.singleLoser]: The game is lost by a single player
|
||||||
|
/// - [Ruleset.mostPoints]: The player with the most points wins.
|
||||||
|
/// - [Ruleset.lastPoints]: The player with the fewest points wins.
|
||||||
|
enum Ruleset { singleWinner, singleLoser, mostPoints, lastPoints }
|
||||||
@@ -15,21 +15,42 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
|||||||
Future<List<Game>> getAllGames() async {
|
Future<List<Game>> getAllGames() async {
|
||||||
final query = select(gameTable);
|
final query = select(gameTable);
|
||||||
final result = await query.get();
|
final result = await query.get();
|
||||||
return result.map((row) => Game(id: row.id, name: row.name)).toList();
|
|
||||||
|
return Future.wait(
|
||||||
|
result.map((row) async {
|
||||||
|
final group = await db.groupGameDao.getGroupOfGame(gameId: row.id);
|
||||||
|
final players = await db.playerGameDao.getPlayersOfGame(gameId: row.id);
|
||||||
|
final winner = row.winnerId != null
|
||||||
|
? await db.playerDao.getPlayerById(playerId: row.winnerId!)
|
||||||
|
: null;
|
||||||
|
return Game(
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
group: group,
|
||||||
|
players: players,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
winner: winner,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves a [Game] by its [gameId].
|
/// Retrieves a [Game] by its [gameId].
|
||||||
Future<Game> getGameById(String gameId) async {
|
Future<Game> getGameById({required String gameId}) async {
|
||||||
final query = select(gameTable)..where((g) => g.id.equals(gameId));
|
final query = select(gameTable)..where((g) => g.id.equals(gameId));
|
||||||
final result = await query.getSingle();
|
final result = await query.getSingle();
|
||||||
|
|
||||||
List<Player>? players;
|
List<Player>? players;
|
||||||
if (await db.playerGameDao.hasGamePlayers(gameId)) {
|
if (await db.playerGameDao.gameHasPlayers(gameId: gameId)) {
|
||||||
players = await db.playerGameDao.getPlayersByGameId(gameId);
|
players = await db.playerGameDao.getPlayersOfGame(gameId: gameId);
|
||||||
}
|
}
|
||||||
Group? group;
|
Group? group;
|
||||||
if (await db.groupGameDao.hasGameGroup(gameId)) {
|
if (await db.groupGameDao.gameHasGroup(gameId: gameId)) {
|
||||||
group = await db.groupGameDao.getGroupByGameId(gameId);
|
group = await db.groupGameDao.getGroupOfGame(gameId: gameId);
|
||||||
|
}
|
||||||
|
Player? winner;
|
||||||
|
if (result.winnerId != null) {
|
||||||
|
winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Game(
|
return Game(
|
||||||
@@ -37,10 +58,183 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
|||||||
name: result.name,
|
name: result.name,
|
||||||
players: players,
|
players: players,
|
||||||
group: group,
|
group: group,
|
||||||
winner: result.winnerId,
|
winner: winner,
|
||||||
|
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 {
|
||||||
|
await into(gameTable).insert(
|
||||||
|
GameTableCompanion.insert(
|
||||||
|
id: game.id,
|
||||||
|
name: game.name,
|
||||||
|
winnerId: Value(game.winner?.id),
|
||||||
|
createdAt: game.createdAt,
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (game.players != null) {
|
||||||
|
await db.playerDao.addPlayersAsList(players: game.players!);
|
||||||
|
for (final p in game.players ?? []) {
|
||||||
|
await db.playerGameDao.addPlayerToGame(
|
||||||
|
gameId: game.id,
|
||||||
|
playerId: p.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.group != null) {
|
||||||
|
await db.groupDao.addGroup(group: game.group!);
|
||||||
|
await db.groupGameDao.addGroupToGame(
|
||||||
|
gameId: game.id,
|
||||||
|
groupId: game.group!.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds multiple [Game]s to the database in a batch operation.
|
||||||
|
/// Also adds associated players and groups if they exist.
|
||||||
|
/// If the [games] list is empty, the method returns immediately.
|
||||||
|
Future<void> addGamesAsList({required List<Game> games}) async {
|
||||||
|
if (games.isEmpty) return;
|
||||||
|
await db.transaction(() async {
|
||||||
|
// Add all games in batch
|
||||||
|
await db.batch(
|
||||||
|
(b) => b.insertAll(
|
||||||
|
gameTable,
|
||||||
|
games
|
||||||
|
.map(
|
||||||
|
(game) => GameTableCompanion.insert(
|
||||||
|
id: game.id,
|
||||||
|
name: game.name,
|
||||||
|
createdAt: game.createdAt,
|
||||||
|
winnerId: Value(game.winner?.id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add all groups of the games in batch
|
||||||
|
await db.batch(
|
||||||
|
(b) => b.insertAll(
|
||||||
|
db.groupTable,
|
||||||
|
games
|
||||||
|
.where((game) => game.group != null)
|
||||||
|
.map(
|
||||||
|
(game) => GroupTableCompanion.insert(
|
||||||
|
id: game.group!.id,
|
||||||
|
name: game.group!.name,
|
||||||
|
createdAt: game.group!.createdAt,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add all players of the games in batch (unique)
|
||||||
|
final uniquePlayers = <String, Player>{};
|
||||||
|
for (final game in games) {
|
||||||
|
if (game.players != null) {
|
||||||
|
for (final p in game.players!) {
|
||||||
|
uniquePlayers[p.id] = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also include members of groups
|
||||||
|
if (game.group != null) {
|
||||||
|
for (final m in game.group!.members) {
|
||||||
|
uniquePlayers[m.id] = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniquePlayers.isNotEmpty) {
|
||||||
|
await db.batch(
|
||||||
|
(b) => b.insertAll(
|
||||||
|
db.playerTable,
|
||||||
|
uniquePlayers.values
|
||||||
|
.map(
|
||||||
|
(p) => PlayerTableCompanion.insert(
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
createdAt: p.createdAt,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all player-game associations in batch
|
||||||
|
await db.batch((b) {
|
||||||
|
for (final game in games) {
|
||||||
|
if (game.players != null) {
|
||||||
|
for (final p in game.players ?? []) {
|
||||||
|
b.insert(
|
||||||
|
db.playerGameTable,
|
||||||
|
PlayerGameTableCompanion.insert(
|
||||||
|
gameId: game.id,
|
||||||
|
playerId: p.id,
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add all player-group associations in batch
|
||||||
|
await db.batch((b) {
|
||||||
|
for (final game in games) {
|
||||||
|
if (game.group != null) {
|
||||||
|
for (final m in game.group!.members) {
|
||||||
|
b.insert(
|
||||||
|
db.playerGroupTable,
|
||||||
|
PlayerGroupTableCompanion.insert(
|
||||||
|
playerId: m.id,
|
||||||
|
groupId: game.group!.id,
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add all group-game associations in batch
|
||||||
|
await db.batch((b) {
|
||||||
|
for (final game in games) {
|
||||||
|
if (game.group != null) {
|
||||||
|
b.insert(
|
||||||
|
db.groupGameTable,
|
||||||
|
GroupGameTableCompanion.insert(
|
||||||
|
gameId: game.id,
|
||||||
|
groupId: game.group!.id,
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
/// Retrieves the number of games in the database.
|
||||||
Future<int> getGameCount() async {
|
Future<int> getGameCount() async {
|
||||||
final count =
|
final count =
|
||||||
@@ -49,4 +243,78 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
|||||||
.getSingle();
|
.getSingle();
|
||||||
return count ?? 0;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes all games from the database.
|
||||||
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
|
Future<bool> deleteAllGames() async {
|
||||||
|
final query = delete(gameTable);
|
||||||
|
final rowsAffected = await query.go();
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the winner of the game with the given [gameId] to the player with
|
||||||
|
/// the given [winnerId].
|
||||||
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
|
Future<bool> setWinner({
|
||||||
|
required String gameId,
|
||||||
|
required String winnerId,
|
||||||
|
}) async {
|
||||||
|
final query = update(gameTable)..where((g) => g.id.equals(gameId));
|
||||||
|
final rowsAffected = await query.write(
|
||||||
|
GameTableCompanion(winnerId: Value(winnerId)),
|
||||||
|
);
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the winner of the game with the given [gameId].
|
||||||
|
/// Returns the [Player] who won the game, or `null` if no winner is set.
|
||||||
|
Future<Player?> getWinner({required String gameId}) async {
|
||||||
|
final query = select(gameTable)..where((g) => g.id.equals(gameId));
|
||||||
|
final result = await query.getSingleOrNull();
|
||||||
|
if (result == null || result.winnerId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
|
||||||
|
return winner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the winner of the game with the given [gameId].
|
||||||
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
|
Future<bool> removeWinner({required String gameId}) async {
|
||||||
|
final query = update(gameTable)..where((g) => g.id.equals(gameId));
|
||||||
|
final rowsAffected = await query.write(
|
||||||
|
const GameTableCompanion(winnerId: Value(null)),
|
||||||
|
);
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the game with the given [gameId] has a winner set.
|
||||||
|
/// Returns `true` if a winner is set, otherwise `false`.
|
||||||
|
Future<bool> hasWinner({required String gameId}) async {
|
||||||
|
final query = select(gameTable)
|
||||||
|
..where((g) => g.id.equals(gameId) & g.winnerId.isNotNull());
|
||||||
|
final result = await query.getSingleOrNull();
|
||||||
|
return result != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the title of the game with the given [gameId] to [newName].
|
||||||
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
|
Future<bool> updateGameName({
|
||||||
|
required String gameId,
|
||||||
|
required String newName,
|
||||||
|
}) async {
|
||||||
|
final query = update(gameTable)..where((g) => g.id.equals(gameId));
|
||||||
|
final rowsAffected = await query.write(
|
||||||
|
GameTableCompanion(name: Value(newName)),
|
||||||
|
);
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,44 +14,169 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
|||||||
Future<List<Group>> getAllGroups() async {
|
Future<List<Group>> getAllGroups() async {
|
||||||
final query = select(groupTable);
|
final query = select(groupTable);
|
||||||
final result = await query.get();
|
final result = await query.get();
|
||||||
return result
|
return Future.wait(
|
||||||
.map((row) => Group(id: row.id, name: row.name, members: []))
|
result.map((groupData) async {
|
||||||
.toList();
|
final members = await db.playerGroupDao.getPlayersOfGroup(
|
||||||
|
groupId: groupData.id,
|
||||||
|
);
|
||||||
|
return Group(
|
||||||
|
id: groupData.id,
|
||||||
|
name: groupData.name,
|
||||||
|
members: members,
|
||||||
|
createdAt: groupData.createdAt,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves a [Group] by its [groupId], including its members.
|
/// Retrieves a [Group] by its [groupId], including its members.
|
||||||
Future<Group> getGroupById(String groupId) async {
|
Future<Group> getGroupById({required String groupId}) async {
|
||||||
final query = select(groupTable)..where((g) => g.id.equals(groupId));
|
final query = select(groupTable)..where((g) => g.id.equals(groupId));
|
||||||
final result = await query.getSingle();
|
final result = await query.getSingle();
|
||||||
|
|
||||||
List<Player> members = await db.playerGroupDao.getPlayersOfGroupById(
|
List<Player> members = await db.playerGroupDao.getPlayersOfGroup(
|
||||||
groupId,
|
groupId: groupId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Group(id: result.id, name: result.name, members: members);
|
return Group(
|
||||||
|
id: result.id,
|
||||||
|
name: result.name,
|
||||||
|
members: members,
|
||||||
|
createdAt: result.createdAt,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a new group with the given [id] and [name] to the database.
|
/// Adds a new group with the given [id] and [name] to the database.
|
||||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
/// This method also adds the group's members to the [PlayerGroupTable].
|
||||||
Future<void> addGroup(String id, String name) async {
|
Future<bool> addGroup({required Group group}) async {
|
||||||
await into(
|
if (!await groupExists(groupId: group.id)) {
|
||||||
groupTable,
|
await db.transaction(() async {
|
||||||
).insert(GroupTableCompanion.insert(id: id, name: name));
|
await into(groupTable).insert(
|
||||||
|
GroupTableCompanion.insert(
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
createdAt: group.createdAt,
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
await Future.wait(
|
||||||
|
group.members.map((player) => db.playerDao.addPlayer(player: player)),
|
||||||
|
);
|
||||||
|
await db.batch(
|
||||||
|
(b) => b.insertAll(
|
||||||
|
db.playerGroupTable,
|
||||||
|
group.members
|
||||||
|
.map(
|
||||||
|
(member) => PlayerGroupTableCompanion.insert(
|
||||||
|
playerId: member.id,
|
||||||
|
groupId: group.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds multiple groups to the database.
|
||||||
|
/// Also adds the group's members to the [PlayerGroupTable].
|
||||||
|
Future<void> addGroupsAsList({required List<Group> groups}) async {
|
||||||
|
if (groups.isEmpty) return;
|
||||||
|
await db.transaction(() async {
|
||||||
|
// Deduplicate groups by id - keep first occurrence
|
||||||
|
final Map<String, Group> uniqueGroups = {};
|
||||||
|
for (final g in groups) {
|
||||||
|
uniqueGroups.putIfAbsent(g.id, () => g);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert unique groups in batch
|
||||||
|
await db.batch(
|
||||||
|
(b) => b.insertAll(
|
||||||
|
groupTable,
|
||||||
|
uniqueGroups.values
|
||||||
|
.map(
|
||||||
|
(group) => GroupTableCompanion.insert(
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
createdAt: group.createdAt,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Collect unique players from all groups
|
||||||
|
final uniquePlayers = <String, Player>{};
|
||||||
|
for (final g in uniqueGroups.values) {
|
||||||
|
for (final m in g.members) {
|
||||||
|
uniquePlayers[m.id] = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniquePlayers.isNotEmpty) {
|
||||||
|
await db.batch(
|
||||||
|
(b) => b.insertAll(
|
||||||
|
db.playerTable,
|
||||||
|
uniquePlayers.values
|
||||||
|
.map(
|
||||||
|
(p) => PlayerTableCompanion.insert(
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
createdAt: p.createdAt,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare all player-group associations in one list (unique pairs)
|
||||||
|
final Set<String> seenPairs = {};
|
||||||
|
final List<PlayerGroupTableCompanion> pgRows = [];
|
||||||
|
for (final g in uniqueGroups.values) {
|
||||||
|
for (final m in g.members) {
|
||||||
|
final key = '${m.id}|${g.id}';
|
||||||
|
if (!seenPairs.contains(key)) {
|
||||||
|
seenPairs.add(key);
|
||||||
|
pgRows.add(
|
||||||
|
PlayerGroupTableCompanion.insert(playerId: m.id, groupId: g.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pgRows.isNotEmpty) {
|
||||||
|
await db.batch((b) {
|
||||||
|
for (final pg in pgRows) {
|
||||||
|
b.insert(db.playerGroupTable, pg, mode: InsertMode.insertOrReplace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the group with the given [id] from the database.
|
/// Deletes the group with the given [id] from the database.
|
||||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
Future<bool> deleteGroup(String id) async {
|
Future<bool> deleteGroup({required String groupId}) async {
|
||||||
final query = (delete(groupTable)..where((g) => g.id.equals(id)));
|
final query = (delete(groupTable)..where((g) => g.id.equals(groupId)));
|
||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the name of the group with the given [id] to [newName].
|
/// Updates the name of the group with the given [id] to [newName].
|
||||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
Future<bool> updateGroupname(String id, String newName) async {
|
Future<bool> updateGroupname({
|
||||||
|
required String groupId,
|
||||||
|
required String newName,
|
||||||
|
}) async {
|
||||||
final rowsAffected =
|
final rowsAffected =
|
||||||
await (update(groupTable)..where((g) => g.id.equals(id))).write(
|
await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
|
||||||
GroupTableCompanion(name: Value(newName)),
|
GroupTableCompanion(name: Value(newName)),
|
||||||
);
|
);
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
@@ -65,4 +190,20 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
|||||||
.getSingle();
|
.getSingle();
|
||||||
return count ?? 0;
|
return count ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if a group with the given [groupId] exists in the database.
|
||||||
|
/// Returns `true` if the group exists, `false` otherwise.
|
||||||
|
Future<bool> groupExists({required String groupId}) async {
|
||||||
|
final query = select(groupTable)..where((g) => g.id.equals(groupId));
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,39 @@ class GroupGameDao extends DatabaseAccessor<AppDatabase>
|
|||||||
with _$GroupGameDaoMixin {
|
with _$GroupGameDaoMixin {
|
||||||
GroupGameDao(super.db);
|
GroupGameDao(super.db);
|
||||||
|
|
||||||
|
/// Associates a group with a game by inserting a record into the
|
||||||
|
/// [GroupGameTable].
|
||||||
|
Future<void> addGroupToGame({
|
||||||
|
required String gameId,
|
||||||
|
required String groupId,
|
||||||
|
}) async {
|
||||||
|
if (await gameHasGroup(gameId: gameId)) {
|
||||||
|
throw Exception('Game already has a group');
|
||||||
|
}
|
||||||
|
await into(groupGameTable).insert(
|
||||||
|
GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the [Group] associated with the given [gameId].
|
||||||
|
/// Returns `null` if no group is found.
|
||||||
|
Future<Group?> getGroupOfGame({required String gameId}) async {
|
||||||
|
final result = await (select(
|
||||||
|
groupGameTable,
|
||||||
|
)..where((g) => g.gameId.equals(gameId))).getSingleOrNull();
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final group = await db.groupDao.getGroupById(groupId: result.groupId);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks if there is a group associated with the given [gameId].
|
/// Checks if there is a group associated with the given [gameId].
|
||||||
/// Returns `true` if there is a group, otherwise `false`.
|
/// Returns `true` if there is a group, otherwise `false`.
|
||||||
Future<bool> hasGameGroup(String gameId) async {
|
Future<bool> gameHasGroup({required String gameId}) async {
|
||||||
final count =
|
final count =
|
||||||
await (selectOnly(groupGameTable)
|
await (selectOnly(groupGameTable)
|
||||||
..where(groupGameTable.gameId.equals(gameId))
|
..where(groupGameTable.gameId.equals(gameId))
|
||||||
@@ -22,12 +52,47 @@ class GroupGameDao extends DatabaseAccessor<AppDatabase>
|
|||||||
return (count ?? 0) > 0;
|
return (count ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Group> getGroupByGameId(String gameId) async {
|
/// Checks if a specific group is associated with a specific game.
|
||||||
final result = await (select(
|
/// Returns `true` if the group is in the game, otherwise `false`.
|
||||||
groupGameTable,
|
Future<bool> isGroupInGame({
|
||||||
)..where((g) => g.gameId.equals(gameId))).getSingle();
|
required String gameId,
|
||||||
|
required String groupId,
|
||||||
|
}) async {
|
||||||
|
final count =
|
||||||
|
await (selectOnly(groupGameTable)
|
||||||
|
..where(
|
||||||
|
groupGameTable.gameId.equals(gameId) &
|
||||||
|
groupGameTable.groupId.equals(groupId),
|
||||||
|
)
|
||||||
|
..addColumns([groupGameTable.groupId.count()]))
|
||||||
|
.map((row) => row.read(groupGameTable.groupId.count()))
|
||||||
|
.getSingle();
|
||||||
|
return (count ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
final group = await db.groupDao.getGroupById(result.groupId);
|
/// Removes the association of a group from a game based on [groupId] and
|
||||||
return group;
|
/// [gameId].
|
||||||
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
|
Future<bool> removeGroupFromGame({
|
||||||
|
required String gameId,
|
||||||
|
required String groupId,
|
||||||
|
}) async {
|
||||||
|
final query = delete(groupGameTable)
|
||||||
|
..where((g) => g.gameId.equals(gameId) & g.groupId.equals(groupId));
|
||||||
|
final rowsAffected = await query.go();
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the group associated with a game to [newGroupId] based on
|
||||||
|
/// [gameId].
|
||||||
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
|
Future<bool> updateGroupOfGame({
|
||||||
|
required String gameId,
|
||||||
|
required String newGroupId,
|
||||||
|
}) async {
|
||||||
|
final updatedRows =
|
||||||
|
await (update(groupGameTable)..where((g) => g.gameId.equals(gameId)))
|
||||||
|
.write(GroupGameTableCompanion(groupId: Value(newGroupId)));
|
||||||
|
return updatedRows > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ part of 'group_game_dao.dart';
|
|||||||
|
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
mixin _$GroupGameDaoMixin on DatabaseAccessor<AppDatabase> {
|
mixin _$GroupGameDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||||
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
|
||||||
$GroupTableTable get groupTable => attachedDatabase.groupTable;
|
$GroupTableTable get groupTable => attachedDatabase.groupTable;
|
||||||
|
$GameTableTable get gameTable => attachedDatabase.gameTable;
|
||||||
$GroupGameTableTable get groupGameTable => attachedDatabase.groupGameTable;
|
$GroupGameTableTable get groupGameTable => attachedDatabase.groupGameTable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,43 +13,105 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
|||||||
Future<List<Player>> getAllPlayers() async {
|
Future<List<Player>> getAllPlayers() async {
|
||||||
final query = select(playerTable);
|
final query = select(playerTable);
|
||||||
final result = await query.get();
|
final result = await query.get();
|
||||||
return result.map((row) => Player(id: row.id, name: row.name)).toList();
|
return result
|
||||||
|
.map(
|
||||||
|
(row) => Player(id: row.id, name: row.name, createdAt: row.createdAt),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves a [Player] by their [id].
|
/// Retrieves a [Player] by their [id].
|
||||||
Future<Player> getPlayerById(String id) async {
|
Future<Player> getPlayerById({required String playerId}) async {
|
||||||
final query = select(playerTable)..where((p) => p.id.equals(id));
|
final query = select(playerTable)..where((p) => p.id.equals(playerId));
|
||||||
final result = await query.getSingle();
|
final result = await query.getSingle();
|
||||||
return Player(id: result.id, name: result.name);
|
return Player(
|
||||||
|
id: result.id,
|
||||||
|
name: result.name,
|
||||||
|
createdAt: result.createdAt,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a new [player] to the database.
|
/// Adds a new [player] to the database.
|
||||||
Future<void> addPlayer(Player player) async {
|
/// If a player with the same ID already exists, updates their name to
|
||||||
await into(
|
/// the new one.
|
||||||
playerTable,
|
Future<bool> addPlayer({required Player player}) async {
|
||||||
).insert(PlayerTableCompanion.insert(id: player.id, name: player.name));
|
if (!await playerExists(playerId: player.id)) {
|
||||||
|
await into(playerTable).insert(
|
||||||
|
PlayerTableCompanion.insert(
|
||||||
|
id: player.id,
|
||||||
|
name: player.name,
|
||||||
|
createdAt: player.createdAt,
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds multiple [players] to the database in a batch operation.
|
||||||
|
Future<bool> addPlayersAsList({required List<Player> players}) async {
|
||||||
|
if (players.isEmpty) return false;
|
||||||
|
|
||||||
|
await db.batch(
|
||||||
|
(b) => b.insertAll(
|
||||||
|
playerTable,
|
||||||
|
players
|
||||||
|
.map(
|
||||||
|
(player) => PlayerTableCompanion.insert(
|
||||||
|
id: player.id,
|
||||||
|
name: player.name,
|
||||||
|
createdAt: player.createdAt,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the player with the given [id] from the database.
|
/// Deletes the player with the given [id] from the database.
|
||||||
/// Returns `true` if the player was deleted, `false` if the player did not exist.
|
/// Returns `true` if the player was deleted, `false` if the player did not exist.
|
||||||
Future<bool> deletePlayer(String id) async {
|
Future<bool> deletePlayer({required String playerId}) async {
|
||||||
final query = delete(playerTable)..where((p) => p.id.equals(id));
|
final query = delete(playerTable)..where((p) => p.id.equals(playerId));
|
||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a player with the given [id] exists in the database.
|
/// Checks if a player with the given [id] exists in the database.
|
||||||
/// Returns `true` if the player exists, `false` otherwise.
|
/// Returns `true` if the player exists, `false` otherwise.
|
||||||
Future<bool> playerExists(String id) async {
|
Future<bool> playerExists({required String playerId}) async {
|
||||||
final query = select(playerTable)..where((p) => p.id.equals(id));
|
final query = select(playerTable)..where((p) => p.id.equals(playerId));
|
||||||
final result = await query.getSingleOrNull();
|
final result = await query.getSingleOrNull();
|
||||||
return result != null;
|
return result != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the name of the player with the given [id] to [newName].
|
/// Updates the name of the player with the given [playerId] to [newName].
|
||||||
Future<void> updatePlayername(String id, String newName) async {
|
Future<void> updatePlayername({
|
||||||
await (update(playerTable)..where((p) => p.id.equals(id))).write(
|
required String playerId,
|
||||||
|
required String newName,
|
||||||
|
}) async {
|
||||||
|
await (update(playerTable)..where((p) => p.id.equals(playerId))).write(
|
||||||
PlayerTableCompanion(name: Value(newName)),
|
PlayerTableCompanion(name: Value(newName)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves the total count of players in the database.
|
||||||
|
Future<int> getPlayerCount() async {
|
||||||
|
final count =
|
||||||
|
await (selectOnly(playerTable)..addColumns([playerTable.id.count()]))
|
||||||
|
.map((row) => row.read(playerTable.id.count()))
|
||||||
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,37 @@ class PlayerGameDao extends DatabaseAccessor<AppDatabase>
|
|||||||
with _$PlayerGameDaoMixin {
|
with _$PlayerGameDaoMixin {
|
||||||
PlayerGameDao(super.db);
|
PlayerGameDao(super.db);
|
||||||
|
|
||||||
|
/// Associates a player with a game by inserting a record into the
|
||||||
|
/// [PlayerGameTable].
|
||||||
|
Future<void> addPlayerToGame({
|
||||||
|
required String gameId,
|
||||||
|
required String playerId,
|
||||||
|
}) async {
|
||||||
|
await into(playerGameTable).insert(
|
||||||
|
PlayerGameTableCompanion.insert(playerId: playerId, gameId: gameId),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves a list of [Player]s associated with the given [gameId].
|
||||||
|
/// Returns null if no players are found.
|
||||||
|
Future<List<Player>?> getPlayersOfGame({required String gameId}) async {
|
||||||
|
final result = await (select(
|
||||||
|
playerGameTable,
|
||||||
|
)..where((p) => p.gameId.equals(gameId))).get();
|
||||||
|
|
||||||
|
if (result.isEmpty) return null;
|
||||||
|
|
||||||
|
final futures = result.map(
|
||||||
|
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
|
||||||
|
);
|
||||||
|
final players = await Future.wait(futures);
|
||||||
|
return players;
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks if there are any players associated with the given [gameId].
|
/// Checks if there are any players associated with the given [gameId].
|
||||||
/// Returns `true` if there are players, otherwise `false`.
|
/// Returns `true` if there are players, otherwise `false`.
|
||||||
Future<bool> hasGamePlayers(String gameId) async {
|
Future<bool> gameHasPlayers({required String gameId}) async {
|
||||||
final count =
|
final count =
|
||||||
await (selectOnly(playerGameTable)
|
await (selectOnly(playerGameTable)
|
||||||
..where(playerGameTable.gameId.equals(gameId))
|
..where(playerGameTable.gameId.equals(gameId))
|
||||||
@@ -22,19 +50,79 @@ class PlayerGameDao extends DatabaseAccessor<AppDatabase>
|
|||||||
return (count ?? 0) > 0;
|
return (count ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves a list of [Player]s associated with the given [gameId].
|
/// Checks if a specific player is associated with a specific game.
|
||||||
/// Returns an empty list if no players are found.
|
/// Returns `true` if the player is in the game, otherwise `false`.
|
||||||
Future<List<Player>> getPlayersByGameId(String gameId) async {
|
Future<bool> isPlayerInGame({
|
||||||
final result = await (select(
|
required String gameId,
|
||||||
playerGameTable,
|
required String playerId,
|
||||||
)..where((p) => p.gameId.equals(gameId))).get();
|
}) async {
|
||||||
|
final count =
|
||||||
|
await (selectOnly(playerGameTable)
|
||||||
|
..where(playerGameTable.gameId.equals(gameId))
|
||||||
|
..where(playerGameTable.playerId.equals(playerId))
|
||||||
|
..addColumns([playerGameTable.playerId.count()]))
|
||||||
|
.map((row) => row.read(playerGameTable.playerId.count()))
|
||||||
|
.getSingle();
|
||||||
|
return (count ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.isEmpty) return <Player>[];
|
/// Removes the association of a player with a game by deleting the record
|
||||||
|
/// from the [PlayerGameTable].
|
||||||
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
|
Future<bool> removePlayerFromGame({
|
||||||
|
required String gameId,
|
||||||
|
required String playerId,
|
||||||
|
}) async {
|
||||||
|
final query = delete(playerGameTable)
|
||||||
|
..where((pg) => pg.gameId.equals(gameId))
|
||||||
|
..where((pg) => pg.playerId.equals(playerId));
|
||||||
|
final rowsAffected = await query.go();
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
final futures = result.map(
|
/// Updates the players associated with a game based on the provided
|
||||||
(row) => db.playerDao.getPlayerById(row.playerId),
|
/// [newPlayer] list. It adds new players and removes players that are no
|
||||||
);
|
/// longer associated with the game.
|
||||||
final players = await Future.wait(futures);
|
Future<void> updatePlayersFromGame({
|
||||||
return players.whereType<Player>().toList();
|
required String gameId,
|
||||||
|
required List<Player> newPlayer,
|
||||||
|
}) async {
|
||||||
|
final currentPlayers = await getPlayersOfGame(gameId: gameId);
|
||||||
|
// Create sets of player IDs for easy comparison
|
||||||
|
final currentPlayerIds = currentPlayers?.map((p) => p.id).toSet() ?? {};
|
||||||
|
final newPlayerIdsSet = newPlayer.map((p) => p.id).toSet();
|
||||||
|
|
||||||
|
// Determine players to add and remove
|
||||||
|
final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds);
|
||||||
|
final playersToRemove = currentPlayerIds.difference(newPlayerIdsSet);
|
||||||
|
|
||||||
|
db.transaction(() async {
|
||||||
|
// Remove old players
|
||||||
|
if (playersToRemove.isNotEmpty) {
|
||||||
|
await (delete(playerGameTable)..where(
|
||||||
|
(pg) =>
|
||||||
|
pg.gameId.equals(gameId) &
|
||||||
|
pg.playerId.isIn(playersToRemove.toList()),
|
||||||
|
))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new players
|
||||||
|
if (playersToAdd.isNotEmpty) {
|
||||||
|
final inserts = playersToAdd
|
||||||
|
.map(
|
||||||
|
(id) =>
|
||||||
|
PlayerGameTableCompanion.insert(playerId: id, gameId: gameId),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
await Future.wait(
|
||||||
|
inserts.map(
|
||||||
|
(c) => into(
|
||||||
|
playerGameTable,
|
||||||
|
).insert(c, mode: InsertMode.insertOrReplace),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ part of 'player_game_dao.dart';
|
|||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
mixin _$PlayerGameDaoMixin on DatabaseAccessor<AppDatabase> {
|
mixin _$PlayerGameDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||||
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
||||||
$GroupTableTable get groupTable => attachedDatabase.groupTable;
|
$GameTableTable get gameTable => attachedDatabase.gameTable;
|
||||||
$PlayerGameTableTable get playerGameTable => attachedDatabase.playerGameTable;
|
$PlayerGameTableTable get playerGameTable => attachedDatabase.playerGameTable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,42 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
|
|||||||
with _$PlayerGroupDaoMixin {
|
with _$PlayerGroupDaoMixin {
|
||||||
PlayerGroupDao(super.db);
|
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].
|
/// Retrieves all players belonging to a specific group by [groupId].
|
||||||
Future<List<Player>> getPlayersOfGroupById(String groupId) async {
|
Future<List<Player>> getPlayersOfGroup({required String groupId}) async {
|
||||||
final query = select(playerGroupTable)
|
final query = select(playerGroupTable)
|
||||||
..where((pG) => pG.groupId.equals(groupId));
|
..where((pG) => pG.groupId.equals(groupId));
|
||||||
final result = await query.get();
|
final result = await query.get();
|
||||||
|
|
||||||
List<Player> groupMembers = [];
|
List<Player> groupMembers = List.empty(growable: true);
|
||||||
|
|
||||||
for (var entry in result) {
|
for (var entry in result) {
|
||||||
final player = await db.playerDao.getPlayerById(entry.playerId);
|
final player = await db.playerDao.getPlayerById(playerId: entry.playerId);
|
||||||
groupMembers.add(player);
|
groupMembers.add(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,17 +54,25 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
|
|||||||
|
|
||||||
/// Removes a player from a group based on [playerId] and [groupId].
|
/// Removes a player from a group based on [playerId] and [groupId].
|
||||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
Future<bool> removePlayerFromGroup(String playerId, String groupId) async {
|
Future<bool> removePlayerFromGroup({
|
||||||
|
required String playerId,
|
||||||
|
required String groupId,
|
||||||
|
}) async {
|
||||||
final query = delete(playerGroupTable)
|
final query = delete(playerGroupTable)
|
||||||
..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId));
|
..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId));
|
||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a player to a group with the given [playerId] and [groupId].
|
/// Checks if a player with [playerId] is in the group with [groupId].
|
||||||
Future<void> addPlayerToGroup(String playerId, String groupId) async {
|
/// Returns `true` if the player is in the group, otherwise `false`.
|
||||||
await into(playerGroupTable).insert(
|
Future<bool> isPlayerInGroup({
|
||||||
PlayerGroupTableCompanion.insert(playerId: playerId, groupId: groupId),
|
required String playerId,
|
||||||
);
|
required String groupId,
|
||||||
|
}) async {
|
||||||
|
final query = select(playerGroupTable)
|
||||||
|
..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId));
|
||||||
|
final result = await query.getSingleOrNull();
|
||||||
|
return result != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ part 'database.g.dart';
|
|||||||
tables: [
|
tables: [
|
||||||
PlayerTable,
|
PlayerTable,
|
||||||
GroupTable,
|
GroupTable,
|
||||||
|
GameTable,
|
||||||
PlayerGroupTable,
|
PlayerGroupTable,
|
||||||
PlayerGameTable,
|
PlayerGameTable,
|
||||||
GroupGameTable,
|
GroupGameTable,
|
||||||
GameTable,
|
|
||||||
],
|
],
|
||||||
daos: [
|
daos: [
|
||||||
GroupDao,
|
|
||||||
PlayerDao,
|
PlayerDao,
|
||||||
|
GroupDao,
|
||||||
GameDao,
|
GameDao,
|
||||||
PlayerGroupDao,
|
PlayerGroupDao,
|
||||||
PlayerGameDao,
|
PlayerGameDao,
|
||||||
@@ -40,6 +40,15 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
@override
|
@override
|
||||||
int get schemaVersion => 1;
|
int get schemaVersion => 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MigrationStrategy get migration {
|
||||||
|
return MigrationStrategy(
|
||||||
|
beforeOpen: (details) async {
|
||||||
|
await customStatement('PRAGMA foreign_keys = ON');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return driftDatabase(
|
return driftDatabase(
|
||||||
name: 'gametracker_db',
|
name: 'gametracker_db',
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import 'package:drift/drift.dart';
|
|||||||
class GameTable extends Table {
|
class GameTable extends Table {
|
||||||
TextColumn get id => text()();
|
TextColumn get id => text()();
|
||||||
TextColumn get name => text()();
|
TextColumn get name => text()();
|
||||||
TextColumn get winnerId => text().nullable()();
|
late final winnerId = text().nullable()();
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column<Object>> get primaryKey => {id};
|
Set<Column<Object>> get primaryKey => {id};
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:drift/drift.dart';
|
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';
|
import 'package:game_tracker/data/db/tables/group_table.dart';
|
||||||
import 'package:game_tracker/data/db/tables/player_table.dart';
|
|
||||||
|
|
||||||
class GroupGameTable extends Table {
|
class GroupGameTable extends Table {
|
||||||
TextColumn get groupId => text().references(PlayerTable, #id)();
|
TextColumn get groupId =>
|
||||||
TextColumn get gameId => text().references(GroupTable, #id)();
|
text().references(GroupTable, #id, onDelete: KeyAction.cascade)();
|
||||||
|
TextColumn get gameId =>
|
||||||
|
text().references(GameTable, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column<Object>> get primaryKey => {groupId, gameId};
|
Set<Column<Object>> get primaryKey => {groupId, gameId};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:drift/drift.dart';
|
|||||||
class GroupTable extends Table {
|
class GroupTable extends Table {
|
||||||
TextColumn get id => text()();
|
TextColumn get id => text()();
|
||||||
TextColumn get name => text()();
|
TextColumn get name => text()();
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column<Object>> get primaryKey => {id};
|
Set<Column<Object>> get primaryKey => {id};
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:game_tracker/data/db/tables/group_table.dart';
|
import 'package:game_tracker/data/db/tables/game_table.dart';
|
||||||
import 'package:game_tracker/data/db/tables/player_table.dart';
|
import 'package:game_tracker/data/db/tables/player_table.dart';
|
||||||
|
|
||||||
class PlayerGameTable extends Table {
|
class PlayerGameTable extends Table {
|
||||||
TextColumn get playerId => text().references(PlayerTable, #id)();
|
TextColumn get playerId =>
|
||||||
TextColumn get gameId => text().references(GroupTable, #id)();
|
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
|
||||||
|
TextColumn get gameId =>
|
||||||
|
text().references(GameTable, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column<Object>> get primaryKey => {playerId, gameId};
|
Set<Column<Object>> get primaryKey => {playerId, gameId};
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import 'package:game_tracker/data/db/tables/group_table.dart';
|
|||||||
import 'package:game_tracker/data/db/tables/player_table.dart';
|
import 'package:game_tracker/data/db/tables/player_table.dart';
|
||||||
|
|
||||||
class PlayerGroupTable extends Table {
|
class PlayerGroupTable extends Table {
|
||||||
TextColumn get playerId => text().references(PlayerTable, #id)();
|
TextColumn get playerId =>
|
||||||
TextColumn get groupId => text().references(GroupTable, #id)();
|
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
|
||||||
|
TextColumn get groupId =>
|
||||||
|
text().references(GroupTable, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column<Object>> get primaryKey => {playerId, groupId};
|
Set<Column<Object>> get primaryKey => {playerId, groupId};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:drift/drift.dart';
|
|||||||
class PlayerTable extends Table {
|
class PlayerTable extends Table {
|
||||||
TextColumn get id => text()();
|
TextColumn get id => text()();
|
||||||
TextColumn get name => text()();
|
TextColumn get name => text()();
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column<Object>> get primaryKey => {id};
|
Set<Column<Object>> get primaryKey => {id};
|
||||||
|
|||||||
@@ -1,18 +1,51 @@
|
|||||||
|
import 'package:clock/clock.dart';
|
||||||
import 'package:game_tracker/data/dto/group.dart';
|
import 'package:game_tracker/data/dto/group.dart';
|
||||||
import 'package:game_tracker/data/dto/player.dart';
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class Game {
|
class Game {
|
||||||
final String id;
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
final String name;
|
final String name;
|
||||||
final List<Player>? players;
|
final List<Player>? players;
|
||||||
final Group? group;
|
final Group? group;
|
||||||
final String? winner;
|
final Player? winner;
|
||||||
|
|
||||||
Game({
|
Game({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
required this.name,
|
||||||
this.players,
|
this.players,
|
||||||
this.group,
|
this.group,
|
||||||
this.winner,
|
this.winner,
|
||||||
required this.id,
|
}) : id = id ?? const Uuid().v4(),
|
||||||
required this.name,
|
createdAt = createdAt ?? clock.now();
|
||||||
});
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Game{\n\tid: $id,\n\tname: $name,\n\tplayers: $players,\n\tgroup: $group,\n\twinner: $winner\n}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Game instance from a JSON object.
|
||||||
|
Game.fromJson(Map<String, dynamic> json)
|
||||||
|
: id = json['id'],
|
||||||
|
name = json['name'],
|
||||||
|
createdAt = DateTime.parse(json['createdAt']),
|
||||||
|
players = json['players'] != null
|
||||||
|
? (json['players'] as List)
|
||||||
|
.map((playerJson) => Player.fromJson(playerJson))
|
||||||
|
.toList()
|
||||||
|
: null,
|
||||||
|
group = json['group'] != null ? Group.fromJson(json['group']) : null,
|
||||||
|
winner = json['winner'] != null ? Player.fromJson(json['winner']) : null;
|
||||||
|
|
||||||
|
/// Converts the Game instance to a JSON object.
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
'name': name,
|
||||||
|
'players': players?.map((player) => player.toJson()).toList(),
|
||||||
|
'group': group?.toJson(),
|
||||||
|
'winner': winner?.toJson(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,40 @@
|
|||||||
|
import 'package:clock/clock.dart';
|
||||||
import 'package:game_tracker/data/dto/player.dart';
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class Group {
|
class Group {
|
||||||
final String id;
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
final String name;
|
final String name;
|
||||||
final List<Player> members;
|
final List<Player> members;
|
||||||
|
|
||||||
Group({required this.id, required this.name, required this.members});
|
Group({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
required this.name,
|
||||||
|
required this.members,
|
||||||
|
}) : id = id ?? const Uuid().v4(),
|
||||||
|
createdAt = createdAt ?? clock.now();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Group{id: $id, name: $name,members: $members}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Group instance from a JSON object.
|
||||||
|
Group.fromJson(Map<String, dynamic> json)
|
||||||
|
: id = json['id'],
|
||||||
|
createdAt = DateTime.parse(json['createdAt']),
|
||||||
|
name = json['name'],
|
||||||
|
members = (json['members'] as List)
|
||||||
|
.map((memberJson) => Player.fromJson(memberJson))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
/// Converts the Group instance to a JSON object.
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
'name': name,
|
||||||
|
'members': members.map((member) => member.toJson()).toList(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,30 @@
|
|||||||
|
import 'package:clock/clock.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class Player {
|
class Player {
|
||||||
final String id;
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
Player({required this.id, required this.name});
|
Player({String? id, DateTime? createdAt, required this.name})
|
||||||
|
: id = id ?? const Uuid().v4(),
|
||||||
|
createdAt = createdAt ?? clock.now();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Player{id: $id,name: $name}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Player instance from a JSON object.
|
||||||
|
Player.fromJson(Map<String, dynamic> json)
|
||||||
|
: id = json['id'],
|
||||||
|
createdAt = DateTime.parse(json['createdAt']),
|
||||||
|
name = json['name'];
|
||||||
|
|
||||||
|
/// Converts the Player instance to a JSON object.
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
'name': name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/data/dto/group.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
|
||||||
|
|
||||||
|
class ChooseGroupView extends StatefulWidget {
|
||||||
|
final List<Group> groups;
|
||||||
|
final int initialGroupIndex;
|
||||||
|
|
||||||
|
const ChooseGroupView({
|
||||||
|
super.key,
|
||||||
|
required this.groups,
|
||||||
|
required this.initialGroupIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChooseGroupView> createState() => _ChooseGroupViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChooseGroupViewState extends State<ChooseGroupView> {
|
||||||
|
late int selectedGroupIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
selectedGroupIndex = widget.initialGroupIndex;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: const Text(
|
||||||
|
'Choose Group',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(bottom: 85),
|
||||||
|
itemCount: widget.groups.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
selectedGroupIndex = index;
|
||||||
|
});
|
||||||
|
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
Navigator.of(context).pop(widget.groups[index]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: GroupTile(
|
||||||
|
group: widget.groups[index],
|
||||||
|
isHighlighted: selectedGroupIndex == index,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/core/enums.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/ruleset_list_tile.dart';
|
||||||
|
|
||||||
|
class ChooseRulesetView extends StatefulWidget {
|
||||||
|
final List<(Ruleset, String, String)> rulesets;
|
||||||
|
final int initialRulesetIndex;
|
||||||
|
const ChooseRulesetView({
|
||||||
|
super.key,
|
||||||
|
required this.rulesets,
|
||||||
|
required this.initialRulesetIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChooseRulesetView> createState() => _ChooseRulesetViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChooseRulesetViewState extends State<ChooseRulesetView> {
|
||||||
|
late int selectedRulesetIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
selectedRulesetIndex = widget.initialRulesetIndex;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
initialIndex: 0,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: const Text(
|
||||||
|
'Choose Ruleset',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: CustomTheme.backgroundColor,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
child: TabBar(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||||
|
// Label Settings
|
||||||
|
labelStyle: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
labelColor: Colors.white,
|
||||||
|
unselectedLabelStyle: const TextStyle(fontSize: 14),
|
||||||
|
unselectedLabelColor: Colors.white70,
|
||||||
|
// Indicator Settings
|
||||||
|
indicator: CustomTheme.standardBoxDecoration,
|
||||||
|
indicatorSize: TabBarIndicatorSize.tab,
|
||||||
|
indicatorWeight: 1,
|
||||||
|
indicatorPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 0,
|
||||||
|
),
|
||||||
|
// Divider Settings
|
||||||
|
dividerHeight: 0,
|
||||||
|
tabs: const [
|
||||||
|
Tab(text: 'Rulesets'),
|
||||||
|
Tab(text: 'Gametypes'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(
|
||||||
|
indent: 30,
|
||||||
|
endIndent: 30,
|
||||||
|
thickness: 3,
|
||||||
|
radius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
children: [
|
||||||
|
ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(bottom: 85),
|
||||||
|
itemCount: widget.rulesets.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return RulesetListTile(
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
selectedRulesetIndex = index;
|
||||||
|
});
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(widget.rulesets[index].$1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
title: widget.rulesets[index].$2,
|
||||||
|
description: widget.rulesets[index].$3,
|
||||||
|
isHighlighted: selectedRulesetIndex == index,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'No gametypes available',
|
||||||
|
style: TextStyle(color: Colors.white70),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/core/enums.dart';
|
||||||
|
import 'package:game_tracker/data/db/database.dart';
|
||||||
|
import 'package:game_tracker/data/dto/game.dart';
|
||||||
|
import 'package:game_tracker/data/dto/group.dart';
|
||||||
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
|
import 'package:game_tracker/presentation/views/main_menu/create_game/choose_group_view.dart';
|
||||||
|
import 'package:game_tracker/presentation/views/main_menu/create_game/choose_ruleset_view.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/player_selection.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class CreateGameView extends StatefulWidget {
|
||||||
|
const CreateGameView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreateGameView> createState() => _CreateGameViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateGameViewState extends State<CreateGameView> {
|
||||||
|
/// Reference to the app database
|
||||||
|
late final AppDatabase db;
|
||||||
|
|
||||||
|
/// Futures to load all groups and players from the database
|
||||||
|
late Future<List<Group>> _allGroupsFuture;
|
||||||
|
|
||||||
|
/// Future to load all players from the database
|
||||||
|
late Future<List<Player>> _allPlayersFuture;
|
||||||
|
|
||||||
|
/// Controller for the game name input field
|
||||||
|
final TextEditingController _gameNameController = TextEditingController();
|
||||||
|
|
||||||
|
/// List of all groups from the database
|
||||||
|
List<Group> groupsList = [];
|
||||||
|
|
||||||
|
/// List of all players from the database
|
||||||
|
List<Player> playerList = [];
|
||||||
|
|
||||||
|
/// The currently selected group
|
||||||
|
Group? selectedGroup;
|
||||||
|
|
||||||
|
/// The index of the currently selected group in [groupsList] to mark it in
|
||||||
|
/// the [ChooseGroupView]
|
||||||
|
int selectedGroupIndex = -1;
|
||||||
|
|
||||||
|
/// The currently selected ruleset
|
||||||
|
Ruleset? selectedRuleset;
|
||||||
|
|
||||||
|
/// The index of the currently selected ruleset in [rulesets] to mark it in
|
||||||
|
/// the [ChooseRulesetView]
|
||||||
|
int selectedRulesetIndex = -1;
|
||||||
|
|
||||||
|
/// The currently selected players
|
||||||
|
List<Player>? selectedPlayers;
|
||||||
|
|
||||||
|
/// List of available rulesets with their display names and descriptions
|
||||||
|
/// as tuples of (Ruleset, String, String)
|
||||||
|
List<(Ruleset, String, String)> rulesets = [
|
||||||
|
(
|
||||||
|
Ruleset.singleWinner,
|
||||||
|
'Single Winner',
|
||||||
|
'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Ruleset.singleLoser,
|
||||||
|
'Single Loser',
|
||||||
|
'Exactly one loser is determined; last place receives the penalty or consequence.',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Ruleset.mostPoints,
|
||||||
|
'Most Points',
|
||||||
|
'Traditional ruleset: the player with the most points wins.',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Ruleset.lastPoints,
|
||||||
|
'Least Points',
|
||||||
|
'Inverse scoring: the player with the fewest points wins.',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
|
|
||||||
|
_allGroupsFuture = db.groupDao.getAllGroups();
|
||||||
|
_allPlayersFuture = db.playerDao.getAllPlayers();
|
||||||
|
|
||||||
|
Future.wait([_allGroupsFuture, _allPlayersFuture]).then((result) async {
|
||||||
|
groupsList = result[0] as List<Group>;
|
||||||
|
playerList = result[1] as List<Player>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: const Text(
|
||||||
|
'Create new game',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
child: TextInputField(
|
||||||
|
controller: _gameNameController,
|
||||||
|
hintText: 'Game name',
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ChooseTile(
|
||||||
|
title: 'Ruleset',
|
||||||
|
trailingText: selectedRuleset == null
|
||||||
|
? 'None'
|
||||||
|
: translateRulesetToString(selectedRuleset!),
|
||||||
|
onPressed: () async {
|
||||||
|
selectedRuleset = await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ChooseRulesetView(
|
||||||
|
rulesets: rulesets,
|
||||||
|
initialRulesetIndex: selectedRulesetIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
selectedRulesetIndex = rulesets.indexWhere(
|
||||||
|
(r) => r.$1 == selectedRuleset,
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ChooseTile(
|
||||||
|
title: 'Group',
|
||||||
|
trailingText: selectedGroup == null
|
||||||
|
? 'None'
|
||||||
|
: selectedGroup!.name,
|
||||||
|
onPressed: () async {
|
||||||
|
selectedGroup = await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ChooseGroupView(
|
||||||
|
groups: groupsList,
|
||||||
|
initialGroupIndex: selectedGroupIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
selectedGroupIndex = groupsList.indexWhere(
|
||||||
|
(g) => g.id == selectedGroup?.id,
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: PlayerSelection(
|
||||||
|
key: ValueKey(selectedGroup?.id ?? 'no_group'),
|
||||||
|
initialPlayers: selectedGroup == null
|
||||||
|
? playerList
|
||||||
|
: playerList
|
||||||
|
.where(
|
||||||
|
(p) => !selectedGroup!.members.any(
|
||||||
|
(m) => m.id == p.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
selectedPlayers = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
CustomWidthButton(
|
||||||
|
text: 'Create game',
|
||||||
|
sizeRelativeToWidth: 0.95,
|
||||||
|
buttonType: ButtonType.primary,
|
||||||
|
onPressed: _enableCreateGameButton()
|
||||||
|
? () async {
|
||||||
|
Game game = Game(
|
||||||
|
name: _gameNameController.text.trim(),
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
group: selectedGroup!,
|
||||||
|
players: selectedPlayers,
|
||||||
|
);
|
||||||
|
// TODO: Replace with navigation to GameResultView()
|
||||||
|
print('Created game: $game');
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Translates a [Ruleset] enum value to its corresponding string representation.
|
||||||
|
String translateRulesetToString(Ruleset ruleset) {
|
||||||
|
switch (ruleset) {
|
||||||
|
case Ruleset.singleWinner:
|
||||||
|
return 'Single Winner';
|
||||||
|
case Ruleset.singleLoser:
|
||||||
|
return 'Single Loser';
|
||||||
|
case Ruleset.mostPoints:
|
||||||
|
return 'Most Points';
|
||||||
|
case Ruleset.lastPoints:
|
||||||
|
return 'Least Points';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines whether the "Create Game" button should be enabled based on
|
||||||
|
/// the current state of the input fields.
|
||||||
|
bool _enableCreateGameButton() {
|
||||||
|
return _gameNameController.text.isNotEmpty &&
|
||||||
|
(selectedGroup != null ||
|
||||||
|
(selectedPlayers != null && selectedPlayers!.isNotEmpty)) &&
|
||||||
|
selectedRuleset != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
112
lib/presentation/views/main_menu/create_group_view.dart
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/core/enums.dart';
|
||||||
|
import 'package:game_tracker/data/db/database.dart';
|
||||||
|
import 'package:game_tracker/data/dto/group.dart';
|
||||||
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/player_selection.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class CreateGroupView extends StatefulWidget {
|
||||||
|
const CreateGroupView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreateGroupView> createState() => _CreateGroupViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateGroupViewState extends State<CreateGroupView> {
|
||||||
|
final _groupNameController = TextEditingController();
|
||||||
|
late final AppDatabase db;
|
||||||
|
List<Player> selectedPlayers = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
|
_groupNameController.addListener(() {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_groupNameController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: const Text(
|
||||||
|
'Create new group',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
child: TextInputField(
|
||||||
|
controller: _groupNameController,
|
||||||
|
hintText: 'Group name',
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: PlayerSelection(
|
||||||
|
onChanged: (value) {
|
||||||
|
selectedPlayers = [...value];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CustomWidthButton(
|
||||||
|
text: 'Create group',
|
||||||
|
sizeRelativeToWidth: 0.95,
|
||||||
|
buttonType: ButtonType.primary,
|
||||||
|
onPressed:
|
||||||
|
(_groupNameController.text.isEmpty || selectedPlayers.isEmpty)
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
bool success = await db.groupDao.addGroup(
|
||||||
|
group: Group(
|
||||||
|
name: _groupNameController.text.trim(),
|
||||||
|
members: selectedPlayers,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (success) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
backgroundColor: CustomTheme.boxColor,
|
||||||
|
content: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Error while creating group, please try again',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,12 +17,7 @@ class CustomNavigationBar extends StatefulWidget {
|
|||||||
class _CustomNavigationBarState extends State<CustomNavigationBar>
|
class _CustomNavigationBarState extends State<CustomNavigationBar>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
int currentIndex = 0;
|
int currentIndex = 0;
|
||||||
final List<Widget> tabs = [
|
int tabKeyCount = 0;
|
||||||
const HomeView(),
|
|
||||||
const GameHistoryView(),
|
|
||||||
const GroupsView(),
|
|
||||||
const StatisticsView(),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -31,6 +26,22 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Pretty ugly but works
|
||||||
|
final List<Widget> tabs = [
|
||||||
|
KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()),
|
||||||
|
KeyedSubtree(
|
||||||
|
key: ValueKey('games_$tabKeyCount'),
|
||||||
|
child: const GameHistoryView(),
|
||||||
|
),
|
||||||
|
KeyedSubtree(
|
||||||
|
key: ValueKey('groups_$tabKeyCount'),
|
||||||
|
child: const GroupsView(),
|
||||||
|
),
|
||||||
|
KeyedSubtree(
|
||||||
|
key: ValueKey('stats_$tabKeyCount'),
|
||||||
|
child: const StatisticsView(),
|
||||||
|
),
|
||||||
|
];
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
@@ -42,10 +53,15 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
|
|||||||
scrolledUnderElevation: 0,
|
scrolledUnderElevation: 0,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.push(
|
onPressed: () async {
|
||||||
context,
|
await Navigator.push(
|
||||||
MaterialPageRoute(builder: (_) => const SettingsView()),
|
context,
|
||||||
),
|
MaterialPageRoute(builder: (_) => const SettingsView()),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
tabKeyCount++;
|
||||||
|
});
|
||||||
|
},
|
||||||
icon: const Icon(Icons.settings),
|
icon: const Icon(Icons.settings),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -54,12 +70,14 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
|
|||||||
backgroundColor: CustomTheme.backgroundColor,
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
body: tabs[currentIndex],
|
body: tabs[currentIndex],
|
||||||
extendBody: true,
|
extendBody: true,
|
||||||
bottomNavigationBar: Padding(
|
bottomNavigationBar: SafeArea(
|
||||||
padding: const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 8.0),
|
minimum: const EdgeInsets.only(bottom: 30),
|
||||||
child: Material(
|
child: Container(
|
||||||
elevation: 10,
|
margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10),
|
||||||
borderRadius: BorderRadius.circular(24),
|
decoration: BoxDecoration(
|
||||||
color: CustomTheme.primaryColor,
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
color: CustomTheme.primaryColor,
|
||||||
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:game_tracker/core/custom_theme.dart';
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/data/db/database.dart';
|
||||||
|
import 'package:game_tracker/data/dto/game.dart';
|
||||||
|
import 'package:game_tracker/data/dto/group.dart';
|
||||||
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
|
import 'package:game_tracker/presentation/views/main_menu/create_group_view.dart';
|
||||||
|
import 'package:game_tracker/presentation/views/main_menu/game_result_view.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/game_history_tile.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/double_row_info_tile.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class GameHistoryView extends StatefulWidget {
|
class GameHistoryView extends StatefulWidget {
|
||||||
const GameHistoryView({super.key});
|
const GameHistoryView({super.key});
|
||||||
@@ -11,119 +21,36 @@ class GameHistoryView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _GameHistoryViewState extends State<GameHistoryView> {
|
class _GameHistoryViewState extends State<GameHistoryView> {
|
||||||
final allGameData = [
|
late Future<List<Game>> _gameListFuture;
|
||||||
{
|
late final AppDatabase db;
|
||||||
'game': 'Schach',
|
|
||||||
'title': 'Abendpartie',
|
late final List<Game> skeletonData = List.filled(
|
||||||
'players': 2,
|
4,
|
||||||
'group': 'Familie',
|
Game(
|
||||||
'date': '01.06.2024',
|
name: 'Skeleton Game',
|
||||||
},
|
group: Group(
|
||||||
{
|
name: 'Skeleton Group',
|
||||||
'game': 'Monopoly',
|
members: [
|
||||||
'title': 'Wochenendspaß mit Gras du Saas',
|
Player(name: 'Player 1'),
|
||||||
'players': 4,
|
Player(name: 'Player 2'),
|
||||||
'group': 'Freunde',
|
Player(name: 'Player 3'),
|
||||||
'date': '28.05.2024',
|
Player(name: 'Long Name Player 4'),
|
||||||
},
|
Player(name: 'Player 5'),
|
||||||
{
|
],
|
||||||
'game': 'Catan',
|
),
|
||||||
'title': 'Strategieabend',
|
winner: Player(name: 'Skeleton Player 1'),
|
||||||
'players': 3,
|
players: [Player(name: 'Skeleton Player 6')],
|
||||||
'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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
suggestedGameData = List.from(allGameData);
|
db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
|
_gameListFuture = Future.delayed(
|
||||||
|
const Duration(milliseconds: 250),
|
||||||
|
() => db.gameDao.getAllGames(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -131,41 +58,87 @@ class _GameHistoryViewState extends State<GameHistoryView> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: CustomTheme.backgroundColor,
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
FutureBuilder<List<Game>>(
|
||||||
children: [
|
future: _gameListFuture,
|
||||||
Container(margin: EdgeInsets.only(bottom: 75)),
|
builder:
|
||||||
Expanded(
|
(BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
|
||||||
child: gameHistoryListView(allGameData, suggestedGameData),
|
if (snapshot.hasError) {
|
||||||
),
|
return const Center(
|
||||||
],
|
child: TopCenteredMessage(
|
||||||
|
icon: Icons.report,
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Game data could not be loaded',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (snapshot.connectionState == ConnectionState.done &&
|
||||||
|
(!snapshot.hasData || snapshot.data!.isEmpty)) {
|
||||||
|
return const Center(
|
||||||
|
child: TopCenteredMessage(
|
||||||
|
icon: Icons.report,
|
||||||
|
title: 'Info',
|
||||||
|
message: 'No games created yet',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final bool isLoading =
|
||||||
|
snapshot.connectionState == ConnectionState.waiting;
|
||||||
|
final List<Game> games =
|
||||||
|
(isLoading ? skeletonData : (snapshot.data ?? [])
|
||||||
|
..sort(
|
||||||
|
(a, b) => b.createdAt.compareTo(a.createdAt),
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
return AppSkeleton(
|
||||||
|
enabled: isLoading,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(bottom: 85),
|
||||||
|
itemCount: games.length + 1,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
if (index == games.length) {
|
||||||
|
return SizedBox(
|
||||||
|
height: MediaQuery.paddingOf(context).bottom - 80,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return GameHistoryTile(
|
||||||
|
onTap: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
CupertinoPageRoute(
|
||||||
|
fullscreenDialog: true,
|
||||||
|
builder: (context) =>
|
||||||
|
GameResultView(game: games[index]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_gameListFuture = db.gameDao.getAllGames();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
game: games[index],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Container(
|
Positioned(
|
||||||
margin: EdgeInsets.only(top: 10, bottom: 10, left: 10, right: 10),
|
bottom: MediaQuery.paddingOf(context).bottom,
|
||||||
child: SearchBar(
|
child: CustomWidthButton(
|
||||||
leading: Icon(Icons.search),
|
text: 'Create Game',
|
||||||
onChanged: (value) {
|
sizeRelativeToWidth: 0.90,
|
||||||
if (value.isEmpty) {
|
onPressed: () async {
|
||||||
setState(() {
|
await Navigator.push(
|
||||||
suggestedGameData.clear();
|
context,
|
||||||
suggestedGameData.addAll(allGameData);
|
MaterialPageRoute(
|
||||||
});
|
builder: (context) {
|
||||||
return;
|
return const CreateGroupView();
|
||||||
}
|
},
|
||||||
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(() {
|
setState(() {
|
||||||
suggestedGameData.clear();
|
_gameListFuture = db.gameDao.getAllGames();
|
||||||
suggestedGameData.addAll(suggestions);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -175,24 +148,3 @@ class _GameHistoryViewState extends State<GameHistoryView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget gameHistoryListView(allGameData, suggestedGameData) {
|
|
||||||
if (suggestedGameData.isEmpty && allGameData.isEmpty) {
|
|
||||||
return TopCenteredMessage("Keine Spiele erstellt");
|
|
||||||
} else if (suggestedGameData.isEmpty) {
|
|
||||||
return TopCenteredMessage("Kein Spiel mit den Suchparametern gefunden.");
|
|
||||||
}
|
|
||||||
return ListView.builder(
|
|
||||||
itemCount: suggestedGameData.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final currentGame = suggestedGameData[index];
|
|
||||||
return doubleRowInfoTile(
|
|
||||||
currentGame['game'] + ": ",
|
|
||||||
currentGame['title'],
|
|
||||||
currentGame['players'].toString() + " Spieler",
|
|
||||||
currentGame['group'],
|
|
||||||
currentGame['date'],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
144
lib/presentation/views/main_menu/game_result_view.dart
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/data/db/database.dart';
|
||||||
|
import 'package:game_tracker/data/dto/game.dart';
|
||||||
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class GameResultView extends StatefulWidget {
|
||||||
|
final Game game;
|
||||||
|
|
||||||
|
const GameResultView({super.key, required this.game});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GameResultView> createState() => _GameResultViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GameResultViewState extends State<GameResultView> {
|
||||||
|
late final List<Player> allPlayers;
|
||||||
|
late final AppDatabase db;
|
||||||
|
Player? _selectedPlayer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
|
allPlayers = getAllPlayers(widget.game);
|
||||||
|
if (widget.game.winner != null) {
|
||||||
|
_selectedPlayer = allPlayers.firstWhere(
|
||||||
|
(p) => p.id == widget.game.winner!.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: Text(
|
||||||
|
widget.game.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10,
|
||||||
|
horizontal: 10,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: CustomTheme.boxColor,
|
||||||
|
border: Border.all(color: CustomTheme.boxBorder),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Select Winner:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Expanded(
|
||||||
|
child: RadioGroup<Player>(
|
||||||
|
groupValue: _selectedPlayer,
|
||||||
|
onChanged: (Player? value) async {
|
||||||
|
setState(() {
|
||||||
|
_selectedPlayer = value;
|
||||||
|
});
|
||||||
|
await _handleWinnerSaving();
|
||||||
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: allPlayers.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return CustomRadioListTile(
|
||||||
|
text: allPlayers[index].name,
|
||||||
|
value: allPlayers[index],
|
||||||
|
onContainerTap: (value) async {
|
||||||
|
setState(() {
|
||||||
|
// Check if the already selected player is the same as the newly tapped player.
|
||||||
|
if (_selectedPlayer == value) {
|
||||||
|
// If yes deselected the player by setting it to null.
|
||||||
|
_selectedPlayer = null;
|
||||||
|
} else {
|
||||||
|
// If no assign the newly tapped player to the selected player.
|
||||||
|
(_selectedPlayer = value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await _handleWinnerSaving();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleWinnerSaving() async {
|
||||||
|
if (_selectedPlayer == null) {
|
||||||
|
await db.gameDao.removeWinner(gameId: widget.game.id);
|
||||||
|
} else {
|
||||||
|
await db.gameDao.setWinner(
|
||||||
|
gameId: widget.game.id,
|
||||||
|
winnerId: _selectedPlayer!.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Player> getAllPlayers(Game game) {
|
||||||
|
if (game.group == null && game.players != null) {
|
||||||
|
return [...game.players!];
|
||||||
|
} else if (game.group != null && game.players != null) {
|
||||||
|
return [...game.players!, ...game.group!.members];
|
||||||
|
}
|
||||||
|
return [...game.group!.members];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,119 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/data/db/database.dart';
|
||||||
|
import 'package:game_tracker/data/dto/group.dart';
|
||||||
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
|
import 'package:game_tracker/presentation/views/main_menu/create_group_view.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class GroupsView extends StatelessWidget {
|
class GroupsView extends StatefulWidget {
|
||||||
const GroupsView({super.key});
|
const GroupsView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GroupsView> createState() => _GroupsViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GroupsViewState extends State<GroupsView> {
|
||||||
|
late Future<List<Group>> _allGroupsFuture;
|
||||||
|
late final AppDatabase db;
|
||||||
|
|
||||||
|
final player = Player(name: 'Skeleton Player');
|
||||||
|
late final List<Group> skeletonData = List.filled(
|
||||||
|
7,
|
||||||
|
Group(
|
||||||
|
name: 'Skeleton Game',
|
||||||
|
members: [player, player, player, player, player, player],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
|
_allGroupsFuture = Future.delayed(
|
||||||
|
const Duration(milliseconds: 250),
|
||||||
|
() => db.groupDao.getAllGroups(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Center(child: Text('Groups View'));
|
return Scaffold(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
body: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
FutureBuilder<List<Group>>(
|
||||||
|
future: _allGroupsFuture,
|
||||||
|
builder:
|
||||||
|
(BuildContext context, AsyncSnapshot<List<Group>> snapshot) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return const Center(
|
||||||
|
child: TopCenteredMessage(
|
||||||
|
icon: Icons.report,
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Group data couldn\'t\nbe loaded',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (snapshot.connectionState == ConnectionState.done &&
|
||||||
|
(!snapshot.hasData || snapshot.data!.isEmpty)) {
|
||||||
|
return const Center(
|
||||||
|
child: TopCenteredMessage(
|
||||||
|
icon: Icons.info,
|
||||||
|
title: 'Info',
|
||||||
|
message: 'No groups created yet',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final bool isLoading =
|
||||||
|
snapshot.connectionState == ConnectionState.waiting;
|
||||||
|
final List<Group> groups =
|
||||||
|
isLoading ? skeletonData : (snapshot.data ?? [])
|
||||||
|
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
|
return AppSkeleton(
|
||||||
|
enabled: isLoading,
|
||||||
|
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: 'Create Group',
|
||||||
|
sizeRelativeToWidth: 0.90,
|
||||||
|
onPressed: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return const CreateGroupView();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_allGroupsFuture = db.groupDao.getAllGroups();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:game_tracker/data/db/database.dart';
|
import 'package:game_tracker/data/db/database.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/game_tile.dart';
|
import 'package:game_tracker/data/dto/game.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/quick_create_button.dart';
|
import 'package:game_tracker/data/dto/group.dart';
|
||||||
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/game_tile.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
|
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart';
|
import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -16,6 +20,23 @@ class HomeView extends StatefulWidget {
|
|||||||
class _HomeViewState extends State<HomeView> {
|
class _HomeViewState extends State<HomeView> {
|
||||||
late Future<int> _gameCountFuture;
|
late Future<int> _gameCountFuture;
|
||||||
late Future<int> _groupCountFuture;
|
late Future<int> _groupCountFuture;
|
||||||
|
late Future<List<Game>> _recentGamesFuture;
|
||||||
|
bool isLoading = true;
|
||||||
|
|
||||||
|
late final List<Game> skeletonData = List.filled(
|
||||||
|
2,
|
||||||
|
Game(
|
||||||
|
name: 'Skeleton Game',
|
||||||
|
group: Group(
|
||||||
|
name: 'Skeleton Group',
|
||||||
|
members: [
|
||||||
|
Player(name: 'Skeleton Player 1'),
|
||||||
|
Player(name: 'Skeleton Player 2'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
winner: Player(name: 'Skeleton Player 1'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
@@ -23,123 +44,219 @@ class _HomeViewState extends State<HomeView> {
|
|||||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
_gameCountFuture = db.gameDao.getGameCount();
|
_gameCountFuture = db.gameDao.getGameCount();
|
||||||
_groupCountFuture = db.groupDao.getGroupCount();
|
_groupCountFuture = db.groupDao.getGroupCount();
|
||||||
|
_recentGamesFuture = db.gameDao.getAllGames();
|
||||||
|
|
||||||
|
Future.wait([_gameCountFuture, _groupCountFuture, _recentGamesFuture]).then(
|
||||||
|
(_) async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 250));
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
return SingleChildScrollView(
|
return AppSkeleton(
|
||||||
child: Column(
|
enabled: isLoading,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: SingleChildScrollView(
|
||||||
children: [
|
child: Column(
|
||||||
Row(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
children: [
|
Row(
|
||||||
FutureBuilder<int>(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
future: _gameCountFuture,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final int count = (snapshot.hasData) ? snapshot.data! : 0;
|
|
||||||
return QuickInfoTile(
|
|
||||||
width: constraints.maxWidth * 0.45,
|
|
||||||
height: constraints.maxHeight * 0.15,
|
|
||||||
title: 'Games',
|
|
||||||
icon: Icons.groups_rounded,
|
|
||||||
value: count,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(width: constraints.maxWidth * 0.05),
|
|
||||||
FutureBuilder<int>(
|
|
||||||
future: _groupCountFuture,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final int count =
|
|
||||||
(snapshot.connectionState == ConnectionState.done &&
|
|
||||||
snapshot.hasData)
|
|
||||||
? snapshot.data!
|
|
||||||
: 0;
|
|
||||||
return QuickInfoTile(
|
|
||||||
width: constraints.maxWidth * 0.45,
|
|
||||||
height: constraints.maxHeight * 0.15,
|
|
||||||
title: 'Groups',
|
|
||||||
icon: Icons.groups_rounded,
|
|
||||||
value: count,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
|
||||||
child: InfoTile(
|
|
||||||
width: constraints.maxWidth * 0.95,
|
|
||||||
title: 'Recent Games',
|
|
||||||
icon: Icons.timer,
|
|
||||||
content: const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 40.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
GameTile(
|
|
||||||
gameTitle: 'Gamenight',
|
|
||||||
gameType: 'Cabo',
|
|
||||||
ruleset: 'Lowest Points',
|
|
||||||
players: '5 Players',
|
|
||||||
winner: 'Leonard',
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: Divider(),
|
|
||||||
),
|
|
||||||
GameTile(
|
|
||||||
gameTitle: 'Schoolbreak',
|
|
||||||
gameType: 'Uno',
|
|
||||||
ruleset: 'Highest Points',
|
|
||||||
players: 'The Gang',
|
|
||||||
winner: 'Lina',
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
width: constraints.maxWidth * 0.95,
|
|
||||||
title: 'Quick Create',
|
|
||||||
icon: Icons.add_box_rounded,
|
|
||||||
content: Column(
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
FutureBuilder<int>(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
future: _gameCountFuture,
|
||||||
children: [
|
builder: (context, snapshot) {
|
||||||
QuickCreateButton(text: 'Category 1', onPressed: () {}),
|
final int count = (snapshot.hasData)
|
||||||
QuickCreateButton(text: 'Category 2', onPressed: () {}),
|
? snapshot.data!
|
||||||
],
|
: 0;
|
||||||
|
return QuickInfoTile(
|
||||||
|
width: constraints.maxWidth * 0.45,
|
||||||
|
height: constraints.maxHeight * 0.15,
|
||||||
|
title: 'Games',
|
||||||
|
icon: Icons.groups_rounded,
|
||||||
|
value: count,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Row(
|
SizedBox(width: constraints.maxWidth * 0.05),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
FutureBuilder<int>(
|
||||||
children: [
|
future: _groupCountFuture,
|
||||||
QuickCreateButton(text: 'Category 3', onPressed: () {}),
|
builder: (context, snapshot) {
|
||||||
QuickCreateButton(text: 'Category 4', onPressed: () {}),
|
final int count =
|
||||||
],
|
(snapshot.connectionState == ConnectionState.done &&
|
||||||
),
|
snapshot.hasData)
|
||||||
Row(
|
? snapshot.data!
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
: 0;
|
||||||
children: [
|
return QuickInfoTile(
|
||||||
QuickCreateButton(text: 'Category 5', onPressed: () {}),
|
width: constraints.maxWidth * 0.45,
|
||||||
QuickCreateButton(text: 'Category 6', onPressed: () {}),
|
height: constraints.maxHeight * 0.15,
|
||||||
],
|
title: 'Groups',
|
||||||
|
icon: Icons.groups_rounded,
|
||||||
|
value: count,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
Padding(
|
||||||
],
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
|
child: InfoTile(
|
||||||
|
width: constraints.maxWidth * 0.95,
|
||||||
|
title: 'Recent Games',
|
||||||
|
icon: Icons.timer,
|
||||||
|
content: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||||
|
child: FutureBuilder(
|
||||||
|
future: _recentGamesFuture,
|
||||||
|
builder:
|
||||||
|
(
|
||||||
|
BuildContext context,
|
||||||
|
AsyncSnapshot<List<Game>> snapshot,
|
||||||
|
) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return const Center(
|
||||||
|
heightFactor: 4,
|
||||||
|
child: Text(
|
||||||
|
'Error while loading recent games.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final List<Game> games =
|
||||||
|
(isLoading
|
||||||
|
? skeletonData
|
||||||
|
: (snapshot.data ?? [])
|
||||||
|
..sort(
|
||||||
|
(a, b) => b.createdAt.compareTo(
|
||||||
|
a.createdAt,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.take(2)
|
||||||
|
.toList();
|
||||||
|
if (games.isNotEmpty) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
GameTile(
|
||||||
|
gameTitle: games[0].name,
|
||||||
|
gameType: 'Winner',
|
||||||
|
ruleset: 'Ruleset',
|
||||||
|
players: _getPlayerText(games[0]),
|
||||||
|
winner: games[0].winner == null
|
||||||
|
? 'Game in progress...'
|
||||||
|
: games[0].winner!.name,
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
if (games.length > 1) ...[
|
||||||
|
GameTile(
|
||||||
|
gameTitle: games[1].name,
|
||||||
|
gameType: 'Winner',
|
||||||
|
ruleset: 'Ruleset',
|
||||||
|
players: _getPlayerText(games[1]),
|
||||||
|
winner: games[1].winner == null
|
||||||
|
? 'Game in progress...'
|
||||||
|
: games[1].winner!.name,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
] else ...[
|
||||||
|
const Center(
|
||||||
|
heightFactor: 4,
|
||||||
|
child: Text(
|
||||||
|
'No second game available.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const Center(
|
||||||
|
heightFactor: 12,
|
||||||
|
child: Text('No recent games available.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InfoTile(
|
||||||
|
width: constraints.maxWidth * 0.95,
|
||||||
|
title: 'Quick Create',
|
||||||
|
icon: Icons.add_box_rounded,
|
||||||
|
content: Column(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
QuickCreateButton(
|
||||||
|
text: 'Category 1',
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
QuickCreateButton(
|
||||||
|
text: 'Category 2',
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
QuickCreateButton(
|
||||||
|
text: 'Category 3',
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
QuickCreateButton(
|
||||||
|
text: 'Category 4',
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
QuickCreateButton(
|
||||||
|
text: 'Category 5',
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
QuickCreateButton(
|
||||||
|
text: 'Category 6',
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getPlayerText(Game game) {
|
||||||
|
if (game.group == null) {
|
||||||
|
final playerCount = game.players?.length ?? 0;
|
||||||
|
return '$playerCount Players';
|
||||||
|
}
|
||||||
|
if (game.players == null || game.players!.isEmpty) {
|
||||||
|
return game.group!.name;
|
||||||
|
}
|
||||||
|
return '${game.group!.name} + ${game.players!.length}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,191 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/core/enums.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart';
|
||||||
|
import 'package:game_tracker/services/data_transfer_service.dart';
|
||||||
|
|
||||||
class SettingsView extends StatelessWidget {
|
class SettingsView extends StatefulWidget {
|
||||||
const SettingsView({super.key});
|
const SettingsView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsView> createState() => _SettingsViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsViewState extends State<SettingsView> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Einstellungen')),
|
appBar: AppBar(backgroundColor: CustomTheme.backgroundColor),
|
||||||
body: const Center(child: Text('Settings View')),
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
body: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) =>
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(24, 0, 24, 10),
|
||||||
|
child: Text(
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
'Menu',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||||
|
child: Text(
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
'Settings',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SettingsListTile(
|
||||||
|
title: 'Export data',
|
||||||
|
icon: Icons.upload_outlined,
|
||||||
|
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||||
|
onPressed: () async {
|
||||||
|
final String json =
|
||||||
|
await DataTransferService.getAppDataAsJson(context);
|
||||||
|
final result = await DataTransferService.exportData(
|
||||||
|
json,
|
||||||
|
'game_tracker-data',
|
||||||
|
);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
showExportSnackBar(context: context, result: result);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SettingsListTile(
|
||||||
|
title: 'Import data',
|
||||||
|
icon: Icons.download_outlined,
|
||||||
|
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await DataTransferService.importData(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
showImportSnackBar(context: context, result: result);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SettingsListTile(
|
||||||
|
title: 'Delete all data',
|
||||||
|
icon: Icons.download_outlined,
|
||||||
|
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Delete all data?'),
|
||||||
|
content: const Text('This can\'t be undone'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Abbrechen'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Löschen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).then((confirmed) {
|
||||||
|
if (confirmed == true && context.mounted) {
|
||||||
|
DataTransferService.deleteAllData(context);
|
||||||
|
showSnackbar(
|
||||||
|
context: context,
|
||||||
|
message: 'Daten erfolgreich gelöscht',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays a snackbar based on the import result.
|
||||||
|
///
|
||||||
|
/// [context] The BuildContext to show the snackbar in.
|
||||||
|
/// [result] The result of the import operation.
|
||||||
|
void showImportSnackBar({
|
||||||
|
required BuildContext context,
|
||||||
|
required ImportResult result,
|
||||||
|
}) {
|
||||||
|
switch (result) {
|
||||||
|
case ImportResult.success:
|
||||||
|
showSnackbar(context: context, message: 'Data successfully imported');
|
||||||
|
case ImportResult.invalidSchema:
|
||||||
|
showSnackbar(context: context, message: 'Invalid Schema');
|
||||||
|
case ImportResult.fileReadError:
|
||||||
|
showSnackbar(context: context, message: 'Error reading file');
|
||||||
|
case ImportResult.canceled:
|
||||||
|
showSnackbar(context: context, message: 'Import canceled');
|
||||||
|
case ImportResult.formatException:
|
||||||
|
showSnackbar(
|
||||||
|
context: context,
|
||||||
|
message: 'Format Exception (see console)',
|
||||||
|
);
|
||||||
|
case ImportResult.unknownException:
|
||||||
|
showSnackbar(
|
||||||
|
context: context,
|
||||||
|
message: 'Unknown Exception (see console)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays a snackbar based on the export result.
|
||||||
|
///
|
||||||
|
/// [context] The BuildContext to show the snackbar in.
|
||||||
|
/// [result] The result of the export operation.
|
||||||
|
void showExportSnackBar({
|
||||||
|
required BuildContext context,
|
||||||
|
required ExportResult result,
|
||||||
|
}) {
|
||||||
|
switch (result) {
|
||||||
|
case ExportResult.success:
|
||||||
|
showSnackbar(context: context, message: 'Data successfully exported');
|
||||||
|
case ExportResult.canceled:
|
||||||
|
showSnackbar(context: context, message: 'Export canceled');
|
||||||
|
case ExportResult.unknownException:
|
||||||
|
showSnackbar(
|
||||||
|
context: context,
|
||||||
|
message: 'Unknown Exception (see console)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays a snackbar with the given message and optional action.
|
||||||
|
///
|
||||||
|
/// [context] The BuildContext to show the snackbar in.
|
||||||
|
/// [message] The message to display in the snackbar.
|
||||||
|
/// [duration] The duration for which the snackbar is displayed.
|
||||||
|
/// [action] An optional callback function to execute when the action button is pressed.
|
||||||
|
void showSnackbar({
|
||||||
|
required BuildContext context,
|
||||||
|
required String message,
|
||||||
|
Duration duration = const Duration(seconds: 3),
|
||||||
|
VoidCallback? action,
|
||||||
|
}) {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
messenger.hideCurrentSnackBar();
|
||||||
|
messenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message, style: const TextStyle(color: Colors.white)),
|
||||||
|
backgroundColor: CustomTheme.onBoxColor,
|
||||||
|
duration: duration,
|
||||||
|
action: action != null
|
||||||
|
? SnackBarAction(label: 'Rückgängig', onPressed: action)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,241 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/data/db/database.dart';
|
||||||
|
import 'package:game_tracker/data/dto/game.dart';
|
||||||
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class StatisticsView extends StatelessWidget {
|
class StatisticsView extends StatefulWidget {
|
||||||
const StatisticsView({super.key});
|
const StatisticsView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatisticsView> createState() => _StatisticsViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatisticsViewState extends State<StatisticsView> {
|
||||||
|
late Future<List<Game>> _gamesFuture;
|
||||||
|
late Future<List<Player>> _playersFuture;
|
||||||
|
List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1));
|
||||||
|
List<(String, int)> gameCounts = List.filled(6, ('Skeleton Player', 1));
|
||||||
|
List<(String, double)> winRates = List.filled(6, ('Skeleton Player', 1));
|
||||||
|
bool isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
|
_gamesFuture = db.gameDao.getAllGames();
|
||||||
|
_playersFuture = db.playerDao.getAllPlayers();
|
||||||
|
|
||||||
|
Future.wait([_gamesFuture, _playersFuture]).then((results) async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 250));
|
||||||
|
final games = results[0] as List<Game>;
|
||||||
|
final players = results[1] as List<Player>;
|
||||||
|
winCounts = _calculateWinsForAllPlayers(games, players);
|
||||||
|
gameCounts = _calculateGameAmountsForAllPlayers(games, players);
|
||||||
|
winRates = computeWinRatePercent(wins: winCounts, games: gameCounts);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Center(child: Text('Statistics View'));
|
return LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: AppSkeleton(
|
||||||
|
enabled: isLoading,
|
||||||
|
fixLayoutBuilder: true,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: constraints.maxHeight * 0.01),
|
||||||
|
StatisticsTile(
|
||||||
|
icon: Icons.sports_score,
|
||||||
|
title: 'Wins per Player',
|
||||||
|
width: constraints.maxWidth * 0.95,
|
||||||
|
values: winCounts,
|
||||||
|
itemCount: 3,
|
||||||
|
barColor: Colors.blue,
|
||||||
|
),
|
||||||
|
SizedBox(height: constraints.maxHeight * 0.02),
|
||||||
|
StatisticsTile(
|
||||||
|
icon: Icons.percent,
|
||||||
|
title: 'Winrate per Player',
|
||||||
|
width: constraints.maxWidth * 0.95,
|
||||||
|
values: winRates,
|
||||||
|
itemCount: 5,
|
||||||
|
barColor: Colors.orange[700]!,
|
||||||
|
),
|
||||||
|
SizedBox(height: constraints.maxHeight * 0.02),
|
||||||
|
StatisticsTile(
|
||||||
|
icon: Icons.casino,
|
||||||
|
title: 'Games per Player',
|
||||||
|
width: constraints.maxWidth * 0.95,
|
||||||
|
values: gameCounts,
|
||||||
|
itemCount: 10,
|
||||||
|
barColor: Colors.green,
|
||||||
|
),
|
||||||
|
SizedBox(height: MediaQuery.paddingOf(context).bottom),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the number of wins for each player
|
||||||
|
/// and returns a sorted list of tuples (playerName, winCount)
|
||||||
|
List<(String, int)> _calculateWinsForAllPlayers(
|
||||||
|
List<Game> games,
|
||||||
|
List<Player> players,
|
||||||
|
) {
|
||||||
|
List<(String, int)> winCounts = [];
|
||||||
|
|
||||||
|
// Getting the winners
|
||||||
|
for (var game in games) {
|
||||||
|
final winner = game.winner;
|
||||||
|
if (winner != null) {
|
||||||
|
final index = winCounts.indexWhere((entry) => entry.$1 == winner.id);
|
||||||
|
// -1 means winner not found in winCounts
|
||||||
|
if (index != -1) {
|
||||||
|
final current = winCounts[index].$2;
|
||||||
|
winCounts[index] = (winner.id, current + 1);
|
||||||
|
} else {
|
||||||
|
winCounts.add((winner.id, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding all players with zero wins
|
||||||
|
for (var player in players) {
|
||||||
|
final index = winCounts.indexWhere((entry) => entry.$1 == player.id);
|
||||||
|
// -1 means player not found in winCounts
|
||||||
|
if (index == -1) {
|
||||||
|
winCounts.add((player.id, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace player IDs with names
|
||||||
|
for (int i = 0; i < winCounts.length; i++) {
|
||||||
|
final playerId = winCounts[i].$1;
|
||||||
|
final player = players.firstWhere(
|
||||||
|
(p) => p.id == playerId,
|
||||||
|
orElse: () => Player(id: playerId, name: 'N.a.'),
|
||||||
|
);
|
||||||
|
winCounts[i] = (player.name, winCounts[i].$2);
|
||||||
|
}
|
||||||
|
|
||||||
|
winCounts.sort((a, b) => b.$2.compareTo(a.$2));
|
||||||
|
|
||||||
|
return winCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the number of games played for each player
|
||||||
|
/// and returns a sorted list of tuples (playerName, gameCount)
|
||||||
|
List<(String, int)> _calculateGameAmountsForAllPlayers(
|
||||||
|
List<Game> games,
|
||||||
|
List<Player> players,
|
||||||
|
) {
|
||||||
|
List<(String, int)> gameCounts = [];
|
||||||
|
|
||||||
|
// Counting games for each player
|
||||||
|
for (var game in games) {
|
||||||
|
if (game.group != null) {
|
||||||
|
final members = game.group!.members.map((p) => p.id).toList();
|
||||||
|
for (var playerId in members) {
|
||||||
|
final index = gameCounts.indexWhere((entry) => entry.$1 == playerId);
|
||||||
|
// -1 means player not found in gameCounts
|
||||||
|
if (index != -1) {
|
||||||
|
final current = gameCounts[index].$2;
|
||||||
|
gameCounts[index] = (playerId, current + 1);
|
||||||
|
} else {
|
||||||
|
gameCounts.add((playerId, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (game.players != null) {
|
||||||
|
final members = game.players!.map((p) => p.id).toList();
|
||||||
|
for (var playerId in members) {
|
||||||
|
final index = gameCounts.indexWhere((entry) => entry.$1 == playerId);
|
||||||
|
// -1 means player not found in gameCounts
|
||||||
|
if (index != -1) {
|
||||||
|
final current = gameCounts[index].$2;
|
||||||
|
gameCounts[index] = (playerId, current + 1);
|
||||||
|
} else {
|
||||||
|
gameCounts.add((playerId, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding all players with zero games
|
||||||
|
for (var player in players) {
|
||||||
|
final index = gameCounts.indexWhere((entry) => entry.$1 == player.id);
|
||||||
|
// -1 means player not found in gameCounts
|
||||||
|
if (index == -1) {
|
||||||
|
gameCounts.add((player.id, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace player IDs with names
|
||||||
|
for (int i = 0; i < gameCounts.length; i++) {
|
||||||
|
final playerId = gameCounts[i].$1;
|
||||||
|
final player = players.firstWhere(
|
||||||
|
(p) => p.id == playerId,
|
||||||
|
orElse: () => Player(id: playerId, name: 'N.a.'),
|
||||||
|
);
|
||||||
|
gameCounts[i] = (player.name, gameCounts[i].$2);
|
||||||
|
}
|
||||||
|
|
||||||
|
gameCounts.sort((a, b) => b.$2.compareTo(a.$2));
|
||||||
|
|
||||||
|
return gameCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart
|
||||||
|
List<(String, double)> computeWinRatePercent({
|
||||||
|
required List<(String, int)> wins,
|
||||||
|
required List<(String, int)> games,
|
||||||
|
}) {
|
||||||
|
final Map<String, int> winsMap = {for (var e in wins) e.$1: e.$2};
|
||||||
|
final Map<String, int> gamesMap = {for (var e in games) e.$1: e.$2};
|
||||||
|
|
||||||
|
// Get all unique player names
|
||||||
|
final names = {...winsMap.keys, ...gamesMap.keys};
|
||||||
|
|
||||||
|
// Calculate win rates
|
||||||
|
final result = names.map((name) {
|
||||||
|
final int w = winsMap[name] ?? 0;
|
||||||
|
final int g = gamesMap[name] ?? 0;
|
||||||
|
// Calculate percentage and round to 2 decimal places
|
||||||
|
// Avoid division by zero
|
||||||
|
final double percent = (g > 0)
|
||||||
|
? double.parse(((w / g)).toStringAsFixed(2))
|
||||||
|
: 0;
|
||||||
|
return (name, percent);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Sort the result: first by winrate descending,
|
||||||
|
// then by wins descending in case of a tie
|
||||||
|
result.sort((a, b) {
|
||||||
|
final cmp = b.$2.compareTo(a.$2);
|
||||||
|
if (cmp != 0) return cmp;
|
||||||
|
final wa = winsMap[a.$1] ?? 0;
|
||||||
|
final wb = winsMap[b.$1] ?? 0;
|
||||||
|
return wb.compareTo(wa);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
lib/presentation/widgets/app_skeleton.dart
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
|
class AppSkeleton extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
final bool enabled;
|
||||||
|
final bool fixLayoutBuilder;
|
||||||
|
|
||||||
|
const AppSkeleton({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.enabled = true,
|
||||||
|
this.fixLayoutBuilder = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppSkeleton> createState() => _AppSkeletonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppSkeletonState extends State<AppSkeleton> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Skeletonizer(
|
||||||
|
effect: PulseEffect(
|
||||||
|
from: Colors.grey[800]!,
|
||||||
|
to: Colors.grey[600]!,
|
||||||
|
duration: const Duration(milliseconds: 800),
|
||||||
|
),
|
||||||
|
enabled: widget.enabled,
|
||||||
|
enableSwitchAnimation: true,
|
||||||
|
switchAnimationConfig: SwitchAnimationConfig(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
switchInCurve: Curves.linear,
|
||||||
|
switchOutCurve: Curves.linear,
|
||||||
|
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
|
||||||
|
layoutBuilder: !widget.fixLayoutBuilder
|
||||||
|
? AnimatedSwitcher.defaultLayoutBuilder
|
||||||
|
: (Widget? currentChild, List<Widget> previousChildren) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
children: [
|
||||||
|
...previousChildren,
|
||||||
|
if (currentChild != null) currentChild,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
120
lib/presentation/widgets/buttons/custom_width_button.dart
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/core/enums.dart';
|
||||||
|
|
||||||
|
class CustomWidthButton extends StatelessWidget {
|
||||||
|
const CustomWidthButton({
|
||||||
|
super.key,
|
||||||
|
required this.text,
|
||||||
|
this.buttonType = ButtonType.primary,
|
||||||
|
required this.sizeRelativeToWidth,
|
||||||
|
this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final double sizeRelativeToWidth;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final ButtonType buttonType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Color buttonBackgroundColor;
|
||||||
|
final Color disabledBackgroundColor;
|
||||||
|
final Color borderSideColor;
|
||||||
|
final Color textcolor;
|
||||||
|
final Color disabledTextColor;
|
||||||
|
|
||||||
|
if (buttonType == ButtonType.primary) {
|
||||||
|
textcolor = Colors.white;
|
||||||
|
disabledTextColor = Color.lerp(textcolor, Colors.black, 0.5)!;
|
||||||
|
buttonBackgroundColor = CustomTheme.primaryColor;
|
||||||
|
disabledBackgroundColor = Color.lerp(
|
||||||
|
buttonBackgroundColor,
|
||||||
|
Colors.black,
|
||||||
|
0.5,
|
||||||
|
)!;
|
||||||
|
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
foregroundColor: textcolor,
|
||||||
|
disabledForegroundColor: disabledTextColor,
|
||||||
|
backgroundColor: buttonBackgroundColor,
|
||||||
|
disabledBackgroundColor: disabledBackgroundColor,
|
||||||
|
animationDuration: const Duration(),
|
||||||
|
minimumSize: Size(
|
||||||
|
MediaQuery.sizeOf(context).width * sizeRelativeToWidth,
|
||||||
|
60,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 22),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (buttonType == ButtonType.secondary) {
|
||||||
|
textcolor = CustomTheme.primaryColor;
|
||||||
|
disabledTextColor = Color.lerp(textcolor, Colors.black, 0.5)!;
|
||||||
|
buttonBackgroundColor = Colors.transparent;
|
||||||
|
disabledBackgroundColor = Colors.transparent;
|
||||||
|
borderSideColor = onPressed != null
|
||||||
|
? CustomTheme.primaryColor
|
||||||
|
: Color.lerp(CustomTheme.primaryColor, Colors.black, 0.5)!;
|
||||||
|
|
||||||
|
return OutlinedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: textcolor,
|
||||||
|
disabledForegroundColor: disabledTextColor,
|
||||||
|
backgroundColor: buttonBackgroundColor,
|
||||||
|
disabledBackgroundColor: disabledBackgroundColor,
|
||||||
|
animationDuration: const Duration(),
|
||||||
|
minimumSize: Size(
|
||||||
|
MediaQuery.sizeOf(context).width * sizeRelativeToWidth,
|
||||||
|
60,
|
||||||
|
),
|
||||||
|
side: BorderSide(color: borderSideColor, width: 2),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 22),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
textcolor = CustomTheme.primaryColor;
|
||||||
|
disabledTextColor = Color.lerp(
|
||||||
|
CustomTheme.primaryColor,
|
||||||
|
Colors.black,
|
||||||
|
0.5,
|
||||||
|
)!;
|
||||||
|
buttonBackgroundColor = Colors.transparent;
|
||||||
|
disabledBackgroundColor = Colors.transparent;
|
||||||
|
|
||||||
|
return TextButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: textcolor,
|
||||||
|
disabledForegroundColor: disabledTextColor,
|
||||||
|
backgroundColor: buttonBackgroundColor,
|
||||||
|
disabledBackgroundColor: disabledBackgroundColor,
|
||||||
|
animationDuration: const Duration(),
|
||||||
|
minimumSize: Size(
|
||||||
|
MediaQuery.sizeOf(context).width * sizeRelativeToWidth,
|
||||||
|
60,
|
||||||
|
),
|
||||||
|
side: const BorderSide(style: BorderStyle.none),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 22),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:game_tracker/core/custom_theme.dart';
|
|
||||||
|
|
||||||
Widget doubleRowInfoTile(
|
|
||||||
String titleOneUpperLeft,
|
|
||||||
String titleTwoUpperLeft,
|
|
||||||
String titleUpperRight,
|
|
||||||
String titleLowerLeft,
|
|
||||||
String titleLowerRight,
|
|
||||||
) {
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
|
||||||
padding: EdgeInsets.all(10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
color: CustomTheme.secondaryColor,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: 10,
|
|
||||||
child: Text(
|
|
||||||
"$titleOneUpperLeft $titleTwoUpperLeft",
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Spacer(),
|
|
||||||
Expanded(
|
|
||||||
flex: 3,
|
|
||||||
child: Text(
|
|
||||||
"$titleUpperRight",
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: 10,
|
|
||||||
child: Text(
|
|
||||||
"$titleLowerLeft",
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Spacer(),
|
|
||||||
Expanded(
|
|
||||||
flex: 4,
|
|
||||||
child: Text(
|
|
||||||
"$titleLowerRight",
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
267
lib/presentation/widgets/player_selection.dart
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/data/db/database.dart';
|
||||||
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/text_icon_list_tile.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class PlayerSelection extends StatefulWidget {
|
||||||
|
final Function(List<Player> value) onChanged;
|
||||||
|
final List<Player> initialPlayers;
|
||||||
|
|
||||||
|
const PlayerSelection({
|
||||||
|
super.key,
|
||||||
|
required this.onChanged,
|
||||||
|
this.initialPlayers = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PlayerSelection> createState() => _PlayerSelectionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlayerSelectionState extends State<PlayerSelection> {
|
||||||
|
List<Player> selectedPlayers = [];
|
||||||
|
List<Player> suggestedPlayers = [];
|
||||||
|
List<Player> allPlayers = [];
|
||||||
|
late final TextEditingController _searchBarController =
|
||||||
|
TextEditingController();
|
||||||
|
late final AppDatabase db;
|
||||||
|
late Future<List<Player>> _allPlayersFuture;
|
||||||
|
late final List<Player> skeletonData = List.filled(
|
||||||
|
7,
|
||||||
|
Player(name: 'Player 0'),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
|
loadPlayerList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadPlayerList() {
|
||||||
|
_allPlayersFuture = Future.delayed(
|
||||||
|
const Duration(milliseconds: 250),
|
||||||
|
() => db.playerDao.getAllPlayers(),
|
||||||
|
);
|
||||||
|
suggestedPlayers = skeletonData;
|
||||||
|
_allPlayersFuture.then((loadedPlayers) {
|
||||||
|
setState(() {
|
||||||
|
if (widget.initialPlayers.isNotEmpty) {
|
||||||
|
allPlayers = [...widget.initialPlayers];
|
||||||
|
suggestedPlayers = [...widget.initialPlayers];
|
||||||
|
} else {
|
||||||
|
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||||
|
allPlayers = [...loadedPlayers];
|
||||||
|
suggestedPlayers = [...loadedPlayers];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
||||||
|
decoration: CustomTheme.standardBoxDecoration,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CustomSearchBar(
|
||||||
|
controller: _searchBarController,
|
||||||
|
constraints: const BoxConstraints(maxHeight: 45, minHeight: 45),
|
||||||
|
hintText: 'Search for players',
|
||||||
|
trailingButtonShown: true,
|
||||||
|
trailingButtonicon: Icons.add_circle,
|
||||||
|
trailingButtonEnabled: _searchBarController.text.trim().isNotEmpty,
|
||||||
|
onTrailingButtonPressed: () async {
|
||||||
|
addNewPlayerFromSearch(context: context);
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
// Filters the list of suggested players based on the search input.
|
||||||
|
if (value.isEmpty) {
|
||||||
|
// If the search is empty, it shows all unselected players.
|
||||||
|
suggestedPlayers = allPlayers.where((player) {
|
||||||
|
return !selectedPlayers.contains(player);
|
||||||
|
}).toList();
|
||||||
|
} else {
|
||||||
|
// If there is input, it filters by name match (case-insensitive) and ensures
|
||||||
|
// that already selected players are excluded from the results.
|
||||||
|
suggestedPlayers = allPlayers.where((player) {
|
||||||
|
final bool nameMatches = player.name.toLowerCase().contains(
|
||||||
|
value.toLowerCase(),
|
||||||
|
);
|
||||||
|
final bool isNotSelected = !selectedPlayers.contains(
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
return nameMatches && isNotSelected;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'Selected players: (${selectedPlayers.length})',
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Wrap(
|
||||||
|
alignment: WrapAlignment.start,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.start,
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: <Widget>[
|
||||||
|
// Generates a TextIconTile for each selected player.
|
||||||
|
for (var player in selectedPlayers)
|
||||||
|
TextIconTile(
|
||||||
|
text: player.name,
|
||||||
|
onIconTap: () {
|
||||||
|
setState(() {
|
||||||
|
// Removes the player from the selection and notifies the parent.
|
||||||
|
final currentSearch = _searchBarController.text
|
||||||
|
.toLowerCase();
|
||||||
|
selectedPlayers.remove(player);
|
||||||
|
widget.onChanged([...selectedPlayers]);
|
||||||
|
// If the player matches the current search query (or search is empty),
|
||||||
|
// they are added back to the suggestions and the list is re-sorted.
|
||||||
|
if (currentSearch.isEmpty ||
|
||||||
|
player.name.toLowerCase().contains(currentSearch)) {
|
||||||
|
suggestedPlayers.add(player);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text(
|
||||||
|
'All players:',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
FutureBuilder(
|
||||||
|
future: _allPlayersFuture,
|
||||||
|
builder:
|
||||||
|
(BuildContext context, AsyncSnapshot<List<Player>> snapshot) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return const Center(
|
||||||
|
child: TopCenteredMessage(
|
||||||
|
icon: Icons.report,
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Player data couldn\'t\nbe loaded.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
bool doneLoading =
|
||||||
|
snapshot.connectionState == ConnectionState.done;
|
||||||
|
bool snapshotDataEmpty =
|
||||||
|
!snapshot.hasData || snapshot.data!.isEmpty;
|
||||||
|
if (doneLoading &&
|
||||||
|
(snapshotDataEmpty && allPlayers.isEmpty)) {
|
||||||
|
return const Center(
|
||||||
|
child: TopCenteredMessage(
|
||||||
|
icon: Icons.info,
|
||||||
|
title: 'Info',
|
||||||
|
message: 'No players created yet.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final bool isLoading =
|
||||||
|
snapshot.connectionState == ConnectionState.waiting;
|
||||||
|
return Expanded(
|
||||||
|
child: AppSkeleton(
|
||||||
|
enabled: isLoading,
|
||||||
|
child: Visibility(
|
||||||
|
visible:
|
||||||
|
(suggestedPlayers.isEmpty && allPlayers.isNotEmpty),
|
||||||
|
replacement: ListView.builder(
|
||||||
|
itemCount: suggestedPlayers.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return TextIconListTile(
|
||||||
|
text: suggestedPlayers[index].name,
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
if (!selectedPlayers.contains(
|
||||||
|
suggestedPlayers[index],
|
||||||
|
)) {
|
||||||
|
selectedPlayers.add(
|
||||||
|
suggestedPlayers[index],
|
||||||
|
);
|
||||||
|
widget.onChanged([...selectedPlayers]);
|
||||||
|
suggestedPlayers.remove(
|
||||||
|
suggestedPlayers[index],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: TopCenteredMessage(
|
||||||
|
icon: Icons.info,
|
||||||
|
title: 'Info',
|
||||||
|
message: (selectedPlayers.length == allPlayers.length)
|
||||||
|
? 'No more players to add.'
|
||||||
|
: 'No players found with that name.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new player to the database from the search bar input.
|
||||||
|
/// Shows a snackbar indicating success or failure.
|
||||||
|
/// [context] - BuildContext to show the snackbar.
|
||||||
|
void addNewPlayerFromSearch({required BuildContext context}) async {
|
||||||
|
String playerName = _searchBarController.text.trim();
|
||||||
|
Player createdPlayer = Player(name: playerName);
|
||||||
|
bool success = await db.playerDao.addPlayer(player: createdPlayer);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (success) {
|
||||||
|
selectedPlayers.add(createdPlayer);
|
||||||
|
widget.onChanged([...selectedPlayers]);
|
||||||
|
allPlayers.add(createdPlayer);
|
||||||
|
setState(() {
|
||||||
|
_searchBarController.clear();
|
||||||
|
suggestedPlayers = allPlayers.where((player) {
|
||||||
|
return !selectedPlayers.contains(player);
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
backgroundColor: CustomTheme.boxColor,
|
||||||
|
content: Center(
|
||||||
|
child: Text(
|
||||||
|
'Successfully added player $playerName.',
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
backgroundColor: CustomTheme.boxColor,
|
||||||
|
content: Center(
|
||||||
|
child: Text(
|
||||||
|
'Could not add player $playerName.',
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/presentation/widgets/text_input/custom_search_bar.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
|
||||||
|
class CustomSearchBar extends StatelessWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String hintText;
|
||||||
|
final ValueChanged<String>? onChanged;
|
||||||
|
final BoxConstraints? constraints;
|
||||||
|
final bool trailingButtonShown;
|
||||||
|
final bool trailingButtonEnabled;
|
||||||
|
final VoidCallback? onTrailingButtonPressed;
|
||||||
|
final IconData trailingButtonicon;
|
||||||
|
|
||||||
|
const CustomSearchBar({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.hintText,
|
||||||
|
this.trailingButtonShown = false,
|
||||||
|
this.trailingButtonicon = Icons.clear,
|
||||||
|
this.trailingButtonEnabled = true,
|
||||||
|
this.onTrailingButtonPressed,
|
||||||
|
this.onChanged,
|
||||||
|
this.constraints,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SearchBar(
|
||||||
|
controller: controller,
|
||||||
|
constraints:
|
||||||
|
constraints ?? const BoxConstraints(maxHeight: 45, minHeight: 45),
|
||||||
|
hintText: hintText,
|
||||||
|
onChanged: trailingButtonEnabled ? onChanged : null,
|
||||||
|
hintStyle: WidgetStateProperty.all(const TextStyle(fontSize: 16)),
|
||||||
|
leading: const Icon(Icons.search),
|
||||||
|
trailing: [
|
||||||
|
Visibility(
|
||||||
|
visible: trailingButtonShown,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: trailingButtonEnabled ? onTrailingButtonPressed : null,
|
||||||
|
child: Icon(
|
||||||
|
trailingButtonicon,
|
||||||
|
color: trailingButtonEnabled
|
||||||
|
? null
|
||||||
|
: Colors.grey.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
],
|
||||||
|
backgroundColor: WidgetStateProperty.all(CustomTheme.boxColor),
|
||||||
|
side: WidgetStateProperty.all(BorderSide(color: CustomTheme.boxBorder)),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
elevation: WidgetStateProperty.all(0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
lib/presentation/widgets/text_input/text_input_field.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
|
||||||
|
class TextInputField extends StatelessWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final ValueChanged<String>? onChanged;
|
||||||
|
final String hintText;
|
||||||
|
|
||||||
|
const TextInputField({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.hintText,
|
||||||
|
this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
onChanged: onChanged,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
filled: true,
|
||||||
|
fillColor: CustomTheme.boxColor,
|
||||||
|
hintText: hintText,
|
||||||
|
hintStyle: const TextStyle(fontSize: 18),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
borderSide: BorderSide(color: CustomTheme.boxBorder),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
borderSide: BorderSide(color: CustomTheme.boxBorder),
|
||||||
|
),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
lib/presentation/widgets/tiles/choose_tile.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
|
||||||
|
class ChooseTile extends StatefulWidget {
|
||||||
|
final String title;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final String? trailingText;
|
||||||
|
const ChooseTile({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
this.trailingText,
|
||||||
|
this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChooseTile> createState() => _ChooseTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChooseTileState extends State<ChooseTile> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: widget.onPressed,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
|
||||||
|
decoration: CustomTheme.standardBoxDecoration,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (widget.trailingText != null) Text(widget.trailingText!),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
const Icon(Icons.arrow_forward_ios, size: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
lib/presentation/widgets/tiles/custom_radio_list_tile.dart
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
|
||||||
|
class CustomRadioListTile<T> extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final T value;
|
||||||
|
final ValueChanged<T> onContainerTap;
|
||||||
|
|
||||||
|
const CustomRadioListTile({
|
||||||
|
super.key,
|
||||||
|
required this.text,
|
||||||
|
required this.value,
|
||||||
|
required this.onContainerTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onContainerTap(value),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: CustomTheme.boxColor,
|
||||||
|
border: Border.all(color: CustomTheme.boxBorder),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Radio<T>(
|
||||||
|
value: value,
|
||||||
|
activeColor: CustomTheme.primaryColor,
|
||||||
|
toggleable: true,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
lib/presentation/widgets/tiles/game_history_tile.dart
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/data/dto/game.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class GameHistoryTile extends StatefulWidget {
|
||||||
|
final Game game;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const GameHistoryTile({super.key, required this.game, required this.onTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GameHistoryTile> createState() => _GameHistoryTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GameHistoryTileState extends State<GameHistoryTile> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final group = widget.game.group;
|
||||||
|
final winner = widget.game.winner;
|
||||||
|
final allPlayers = _getAllPlayers();
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: widget.onTap,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: CustomTheme.boxColor,
|
||||||
|
border: Border.all(color: CustomTheme.boxBorder),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.game.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_formatDate(widget.game.createdAt),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
if (group != null) ...[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.group, size: 16, color: Colors.grey),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
group.name,
|
||||||
|
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (winner != null) ...[
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: 12,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.green.withValues(alpha: 0.3),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.emoji_events,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.amber,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Winner: ${winner.name}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (allPlayers.isNotEmpty) ...[
|
||||||
|
const Text(
|
||||||
|
'Players',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: allPlayers.map((player) {
|
||||||
|
return TextIconTile(text: player.name, iconEnabled: false);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDate(DateTime dateTime) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final difference = now.difference(dateTime);
|
||||||
|
|
||||||
|
if (difference.inDays == 0) {
|
||||||
|
return 'Today at ${DateFormat('HH:mm').format(dateTime)}';
|
||||||
|
} else if (difference.inDays == 1) {
|
||||||
|
return 'Yesterday at ${DateFormat('HH:mm').format(dateTime)}';
|
||||||
|
} else if (difference.inDays < 7) {
|
||||||
|
return '${difference.inDays} days ago';
|
||||||
|
} else {
|
||||||
|
return DateFormat('MMM d, yyyy').format(dateTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<dynamic> _getAllPlayers() {
|
||||||
|
final allPlayers = <dynamic>[];
|
||||||
|
final playerIds = <String>{};
|
||||||
|
|
||||||
|
// Add players from game.players
|
||||||
|
if (widget.game.players != null) {
|
||||||
|
for (var player in widget.game.players!) {
|
||||||
|
if (!playerIds.contains(player.id)) {
|
||||||
|
allPlayers.add(player);
|
||||||
|
playerIds.add(player.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add players from game.group.players
|
||||||
|
if (widget.game.group?.members != null) {
|
||||||
|
for (var player in widget.game.group!.members) {
|
||||||
|
if (!playerIds.contains(player.id)) {
|
||||||
|
allPlayers.add(player);
|
||||||
|
playerIds.add(player.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allPlayers;
|
||||||
|
}
|
||||||
|
}
|
||||||