158 Commits

Author SHA1 Message Date
8e26f6b337 fixBuild (#30)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: fabian/TichuCounter#30
2023-03-04 08:34:26 +01:00
9e658853eb Update Gradle and Java
Some checks failed
continuous-integration/drone/push Build is failing
2023-03-03 15:31:36 +01:00
5a229d6c57 [#29] Disable swap button on invalid score.
Some checks failed
continuous-integration/drone/push Build is failing
closed [#29]
2023-03-03 15:04:02 +01:00
bcc3bd3848 Freeze build image version
Some checks failed
continuous-integration/drone/push Build is failing
2023-03-03 14:28:13 +01:00
17d861403e [#24] Add long press to delete functionality.
Some checks are pending
continuous-integration/drone/push Build is running
Reformat code.
closes #24
2023-03-03 13:20:22 +01:00
a1f344580d [#23] Prevent trailing zeros stacking up.
Some checks failed
continuous-integration/drone/push Build was killed
closes #23
2023-03-03 11:49:39 +01:00
b3bdbfbc05 [#23] Limit input to 5 digits. 2023-03-03 11:27:18 +01:00
a611de6da4 Add instrumented test to test repository together with room database. Improve data handling on first boot.
Some checks failed
continuous-integration/drone/push Build was killed
2023-02-17 15:19:00 +01:00
4108512139 Merge pull request 'release/2.0' (#22) from release/2.0 into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#22
2023-01-28 23:30:57 +01:00
661b88b961 Keep back stack of navigation controller clean.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-01-28 13:06:50 +01:00
c41816898e Add about page.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 12:33:45 +01:00
2e8d6a7a4e Add App name to navigation drawer.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 11:19:37 +01:00
ae0f85bec0 Rename screen to DrawerItem
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 10:25:07 +01:00
e1e25ff607 Move drawerContent to own file.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 10:14:06 +01:00
6aedb0d7f9 Navigate with enum defines. Create new TopBarState
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 10:06:58 +01:00
48374c5980 Move new game back to app bar.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 00:18:26 +01:00
02213f41b6 Add default android locale. Simplify language settings.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-27 23:09:08 +01:00
9ae0890f71 Restyle history page
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-27 20:52:17 +01:00
cd8f1959af Simplify history page. Fix warnings.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-27 20:13:14 +01:00
58d4fc0e43 Show blinking cursor in keyboard view. Give focus back to last focused input.
All checks were successful
continuous-integration/drone/push Build is passing
closes #12
2023-01-27 18:07:16 +01:00
c71b608a7b [#13] Hide keyboard in landscape.
All checks were successful
continuous-integration/drone/push Build is passing
closes #13
2023-01-27 13:18:00 +01:00
89f2e3ecd5 Improve default color scheme.
Some checks are pending
continuous-integration/drone/push Build is pending
2023-01-27 13:11:36 +01:00
f52bfa64ce [#18] Add a swap score button.
Some checks are pending
continuous-integration/drone/push Build is pending
closes #18
2023-01-27 12:37:59 +01:00
4346af3d2b Improve settings composable. Remove delay before setting the theme. as this did not help [#11]
Some checks are pending
continuous-integration/drone/push Build is pending
2023-01-27 12:03:36 +01:00
ca88bd1054 Move ui variable from viewModel to compose function. Move TopBar to separate file. 2023-01-27 10:22:32 +01:00
c54f63736e Enable drawer close gesture.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-22 21:42:38 +01:00
8d24e46687 Simplify database handling.
Some checks are pending
continuous-integration/drone/push Build is running
2023-01-22 21:40:47 +01:00
f40b66077b [#11] Add a small delay before applying theme so compose has enough time to update remember states before the theme is applied.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-22 20:45:45 +01:00
9ca830a707 [#11] ApplyDayNight should not be used.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-22 18:36:38 +01:00
db58e475d1 [#11] Change how settings adapter work. Directly set system settings in MainActivity.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-22 17:57:51 +01:00
33e57bcfd7 Show fab only on counter screen.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 18:21:40 +01:00
c1567efe52 [#11] Close dropdown list first. the callback might restart the application on a settings change and keep the menu open.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 18:12:30 +01:00
4d37e77f55 Fix round index.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 16:40:00 +01:00
984afb610f Add counter again to navigation.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 16:32:07 +01:00
c3c6c253bc no message
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 15:58:18 +01:00
9189d79982 Disable gesture navigation.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 15:55:49 +01:00
ad0236556e Add delete all history with confirm dialog.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 15:48:07 +01:00
52a73bf204 Simplify history card. Activate open for active game. 2023-01-21 14:47:36 +01:00
6d0192df18 Style game history. Add functionality to delete and open game from history.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 13:04:45 +01:00
63f213bc75 Add hide show keyboard function. Fix keyboard too big on small screens. Improve colors.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 11:30:20 +01:00
68a5d34e45 Add versions code to VersionName.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 10:21:51 +01:00
98ea66772f Fix warnings. Remove unused files.
Some checks are pending
continuous-integration/drone/push Build is running
2023-01-21 10:15:59 +01:00
84a63a2bcb Revert "Upload files to 'app'"
This reverts commit c5de57c416.
2023-01-21 09:02:20 +01:00
c5de57c416 Upload files to 'app'
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 07:48:21 +01:00
7f904f916f Fix typo.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-01-20 23:36:55 +01:00
c4a552ad8c Move theme to top composable. Increase button size.
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-20 23:14:59 +01:00
45746aef0b Clean up.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 19:33:34 +01:00
cd39384207 Add undo redo functionality. Move new game to navigation drawer 2023-01-20 19:28:50 +01:00
f44b51c075 Show overflow on long text.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 16:54:53 +01:00
9ec2a0e465 Improve settings. 2023-01-20 16:54:32 +01:00
f7ccf46b55 Beautify settings page.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 13:55:06 +01:00
0da8a508f5 Style setting screen.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 13:01:15 +01:00
90f0b09e3d Improve navigation.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 12:20:59 +01:00
eac916d8ec Style keyboard. Add Counter previerw.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 11:33:05 +01:00
57bb4deebe Do some styling.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 08:58:51 +01:00
8b4ce20c99 Fix Theme
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 08:39:27 +01:00
bd19858834 Fix copy paste mistake
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-20 07:44:23 +01:00
395f93ca89 Enable materialYou theme usage.
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-20 07:39:24 +01:00
45f11d5caf Portrait mode for counter.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-19 21:10:16 +01:00
f947c5aeb2 Color the android app bar.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-19 20:13:59 +01:00
fb10dce89e Remove old files. Add theme and apply it
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-19 19:47:34 +01:00
e09bd26859 Update android studio. Add compose settings screen.
Some checks are pending
continuous-integration/drone/push Build is pending
2023-01-19 19:38:01 +01:00
bcc08e4605 Add actions.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-13 16:14:24 +01:00
f2cd02e130 Use composable scaffold.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-13 15:24:35 +01:00
9c653d788b Fix build. Remove unneeded import.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-10 11:40:37 +01:00
a4ccd62d72 Composify more fragments.
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-09 20:08:13 +01:00
823d1a6ca4 Simplify composables.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-08 18:47:08 +01:00
0f4d008104 Remove unused code.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 22:43:38 +01:00
2ed221a99f Replace RoundList with compose.
Some checks are pending
continuous-integration/drone/push Build is running
2023-01-07 22:42:21 +01:00
5f6da1d7d4 Move all functionality to viewmodel. Use simple button format.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 19:30:56 +01:00
ae6210073d Use compose for keyboard.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 14:31:40 +01:00
26a44dcc18 Different style for history.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 09:13:51 +01:00
b73ddbf4cc Fix app not installed bug. Delete obsolete files. Add compose preview.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 08:51:31 +01:00
b7a821b9f6 Add compose to project. display history with compose.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 01:15:04 +01:00
6b396dba24 Refromat code.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-06 22:15:07 +01:00
2bf0666946 Fix names and strings. Fix not all games shown in history.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-06 22:01:00 +01:00
74be455d48 Show history.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-06 19:44:02 +01:00
1e428e854e Move all fragments to ui package. Store created and modified date to game.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-06 11:09:06 +01:00
94cdbcad0b Use when statement instead of if. Change var to val.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-05 18:27:36 +01:00
f8b35bddda Reformat. Remove unneeded application restart after language change.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-05 18:18:48 +01:00
ec765b5fec Add Drawer navigation. Convert to multiple fragments shown with app drawer.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-05 17:58:20 +01:00
39b092c7c5 Fix preference listener unregistered.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-31 12:32:04 +01:00
8fb90bd6d0 Default to false for screen_on.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-31 12:02:37 +01:00
e01df3a5c7 Apply application preferences.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-31 11:54:20 +01:00
3b7b71ce77 Add preference activity.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-29 14:30:04 +01:00
968edfbb67 Optimize query. Update teamname on change.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-29 10:08:04 +01:00
6ded9efe68 Remove unneeded function calls.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-28 16:05:59 +01:00
bdc9b64c63 Extract DaoBase
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-28 15:56:05 +01:00
bdb4410638 Extract fragmentBase class
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-28 15:37:51 +01:00
f6b3f70e18 Split in more fragments. Trigger gui update trough db change
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-28 12:30:31 +01:00
70da57df9c Format all files. 2022-12-27 19:04:55 +01:00
17ed7d18f6 Implement undo last round.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 19:00:59 +01:00
30c8941bd1 Fix crash on empty database at startup. 2022-12-27 18:39:07 +01:00
bdf3c0a98e Change primary key to long. Implement newGame menu option.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 18:16:14 +01:00
09739ccc8e Fix tests.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 17:25:57 +01:00
7655d1d7a3 Add database. Write score to database and update.
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-27 17:19:55 +01:00
6edbe12fd1 Add unit test for Tichu. 2022-12-27 17:18:24 +01:00
db5384201f Add test for Tichu.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 11:58:59 +01:00
252889eff7 Fix hilt integration.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 11:40:14 +01:00
4e6193501b Use single live event to prevent false trigger after rotation. remove hilt (for the moment)
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 08:57:02 +01:00
637a34efd7 Fix app crash
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-24 15:00:41 +01:00
346ac10e68 Remove tests. need to rewrite them.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-24 12:15:27 +01:00
0e31908c7a Use viewmodels with LiveData.
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-24 12:12:19 +01:00
5e0c80be17 Split layout in Fragments.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-21 22:46:07 +01:00
a835580682 Target API33. Setup Hilt DI framework. Apply formatting
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-21 20:20:22 +01:00
479f5476e0 Merge pull request 'feature/Publish_artifacts_directly_to_seafile_#8' (#9) from feature/Publish_artifacts_directly_to_seafile_#8 into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#9
2022-12-17 21:25:34 +01:00
e825c86855 Fix file name extension
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-12-17 20:54:49 +01:00
b3793caba2 Fix more copy paste mistakes.
Some checks failed
continuous-integration/drone/push Build was killed
2022-12-17 19:32:19 +01:00
e972400313 Fix wrong merge from stash.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-17 19:10:13 +01:00
72bb963bd2 Extract variables to enviroment. Rename file after uplaod on tagged deploy
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-17 19:05:00 +01:00
beddcb7125 Fix quote mismatch in commamd
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2022-12-17 07:53:41 +01:00
f3e50dea8b Remove double quotes from upload link parameter
Some checks failed
continuous-integration/drone/push Build encountered an error
2022-12-17 07:48:12 +01:00
1cc60756d1 Remove double quotes from upload link parameter
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-17 00:24:08 +01:00
d9486dfec4 Fix file path references
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-17 00:09:54 +01:00
c3999f45d9 Use curl image for deployment
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-16 23:57:01 +01:00
5ee8b4114b put complex commands in quotes.
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-16 21:10:01 +01:00
e1634dafd9 Fix format.
Some checks failed
continuous-integration/drone/push Build encountered an error
2022-12-16 20:15:07 +01:00
1291d24ee3 Use seafile api to upload files directly
Some checks failed
continuous-integration/drone/push Build encountered an error
2022-12-16 20:13:23 +01:00
06367bb101 Merge pull request 'feature/#4-keep-tagged-builds' (#5) from feature/#4-keep-tagged-builds into develop
Some checks failed
continuous-integration/drone Build is passing
continuous-integration/drone/push Build encountered an error
Reviewed-on: fabian/TichuCounter#5
2022-12-10 19:00:10 +01:00
f9aec95547 Remove ignored files from repository.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-12-10 18:58:41 +01:00
50391c3018 Ignore release folder. 2022-12-10 18:57:40 +01:00
6f221d7880 Fix deployment for tagged commit.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-12-10 18:26:22 +01:00
a4355df9d0 Fix build again.
Some checks are pending
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is running
2022-12-10 18:14:24 +01:00
ab6bc2212d Send slack also on failure.
Some checks failed
continuous-integration/drone/push Build is failing
Fail build temporarily.
2022-12-10 18:12:31 +01:00
c16a865937 Fix versionCode not found in release build.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 18:05:55 +01:00
12de9b9ebd Generate signed release apk instead of debug variant. 2022-12-10 17:52:24 +01:00
ba337a3e0e Generate versionCode from timestamp.
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-10 17:44:42 +01:00
c2882a9751 Add step to be executed on tagged build.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 11:28:09 +01:00
15e24afe12 Merge branch 'develop' into release/1.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 10:59:10 +01:00
5ec74da139 Add slack notification
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 09:11:12 +01:00
fa24e254dc Extract prepare signing step
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 08:57:54 +01:00
5447c47d61 Fix copy paste mistake
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 08:42:37 +01:00
9986fd7565 Extract deploy step
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-10 08:24:15 +01:00
bc44dfd386 Add -p option to mkdir to fix error
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 08:06:52 +01:00
c895c08924 Fix file generation
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-10 07:57:25 +01:00
3022e8442b Add keystore and build on sign on release
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-09 19:33:52 +01:00
fe95de53b4 revert 383162ea3a
Some checks failed
continuous-integration/drone/push Build was killed
revert Output content of storefile temporarily
2022-12-09 19:33:31 +01:00
383162ea3a Output content of storefile temporarily
Some checks failed
continuous-integration/drone/push Build was killed
2022-12-09 19:31:48 +01:00
818965e16c Create keystore.properties before build
Some checks failed
continuous-integration/drone/push Build was killed
2022-12-09 19:29:01 +01:00
6cec709476 Revert to cp make sure directory exists
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-09 16:03:51 +01:00
78059fd187 Try rsync
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-09 15:57:38 +01:00
630f9689ad revert 531bfb42b6
Some checks failed
continuous-integration/drone/push Build was killed
revert Test gradle image
2022-12-09 15:54:47 +01:00
531bfb42b6 Test gradle image
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-09 15:52:42 +01:00
cdb00abbe3 Deploy builds to seafile library
Some checks failed
continuous-integration/drone/push Build encountered an error
continuous-integration/drone Build is failing
2022-12-09 15:31:45 +01:00
bc673b4ef7 Merge branch 'develop' into release/1.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-08 23:30:10 +01:00
f7b68c368a Merge pull request 'Add drone build script.' (#3) from feature/droneBuild into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#3
2022-12-08 23:24:50 +01:00
88c5096c7c Add conditions
All checks were successful
continuous-integration/drone Build is passing
continuous-integration/drone/pr Build is passing
2022-12-08 22:48:57 +01:00
bbab1a1eb4 Try different container 2022-12-08 22:45:36 +01:00
d4fa1483cd Fit tests.
All checks were successful
continuous-integration/drone Build is passing
2022-12-08 22:33:24 +01:00
6146219d9c add drone yml file
Some checks failed
continuous-integration/drone Build was killed
2022-12-03 14:49:38 +01:00
eec11bef81 Bump version. 2022-11-02 22:19:45 +01:00
b8ad540b57 Format all files. And fix Round unit test (that now fails...) 2022-11-01 18:35:20 +01:00
ed28d054ec Use int extensions. 2022-11-01 18:07:41 +01:00
78481d29fc Update gradle and IDE. 2022-10-30 17:12:56 +01:00
a7b3247796 Use correct binding. Remove unneeded code. 2022-06-28 10:14:50 +02:00
0ed30dc87a Update deprecated extensions. 2022-06-26 19:40:42 +02:00
cc8ae173f8 Update. Add language setting. 2022-06-26 14:08:53 +02:00
00ace8ddc0 Merge branch 'master' into develop 2020-10-04 21:50:32 +02:00
87 changed files with 3616 additions and 1734 deletions

75
.drone.yml Normal file
View File

@@ -0,0 +1,75 @@
---
kind: pipeline
type: docker
name: Android
steps:
- name: prepare signing
image: busybox
environment:
STOREPASSWORD:
from_secret: StorePassword
KEYPASSWORD:
from_secret: KeyPassword
commands:
- touch keystore.properties
- echo "storePassword=$STOREPASSWORD" >> keystore.properties
- echo "keyPassword=$KEYPASSWORD" >> keystore.properties
- echo "keyAlias=key0" >> keystore.properties
- echo "storeFile=../AndroidKey" >> keystore.properties
- name: generate versionCode
image: busybox
commands:
- touch version.properties
- let timestamp=$(date +%s)/10
- echo "versionCode=$timestamp" >> version.properties
- name: build
image: mingc/android-build-box
commands:
- ./gradlew test
- ./gradlew assembleRelease
- ./gradlew bundleRelease
- name: deploy latest build
image: curlimages/curl
environment:
SEAFILE_API_KEY:
from_secret: SeafileApiKey
APK_FILE: app/build/outputs/apk/release/app-release.apk
BUNDLE_FILE: app/build/outputs/bundle/release/app-release.aab
SEAFILE_REPO: 6debeef9-121e-46ba-acc7-81e109fdcbdd
commands:
- 'UPLOAD_URL=$(curl -H "Authorization: Token $SEAFILE_API_KEY" https://seafile.zobrist.me/api2/repos/$SEAFILE_REPO/upload-link/ | tr -d "\"")'
- echo $UPLOAD_URL
- 'curl -H "Authorization: Token $SEAFILE_API_KEY" -F file=@$APK_FILE -F parent_dir=/ -F relative_path=latest/ -F replace=1 "$UPLOAD_URL"'
- 'curl -H "Authorization: Token $SEAFILE_API_KEY" -F file=@$BUNDLE_FILE -F parent_dir=/ -F relative_path=latest/ -F replace=1 "$UPLOAD_URL"'
- name: deploy tagged build
image: curlimages/curl
environment:
SEAFILE_API_KEY:
from_secret: SeafileApiKey
APK_FILE: app/build/outputs/apk/release/app-release.apk
BUNDLE_FILE: app/build/outputs/bundle/release/app-release.aab
SEAFILE_REPO: 6debeef9-121e-46ba-acc7-81e109fdcbdd
commands:
- 'UPLOAD_URL=$(curl -H "Authorization: Token $SEAFILE_API_KEY" https://seafile.zobrist.me/api2/repos/$SEAFILE_REPO/upload-link/ | tr -d "\"")'
- 'curl -H "Authorization: Token $SEAFILE_API_KEY" -F file=@$APK_FILE -F parent_dir=/ -F relative_path=tagged/$DRONE_TAG/ -F replace=1 "$UPLOAD_URL"'
- 'curl -H "Authorization: Token $SEAFILE_API_KEY" -F file=@$BUNDLE_FILE -F parent_dir=/ -F relative_path=tagged/$DRONE_TAG/ -F replace=1 "$UPLOAD_URL"'
- 'curl -d "operation=rename&newname=app-release$DRONE_TAG.apk" -H "Authorization: Token $SEAFILE_API_KEY" https://seafile.zobrist.me/api2/repos/$SEAFILE_REPO/file/?p=/tagged/$DRONE_TAG/app-release.apk'
- 'curl -d "operation=rename&newname=app-release$DRONE_TAG.aab" -H "Authorization: Token $SEAFILE_API_KEY" https://seafile.zobrist.me/api2/repos/$SEAFILE_REPO/file/?p=/tagged/$DRONE_TAG/app-release.aab'
when:
event:
- tag
- name: slack notification
image: plugins/slack
settings:
webhook:
from_secret: SlackWebhook
when:
status:
- failure
- success

3
.gitignore vendored
View File

@@ -12,3 +12,6 @@
/captures /captures
.externalNativeBuild .externalNativeBuild
.cxx .cxx
.idea
keystore.properties
version.properties

1
.idea/.name generated
View File

@@ -1 +0,0 @@
Tichu Counter

View File

@@ -1,138 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@@ -1,10 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="fabian">
<words>
<w>checkmark</w>
<w>tichu</w>
<w>tichucounter</w>
<w>zobrist</w>
</words>
</dictionary>
</component>

21
.idea/gradle.xml generated
View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

48
.idea/misc.xml generated
View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="androidx.annotation.Nullable" />
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="12">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="7" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="11" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="11">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="10" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

BIN
AndroidKey Normal file

Binary file not shown.

1
app/.gitignore vendored
View File

@@ -1 +1,2 @@
/build /build
/release

View File

@@ -1,51 +1,135 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' id 'com.android.application'
apply plugin: 'kotlin-android-extensions' id 'kotlin-android'
id 'com.google.dagger.hilt.android'
id 'kotlin-kapt'
}
// Create a variable called keystorePropertiesFile, and initialize it to your
// keystore.properties file, in the rootProject folder.
def keystorePropertiesFile = rootProject.file("keystore.properties")
def versionPropertiesFile = rootProject.file("version.properties")
// Initialize a new Properties() object called keystoreProperties.
def keystoreProperties = new Properties()
def versionProperties = new Properties()
def versionMajor = 2
def versionMinor = 0
// Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
versionProperties.load(new FileInputStream(versionPropertiesFile))
android { android {
compileSdkVersion 30 compileSdkVersion 33
buildToolsVersion "30.0.2"
defaultConfig { defaultConfig {
applicationId "me.zobrist.tichucounter" applicationId "me.zobrist.tichucounter"
minSdkVersion 16 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 33
versionCode 7 versionCode versionProperties["versionCode"].toInteger()
versionName "1.0.0" versionName "${versionMajor}.${versionMinor}.${versionProperties["versionCode"].toInteger()}"
resConfigs 'de', 'en'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables {
useSupportLibrary true
} }
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"]
keyPassword = keystoreProperties["keyPassword"]
storeFile = file(keystoreProperties["storeFile"])
storePassword = keystoreProperties["storePassword"]
}
}
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig = signingConfigs.getByName("release")
} }
} }
buildFeatures {
viewBinding = true
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
}
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '17'
}
namespace 'me.zobrist.tichucounter'
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
} }
} }
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation 'com.google.android.material:material:1.2.0' implementation "androidx.compose.material3:material3:1.0.1"
implementation 'com.google.android.play:core:1.8.0'
implementation 'com.google.android.play:core-ktx:1.8.1' implementation 'com.google.android.play:core-ktx:1.8.1'
implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.google.android.play:core-ktx:1.8.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'com.google.code.gson:gson:2.9.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0' implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
testImplementation 'junit:junit:4.12' implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.fragment:fragment-ktx:1.5.5'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.compose.material:material-icons-extended:1.3.1'
implementation "com.google.accompanist:accompanist-systemuicontroller:0.27.0"
implementation 'androidx.activity:activity-compose:1.6.1'
implementation "androidx.compose.ui:ui:1.3.3"
implementation "androidx.compose.ui:ui-tooling-preview:1.3.3"
implementation "androidx.compose.runtime:runtime-livedata:1.3.3"
implementation "androidx.navigation:navigation-compose:2.5.3"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation "com.google.dagger:hilt-android:2.44"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.3.3"
debugImplementation "androidx.compose.ui:ui-tooling:1.3.3"
debugImplementation "androidx.compose.ui:ui-test-manifest:1.3.3"
kapt "com.google.dagger:hilt-compiler:2.44"
implementation "androidx.room:room-runtime:2.5.0"
annotationProcessor "androidx.room:room-compiler:2.5.0"
kapt "androidx.room:room-compiler:2.5.0"
implementation "androidx.room:room-ktx:2.5.0"
implementation "androidx.multidex:multidex:2.0.1"
api "androidx.navigation:navigation-fragment-ktx:2.5.3"
}
// Allow references to generated code
kapt {
correctErrorTypes true
} }

View File

@@ -0,0 +1,102 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "1739540cd7d5436941316932a1036d83",
"entities": [
{
"tableName": "Round",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gameId` INTEGER NOT NULL, `scoreA` INTEGER NOT NULL, `scoreB` INTEGER NOT NULL, `uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "gameId",
"columnName": "gameId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scoreA",
"columnName": "scoreA",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scoreB",
"columnName": "scoreB",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Game",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`active` INTEGER NOT NULL, `nameA` TEXT NOT NULL, `nameB` TEXT NOT NULL, `created` INTEGER NOT NULL, `modified` INTEGER NOT NULL, `uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "active",
"columnName": "active",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "nameA",
"columnName": "nameA",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "nameB",
"columnName": "nameB",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "modified",
"columnName": "modified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1739540cd7d5436941316932a1036d83')"
]
}
}

View File

@@ -0,0 +1,102 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "1739540cd7d5436941316932a1036d83",
"entities": [
{
"tableName": "Round",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gameId` INTEGER NOT NULL, `scoreA` INTEGER NOT NULL, `scoreB` INTEGER NOT NULL, `uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "gameId",
"columnName": "gameId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scoreA",
"columnName": "scoreA",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scoreB",
"columnName": "scoreB",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Game",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`active` INTEGER NOT NULL, `nameA` TEXT NOT NULL, `nameB` TEXT NOT NULL, `created` INTEGER NOT NULL, `modified` INTEGER NOT NULL, `uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "active",
"columnName": "active",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "nameA",
"columnName": "nameA",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "nameB",
"columnName": "nameB",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "modified",
"columnName": "modified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1739540cd7d5436941316932a1036d83')"
]
}
}

View File

@@ -1,24 +0,0 @@
package me.zobrist.tichucounter
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("me.zobrist.tichucounter", appContext.packageName)
}
}

View File

@@ -0,0 +1,275 @@
package me.zobrist.tichucounter
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import me.zobrist.tichucounter.data.AppDatabase
import me.zobrist.tichucounter.data.GameDao
import me.zobrist.tichucounter.data.RoundDao
import me.zobrist.tichucounter.repository.GameRepository
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
import java.util.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class RepositoryInstrumentedTest {
private lateinit var gameDao: GameDao
private lateinit var roundDao: RoundDao
private lateinit var repository: GameRepository
private lateinit var db: AppDatabase
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java
).build()
roundDao = db.roundDao()
gameDao = db.gameDao()
repository = GameRepository(gameDao, roundDao)
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
@Throws(Exception::class)
fun gameInitialisation() = runTest {
repository.getActiveGameFlow().take(1).collect {
assertEquals("TeamA", it.game.nameA)
assertEquals("TeamB", it.game.nameB)
assertTrue(it.game.active)
assertEquals(0, it.rounds.count())
}
}
@Test
@Throws(Exception::class)
fun modifyNames() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
repository.updateActiveTeamName(nameA = "aaa")
repository.getActiveGameFlow().take(1).collect {
assertEquals("aaa", it.game.nameA)
assertEquals("TeamB", it.game.nameB)
}
repository.updateActiveTeamName(nameB = "bbb")
repository.getActiveGameFlow().take(1).collect {
assertEquals("aaa", it.game.nameA)
assertEquals("bbb", it.game.nameB)
}
}
@Test
@Throws(Exception::class)
fun newGame() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.getAllWithRoundFlow().take(1).collect() { it ->
assertEquals(6, it.count())
var uid: Long = 1
it.forEach { game ->
assertEquals(uid++, game.game.uid)
assertEquals(0, game.rounds.count())
}
}
}
@Test
@Throws(Exception::class)
fun setActive() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.getAllWithRoundFlow().take(1).collect() { it ->
val filtered = it.filter { it.game.active }
assertEquals(1, filtered.count())
assertEquals(6, filtered.first().game.uid)
}
repository.setActive(2)
repository.getAllWithRoundFlow().take(1).collect() { it ->
val filtered = it.filter { it.game.active }
assertEquals(1, filtered.count())
assertEquals(2, filtered.first().game.uid)
}
}
@Test
@Throws(Exception::class)
fun addRoundToActiveGame() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.addRoundToActiveGame(1, 1)
repository.addRoundToActiveGame(2, 2)
repository.addRoundToActiveGame(3, 3)
repository.addRoundToActiveGame(4, 4)
repository.addRoundToActiveGame(5, 5)
repository.addRoundToActiveGame(6, 6)
repository.getAllWithRoundFlow().take(1).collect() { it ->
val filtered = it.filter { it.rounds.isNotEmpty() }
assertEquals(1, filtered.count())
assertEquals(6, filtered.first().rounds.count())
}
}
@Test
@Throws(Exception::class)
fun lastRound() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
assertNull(repository.getLastRound())
repository.addRoundToActiveGame(1, 1)
repository.addRoundToActiveGame(2, 2)
repository.addRoundToActiveGame(3, 3)
repository.addRoundToActiveGame(4, 4)
repository.addRoundToActiveGame(5, 5)
repository.addRoundToActiveGame(6, 6)
var lastRound = repository.getLastRound()
assertEquals(6, lastRound?.scoreA)
assertEquals(6, lastRound?.scoreB)
repository.deleteLastRound()
lastRound = repository.getLastRound()
assertEquals(5, lastRound?.scoreA)
assertEquals(5, lastRound?.scoreB)
repository.deleteLastRound()
repository.deleteLastRound()
repository.deleteLastRound()
repository.deleteLastRound()
repository.deleteLastRound()
assertNull(repository.getLastRound())
// No error thrown
repository.deleteLastRound()
}
@Test
@Throws(Exception::class)
fun deleteInactive() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
for (i in 1..6) {
repository.newGame()
repository.addRoundToActiveGame(1, 1)
repository.addRoundToActiveGame(2, 2)
repository.addRoundToActiveGame(3, 3)
repository.addRoundToActiveGame(4, 4)
repository.addRoundToActiveGame(5, 5)
repository.addRoundToActiveGame(6, 6)
}
assertEquals(6 * 6, roundDao.getAll().count())
repository.deleteAllInactive()
// Consists of two transactions. Delete games then delete rounds.
repository.getAllWithRoundFlow().take(1).collect() { it ->
assertEquals(1, it.count())
assertEquals(6, it.first().rounds.count())
}
assertEquals(6, roundDao.getAll().count())
}
@Test
@Throws(Exception::class)
fun deleteById() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
for (i in 1..6) {
repository.newGame()
repository.addRoundToActiveGame(1, 1)
repository.addRoundToActiveGame(2, 2)
repository.addRoundToActiveGame(3, 3)
repository.addRoundToActiveGame(4, 4)
repository.addRoundToActiveGame(5, 5)
repository.addRoundToActiveGame(6, 6)
}
// Non existing Id
repository.deleteGame(10)
repository.getAllWithRoundFlow().take(1).collect() { it ->
assertEquals(7, it.count())
}
// Non existing Id
val toDelete: Long = 3
repository.deleteGame(toDelete)
repository.getAllWithRoundFlow().take(1).collect() { it ->
assertEquals(6, it.count())
assertEquals(0, it.count { it.game.uid == toDelete })
}
}
}

View File

@@ -1,26 +1,40 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="me.zobrist.tichucounter">
<application <application
android:name=".framework.TichuCounterApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme">
android:fullBackupContent="@xml/backup_descriptor">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:windowSoftInputMode="adjustPan" android:exported="true"
android:label="@string/app_name" android:windowSoftInputMode="adjustPan">
android:theme="@style/AppTheme.NoActionBar"> <meta-data
android:name="android.app.lib_name"
android:value="" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
</application> </application>
</manifest> </manifest>

View File

@@ -1,58 +0,0 @@
@file:Suppress("unused")
package me.zobrist.tichucounter
class History {
private var scores: ArrayList<Round> = ArrayList()
fun getScoreA(): Int {
var tempScore = 0
scores.forEach {
tempScore += it.scoreA
}
return tempScore
}
fun getScoreB(): Int {
var tempScore = 0
scores.forEach {
tempScore += it.scoreB
}
return tempScore
}
fun getHistoryA(): String {
var tempHistory = String()
scores.forEach {
tempHistory += it.scoreA.toString() + "\n"
}
return tempHistory
}
fun getHistoryB(): String {
var tempHistory = String()
scores.forEach {
tempHistory += it.scoreB.toString() + "\n"
}
return tempHistory
}
fun logRound(round: Round) {
scores.add(round)
}
fun revertLastRound() {
if (scores.isNotEmpty()) {
scores.removeAt(scores.size - 1)
}
}
fun clearAll() {
scores.clear()
}
fun isEmpty(): Boolean {
return scores.isEmpty()
}
}

View File

@@ -1,454 +1,234 @@
package me.zobrist.tichucounter package me.zobrist.tichucounter
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.view.Menu
import android.view.MenuItem
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.InputMethodManager import androidx.activity.compose.setContent
import android.widget.ScrollView import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.widget.doOnTextChanged import androidx.compose.foundation.layout.*
import com.google.gson.Gson import androidx.compose.material.icons.Icons
import kotlinx.android.synthetic.main.content_main.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.domain.*
import me.zobrist.tichucounter.ui.AppTheme
import me.zobrist.tichucounter.ui.MainViewModel
import me.zobrist.tichucounter.ui.about.AboutView
import me.zobrist.tichucounter.ui.composables.DropDownMenu
import me.zobrist.tichucounter.ui.counter.*
import me.zobrist.tichucounter.ui.history.HistoryList
import me.zobrist.tichucounter.ui.history.HistoryViewModel
import me.zobrist.tichucounter.ui.layout.DrawerContent
import me.zobrist.tichucounter.ui.layout.TopBar
import me.zobrist.tichucounter.ui.settings.SettingsView
import me.zobrist.tichucounter.ui.settings.SettingsViewModel
import javax.inject.Inject
class MainActivity : AppCompatActivity() { @AndroidEntryPoint
class MainActivity : AppCompatActivity(), ISettingsChangeListener {
private var updateOnChange: Boolean = true @Inject
lateinit var settingsAdapter: SettingsAdapter
private val counterViewModel: CounterViewModel by viewModels()
private val historyViewModel: HistoryViewModel by viewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
private val mainViewModel: MainViewModel by viewModels()
private lateinit var history: History
private var currentRound = Round()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
inputTeamA.setRawInputType(InputType.TYPE_NULL)
inputTeamB.setRawInputType(InputType.TYPE_NULL)
inputTeamA.requestFocus()
disableSubmitButton()
updateTheme(this.getSharedPreferences("Settings", Context.MODE_PRIVATE).getInt("Theme", 2))
keepScreenOn(
this.getSharedPreferences("Settings", Context.MODE_PRIVATE)
.getBoolean("Screen_On", false)
)
val json = this.getSharedPreferences("Settings", Context.MODE_PRIVATE).getString("history", "{\"scores\":[]}") settingsAdapter.registerOnChangeListener(this)
history = Gson().fromJson(json, History::class.java)
nameTeamA.setText(this.getSharedPreferences("Settings", Context.MODE_PRIVATE).getString("nameTeamA", "TeamA"))
nameTeamB.setText(this.getSharedPreferences("Settings", Context.MODE_PRIVATE).getString("nameTeamB", "TeamB"))
updateView()
setContent {
inputTeamA.setOnFocusChangeListener { view, b -> AppTheme {
if (b) { val systemUiController = rememberSystemUiController()
hideKeyboard() systemUiController.setStatusBarColor(MaterialTheme.colorScheme.background)
} NavigationDrawer()
}
inputTeamB.setOnFocusChangeListener { view, b ->
if (b) {
hideKeyboard()
}
}
inputTeamA.doOnTextChanged { text, start, count, after ->
if (inputTeamA.isFocused) {
if (inputTeamA.text.isNotEmpty()) {
if (updateOnChange) {
currentRound = try {
Round(text.toString().toInt(), true)
} catch (e: java.lang.Exception) {
Round(1, 1)
}
inputTeamB.setText(currentRound.scoreB.toString())
} else {
updateOnChange = true
}
} else {
inputTeamA.text.clear()
inputTeamB.text.clear()
}
}
if (currentRound.isValidRound() && inputTeamA.text.isNotEmpty() && inputTeamB.text.isNotEmpty()) {
enableSubmitButton()
} else {
disableSubmitButton()
}
}
inputTeamB.doOnTextChanged { text, start, count, after ->
if (inputTeamB.isFocused) {
if (inputTeamB.text.isNotEmpty()) {
if (updateOnChange) {
currentRound = try {
Round(text.toString().toInt(), false)
} catch (e: java.lang.Exception) {
Round(1, 1)
}
inputTeamA.setText(currentRound.scoreA.toString())
} else {
updateOnChange = true
}
} else {
inputTeamA.text.clear()
inputTeamB.text.clear()
}
}
if (currentRound.isValidRound() && inputTeamA.text.isNotEmpty() && inputTeamB.text.isNotEmpty()) {
enableSubmitButton()
} else {
disableSubmitButton()
}
}
buttonAdd100.setOnClickListener {
giveFocusToAIfNone()
if (inputTeamA.isFocused) {
currentRound.scoreA = try {
inputTeamA.text.toString().toInt() + 100
} catch (e: Exception) {
currentRound.scoreB = 0
inputTeamB.setText(currentRound.scoreB.toString())
100
}
updateOnChange = false
inputTeamA.setText(currentRound.scoreA.toString())
}
if (inputTeamB.isFocused) {
currentRound.scoreB = try {
inputTeamB.text.toString().toInt() + 100
} catch (e: Exception) {
currentRound.scoreA = 0
inputTeamA.setText(currentRound.scoreA.toString())
100
}
updateOnChange = false
inputTeamB.setText(currentRound.scoreB.toString())
}
}
buttonSub100.setOnClickListener {
giveFocusToAIfNone()
if (inputTeamA.isFocused) {
currentRound.scoreA = try {
inputTeamA.text.toString().toInt() - 100
} catch (e: Exception) {
currentRound.scoreB = 0
inputTeamB.setText(currentRound.scoreB.toString())
-100
}
updateOnChange = false
inputTeamA.setText(currentRound.scoreA.toString())
}
if (inputTeamB.isFocused) {
currentRound.scoreB = try {
inputTeamB.text.toString().toInt() - 100
} catch (e: Exception) {
currentRound.scoreA = 0
inputTeamA.setText(currentRound.scoreA.toString())
-100
}
updateOnChange = false
inputTeamB.setText(currentRound.scoreB.toString())
}
}
button0.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('0')
}
button1.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('1')
}
button2.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('2')
} }
button3.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('3')
} }
button4.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('4')
}
button5.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('5')
}
button6.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('6')
}
button7.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('7')
}
button8.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('8')
}
button9.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('9')
}
buttonInv.setOnClickListener {
val tempInt: Int
giveFocusToAIfNone()
if (inputTeamA.isFocused) {
if (inputTeamA.text.toString().equals("-")) {
inputTeamA.text.clear()
} else if (inputTeamA.text.isNotEmpty()) {
tempInt = inputTeamA.text.toString().toInt() * -1
inputTeamA.setText(tempInt.toString())
} else {
updateOnChange = false
appendToFocusedInput('-')
currentRound = Round(1,1)
} }
override fun onDestroy() {
} else if (inputTeamB.isFocused) { super.onDestroy()
if (inputTeamB.text.toString().equals("-")) { settingsAdapter.unregisterOnChangeListener(this)
inputTeamB.text.clear()
} else if (inputTeamB.text.isNotEmpty()) {
tempInt = inputTeamB.text.toString().toInt() * -1
inputTeamB.setText(tempInt.toString())
} else {
updateOnChange = false
appendToFocusedInput('-')
currentRound = Round(1,1)
}
}
}
buttonBack.setOnClickListener {
giveFocusToAIfNone()
if (inputTeamA.isFocused) {
if (inputTeamA.text.isNotEmpty()) {
val string = inputTeamA.text.toString()
inputTeamA.setText(string.substring(0, string.length - 1))
}
} else if (inputTeamB.isFocused) {
if (inputTeamB.text.isNotEmpty()) {
val string = inputTeamB.text.toString()
inputTeamB.setText(string.substring(0, string.length - 1))
}
}
}
submit.setOnClickListener {
giveFocusToAIfNone()
if (inputTeamA.text.isNotEmpty() && inputTeamB.text.isNotEmpty()) {
history.logRound(
Round(
inputTeamA.text.toString().toInt(),
inputTeamB.text.toString().toInt()
)
)
updateView()
inputTeamA.text.clear()
inputTeamB.text.clear()
disableSubmitButton()
scrollViewHistory.fullScroll(ScrollView.FOCUS_DOWN)
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val prefs = this.getSharedPreferences("Settings", Context.MODE_PRIVATE).edit()
prefs.putString("history", Gson().toJson(history))
prefs.putString("nameTeamA", nameTeamA.text.toString())
prefs.putString("nameTeamB", nameTeamB.text.toString())
prefs.apply()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
menu.findItem(R.id.action_screenOn).isChecked =
this.getSharedPreferences("Settings", Context.MODE_PRIVATE)
.getBoolean("Screen_On", false)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_clear -> {
val builder = AlertDialog.Builder(this)
builder.setMessage(getString(R.string.confirmClear))
.setTitle(R.string.clear)
.setCancelable(false)
.setPositiveButton(getString(R.string.yes)) { dialog, id ->
dialog.dismiss()
clearAll()
}
.setNegativeButton(getString(R.string.no)) { dialog, id ->
dialog.cancel()
}
builder.create().show()
true
}
R.id.action_undo -> {
undoLastRound()
true
}
R.id.action_theme -> {
chooseThemeDialog()
true
}
R.id.action_screenOn -> {
item.isChecked = !item.isChecked
keepScreenOn(item.isChecked)
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun hideKeyboard() {
val imm: InputMethodManager =
getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(currentFocus!!.windowToken, 0)
}
private fun giveFocusToAIfNone() {
if (!inputTeamA.isFocused && !inputTeamB.isFocused) {
inputTeamA.requestFocus()
}
}
private fun undoLastRound() {
history.revertLastRound()
updateView()
}
private fun updateView() {
scoreA.text = history.getScoreA().toString()
scoreB.text = history.getScoreB().toString()
historyA.text = history.getHistoryA()
historyB.text = history.getHistoryB()
}
private fun clearAll() {
historyA.text = ""
historyB.text = ""
inputTeamA.text.clear()
inputTeamB.text.clear()
scoreA.text = "0"
scoreB.text = "0"
history.clearAll()
}
private fun appendToFocusedInput(toAppend: Char) {
if (inputTeamA.isFocused) {
inputTeamA.text.append(toAppend)
} else if (inputTeamB.isFocused) {
inputTeamB.text.append(toAppend)
}
}
private fun enableSubmitButton() {
submit.imageAlpha = 255 // 0 being transparent and 255 being opaque
submit.isEnabled = true
}
private fun disableSubmitButton() {
submit.imageAlpha = 60 // 0 being transparent and 255 being opaque
submit.isEnabled = false
}
private fun chooseThemeDialog() {
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.choose_theme_text))
val styles = arrayOf("Light", "Dark", "System default")
val checkedItem =
this.getSharedPreferences("Settings", Context.MODE_PRIVATE).getInt("Theme", 2)
val prefs = this.getSharedPreferences("Settings", Context.MODE_PRIVATE).edit()
builder.setSingleChoiceItems(styles, checkedItem) { dialog, which ->
prefs.putInt("Theme", which)
prefs.apply()
updateTheme(which)
dialog.dismiss()
} }
val dialog = builder.create() override fun onLanguageChanged(language: Language) {
dialog.show() AppCompatDelegate.setApplicationLocales(language.value)
} }
private fun updateTheme(which: Int) { override fun onThemeChanged(theme: Theme) {
when (which) { val themeValue = when (theme) {
0 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
1 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
2 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) Theme.DEFAULT -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
} }
delegate.applyDayNight() AppCompatDelegate.setDefaultNightMode(themeValue)
} }
private fun keepScreenOn(keepOn: Boolean) { override fun onScreenOnChanged(keepOn: KeepScreenOn) {
if (keepOn) { if (keepOn.value) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else { } else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} }
}
val prefs = this.getSharedPreferences("Settings", Context.MODE_PRIVATE).edit() @OptIn(ExperimentalMaterial3Api::class)
prefs.putBoolean("Screen_On", keepOn) @Composable
prefs.apply() private fun NavigationDrawer() {
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
val navController = rememberNavController()
val items = listOf(
DrawerItem(
Route.COUNTER,
Icons.Outlined.Calculate,
stringResource(R.string.menu_counter)
),
DrawerItem(Route.HISTORY, Icons.Outlined.List, stringResource(R.string.menu_history)),
DrawerItem(
Route.SETTINGS,
Icons.Outlined.Settings,
stringResource(R.string.menu_settings)
),
DrawerItem(
Route.ABOUT,
Icons.Outlined.Info,
stringResource(R.string.menu_about)
)
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination =
Route.valueOf(navBackStackEntry?.destination?.route ?: Route.COUNTER.name)
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = drawerState.isOpen,
drawerContent = {
DrawerContent(
drawerItems = items,
selectedDrawerItem = items.first { it.route == currentDestination }) {
scope.launch {
drawerState.close()
}
navController.navigate(it)
}
}
) {
MyScaffoldLayout(
drawerState,
scope,
navController,
counterViewModel.keyboardHidden && (currentDestination == Route.COUNTER)
) { counterViewModel.showKeyboard() }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScaffoldLayout(
drawerState: DrawerState,
scope: CoroutineScope,
navController: NavHostController,
showFab: Boolean,
fabAction: () -> Unit
) {
var topBarState by remember { mutableStateOf(TopBarState()) }
Scaffold(
floatingActionButton = {
if (showFab) {
FloatingActionButton(
onClick = { fabAction() }) {
Icon(Icons.Outlined.Keyboard, null)
}
}
},
topBar = { TopBar(topBarState) }) { paddings ->
NavHost(
navController = navController,
startDestination = Route.COUNTER.name,
modifier = Modifier.padding(paddings)
) {
composable(Route.COUNTER) {
var expanded by remember { mutableStateOf(false) }
topBarState = TopBarState(
title = stringResource(R.string.app_name),
actions = (listOf(
TopBarAction(
Icons.Outlined.Undo,
mainViewModel.isUndoActionActive,
{ mainViewModel.undoLastRound() }),
TopBarAction(
Icons.Outlined.Redo,
mainViewModel.isRedoActionActive,
{ mainViewModel.redoLastRound() }),
TopBarAction(
Icons.Outlined.MoreVert,
mainViewModel.activeGameHasRounds,
{ expanded = true }
) {
DropDownMenu(
mapOf("new" to R.string.newGame),
"",
expanded,
) {
expanded = false
it?.let {
when (it) {
"new" -> mainViewModel.newGame()
}
}
}
},
))
) { scope.launch { drawerState.open() } }
Counter(counterViewModel)
}
composable(Route.HISTORY) {
topBarState =
TopBarState(title = stringResource(R.string.menu_history)) { scope.launch { drawerState.open() } }
HistoryList(historyViewModel) { navController.navigate(Route.COUNTER) }
}
composable(Route.SETTINGS) {
topBarState =
TopBarState(title = stringResource(R.string.menu_settings)) { scope.launch { drawerState.open() } }
SettingsView(settingsViewModel)
}
composable(Route.ABOUT) {
topBarState =
TopBarState(title = stringResource(R.string.menu_about)) { scope.launch { drawerState.open() } }
AboutView()
}
}
}
} }
} }

View File

@@ -1,38 +0,0 @@
package me.zobrist.tichucounter
import java.io.Serializable
class Round() : Serializable {
var scoreA: Int = 0
var scoreB: Int = 0
constructor(score: Int, isScoreA: Boolean) : this() {
if (isScoreA) {
scoreA = score
scoreB = calculateOtherScore(scoreA)
} else {
scoreB = score
scoreA = calculateOtherScore(scoreB)
}
}
constructor(scoreA: Int, scoreB: Int) : this() {
this.scoreA = scoreA
this.scoreB = scoreB
}
private fun calculateOtherScore(score: Int): Int {
if (isMultipleOf100(score)) {
return 0
}
return 100 - (score % 100)
}
private fun isMultipleOf100(score: Int): Boolean {
return (score / 100) >= 1 && (score % 100) == 0
}
fun isValidRound(): Boolean {
return (scoreA % 5 == 0) && (scoreB % 5 == 0) && ((scoreA + scoreB) % 100 == 0)
}
}

View File

@@ -0,0 +1,14 @@
package me.zobrist.tichucounter.data
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round
@Database(entities = [Round::class, Game::class], version = 1)
@TypeConverters(DateConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun roundDao(): RoundDao
abstract fun gameDao(): GameDao
}

View File

@@ -0,0 +1,22 @@
package me.zobrist.tichucounter.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Update
@Dao
interface DaoBase<T> {
@Insert
fun insert(entity: T): Long
@Update
fun update(entity: T)
@Delete
fun delete(entity: T)
@Delete
fun delete(entity: List<T>)
}

View File

@@ -0,0 +1,34 @@
package me.zobrist.tichucounter.data
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule {
@Provides
fun provideRoundDao(appDatabase: AppDatabase): RoundDao {
return appDatabase.roundDao()
}
@Provides
fun provideGameDao(appDatabase: AppDatabase): GameDao {
return appDatabase.gameDao()
}
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext appContext: Context): AppDatabase {
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"TichuCounterDb"
).build()
}
}

View File

@@ -0,0 +1,18 @@
package me.zobrist.tichucounter.data
import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import java.util.*
@ProvidedTypeConverter
object DateConverter {
@TypeConverter
fun toDate(dateLong: Long?): Date? {
return dateLong?.let { Date(it) }
}
@TypeConverter
fun fromDate(date: Date?): Long? {
return date?.time
}
}

View File

@@ -0,0 +1,38 @@
package me.zobrist.tichucounter.data
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import me.zobrist.tichucounter.data.entity.Game
@Dao
interface GameDao : DaoBase<Game> {
@Query("SELECT * FROM game")
fun getAll(): Flow<List<Game>>
@Transaction
@Query("SELECT * FROM game")
fun getGamesWithRounds(): Flow<List<GameWithScores>>
@Transaction
@Query("SELECT * FROM game WHERE active is 1")
fun getActiveWithRounds(): Flow<GameWithScores?>
@Query("SELECT * FROM game WHERE uid is :gameId")
fun getGameById(gameId: Long): Game
@Query("SELECT * FROM game WHERE active is 1")
fun getActiveAsFlow(): Flow<Game?>
@Query("SELECT * FROM game WHERE active is 1")
fun getActive(): Game?
@Query("UPDATE game SET active = 1 WHERE uid is :gameId;")
fun setActive(gameId: Long)
@Query("UPDATE game SET active = 0 WHERE uid is not :gameId;")
fun setOthersInactive(gameId: Long)
}

View File

@@ -0,0 +1,17 @@
package me.zobrist.tichucounter.data
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.Relation
import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round
@Entity
data class GameWithScores(
@Embedded val game: Game = Game(),
@Relation(
parentColumn = "uid",
entityColumn = "gameId"
)
val rounds: List<Round> = emptyList()
)

View File

@@ -0,0 +1,15 @@
package me.zobrist.tichucounter.data
import androidx.room.*
import me.zobrist.tichucounter.data.entity.Round
@Dao
interface RoundDao : DaoBase<Round> {
@Query("SELECT * FROM round")
fun getAll(): List<Round>
@Query("SELECT * FROM round WHERE gameId is :gameId")
fun getAllForGame(gameId: Long?): List<Round>
}

View File

@@ -0,0 +1,15 @@
package me.zobrist.tichucounter.data.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.*
@Entity
data class Game(
var active: Boolean = true,
var nameA: String = "TeamA",
var nameB: String = "TeamB",
val created: Date = Date(),
var modified: Date = Date(),
@PrimaryKey(autoGenerate = true) override val uid: Long = 0
) : IEntity

View File

@@ -0,0 +1,5 @@
package me.zobrist.tichucounter.data.entity
interface IEntity {
val uid: Long
}

View File

@@ -0,0 +1,12 @@
package me.zobrist.tichucounter.data.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Round(
var gameId: Long,
var scoreA: Int,
var scoreB: Int,
@PrimaryKey(autoGenerate = true) override val uid: Long = 0
) : IEntity

View File

@@ -0,0 +1,5 @@
package me.zobrist.tichucounter.domain
import androidx.compose.ui.graphics.vector.ImageVector
data class DrawerItem(val route: Route, val menuIcon: ImageVector, val menuName: String)

View File

@@ -0,0 +1,14 @@
package me.zobrist.tichucounter.domain
import me.zobrist.tichucounter.data.GameWithScores
fun GameWithScores.getTotalPoints(): Pair<Int, Int> {
var scoreA = 0
var scoreB = 0
this.rounds.forEach {
scoreA += it.scoreA
scoreB += it.scoreB
}
return Pair(scoreA, scoreB)
}

View File

@@ -0,0 +1,11 @@
package me.zobrist.tichucounter.domain
fun Int.isMultipleOf5(): Boolean {
return (this % 5) == 0
}
fun Int.isMultipleOf100(): Boolean {
return (this % 100) == 0
}

View File

@@ -0,0 +1,30 @@
package me.zobrist.tichucounter.domain
import androidx.compose.runtime.Composable
import androidx.navigation.*
import androidx.navigation.compose.composable
fun NavController.navigate(route: Route) {
this.navigate(route.name) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(Route.COUNTER.name) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
fun NavGraphBuilder.composable(
route: Route,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable (NavBackStackEntry) -> Unit
) {
this.composable(route.name, arguments, deepLinks, content)
}

View File

@@ -0,0 +1,3 @@
package me.zobrist.tichucounter.domain
enum class Route { COUNTER, HISTORY, SETTINGS, ABOUT }

View File

@@ -0,0 +1,116 @@
package me.zobrist.tichucounter.domain
import android.content.Context
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
enum class Theme { DEFAULT, DARK, LIGHT }
enum class Language(val value: LocaleListCompat) {
DEFAULT(LocaleListCompat.getEmptyLocaleList()),
ENGLISH(LocaleListCompat.forLanguageTags("en")),
GERMAN(LocaleListCompat.forLanguageTags("de"))
}
enum class KeepScreenOn(val value: Boolean) { ON(true), OFF(false) }
interface ISettingsChangeListener {
fun onLanguageChanged(language: Language)
fun onThemeChanged(theme: Theme)
fun onScreenOnChanged(keepOn: KeepScreenOn)
}
@Singleton
class SettingsAdapter @Inject constructor(@ApplicationContext private val context: Context) {
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private var listenerList = mutableListOf<ISettingsChangeListener>()
var language: Language
private set
var theme: Theme
private set
var keepScreenOn: KeepScreenOn
private set
init {
language = try {
enumValueOf(sharedPreferences.getString(Language::class.simpleName, null)!!)
} catch (_: NullPointerException) {
Language.DEFAULT
}
theme = try {
enumValueOf(sharedPreferences.getString(Theme::class.simpleName, null)!!)
} catch (_: java.lang.Exception) {
Theme.DEFAULT
}
keepScreenOn = try {
enumValueOf(sharedPreferences.getString(KeepScreenOn::class.simpleName, null)!!)
} catch (_: java.lang.Exception) {
KeepScreenOn.OFF
}
}
fun registerOnChangeListener(listener: ISettingsChangeListener) {
listenerList.add(listener)
listener.onThemeChanged(theme)
listener.onLanguageChanged(language)
listener.onScreenOnChanged(keepScreenOn)
}
fun unregisterOnChangeListener(listener: ISettingsChangeListener?) {
if (listener != null) {
listenerList.remove(listener)
}
}
fun setLanguage(language: Language) {
this.language = language
updatePreference(Language::class.simpleName, language.name)
notifyListeners(language)
}
fun setTheme(theme: Theme) {
this.theme = theme
updatePreference(Theme::class.simpleName, theme.name)
notifyListeners(theme)
}
fun setKeepScreenOn(setting: KeepScreenOn) {
this.keepScreenOn = setting
updatePreference(KeepScreenOn::class.simpleName, setting.name)
notifyListeners(setting)
}
private fun updatePreference(name: String?, value: String) {
val editor = sharedPreferences.edit()
editor.putString(name, value)
editor.apply()
}
private fun notifyListeners(language: Language) {
listenerList.forEach {
it.onLanguageChanged(language)
}
}
private fun notifyListeners(theme: Theme) {
listenerList.forEach {
it.onThemeChanged(theme)
}
}
private fun notifyListeners(keepScreenOn: KeepScreenOn) {
listenerList.forEach {
it.onScreenOnChanged(keepScreenOn)
}
}
}

View File

@@ -0,0 +1,12 @@
package me.zobrist.tichucounter.domain
fun String.digitCount(): Int {
var count = 0
this.forEach {
if (it.isDigit()) {
count++
}
}
return count
}

View File

@@ -0,0 +1,26 @@
package me.zobrist.tichucounter.domain
import javax.inject.Inject
class Tichu @Inject constructor() {
fun calculateOtherScore(score: Int): Int? {
if (!score.isMultipleOf5()) {
return null
}
if (score.isMultipleOf100() && score != 0) {
return 0
}
if (score in 101..125) {
return 0 - (score % 100)
}
return 100 - (score % 100)
}
fun isValidRound(scoreA: Int?, scoreB: Int?): Boolean {
if (scoreA == null || scoreB == null) {
return false
}
return (scoreA.isMultipleOf5()) && scoreB.isMultipleOf5() && (scoreA + scoreB).isMultipleOf100()
}
}

View File

@@ -0,0 +1,11 @@
package me.zobrist.tichucounter.domain
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
class TopBarAction(
val imageVector: ImageVector,
val isActive: Boolean,
val action: () -> Unit,
val composeCode: @Composable () -> Unit = {}
)

View File

@@ -0,0 +1,12 @@
package me.zobrist.tichucounter.domain
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Menu
import androidx.compose.ui.graphics.vector.ImageVector
data class TopBarState(
var title: String = "",
var icon: ImageVector = Icons.Outlined.Menu,
var actions: List<TopBarAction> = emptyList(),
var onNavigate: () -> Unit = {}
)

View File

@@ -0,0 +1,7 @@
package me.zobrist.tichucounter.framework
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class TichuCounterApplication : Application()

View File

@@ -0,0 +1,133 @@
package me.zobrist.tichucounter.repository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.zobrist.tichucounter.data.GameDao
import me.zobrist.tichucounter.data.GameWithScores
import me.zobrist.tichucounter.data.RoundDao
import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round
import java.util.*
import javax.inject.Inject
class GameRepository @Inject constructor(
private val gameDao: GameDao,
private val roundDao: RoundDao
) {
private var activeGame: Game = Game(true, "TeamA", "TeamB", Date(), Date())
init {
CoroutineScope(Dispatchers.IO).launch {
gameDao.getActiveAsFlow().collect {
if (it == null) {
newGame()
} else {
activeGame = it
}
}
}
}
suspend fun newGame() {
withContext(Dispatchers.IO) {
val id = gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date()))
setActive(id)
}
}
suspend fun updateActiveTeamName(nameA: String? = null, nameB: String? = null) {
val newA = nameA ?: activeGame.nameA
val newB = nameB ?: activeGame.nameB
if (newA == activeGame.nameA && newB == activeGame.nameB) {
return
}
activeGame.modified = Date()
activeGame.nameA = newA
activeGame.nameB = newB
withContext(Dispatchers.IO) {
gameDao.update(activeGame)
}
}
suspend fun setActive(id: Long) {
withContext(Dispatchers.IO) {
gameDao.setActive(id)
gameDao.setOthersInactive(id)
}
}
suspend fun getLastRound(): Round? {
return try {
withContext(Dispatchers.IO) {
roundDao.getAllForGame(activeGame.uid).last()
}
} catch (_: NoSuchElementException) {
null
}
}
suspend fun deleteLastRound() {
withContext(Dispatchers.IO) {
try {
roundDao.delete(getLastRound()!!)
} catch (_: NullPointerException) {
}
}
}
suspend fun addRoundToActiveGame(scoreA: Int, scoreB: Int) {
withContext(Dispatchers.IO) {
val active = activeGame
active.modified = Date()
val round = Round(active.uid, scoreA, scoreB)
roundDao.insert(round)
gameDao.update(active)
}
}
suspend fun deleteGame(uid: Long) {
withContext(Dispatchers.IO) {
try {
val game = gameDao.getGameById(uid)
gameDao.delete(game)
val rounds = roundDao.getAllForGame(game.uid)
roundDao.delete(rounds)
} catch (_: NullPointerException) {
}
}
}
suspend fun deleteAllInactive() {
withContext(Dispatchers.IO) {
try {
gameDao.getAll().take(1).collect { games ->
val gamesToDelete = games.filter { it.uid != activeGame.uid }
val roundsToDelete = roundDao.getAll().filter { it.gameId != activeGame.uid }
gameDao.delete(gamesToDelete)
roundDao.delete(roundsToDelete)
}
} catch (_: NullPointerException) {
}
}
}
fun getActiveGameFlow(): Flow<GameWithScores> {
return gameDao.getActiveWithRounds().filter { it != null }.map { it!! }
}
fun getAllWithRoundFlow(): Flow<List<GameWithScores>> {
return gameDao.getGamesWithRounds()
}
}

View File

@@ -0,0 +1,70 @@
@file:Suppress("unused")
package me.zobrist.tichucounter.ui
import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFF9C404D)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFFFDADB)
val md_theme_light_onPrimaryContainer = Color(0xFF40000F)
val md_theme_light_secondary = Color(0xFF765659)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFFFDADB)
val md_theme_light_onSecondaryContainer = Color(0xFF2C1517)
val md_theme_light_tertiary = Color(0xFF775930)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFFFDDB5)
val md_theme_light_onTertiaryContainer = Color(0xFF2A1800)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFFFBFF)
val md_theme_light_onBackground = Color(0xFF201A1A)
val md_theme_light_surface = Color(0xFFFFFBFF)
val md_theme_light_onSurface = Color(0xFF201A1A)
val md_theme_light_surfaceVariant = Color(0xFFF4DDDE)
val md_theme_light_onSurfaceVariant = Color(0xFF524344)
val md_theme_light_outline = Color(0xFF857374)
val md_theme_light_inverseOnSurface = Color(0xFFFBEEEE)
val md_theme_light_inverseSurface = Color(0xFF362F2F)
val md_theme_light_inversePrimary = Color(0xFFFFB2B9)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF9C404D)
val md_theme_light_outlineVariant = Color(0xFFD7C1C2)
val md_theme_light_scrim = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFFFFB2B9)
val md_theme_dark_onPrimary = Color(0xFF5F1222)
val md_theme_dark_primaryContainer = Color(0xFF7D2937)
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDADB)
val md_theme_dark_secondary = Color(0xFFE5BDBF)
val md_theme_dark_onSecondary = Color(0xFF44292C)
val md_theme_dark_secondaryContainer = Color(0xFF5C3F41)
val md_theme_dark_onSecondaryContainer = Color(0xFFFFDADB)
val md_theme_dark_tertiary = Color(0xFFE8C08E)
val md_theme_dark_onTertiary = Color(0xFF442B06)
val md_theme_dark_tertiaryContainer = Color(0xFF5D411B)
val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDB5)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF201A1A)
val md_theme_dark_onBackground = Color(0xFFECE0DF)
val md_theme_dark_surface = Color(0xFF201A1A)
val md_theme_dark_onSurface = Color(0xFFECE0DF)
val md_theme_dark_surfaceVariant = Color(0xFF524344)
val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C2)
val md_theme_dark_outline = Color(0xFF9F8C8D)
val md_theme_dark_inverseOnSurface = Color(0xFF201A1A)
val md_theme_dark_inverseSurface = Color(0xFFECE0DF)
val md_theme_dark_inversePrimary = Color(0xFF9C404D)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFFFFB2B9)
val md_theme_dark_outlineVariant = Color(0xFF524344)
val md_theme_dark_scrim = Color(0xFF000000)
val seed = Color(0xFF833842)

View File

@@ -0,0 +1,79 @@
package me.zobrist.tichucounter.ui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val gameRepository: GameRepository
) : ViewModel() {
private var redoRounds = mutableStateListOf<Round>()
private var expectedRoundCount = 0
var isUndoActionActive by mutableStateOf(false)
val isRedoActionActive: Boolean
get() = redoRounds.isNotEmpty()
var activeGameHasRounds by mutableStateOf(false)
private set
init {
viewModelScope.launch {
gameRepository.getActiveGameFlow().collect {
activeGameHasRounds = it.rounds.isNotEmpty() == true
isUndoActionActive = it.rounds.isNotEmpty()
if (expectedRoundCount != it.rounds.count()) {
redoRounds.clear()
}
expectedRoundCount = it.rounds.count()
}
}
}
fun undoLastRound() {
viewModelScope.launch {
val round = gameRepository.getLastRound()
if (round != null) {
redoRounds.add(round)
expectedRoundCount--
gameRepository.deleteLastRound()
}
}
}
fun redoLastRound() {
viewModelScope.launch {
try {
val round = redoRounds.last()
redoRounds.remove(round)
expectedRoundCount++
gameRepository.addRoundToActiveGame(round.scoreA, round.scoreB)
} catch (_: NoSuchElementException) {
}
}
}
fun newGame() {
viewModelScope.launch {
redoRounds.clear()
gameRepository.newGame()
}
}
}

View File

@@ -0,0 +1,95 @@
package me.zobrist.tichucounter.ui
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val LightColors = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
private val DarkColors = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)
@Composable
fun AppTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colors = when {
dynamicColor && useDarkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !useDarkTheme -> dynamicLightColorScheme(LocalContext.current)
useDarkTheme -> DarkColors
else -> LightColors
}
MaterialTheme(
colorScheme = colors,
content = content
)
}

View File

@@ -0,0 +1,57 @@
package me.zobrist.tichucounter.ui.about
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.Top
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.BuildConfig
import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.ui.AppTheme
@Composable
fun AboutView() {
Row(Modifier.padding(20.dp)) {
Image(
modifier = Modifier
.height(80.dp)
.padding(end = 10.dp)
.align(Top),
painter = painterResource(R.drawable.app_logo),
contentDescription = null,
contentScale = ContentScale.Fit
)
Column {
Text(
text = stringResource(id = R.string.app_name),
style = MaterialTheme.typography.headlineMedium
)
Text(text = "V" + BuildConfig.VERSION_NAME)
}
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun AboutViewPreview() {
AppTheme() {
Surface {
AboutView()
}
}
}

View File

@@ -0,0 +1,32 @@
package me.zobrist.tichucounter.ui.composables
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
@Composable
fun <T> DropDownMenu(map: Map<T, Int>, selected: T, expanded: Boolean, onSelected: (T?) -> Unit) {
DropdownMenu(
expanded = expanded,
onDismissRequest = { onSelected(null) }
) {
map.forEach {
DropdownMenuItem(
onClick = {
onSelected(it.key)
},
trailingIcon = {
if (it.key == selected) {
Icon(Icons.Outlined.Check, null)
}
},
text = { Text(stringResource(it.value)) },
)
}
}
}

View File

@@ -0,0 +1,164 @@
package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.tooling.preview.Preview
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.ui.AppTheme
@Composable
fun Counter(viewModel: ICounterViewModel = PreviewViewModel()) {
var orientation by remember { mutableStateOf(Configuration.ORIENTATION_PORTRAIT) }
orientation = LocalConfiguration.current.orientation
Surface {
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
Landscape(viewModel)
} else {
Portrait(viewModel)
}
}
}
@Composable
fun Landscape(viewModel: ICounterViewModel) {
Row {
Column(Modifier.weight(1f)) {
TeamNamesView(
viewModel.teamNameA,
viewModel.teamNameB,
{ viewModel.updateNameA(it) },
{ viewModel.updateNameB(it) }
)
TeamScoresView(
viewModel.totalScoreA,
viewModel.totalScoreB
)
RoundListView(
viewModel.roundScoreList,
Modifier.weight(1f)
)
}
if (!viewModel.keyboardHidden) {
Column(Modifier.weight(1f)) {
KeyBoardView(viewModel = viewModel)
}
}
}
}
@Composable
fun Portrait(viewModel: ICounterViewModel) {
Column {
TeamNamesView(
viewModel.teamNameA,
viewModel.teamNameB,
{ viewModel.updateNameA(it) },
{ viewModel.updateNameB(it) }
)
TeamScoresView(
viewModel.totalScoreA,
viewModel.totalScoreB
)
RoundListView(
viewModel.roundScoreList,
Modifier.weight(1f)
)
if (!viewModel.keyboardHidden) {
KeyBoardView(viewModel = viewModel)
}
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun CounterViewPreview() {
AppTheme {
Counter()
}
}
internal class PreviewViewModel : ICounterViewModel {
override var roundScoreList: List<Round> =
listOf(Round(1, 10, 90), Round(1, 50, 50), Round(1, 70, 30))
override var totalScoreA: Int = 350
override var totalScoreB: Int = 750
override var teamNameA: String = "Team A"
override var teamNameB: String = "Team B"
override var currentScoreA: String = ""
override var currentScoreB: String = "45"
override var isValidRound: Boolean = false
override var isAFocused: Boolean = false
override var isBFocused: Boolean = false
override var requestFocusA: FocusRequester = FocusRequester()
override var requestFocusB: FocusRequester = FocusRequester()
override var activeValue: String = currentScoreA
override var inactiveValue: String = currentScoreB
override var keyboardHidden: Boolean = false
override fun focusLastInput() {
}
override fun updateOtherScore() {
}
override fun isValidTichuRound(): Boolean {
return true
}
override fun updateSubmitButton() {
}
override fun submitClicked() {
}
override fun digitClicked(digit: String) {
}
override fun negateClicked() {
}
override fun addSub100Clicked(toAdd: Int) {
}
override fun updateNameA(value: String) {
}
override fun updateNameB(value: String) {
}
override fun updateFocusStateA(state: Boolean) {
}
override fun updateFocusStateB(state: Boolean) {
}
override fun swapInputScores() {
}
override fun hideKeyboard() {
}
override fun showKeyboard() {
}
override fun deleteState(pressed: Boolean) {
}
}

View File

@@ -0,0 +1,327 @@
package me.zobrist.tichucounter.ui.counter
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.focus.FocusRequester
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.Tichu
import me.zobrist.tichucounter.domain.digitCount
import me.zobrist.tichucounter.domain.getTotalPoints
import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject
private enum class Focused { TEAM_A, TEAM_B }
interface IKeyBoardViewModel {
val currentScoreA: String
val currentScoreB: String
val isValidRound: Boolean
val isAFocused: Boolean
val isBFocused: Boolean
val requestFocusA: FocusRequester
val requestFocusB: FocusRequester
val activeValue: String
val inactiveValue: String
val keyboardHidden: Boolean
fun focusLastInput()
fun updateOtherScore()
fun isValidTichuRound(): Boolean
fun updateSubmitButton()
fun submitClicked()
fun digitClicked(digit: String)
fun negateClicked()
fun addSub100Clicked(toAdd: Int)
fun updateFocusStateA(state: Boolean)
fun updateFocusStateB(state: Boolean)
fun swapInputScores()
fun hideKeyboard()
fun showKeyboard()
fun deleteState(pressed: Boolean)
}
interface ICounterViewModel : IKeyBoardViewModel {
val roundScoreList: List<Round>
val totalScoreA: Int
val totalScoreB: Int
val teamNameA: String
val teamNameB: String
fun updateNameA(value: String)
fun updateNameB(value: String)
}
@HiltViewModel
class CounterViewModel @Inject constructor(
private val gameRepository: GameRepository
) :
ViewModel(), ICounterViewModel {
override var roundScoreList by mutableStateOf(emptyList<Round>())
private set
override var totalScoreA by mutableStateOf(0)
private set
override var totalScoreB by mutableStateOf(0)
private set
override var teamNameA by mutableStateOf("")
private set
override var teamNameB by mutableStateOf("")
private set
override var currentScoreA by mutableStateOf("")
private set
override var currentScoreB by mutableStateOf("")
private set
override var isValidRound by mutableStateOf(false)
private set
override var isAFocused by mutableStateOf(false)
private set
override var isBFocused by mutableStateOf(false)
private set
override var requestFocusA by mutableStateOf(FocusRequester())
private set
override var requestFocusB by mutableStateOf(FocusRequester())
private set
override var keyboardHidden by mutableStateOf(false)
private set
override var activeValue: String
get() {
return if (isBFocused) {
currentScoreB
} else {
currentScoreA
}
}
set(value) {
if (isBFocused) {
currentScoreB = value
} else {
currentScoreA = value
}
}
override var inactiveValue: String
get() {
return if (isAFocused) {
currentScoreB
} else {
currentScoreA
}
}
set(value) {
if (isAFocused) {
currentScoreB = value
} else {
currentScoreA = value
}
}
private var lastFocused = Focused.TEAM_A
private var deletePressed = false
private var deleteJob: Job? = null
init {
viewModelScope.launch {
gameRepository.getActiveGameFlow().collect {
if (it != null) {
val score = it.getTotalPoints()
roundScoreList = it.rounds
totalScoreA = score.first
totalScoreB = score.second
teamNameA = it.game.nameA
teamNameB = it.game.nameB
}
}
}
}
override fun focusLastInput() {
when (lastFocused) {
Focused.TEAM_A -> if (!isAFocused) requestFocusA.requestFocus()
Focused.TEAM_B -> if (!isBFocused) requestFocusB.requestFocus()
}
}
override fun updateOtherScore() {
inactiveValue = try {
val tichu = Tichu()
val myScore = activeValue.toInt()
val hisScore = tichu.calculateOtherScore(myScore)
if (tichu.isValidRound(myScore, hisScore)) {
hisScore?.toString() ?: ""
} else {
""
}
} catch (_: Exception) {
""
}
}
override fun isValidTichuRound(): Boolean {
return try {
val tichu = Tichu()
tichu.isValidRound(currentScoreA.toInt(), currentScoreB.toInt())
} catch (_: java.lang.NumberFormatException) {
false
}
}
override fun updateSubmitButton() {
isValidRound = isValidTichuRound()
}
override fun submitClicked() {
viewModelScope.launch {
gameRepository.addRoundToActiveGame(currentScoreA.toInt(), currentScoreB.toInt())
}
currentScoreA = ""
currentScoreB = ""
isValidRound = false
}
override fun digitClicked(digit: String) {
focusLastInput()
if (activeValue.digitCount() >= 5) {
// 5 digits is enough
return
}
val newValue = activeValue + digit
try {
activeValue = newValue.toInt().toString()
} catch (_: NumberFormatException) {
}
updateOtherScore()
updateSubmitButton()
}
override fun negateClicked() {
focusLastInput()
activeValue = if (activeValue.contains("-")) {
activeValue.replace("-", "")
} else {
"-$activeValue"
}
updateOtherScore()
updateSubmitButton()
}
override fun addSub100Clicked(toAdd: Int) {
focusLastInput()
activeValue = try {
val temp = activeValue.toInt() + toAdd
temp.toString()
} catch (e: Exception) {
toAdd.toString()
}
if (inactiveValue == "") {
updateOtherScore()
}
updateSubmitButton()
}
override fun updateNameA(value: String) {
viewModelScope.launch {
gameRepository.updateActiveTeamName(nameA = value)
}
}
override fun updateNameB(value: String) {
viewModelScope.launch {
gameRepository.updateActiveTeamName(nameB = value)
}
}
override fun updateFocusStateA(state: Boolean) {
isAFocused = state
if (state) {
lastFocused = Focused.TEAM_A
}
}
override fun updateFocusStateB(state: Boolean) {
isBFocused = state
if (state) {
lastFocused = Focused.TEAM_B
}
}
override fun swapInputScores() {
val swap = currentScoreA
currentScoreA = currentScoreB
currentScoreB = swap
}
override fun hideKeyboard() {
keyboardHidden = true
}
override fun showKeyboard() {
keyboardHidden = false
}
override fun deleteState(pressed: Boolean) {
deletePressed = pressed
if (deletePressed) {
if (deleteJob?.isActive != true) {
deleteJob = deleteRepeatedlyUntilRelease()
}
} else {
deleteJob?.cancel()
}
}
private fun deleteLastDigitActive() {
if (activeValue != "") {
activeValue = activeValue.dropLast(1)
}
updateOtherScore()
updateSubmitButton()
}
private fun deleteRepeatedlyUntilRelease(): Job {
return viewModelScope.launch {
deleteLastDigitActive()
delay(500)
while (deletePressed) {
deleteLastDigitActive()
delay(100)
}
}
}
}

View File

@@ -0,0 +1,344 @@
package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration
import androidx.compose.animation.core.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Backspace
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.KeyboardHide
import androidx.compose.material.icons.outlined.SwapHoriz
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.ui.AppTheme
@Composable
fun KeyBoardView(viewModel: IKeyBoardViewModel) {
KeyboardView(
viewModel.currentScoreA,
viewModel.currentScoreB,
viewModel.requestFocusA,
viewModel.requestFocusB,
viewModel.isValidRound,
viewModel.isAFocused,
viewModel.isBFocused,
{ viewModel.updateFocusStateA(it) },
{ viewModel.updateFocusStateB(it) },
{ viewModel.digitClicked(it) },
{ viewModel.addSub100Clicked(it) },
{ viewModel.negateClicked() },
{ viewModel.submitClicked() },
{ viewModel.hideKeyboard() },
{ viewModel.swapInputScores() },
{ viewModel.deleteState(it) }
)
}
@Composable
fun KeyboardView(
scoreA: String,
scoreB: String,
requestFocusA: FocusRequester,
requestFocusB: FocusRequester,
isValidScore: Boolean,
focusStateA: Boolean,
focusStateB: Boolean,
updateFocusStateA: (Boolean) -> Unit,
updateFocusStateB: (Boolean) -> Unit,
digitClicked: (String) -> Unit,
addSub100Clicked: (Int) -> Unit,
negateClicked: () -> Unit,
submitClicked: () -> Unit,
hideKeyboardClicked: () -> Unit,
onSwapClicked: () -> Unit,
deleteButtonPressedState: (Boolean) -> Unit
) {
Column {
Row(Modifier.height(IntrinsicSize.Max)) {
Column(Modifier.weight(1f)) {
CenteredTextField(
scoreA,
"0",
focusStateA,
requestFocusA
) { updateFocusStateA(it.isFocused) }
}
Surface(
Modifier
.wrapContentWidth()
.fillMaxHeight(),
tonalElevation = 3.dp,
shape = MaterialTheme.shapes.extraSmall
) {
Column {
IconButton(onClick = onSwapClicked, enabled = isValidScore) {
Icon(Icons.Outlined.SwapHoriz, null)
}
}
}
Column(Modifier.weight(1f)) {
CenteredTextField(
scoreB,
"0",
focusStateB,
requestFocusB
) {
updateFocusStateB(it.isFocused)
}
}
}
Row {
Column(Modifier.weight(1f)) {
KeyboardTextButton("1") {
digitClicked("1")
}
}
Column(Modifier.weight(1f)) {
KeyboardTextButton("2") {
digitClicked("2")
}
}
Column(Modifier.weight(1f)) {
KeyboardTextButton("3") {
digitClicked("3")
}
}
Column(Modifier.weight(1f)) {
KeyboardTextButton("+100") {
addSub100Clicked(100)
}
}
}
Row {
Column(Modifier.weight(1f)) {
KeyboardTextButton("4") {
digitClicked("4")
}
}
Column(Modifier.weight(1f)) {
KeyboardTextButton("5") {
digitClicked("5")
}
}
Column(Modifier.weight(1f)) {
KeyboardTextButton("6") {
digitClicked("6")
}
}
Column(Modifier.weight(1f)) {
KeyboardTextButton("-100") {
addSub100Clicked(-100)
}
}
}
Row {
Column(Modifier.weight(1f)) {
KeyboardTextButton("7") {
digitClicked("7")
}
}
Column(Modifier.weight(1f)) {
KeyboardTextButton("8") {
digitClicked("8")
}
}
Column(Modifier.weight(1f)) {
KeyboardTextButton("9") {
digitClicked("9")
}
}
Column(Modifier.weight(1f)) {
val interactionSource = remember { MutableInteractionSource() }
val deletePressed by interactionSource.collectIsPressedAsState()
deleteButtonPressedState(deletePressed)
KeyboardIconButton(
icon = Icons.Outlined.Backspace,
interactionSource = interactionSource
) {}
}
}
Row {
Column(Modifier.weight(1f)) {
KeyboardIconButton(Icons.Outlined.KeyboardHide) {
hideKeyboardClicked()
}
}
Column(Modifier.weight(1f)) {
KeyboardTextButton("0") {
digitClicked("0")
}
}
Column(Modifier.weight(1f)) {
KeyboardTextButton("+/-") {
negateClicked()
}
}
Column(Modifier.weight(1f)) {
KeyboardIconButton(Icons.Outlined.Check, isValidScore) {
submitClicked()
}
}
}
}
}
@Composable
fun KeyboardTextButton(text: String, onClicked: () -> Unit) {
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
val style = if (screenWidth < 350.dp) {
MaterialTheme.typography.labelSmall
} else {
MaterialTheme.typography.labelLarge
}
ElevatedButton(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.padding(2.dp),
onClick = { onClicked() },
) { Text(text, style = style) }
}
@Composable
fun KeyboardIconButton(
icon: ImageVector,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
onClicked: () -> Unit
) {
ElevatedButton(
onClick = { onClicked() },
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.padding(2.dp),
enabled = enabled,
interactionSource = interactionSource
) {
Icon(
icon,
contentDescription = null,
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CenteredTextField(
value: String,
placeholder: String,
focused: Boolean,
focusRequester: FocusRequester? = null,
onFocusStateChanged: (FocusState) -> Unit
) {
val modifier = if (focusRequester != null) {
Modifier.focusRequester(focusRequester)
} else {
Modifier
}
Box(contentAlignment = Alignment.Center) {
TextField(
value = value,
onValueChange = { },
placeholder = {
if (!focused) {
Text(
placeholder,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
},
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
singleLine = true,
readOnly = true,
modifier = modifier
.fillMaxWidth()
.onFocusChanged {
onFocusStateChanged(it)
}
)
if (focused) {
val cursorColor = MaterialTheme.colorScheme.onSurface
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
0f,
cursorColor.alpha,
animationSpec = infiniteRepeatable(
animation = tween(500),
repeatMode = RepeatMode.Reverse
)
)
Row {
Text(text = value, color = cursorColor.copy(alpha = 0f))
Divider(
modifier = Modifier
.padding(start = 3.dp, top = 15.dp, bottom = 15.dp)
.width(1.dp)
.fillMaxHeight(),
color = cursorColor.copy(alpha = alpha)
)
}
}
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun KeyboardViewPreview() {
AppTheme {
Surface {
KeyboardView(
"10",
"190",
FocusRequester(),
FocusRequester(),
isValidScore = false,
focusStateA = true,
focusStateB = false,
updateFocusStateA = {},
updateFocusStateB = {},
digitClicked = {},
addSub100Clicked = {},
negateClicked = {},
submitClicked = {},
hideKeyboardClicked = {},
onSwapClicked = {},
deleteButtonPressedState = {})
}
}
}

View File

@@ -0,0 +1,84 @@
package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.ui.AppTheme
@Composable
fun RoundListView(rounds: List<Round>, modifier: Modifier) {
val lazyListState = rememberLazyListState()
val scope = rememberCoroutineScope()
LazyColumn(state = lazyListState, modifier = modifier) {
itemsIndexed(rounds) { index, item ->
RoundListItem(item, index)
}
scope.launch {
lazyListState.animateScrollToItem(rounds.size)
}
}
}
@Composable
private fun RoundListItem(round: Round, index: Int) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(all = 4.dp)
) {
Text(
text = round.scoreA.toString(),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(5f),
textAlign = TextAlign.Center
)
Text(
text = (index + 1).toString(),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center
)
Text(
text = round.scoreB.toString(),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(5f),
textAlign = TextAlign.Center
)
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun RoundListViewPreview() {
val rounds = listOf(
Round(1, 10, 90),
Round(1, 5, 95),
Round(1, 100, 0),
Round(1, 125, -25),
Round(1, 50, 50)
)
AppTheme {
Surface {
RoundListView(rounds, Modifier)
}
}
}

View File

@@ -0,0 +1,55 @@
package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.ui.AppTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TeamNamesView(
nameA: String,
nameB: String,
updateA: (String) -> Unit,
updateB: (String) -> Unit
) {
val color = TextFieldDefaults.textFieldColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
)
Row {
TextField(
value = nameA,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
onValueChange = { updateA(it) },
singleLine = true,
modifier = Modifier.weight(1f),
colors = color
)
TextField(
value = nameB,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
onValueChange = { updateB(it) },
singleLine = true,
modifier = Modifier.weight(1f),
colors = color
)
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
private fun TeamNamesViewPreview() {
AppTheme {
TeamNamesView("TeamA", "TeamB", {}, {})
}
}

View File

@@ -0,0 +1,53 @@
package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.ui.AppTheme
@Composable
fun TeamScoresView(scoreA: Int, scoreB: Int) {
ElevatedCard(elevation = CardDefaults.elevatedCardElevation(3.dp)) {
Row {
Text(
style = MaterialTheme.typography.headlineSmall,
text = scoreA.toString(),
modifier = Modifier
.weight(5f)
.padding(6.dp),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.weight(1f))
Text(
style = MaterialTheme.typography.headlineSmall,
text = scoreB.toString(),
modifier = Modifier
.weight(5f)
.padding(6.dp),
textAlign = TextAlign.Center
)
}
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
private fun TeamScoresViewPreview() {
AppTheme {
Surface {
TeamScoresView(10, 90)
}
}
}

View File

@@ -0,0 +1,240 @@
package me.zobrist.tichucounter.ui.history
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DeleteForever
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.data.GameWithScores
import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.getTotalPoints
import me.zobrist.tichucounter.ui.composables.DropDownMenu
import java.text.DateFormat
import java.util.*
@Composable
fun HistoryList(
viewModel: HistoryViewModel,
navigateToCalculator: () -> Unit
) {
var showDeleteDialog by remember { mutableStateOf(false) }
DeleteConfirmDialog(showDeleteDialog) {
showDeleteDialog = false
if (it) {
viewModel.deleteAllInactiveGames()
}
}
HistoryList(
viewModel.gameAndHistory,
{
viewModel.activateGame(it)
navigateToCalculator()
},
{ viewModel.deleteGame(it) },
{ showDeleteDialog = true },
)
}
@Preview
@Composable
fun DeleteConfirmDialog(show: Boolean = true, onExecuted: (Boolean) -> Unit = {}) {
if (show) {
AlertDialog(
onDismissRequest = { onExecuted(false) },
dismissButton = {
TextButton({ onExecuted(false) })
{
Text(stringResource(R.string.cancel))
}
},
confirmButton = {
TextButton({ onExecuted(true) })
{
Text(stringResource(R.string.ok))
}
},
title = { Text(stringResource(R.string.delete_inactive_title)) },
text = { Text(stringResource(R.string.delete_inactive_text)) },
)
}
}
@Composable
fun HistoryList(
games: List<GameWithScores>,
onOpenClicked: (GameId: Long) -> Unit,
onDeleteClicked: (GameId: Long) -> Unit,
onDeleteAllClicked: () -> Unit
) {
Row {
LazyColumn {
item {
Text(
modifier = Modifier.padding(start = 10.dp, end = 10.dp),
text = stringResource(R.string.active),
style = MaterialTheme.typography.headlineSmall
)
}
items(games.filter { it.game.active }) {
HistoryListItem(it, onOpenClicked, onDeleteClicked)
}
if (games.count() > 1) {
item {
Text(
modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp),
text = stringResource(R.string.inactive),
style = MaterialTheme.typography.headlineSmall
)
}
items(games.filter { !it.game.active }) {
HistoryListItem(it, onOpenClicked, onDeleteClicked)
}
item {
Button(
enabled = games.count() > 1,
modifier = Modifier
.padding(start = 4.dp, end = 4.dp, top = 10.dp)
.align(CenterVertically)
.fillMaxWidth(),
onClick = { onDeleteAllClicked() }) {
Icon(imageVector = Icons.Outlined.DeleteForever, contentDescription = null)
Text(text = stringResource(id = R.string.deleteAll))
}
}
}
}
}
}
@Composable
fun HistoryListItem(
game: GameWithScores,
onOpenClicked: (GameId: Long) -> Unit,
onDeleteClicked: (GameId: Long) -> Unit
) {
val format =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault())
val cardColor = if (game.game.active) {
CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
} else {
CardDefaults.cardColors()
}
val totalScores = game.getTotalPoints()
Card(
modifier = Modifier
.fillMaxWidth()
.padding(all = 4.dp)
.clickable { onOpenClicked(game.game.uid) },
colors = cardColor
) {
Row(
Modifier
.padding(all = 12.dp)
) {
Column(Modifier.weight(4f)) {
Text(
text = game.game.nameA + " vs " + game.game.nameB,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.headlineSmall
)
Text(
text = totalScores.first.toString() + " : " + totalScores.second.toString(),
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.padding(5.dp))
Text(
text = format.format(game.game.modified),
style = MaterialTheme.typography.labelSmall
)
}
Column(
Modifier
.wrapContentSize()
.width(40.dp)
) {
if (!game.game.active) {
var expanded by remember { mutableStateOf(false) }
Icon(
modifier = Modifier
.padding(start = 20.dp, bottom = 20.dp)
.clickable { expanded = true },
imageVector = Icons.Outlined.MoreVert,
contentDescription = null
)
DropDownMenu(
mapOf("delete" to R.string.delete),
"",
expanded,
) {
expanded = false
it?.let {
when (it) {
"delete" -> onDeleteClicked(game.game.uid)
}
}
}
}
}
}
}
}
@Preview
@Composable
private fun HistoryListPreview() {
val tempData = listOf(
GameWithScores(
Game(true, "abc", "def", Date(), Date()),
listOf(Round(1, 550, 500))
),
GameWithScores(
Game(false, "ADTH", "dogfg", Date(), Date()),
listOf(Round(2, 20, 60))
),
GameWithScores(
Game(false, "TeamA3 langer Name", "TeamB3", Date(), Date()),
listOf(Round(3, 30, 70))
),
GameWithScores(
Game(false, "TeamA4", "TeamB4", Date(), Date()),
listOf(Round(4, 40, 80))
),
GameWithScores(
Game(false, "TeamA5", "TeamB5", Date(), Date()),
listOf(Round(5, 50, 90))
)
)
HistoryList(tempData, {}, {}) {}
}

View File

@@ -0,0 +1,51 @@
package me.zobrist.tichucounter.ui.history
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.GameWithScores
import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject
@HiltViewModel
class HistoryViewModel @Inject constructor(
private val gameRepository: GameRepository
) : ViewModel() {
var gameAndHistory by mutableStateOf(emptyList<GameWithScores>())
private set
init {
viewModelScope.launch {
gameRepository.getAllWithRoundFlow().collect { games ->
gameAndHistory =
games.sortedBy { it.game.modified }.sortedBy { it.game.active }.reversed()
}
}
}
fun deleteGame(gameId: Long) {
viewModelScope.launch {
gameRepository.deleteGame(gameId)
}
}
fun activateGame(gameId: Long) {
viewModelScope.launch {
gameRepository.setActive(gameId)
}
}
fun deleteAllInactiveGames() {
viewModelScope.launch {
gameRepository.deleteAllInactive()
}
}
}

View File

@@ -0,0 +1,64 @@
package me.zobrist.tichucounter.ui.layout
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.domain.*
import me.zobrist.tichucounter.ui.AppTheme
import me.zobrist.tichucounter.ui.counter.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DrawerContent(
drawerItems: List<DrawerItem>,
selectedDrawerItem: DrawerItem,
onElementClicked: (Route) -> Unit
) {
ModalDrawerSheet {
Text(
modifier = Modifier.padding(start = 10.dp, top = 10.dp),
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineSmall
)
Divider(modifier = Modifier.padding(10.dp))
drawerItems.forEach { screen ->
NavigationDrawerItem(
icon = { Icon(screen.menuIcon, contentDescription = null) },
label = { Text(screen.menuName) },
selected = screen == selectedDrawerItem,
onClick = { onElementClicked(screen.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun DrawerContentPreview() {
val counter = DrawerItem(Route.COUNTER, Icons.Outlined.Calculate, "Counter")
val history = DrawerItem(Route.HISTORY, Icons.Outlined.List, "History")
val settings = DrawerItem(Route.SETTINGS, Icons.Outlined.Settings, "Settings")
AppTheme {
Surface {
DrawerContent(
listOf(counter, history, settings),
counter
) {}
}
}
}

View File

@@ -0,0 +1,56 @@
package me.zobrist.tichucounter.ui.layout
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import me.zobrist.tichucounter.domain.TopBarAction
import me.zobrist.tichucounter.domain.TopBarState
@Composable
fun TopBar(topBarState: TopBarState) {
TopBar(
topBarState.title,
topBarState.icon,
topBarState.onNavigate,
topBarState.actions
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopBar(
title: String,
icon: ImageVector,
navigateAction: () -> Unit,
actions: List<TopBarAction>
) {
TopAppBar(
title = {
Text(
title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = {
IconButton(onClick = { navigateAction() }) {
Icon(
imageVector = icon,
contentDescription = "Localized description"
)
}
},
actions = {
actions.forEach {
IconButton(onClick = { it.action() }, enabled = it.isActive) {
Icon(
imageVector = it.imageVector,
contentDescription = null,
)
it.composeCode()
}
}
}
)
}

View File

@@ -0,0 +1,178 @@
package me.zobrist.tichucounter.ui.settings
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment.Companion.End
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.domain.KeepScreenOn
import me.zobrist.tichucounter.domain.Language
import me.zobrist.tichucounter.domain.Theme
import me.zobrist.tichucounter.ui.AppTheme
import me.zobrist.tichucounter.ui.composables.DropDownMenu
val languageMap = mapOf(
Language.DEFAULT to R.string.android_default_text,
Language.ENGLISH to R.string.english,
Language.GERMAN to R.string.german
)
val themeMap = mapOf(
Theme.DEFAULT to R.string.android_default_text,
Theme.DARK to R.string.dark,
Theme.LIGHT to R.string.light
)
@Composable
fun SettingsView(viewModel: SettingsViewModel) {
SettingsView(
viewModel.screenOn.value,
viewModel.language,
viewModel.theme,
{ viewModel.updateScreenOn(it) },
{ viewModel.updateLanguage(it) },
{ viewModel.updateTheme(it) })
}
@Composable
fun SettingsView(
valueScreenOn: Boolean = true,
valueLanguage: Language = Language.ENGLISH,
valueTheme: Theme = Theme.DARK,
updateScreenOn: (KeepScreenOn) -> Unit = {},
updateLanguage: (Language) -> Unit = {},
updateTheme: (Theme) -> Unit = {}
) {
Column {
BooleanSetting(
stringResource(R.string.keep_screen_on),
valueScreenOn
) { updateScreenOn(if (it) KeepScreenOn.ON else KeepScreenOn.OFF) }
StringSetting(
stringResource(R.string.choose_language_text),
languageMap,
valueLanguage,
) { updateLanguage(it) }
StringSetting(
stringResource(R.string.choose_theme_text),
themeMap,
valueTheme,
) { updateTheme(it) }
}
}
@Composable
fun BooleanSetting(name: String, value: Boolean, updateValue: (Boolean) -> Unit) {
Row(
Modifier
.padding(20.dp)
.fillMaxWidth()
) {
Column(Modifier.weight(5f)) {
Text(
name,
maxLines = 1,
style = MaterialTheme.typography.bodyLarge,
overflow = TextOverflow.Ellipsis
)
Text(
stringResource(if (value) R.string.on else R.string.off),
style = MaterialTheme.typography.labelLarge
)
}
Column(Modifier.weight(1f))
{
Switch(
checked = value,
modifier = Modifier.align(End),
onCheckedChange = { updateValue(it) })
}
}
}
@Composable
fun <T> StringSetting(name: String, map: Map<T, Int>, selected: T, onSelected: (T) -> Unit) {
var expanded by remember { mutableStateOf(false) }
Row(
Modifier
.fillMaxWidth()
.padding(20.dp)
.clickable { expanded = true }) {
Column(Modifier.weight(5f)) {
Text(name, style = MaterialTheme.typography.bodyLarge, overflow = TextOverflow.Ellipsis)
map[selected]?.let {
Text(
stringResource(it),
style = MaterialTheme.typography.labelLarge
)
}
}
Column(Modifier.weight(1f)) {
Icon(
Icons.Outlined.ArrowDropDown,
contentDescription = null,
modifier = Modifier.align(End)
)
}
DropDownMenu(
map,
selected,
expanded,
) {
expanded = false
it?.let { onSelected(it) }
}
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun SettingsViewPreview() {
AppTheme {
Surface {
SettingsView()
}
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun StringSettingPreview() {
AppTheme {
Surface {
DropDownMenu(
themeMap,
Theme.LIGHT,
true,
) {}
}
}
}

View File

@@ -0,0 +1,42 @@
package me.zobrist.tichucounter.ui.settings
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import me.zobrist.tichucounter.domain.KeepScreenOn
import me.zobrist.tichucounter.domain.Language
import me.zobrist.tichucounter.domain.SettingsAdapter
import me.zobrist.tichucounter.domain.Theme
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(private val settings: SettingsAdapter) : ViewModel() {
var language by mutableStateOf(settings.language)
private set
var theme by mutableStateOf(settings.theme)
private set
var screenOn by mutableStateOf(settings.keepScreenOn)
private set
fun updateLanguage(language: Language) {
settings.setLanguage(language)
this.language = settings.language
}
fun updateTheme(theme: Theme) {
settings.setTheme(theme)
this.theme = settings.theme
}
fun updateScreenOn(value: KeepScreenOn) {
settings.setKeepScreenOn(value)
screenOn = settings.keepScreenOn
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,321 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/left"
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="2"
app:layout_constraintEnd_toStartOf="@+id/right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/viewNames"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/nameTeamA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autofillHints=""
android:gravity="center"
android:imeOptions="actionDone"
android:inputType="text"
android:selectAllOnFocus="true"
android:singleLine="true"
android:text="@string/team_a"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<EditText
android:id="@+id/nameTeamB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autofillHints=""
android:gravity="center"
android:imeOptions="actionDone"
android:inputType="text"
android:selectAllOnFocus="true"
android:singleLine="true"
android:text="@string/team_b"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</LinearLayout>
<LinearLayout
android:id="@+id/viewScore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/scoreA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="0"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/scoreB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="0"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
<ScrollView
android:id="@+id/scrollViewHistory"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
android:clickable="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/historyA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.2"
android:gravity="center" />
<TextView
android:id="@+id/historyB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.2"
android:gravity="center" />
</LinearLayout>
</ScrollView>
</LinearLayout>
<LinearLayout
android:id="@+id/right"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="bottom"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/left">
<LinearLayout
android:id="@+id/viewInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/inputTeamA"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:gravity="center"
android:hint="0"
android:importantForAutofill="no"
android:inputType="numberSigned" />
<EditText
android:id="@+id/inputTeamB"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:gravity="center"
android:hint="0"
android:importantForAutofill="no"
android:inputType="numberSigned" />
</LinearLayout>
<LinearLayout
android:id="@+id/ButtonRow1"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/button1"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="1" />
<Button
android:id="@+id/button2"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="2" />
<Button
android:id="@+id/button3"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="3" />
<Button
android:id="@+id/buttonAdd100"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="+100" />
</LinearLayout>
<LinearLayout
android:id="@+id/ButtonRow2"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/button4"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="4" />
<Button
android:id="@+id/button5"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="5" />
<Button
android:id="@+id/button6"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="6" />
<Button
android:id="@+id/buttonSub100"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="-100" />
</LinearLayout>
<LinearLayout
android:id="@+id/ButtonRow3"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/button7"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="7" />
<Button
android:id="@+id/button8"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="8" />
<Button
android:id="@+id/button9"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="9" />
<ImageButton
android:id="@+id/buttonBack"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:cropToPadding="false"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/back"
android:contentDescription="TODO" />
</LinearLayout>
<LinearLayout
android:id="@+id/ButtonRow4"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/buttonInv"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:text="+/-" />
<Button
android:id="@+id/button0"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:text="0" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0" />
<ImageButton
android:id="@+id/submit"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:scaleType="fitCenter"
app:srcCompat="@drawable/checkmark"
android:contentDescription="TODO" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_main" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,323 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/viewNames"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent">
<EditText
android:id="@+id/nameTeamA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autofillHints=""
android:gravity="center"
android:imeOptions="actionDone"
android:inputType="text"
android:selectAllOnFocus="true"
android:singleLine="true"
android:text="@string/team_a"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<EditText
android:id="@+id/nameTeamB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autofillHints=""
android:gravity="center"
android:imeOptions="actionDone"
android:inputType="text"
android:selectAllOnFocus="true"
android:singleLine="true"
android:text="@string/team_b"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</LinearLayout>
<LinearLayout
android:id="@+id/viewScore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/viewNames">
<TextView
android:id="@+id/scoreA"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="0"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/scoreB"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="0"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:id="@+id/divider5"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
app:layout_constraintBottom_toBottomOf="@+id/scrollViewHistory" />
<ScrollView
android:id="@+id/scrollViewHistory"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@+id/viewInput"
app:layout_constraintTop_toBottomOf="@+id/viewScore">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/historyA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.2"
android:gravity="center" />
<TextView
android:id="@+id/historyB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.2"
android:gravity="center" />
</LinearLayout>
</ScrollView>
<View
android:id="@+id/divider6"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
app:layout_constraintBottom_toBottomOf="@+id/viewScore" />
<LinearLayout
android:id="@+id/viewInput"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@+id/ButtonRow1">
<EditText
android:id="@+id/inputTeamA"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:gravity="center"
android:hint="0"
android:importantForAutofill="no"
android:inputType="numberSigned" />
<EditText
android:id="@+id/inputTeamB"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:gravity="center"
android:hint="0"
android:importantForAutofill="no"
android:inputType="numberSigned" />
</LinearLayout>
<LinearLayout
android:id="@+id/ButtonRow1"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@+id/ButtonRow2">
<Button
android:id="@+id/button1"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="1" />
<Button
android:id="@+id/button2"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="2" />
<Button
android:id="@+id/button3"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="3" />
<Button
android:id="@+id/buttonAdd100"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="+100" />
</LinearLayout>
<LinearLayout
android:id="@+id/ButtonRow2"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@+id/ButtonRow3">
<Button
android:id="@+id/button4"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="4" />
<Button
android:id="@+id/button5"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="5" />
<Button
android:id="@+id/button6"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="6" />
<Button
android:id="@+id/buttonSub100"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="-100" />
</LinearLayout>
<LinearLayout
android:id="@+id/ButtonRow3"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@+id/ButtonRow4">
<Button
android:id="@+id/button7"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="7" />
<Button
android:id="@+id/button8"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="8" />
<Button
android:id="@+id/button9"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="9" />
<ImageButton
android:id="@+id/buttonBack"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:cropToPadding="false"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/back"
android:contentDescription="TODO" />
</LinearLayout>
<LinearLayout
android:id="@+id/ButtonRow4"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="horizontal"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
tools:layout_editor_absoluteX="1dp">
<Button
android:id="@+id/buttonInv"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:text="+/-" />
<Button
android:id="@+id/button0"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:text="0" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0" />
<ImageButton
android:id="@+id/submit"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:scaleType="fitCenter"
app:srcCompat="@drawable/checkmark"
android:contentDescription="TODO" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,25 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="me.zobrist.tichucounter.MainActivity">
<item
android:id="@+id/action_undo"
android:icon="@android:drawable/ic_menu_revert"
android:orderInCategory="5"
android:title="@string/undo" />
<item
android:id="@+id/action_clear"
android:checkable="false"
android:orderInCategory="10"
android:title="@string/clear"
app:showAsAction="never" />
<item
android:id="@+id/action_theme"
android:orderInCategory="15"
android:title="@string/choose_theme_text" />
<item
android:id="@+id/action_screenOn"
android:checkable="true"
android:orderInCategory="20"
android:title="@string/keep_screen_on" />
</menu>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

View File

@@ -1,10 +1,26 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="clear">Neues Spiel starten</string>
<string name="undo">Letzte Runde löschen</string>
<string name="choose_theme_text">Theme auswählen</string> <string name="choose_theme_text">Theme auswählen</string>
<string name="keep_screen_on">Bildschirm eingeschaltet lassen</string> <string name="keep_screen_on">Bildschirm eingeschaltet lassen</string>
<string name="confirmClear">Möchten Sie das laufende Spiel wirklich löschen?</string> <string name="choose_language_text">Sprache wählen</string>
<string name="yes">Ja</string> <string name="android_default_text">Android Standard</string>
<string name="no">Nein</string> <string name="english">Englisch</string>
<string name="german">Deutsch</string>
<string name="light">Hell</string>
<string name="dark">Dunkel</string>
<string name="menu_history">Verlauf</string>
<string name="menu_settings">Einstellungen</string>
<string name="on">Ein</string>
<string name="off">Aus</string>
<string name="newGame">Neues Spiel</string>
<string name="delete_inactive_title">Verlauf löschen</string>
<string name="delete_inactive_text">Wirklich den gesamten Verlauf löschen? Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="cancel">Abbrechen</string>
<string name="ok">Ok</string>
<string name="delete">Löschen</string>
<string name="deleteAll">Alle löschen</string>
<string name="active">Aktives Spiel</string>
<string name="inactive">Vergangene Spiele</string>
<string name="menu_counter">Counter</string>
</resources> </resources>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="clear">Neus Spil starte</string>
<string name="undo">Letschti Rundi lösche</string>
<string name="choose_theme_text">Usgsehe ändere</string>
<string name="keep_screen_on">Bildschirm igschalted la</string>
<string name="confirmClear">Wosch ds loufende Spil würklech lösche?</string>
<string name="yes">Ja</string>
<string name="no">Nei</string>
</resources>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorPrimary">@color/ic_launcher_background</color> <color name="primaryColor">#d50000</color>
<color name="colorPrimaryDark">#830000</color>
<color name="colorAccent">#F57F17</color>
</resources> </resources>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#DC0E00</color> <color name="ic_launcher_background">@color/primaryColor</color>
</resources> </resources>

View File

@@ -1,14 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">Tichu Counter</string> <string name="app_name" translatable="false">Tichu Counter</string>
<!-- Strings used for fragments for navigation -->
<string name="team_a" translatable="false">Team A</string>
<string name="team_b" translatable="false">Team B</string>
<string name="clear">Start new game</string>
<string name="undo">Undo last round</string>
<string name="choose_theme_text">Choose theme</string> <string name="choose_theme_text">Choose theme</string>
<string name="keep_screen_on">Keep screen on</string> <string name="keep_screen_on">Keep screen on</string>
<string name="confirmClear">Do you really want to delete the current game?</string> <string name="choose_language_text">Choose language</string>
<string name="yes">Yes</string> <string name="android_default_text">Android Default</string>
<string name="no">No</string> <string name="english">English</string>
<string name="german">German</string>
<string name="light">Light</string>
<string name="dark">Dark</string>
<string name="menu_history">History</string>
<string name="menu_settings">Settings</string>
<string name="on">On</string>
<string name="off">Off</string>
<string name="newGame">New Game</string>
<string name="delete_inactive_title">Delete history</string>
<string name="delete_inactive_text">You really want to delete the the history? This action can\'t be undone.</string>
<string name="cancel">Cancel</string>
<string name="ok">Ok</string>
<string name="delete">Delete</string>
<string name="deleteAll">Delete all</string>
<string name="active">Current Game</string>
<string name="inactive">Old Games</string>
<string name="menu_counter">Counter</string>
<string name="menu_about">About</string>
</resources> </resources>

View File

@@ -1,20 +1,3 @@
<resources> <resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources> </resources>

View File

@@ -0,0 +1,5 @@
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar" />
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
</cloud-backup>
</data-extraction-rules>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en" />
<locale android:name="de" />
</locale-config>

View File

@@ -1,53 +0,0 @@
package me.zobrist.tichucounter
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class HistoryUnitTest {
@Test
fun calculation_isCorrect() {
val history = History()
history.revertLastRound()
history.getHistoryA()
history.getHistoryB()
history.getScoreA()
history.getScoreB()
history.logRound(Round(10, 10))
history.logRound(Round(10, 10))
history.logRound(Round(10, 10))
history.logRound(Round(10, 10))
history.logRound(Round(10, 10))
history.logRound(Round(10, 10))
history.logRound(Round(10, 10))
history.logRound(Round(10, 10))
history.logRound(Round(10, 10))
history.logRound(Round(10, 10))
assertEquals(100, history.getScoreA())
assertEquals(100, history.getScoreB())
history.revertLastRound()
assertEquals(90, history.getScoreA())
assertEquals(90, history.getScoreB())
assertNotEquals("", history.getHistoryA())
assertNotEquals("", history.getHistoryB())
history.clearAll()
assertEquals(0, history.getScoreA())
assertEquals(0, history.getScoreB())
assertEquals("", history.getHistoryA())
assertEquals("", history.getHistoryB())
}
}

View File

@@ -1,92 +0,0 @@
package me.zobrist.tichucounter
import org.junit.Assert.*
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class RoundUnitTest {
@Test
fun calculation_isCorrect() {
var inputScoreA = 125
var inputScoreB = -25
var temp: Round
// Normal round range -25 to 125 as input
while (inputScoreB >= 125) {
temp = Round(inputScoreA, true)
assertEquals(inputScoreB, temp.scoreB)
assertTrue(temp.isValidRound())
temp = Round(inputScoreA, false)
assertEquals(inputScoreB, temp.scoreA)
assertTrue(temp.isValidRound())
inputScoreA -= 5
inputScoreB += 5
}
// Team a +100 points for Tichu
inputScoreA = 125 + 100
inputScoreB = -25
// Normal round range -25 to 125 as input
while (inputScoreB >= 125) {
temp = Round(inputScoreA, true)
assertEquals(inputScoreB, temp.scoreB)
assertTrue(temp.isValidRound())
temp = Round(inputScoreA, false)
assertEquals(inputScoreB, temp.scoreA)
assertTrue(temp.isValidRound())
inputScoreA -= 5
inputScoreB += 5
}
// Double win
temp = Round(200, true)
assertEquals(0, temp.scoreB)
assertTrue(temp.isValidRound())
temp = Round(200, false)
assertEquals(0, temp.scoreA)
assertTrue(temp.isValidRound())
// Double win with Tichu
temp = Round(300, true)
assertEquals(0, temp.scoreB)
assertTrue(temp.isValidRound())
temp = Round(300, false)
assertEquals(0, temp.scoreA)
assertTrue(temp.isValidRound())
// Double win with Grand Tichu
temp = Round(400, true)
assertEquals(0, temp.scoreB)
assertTrue(temp.isValidRound())
temp = Round(400, false)
assertEquals(0, temp.scoreA)
assertTrue(temp.isValidRound())
//Good rounds
temp = Round(0, 0)
assertTrue(temp.isValidRound())
//Bad rounds
temp = Round(5, 12)
assertFalse(temp.isValidRound())
temp = Round(12, 5)
assertFalse(temp.isValidRound())
temp = Round(5, 55)
assertFalse(temp.isValidRound())
}
}

View File

@@ -0,0 +1,22 @@
package me.zobrist.tichucounter
import me.zobrist.tichucounter.domain.digitCount
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class StringExtensionTest {
@Test
fun calculation_isCorrect() {
assertEquals(0, "-".digitCount())
assertEquals(0, "".digitCount())
assertEquals(2, "-10".digitCount())
assertEquals(2, "10".digitCount())
assertEquals(10, "1234567890".digitCount())
assertEquals(10, "-1234567890".digitCount())
}
}

View File

@@ -0,0 +1,61 @@
package me.zobrist.tichucounter
import me.zobrist.tichucounter.domain.Tichu
import org.junit.Assert.*
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class TichuUnitTest {
@Test
fun calculation_isCorrect() {
var inputScoreA = 125
var inputScoreB = -25
val tichu = Tichu()
// Normal round range -25 to 125 as input
while (inputScoreB <= 125) {
assertGeneratedRound(tichu, inputScoreA, inputScoreB)
inputScoreA -= 5
inputScoreB += 5
}
// Double win
assertGeneratedRound(tichu, 200, 0)
// Double win with Tichu
assertGeneratedRound(tichu, 300, 0)
// Double win with Grand Tichu
assertGeneratedRound(tichu, 400, 0)
//Good rounds trough Tichu
assertValidRound(tichu, 0, 0)
assertValidRound(tichu, -100, 0)
//Bad rounds
assertInvalidRound(tichu, 5, 12)
assertInvalidRound(tichu, 12, 5)
assertInvalidRound(tichu, 5, 55)
}
private fun assertGeneratedRound(tichu: Tichu, scoreA: Int, expectedScoreB: Int) {
val scoreB = tichu.calculateOtherScore(scoreA)
assertEquals(expectedScoreB, scoreB)
assertTrue(tichu.isValidRound(scoreA, scoreB!!))
}
private fun assertInvalidRound(tichu: Tichu, scoreA: Int, scoreB: Int) {
assertFalse(tichu.isValidRound(scoreA, scoreB))
}
private fun assertValidRound(tichu: Tichu, scoreA: Int, scoreB: Int) {
assertTrue(tichu.isValidRound(scoreA, scoreB))
}
}

View File

@@ -1,12 +1,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.4.10" ext {
compose_version = '1.1.1'
}
ext.kotlin_version = "1.7.20"
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
dependencies { dependencies {
classpath "com.android.tools.build:gradle:4.0.1" classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
@@ -14,10 +17,15 @@ buildscript {
} }
} }
plugins {
id 'com.google.dagger.hilt.android' version '2.44' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
}
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
} }

View File

@@ -16,6 +16,6 @@ org.gradle.jvmargs=-Xmx2048m
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true android.enableJetifier=false
# Kotlin code style for this project: "official" or "obsolete": # Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip