Compare commits

...
165 Commits
Author SHA1 Message Date
fabian 661b88b961 Keep back stack of navigation controller clean.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-01-28 13:06:50 +01:00
fabian c41816898e Add about page.
continuous-integration/drone/push Build is passing
2023-01-28 12:33:45 +01:00
fabian 2e8d6a7a4e Add App name to navigation drawer.
continuous-integration/drone/push Build is passing
2023-01-28 11:19:37 +01:00
fabian ae0f85bec0 Rename screen to DrawerItem
continuous-integration/drone/push Build is passing
2023-01-28 10:25:07 +01:00
fabian e1e25ff607 Move drawerContent to own file.
continuous-integration/drone/push Build is passing
2023-01-28 10:14:06 +01:00
fabian 6aedb0d7f9 Navigate with enum defines. Create new TopBarState
continuous-integration/drone/push Build is passing
2023-01-28 10:06:58 +01:00
fabian 48374c5980 Move new game back to app bar.
continuous-integration/drone/push Build is passing
2023-01-28 00:18:26 +01:00
fabian 02213f41b6 Add default android locale. Simplify language settings.
continuous-integration/drone/push Build is passing
2023-01-27 23:09:08 +01:00
fabian 9ae0890f71 Restyle history page
continuous-integration/drone/push Build is passing
2023-01-27 20:52:17 +01:00
fabian cd8f1959af Simplify history page. Fix warnings.
continuous-integration/drone/push Build is passing
2023-01-27 20:13:14 +01:00
fabian 58d4fc0e43 Show blinking cursor in keyboard view. Give focus back to last focused input.
continuous-integration/drone/push Build is passing
closes #12
2023-01-27 18:07:16 +01:00
fabian c71b608a7b [#13] Hide keyboard in landscape.
continuous-integration/drone/push Build is passing
closes #13
2023-01-27 13:18:00 +01:00
fabian 89f2e3ecd5 Improve default color scheme.
continuous-integration/drone/push Build is pending
2023-01-27 13:11:36 +01:00
fabian f52bfa64ce [#18] Add a swap score button.
continuous-integration/drone/push Build is pending
closes #18
2023-01-27 12:37:59 +01:00
fabian 4346af3d2b Improve settings composable. Remove delay before setting the theme. as this did not help [#11]
continuous-integration/drone/push Build is pending
2023-01-27 12:03:36 +01:00
fabian ca88bd1054 Move ui variable from viewModel to compose function. Move TopBar to separate file. 2023-01-27 10:22:32 +01:00
fabian c54f63736e Enable drawer close gesture.
continuous-integration/drone/push Build is passing
2023-01-22 21:42:38 +01:00
fabian 8d24e46687 Simplify database handling.
continuous-integration/drone/push Build is running
2023-01-22 21:40:47 +01:00
fabian f40b66077b [#11] Add a small delay before applying theme so compose has enough time to update remember states before the theme is applied.
continuous-integration/drone/push Build is passing
2023-01-22 20:45:45 +01:00
fabian 9ca830a707 [#11] ApplyDayNight should not be used.
continuous-integration/drone/push Build is passing
2023-01-22 18:36:38 +01:00
fabian db58e475d1 [#11] Change how settings adapter work. Directly set system settings in MainActivity.
continuous-integration/drone/push Build is passing
2023-01-22 17:57:51 +01:00
fabian 33e57bcfd7 Show fab only on counter screen.
continuous-integration/drone/push Build is passing
2023-01-21 18:21:40 +01:00
fabian c1567efe52 [#11] Close dropdown list first. the callback might restart the application on a settings change and keep the menu open.
continuous-integration/drone/push Build is passing
2023-01-21 18:12:30 +01:00
fabian 4d37e77f55 Fix round index.
continuous-integration/drone/push Build is passing
2023-01-21 16:40:00 +01:00
fabian 984afb610f Add counter again to navigation.
continuous-integration/drone/push Build is passing
2023-01-21 16:32:07 +01:00
fabian c3c6c253bc no message
continuous-integration/drone/push Build is passing
2023-01-21 15:58:18 +01:00
fabian 9189d79982 Disable gesture navigation.
continuous-integration/drone/push Build is passing
2023-01-21 15:55:49 +01:00
fabian ad0236556e Add delete all history with confirm dialog.
continuous-integration/drone/push Build is passing
2023-01-21 15:48:07 +01:00
fabian 52a73bf204 Simplify history card. Activate open for active game. 2023-01-21 14:47:36 +01:00
fabian 6d0192df18 Style game history. Add functionality to delete and open game from history.
continuous-integration/drone/push Build is passing
2023-01-21 13:04:45 +01:00
fabian 63f213bc75 Add hide show keyboard function. Fix keyboard too big on small screens. Improve colors.
continuous-integration/drone/push Build is passing
2023-01-21 11:30:20 +01:00
fabian 68a5d34e45 Add versions code to VersionName.
continuous-integration/drone/push Build is passing
2023-01-21 10:21:51 +01:00
fabian 98ea66772f Fix warnings. Remove unused files.
continuous-integration/drone/push Build is running
2023-01-21 10:15:59 +01:00
fabian 84a63a2bcb Revert "Upload files to 'app'"
This reverts commit c5de57c416.
2023-01-21 09:02:20 +01:00
fabian c5de57c416 Upload files to 'app'
continuous-integration/drone/push Build is passing
2023-01-21 07:48:21 +01:00
fabian 7f904f916f Fix typo.
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-01-20 23:36:55 +01:00
fabian c4a552ad8c Move theme to top composable. Increase button size.
continuous-integration/drone/push Build is failing
2023-01-20 23:14:59 +01:00
fabian 45746aef0b Clean up.
continuous-integration/drone/push Build is passing
2023-01-20 19:33:34 +01:00
fabian cd39384207 Add undo redo functionality. Move new game to navigation drawer 2023-01-20 19:28:50 +01:00
fabian f44b51c075 Show overflow on long text.
continuous-integration/drone/push Build is passing
2023-01-20 16:54:53 +01:00
fabian 9ec2a0e465 Improve settings. 2023-01-20 16:54:32 +01:00
fabian f7ccf46b55 Beautify settings page.
continuous-integration/drone/push Build is passing
2023-01-20 13:55:06 +01:00
fabian 0da8a508f5 Style setting screen.
continuous-integration/drone/push Build is passing
2023-01-20 13:01:15 +01:00
fabian 90f0b09e3d Improve navigation.
continuous-integration/drone/push Build is passing
2023-01-20 12:20:59 +01:00
fabian eac916d8ec Style keyboard. Add Counter previerw.
continuous-integration/drone/push Build is passing
2023-01-20 11:33:05 +01:00
fabian 57bb4deebe Do some styling.
continuous-integration/drone/push Build is passing
2023-01-20 08:58:51 +01:00
fabian 8b4ce20c99 Fix Theme
continuous-integration/drone/push Build is passing
2023-01-20 08:39:27 +01:00
fabian bd19858834 Fix copy paste mistake
continuous-integration/drone/push Build is failing
2023-01-20 07:44:23 +01:00
fabian 395f93ca89 Enable materialYou theme usage.
continuous-integration/drone/push Build is failing
2023-01-20 07:39:24 +01:00
fabian 45f11d5caf Portrait mode for counter.
continuous-integration/drone/push Build is passing
2023-01-19 21:10:16 +01:00
fabian f947c5aeb2 Color the android app bar.
continuous-integration/drone/push Build is passing
2023-01-19 20:13:59 +01:00
fabian fb10dce89e Remove old files. Add theme and apply it
continuous-integration/drone/push Build is passing
2023-01-19 19:47:34 +01:00
fabian e09bd26859 Update android studio. Add compose settings screen.
continuous-integration/drone/push Build is pending
2023-01-19 19:38:01 +01:00
fabian bcc08e4605 Add actions.
continuous-integration/drone/push Build is passing
2023-01-13 16:14:24 +01:00
fabian f2cd02e130 Use composable scaffold.
continuous-integration/drone/push Build is passing
2023-01-13 15:24:35 +01:00
fabian 9c653d788b Fix build. Remove unneeded import.
continuous-integration/drone/push Build is passing
2023-01-10 11:40:37 +01:00
fabian a4ccd62d72 Composify more fragments.
continuous-integration/drone/push Build is failing
2023-01-09 20:08:13 +01:00
fabian 823d1a6ca4 Simplify composables.
continuous-integration/drone/push Build is passing
2023-01-08 18:47:08 +01:00
fabian 0f4d008104 Remove unused code.
continuous-integration/drone/push Build is passing
2023-01-07 22:43:38 +01:00
fabian 2ed221a99f Replace RoundList with compose.
continuous-integration/drone/push Build is running
2023-01-07 22:42:21 +01:00
fabian 5f6da1d7d4 Move all functionality to viewmodel. Use simple button format.
continuous-integration/drone/push Build is passing
2023-01-07 19:30:56 +01:00
fabian ae6210073d Use compose for keyboard.
continuous-integration/drone/push Build is passing
2023-01-07 14:31:40 +01:00
fabian 26a44dcc18 Different style for history.
continuous-integration/drone/push Build is passing
2023-01-07 09:13:51 +01:00
fabian b73ddbf4cc Fix app not installed bug. Delete obsolete files. Add compose preview.
continuous-integration/drone/push Build is passing
2023-01-07 08:51:31 +01:00
fabian b7a821b9f6 Add compose to project. display history with compose.
continuous-integration/drone/push Build is passing
2023-01-07 01:15:04 +01:00
fabian 6b396dba24 Refromat code.
continuous-integration/drone/push Build is passing
2023-01-06 22:15:07 +01:00
fabian 2bf0666946 Fix names and strings. Fix not all games shown in history.
continuous-integration/drone/push Build is passing
2023-01-06 22:01:00 +01:00
fabian 74be455d48 Show history.
continuous-integration/drone/push Build is passing
2023-01-06 19:44:02 +01:00
fabian 1e428e854e Move all fragments to ui package. Store created and modified date to game.
continuous-integration/drone/push Build is passing
2023-01-06 11:09:06 +01:00
fabian 94cdbcad0b Use when statement instead of if. Change var to val.
continuous-integration/drone/push Build is passing
2023-01-05 18:27:36 +01:00
fabian f8b35bddda Reformat. Remove unneeded application restart after language change.
continuous-integration/drone/push Build is passing
2023-01-05 18:18:48 +01:00
fabian ec765b5fec Add Drawer navigation. Convert to multiple fragments shown with app drawer.
continuous-integration/drone/push Build is passing
2023-01-05 17:58:20 +01:00
fabian 39b092c7c5 Fix preference listener unregistered.
continuous-integration/drone/push Build is passing
2022-12-31 12:32:04 +01:00
fabian 8fb90bd6d0 Default to false for screen_on.
continuous-integration/drone/push Build is passing
2022-12-31 12:02:37 +01:00
fabian e01df3a5c7 Apply application preferences.
continuous-integration/drone/push Build is passing
2022-12-31 11:54:20 +01:00
fabian 3b7b71ce77 Add preference activity.
continuous-integration/drone/push Build is passing
2022-12-29 14:30:04 +01:00
fabian 968edfbb67 Optimize query. Update teamname on change.
continuous-integration/drone/push Build is passing
2022-12-29 10:08:04 +01:00
fabian 6ded9efe68 Remove unneeded function calls.
continuous-integration/drone/push Build is passing
2022-12-28 16:05:59 +01:00
fabian bdc9b64c63 Extract DaoBase
continuous-integration/drone/push Build is passing
2022-12-28 15:56:05 +01:00
fabian bdb4410638 Extract fragmentBase class
continuous-integration/drone/push Build is passing
2022-12-28 15:37:51 +01:00
fabian f6b3f70e18 Split in more fragments. Trigger gui update trough db change
continuous-integration/drone/push Build is passing
2022-12-28 12:30:31 +01:00
fabian 70da57df9c Format all files. 2022-12-27 19:04:55 +01:00
fabian 17ed7d18f6 Implement undo last round.
continuous-integration/drone/push Build is passing
2022-12-27 19:00:59 +01:00
fabian 30c8941bd1 Fix crash on empty database at startup. 2022-12-27 18:39:07 +01:00
fabian bdf3c0a98e Change primary key to long. Implement newGame menu option.
continuous-integration/drone/push Build is passing
2022-12-27 18:16:14 +01:00
fabian 09739ccc8e Fix tests.
continuous-integration/drone/push Build is passing
2022-12-27 17:25:57 +01:00
fabian 7655d1d7a3 Add database. Write score to database and update.
continuous-integration/drone/push Build is failing
2022-12-27 17:19:55 +01:00
fabian 6edbe12fd1 Add unit test for Tichu. 2022-12-27 17:18:24 +01:00
fabian db5384201f Add test for Tichu.
continuous-integration/drone/push Build is passing
2022-12-27 11:58:59 +01:00
fabian 252889eff7 Fix hilt integration.
continuous-integration/drone/push Build is passing
2022-12-27 11:40:14 +01:00
fabian 4e6193501b Use single live event to prevent false trigger after rotation. remove hilt (for the moment)
continuous-integration/drone/push Build is passing
2022-12-27 08:57:02 +01:00
fabian 637a34efd7 Fix app crash
continuous-integration/drone/push Build is passing
2022-12-24 15:00:41 +01:00
fabian 346ac10e68 Remove tests. need to rewrite them.
continuous-integration/drone/push Build is passing
2022-12-24 12:15:27 +01:00
fabian 0e31908c7a Use viewmodels with LiveData.
continuous-integration/drone/push Build is failing
2022-12-24 12:12:19 +01:00
fabian 5e0c80be17 Split layout in Fragments.
continuous-integration/drone/push Build is passing
2022-12-21 22:46:07 +01:00
fabian a835580682 Target API33. Setup Hilt DI framework. Apply formatting
continuous-integration/drone/push Build is passing
2022-12-21 20:20:22 +01:00
fabian 479f5476e0 Merge pull request 'feature/Publish_artifacts_directly_to_seafile_#8' (#9) from feature/Publish_artifacts_directly_to_seafile_#8 into develop
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#9
2022-12-17 21:25:34 +01:00
fabian e825c86855 Fix file name extension
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-12-17 20:54:49 +01:00
fabian b3793caba2 Fix more copy paste mistakes.
continuous-integration/drone/push Build was killed
2022-12-17 19:32:19 +01:00
fabian e972400313 Fix wrong merge from stash.
continuous-integration/drone/push Build is passing
2022-12-17 19:10:13 +01:00
fabian 72bb963bd2 Extract variables to enviroment. Rename file after uplaod on tagged deploy
continuous-integration/drone/push Build is failing
2022-12-17 19:05:00 +01:00
fabian beddcb7125 Fix quote mismatch in commamd
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2022-12-17 07:53:41 +01:00
fabian f3e50dea8b Remove double quotes from upload link parameter
continuous-integration/drone/push Build encountered an error
2022-12-17 07:48:12 +01:00
fabian 1cc60756d1 Remove double quotes from upload link parameter
continuous-integration/drone/push Build is failing
2022-12-17 00:24:08 +01:00
fabian d9486dfec4 Fix file path references
continuous-integration/drone/push Build is failing
2022-12-17 00:09:54 +01:00
fabian c3999f45d9 Use curl image for deployment
continuous-integration/drone/push Build is failing
2022-12-16 23:57:01 +01:00
fabian 5ee8b4114b put complex commands in quotes.
continuous-integration/drone/push Build is failing
2022-12-16 21:10:01 +01:00
fabian e1634dafd9 Fix format.
continuous-integration/drone/push Build encountered an error
2022-12-16 20:15:07 +01:00
fabian 1291d24ee3 Use seafile api to upload files directly
continuous-integration/drone/push Build encountered an error
2022-12-16 20:13:23 +01:00
fabian 06367bb101 Merge pull request 'feature/#4-keep-tagged-builds' (#5) from feature/#4-keep-tagged-builds into develop
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
fabian f9aec95547 Remove ignored files from repository.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-12-10 18:58:41 +01:00
fabian 50391c3018 Ignore release folder. 2022-12-10 18:57:40 +01:00
fabian 6f221d7880 Fix deployment for tagged commit.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-12-10 18:26:22 +01:00
fabian a4355df9d0 Fix build again.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is running
2022-12-10 18:14:24 +01:00
fabian ab6bc2212d Send slack also on failure.
continuous-integration/drone/push Build is failing
Fail build temporarily.
2022-12-10 18:12:31 +01:00
fabian c16a865937 Fix versionCode not found in release build.
continuous-integration/drone/push Build is passing
2022-12-10 18:05:55 +01:00
fabian 12de9b9ebd Generate signed release apk instead of debug variant. 2022-12-10 17:52:24 +01:00
fabian ba337a3e0e Generate versionCode from timestamp.
continuous-integration/drone/push Build is failing
2022-12-10 17:44:42 +01:00
fabian c2882a9751 Add step to be executed on tagged build.
continuous-integration/drone/push Build is passing
2022-12-10 11:28:09 +01:00
fabian 15e24afe12 Merge branch 'develop' into release/1.1.0
continuous-integration/drone/push Build is passing
2022-12-10 10:59:10 +01:00
fabian 5ec74da139 Add slack notification
continuous-integration/drone/push Build is passing
2022-12-10 09:11:12 +01:00
fabian fa24e254dc Extract prepare signing step
continuous-integration/drone/push Build is passing
2022-12-10 08:57:54 +01:00
fabian 5447c47d61 Fix copy paste mistake
continuous-integration/drone/push Build is passing
2022-12-10 08:42:37 +01:00
fabian 9986fd7565 Extract deploy step
continuous-integration/drone/push Build is failing
2022-12-10 08:24:15 +01:00
fabian bc44dfd386 Add -p option to mkdir to fix error
continuous-integration/drone/push Build is passing
2022-12-10 08:06:52 +01:00
fabian c895c08924 Fix file generation
continuous-integration/drone/push Build is failing
2022-12-10 07:57:25 +01:00
fabian 3022e8442b Add keystore and build on sign on release
continuous-integration/drone/push Build is failing
2022-12-09 19:33:52 +01:00
fabian fe95de53b4 revert 383162ea3a
continuous-integration/drone/push Build was killed
revert Output content of storefile temporarily
2022-12-09 19:33:31 +01:00
fabian 383162ea3a Output content of storefile temporarily
continuous-integration/drone/push Build was killed
2022-12-09 19:31:48 +01:00
fabian 818965e16c Create keystore.properties before build
continuous-integration/drone/push Build was killed
2022-12-09 19:29:01 +01:00
fabian 6cec709476 Revert to cp make sure directory exists
continuous-integration/drone/push Build is passing
2022-12-09 16:03:51 +01:00
fabian 78059fd187 Try rsync
continuous-integration/drone/push Build is failing
2022-12-09 15:57:38 +01:00
fabian 630f9689ad revert 531bfb42b6
continuous-integration/drone/push Build was killed
revert Test gradle image
2022-12-09 15:54:47 +01:00
fabian 531bfb42b6 Test gradle image
continuous-integration/drone/push Build is failing
2022-12-09 15:52:42 +01:00
fabian cdb00abbe3 Deploy builds to seafile library
continuous-integration/drone/push Build encountered an error
continuous-integration/drone Build is failing
2022-12-09 15:31:45 +01:00
fabian bc673b4ef7 Merge branch 'develop' into release/1.1.0
continuous-integration/drone/push Build is passing
2022-12-08 23:30:10 +01:00
fabian f7b68c368a Merge pull request 'Add drone build script.' (#3) from feature/droneBuild into master
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#3
2022-12-08 23:24:50 +01:00
fabian 88c5096c7c Add conditions
continuous-integration/drone Build is passing
continuous-integration/drone/pr Build is passing
2022-12-08 22:48:57 +01:00
fabian bbab1a1eb4 Try different container 2022-12-08 22:45:36 +01:00
fabian d4fa1483cd Fit tests.
continuous-integration/drone Build is passing
2022-12-08 22:33:24 +01:00
fabian 6146219d9c add drone yml file
continuous-integration/drone Build was killed
2022-12-03 14:49:38 +01:00
fabian eec11bef81 Bump version. 2022-11-02 22:19:45 +01:00
fabian b8ad540b57 Format all files. And fix Round unit test (that now fails...) 2022-11-01 18:35:20 +01:00
fabian ed28d054ec Use int extensions. 2022-11-01 18:07:41 +01:00
fabian 78481d29fc Update gradle and IDE. 2022-10-30 17:12:56 +01:00
fabian a7b3247796 Use correct binding. Remove unneeded code. 2022-06-28 10:14:50 +02:00
fabian 0ed30dc87a Update deprecated extensions. 2022-06-26 19:40:42 +02:00
fabian cc8ae173f8 Update. Add language setting. 2022-06-26 14:08:53 +02:00
fabian 4eaf2b286e Merge branch 'release/1.0.0' 2020-10-04 21:50:32 +02:00
fabian 00ace8ddc0 Merge branch 'master' into develop 2020-10-04 21:50:32 +02:00
fabian 5b6e33304c Increase App version 2020-10-04 18:14:36 +02:00
fabian aa777ebe02 Merge branch 'release/1.0.0' of https://bitbucket.org/fabian_zobrist/counter into release/1.0.0 2020-10-04 18:12:05 +02:00
Fabian Zobrist 00fbc60eee Revert "implement in app review"
This reverts commit 92bb0dc8
2020-09-29 11:07:50 +02:00
fabian 16bfcd3288 clean upü 2020-09-21 08:55:52 +02:00
fabian 92bb0dc86f implement in app review 2020-09-21 08:52:15 +02:00
fabian 2cb496ea8f correct typo 2020-09-21 08:51:54 +02:00
fabian 3229e51d01 Add in app review. 2020-09-21 08:50:19 +02:00
fabian ac468e2e84 Bugfix: prevent submit when both inputs hold a '-' 2020-09-21 08:49:51 +02:00
fabian 437fae633a Store history and team names persistent in shared preferences. 2020-09-20 22:33:38 +02:00
fabian f1dad279e0 Show negative sign if entered first. 2020-09-20 20:30:02 +02:00
fabian 214d5e61a4 Increase Version 2020-09-20 20:29:20 +02:00
fabian 20d9fa79a6 bump version 2020-08-31 22:57:41 +02:00
fabian be1e819f90 Remove unneeded attribute 2020-08-31 22:52:59 +02:00
fabian 65581ae257 Bugfix: choosen Theme isnot displayed when setting is opened. 2020-08-31 22:03:08 +02:00
fabian be66a50541 cleanup and optimizations 2020-08-30 21:46:54 +02:00
85 changed files with 3250 additions and 1725 deletions
+74
View File
@@ -0,0 +1,74 @@
---
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: daffda8b-5840-4a65-b6d0-73b991facfb6
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=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: daffda8b-5840-4a65-b6d0-73b991facfb6
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
View File
@@ -12,3 +12,6 @@
/captures
.externalNativeBuild
.cxx
.idea
keystore.properties
version.properties
Generated
-1
View File
@@ -1 +0,0 @@
Tichu Counter
-138
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>
-5
View File
@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
-10
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
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>
-25
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
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>
-12
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>
Generated
-6
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
View File
Binary file not shown.
+2 -1
View File
@@ -1 +1,2 @@
/build
/build
/release
+107 -20
View File
@@ -1,28 +1,76 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
plugins {
id 'com.android.application'
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 {
compileSdkVersion 30
buildToolsVersion "30.0.2"
compileSdkVersion 33
defaultConfig {
applicationId "me.zobrist.tichucounter"
minSdkVersion 16
targetSdkVersion 30
versionCode 3
versionName "1.0.0Beta2"
minSdkVersion 21
targetSdkVersion 33
versionCode versionProperties["versionCode"].toInteger()
versionName "${versionMajor}.${versionMinor}.${versionProperties["versionCode"].toInteger()}"
resConfigs 'de', 'en'
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 {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig = signingConfigs.getByName("release")
}
}
buildFeatures {
viewBinding = true
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@@ -30,19 +78,58 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
namespace 'me.zobrist.tichucounter'
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation "androidx.compose.material3:material3:1.0.1"
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.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
implementation 'androidx.legacy:legacy-support-v4:1.0.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
}
@@ -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')"
]
}
}
@@ -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')"
]
}
}
@@ -1,13 +1,11 @@
package me.zobrist.tichucounter
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
+20 -5
View File
@@ -1,25 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.zobrist.tichucounter">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".framework.TichuCounterApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustPan"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
android:exported="true"
android:windowSoftInputMode="adjustPan">
<meta-data
android:name="android.app.lib_name"
android:value="" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
</application>
</manifest>
@@ -1,83 +0,0 @@
@file:Suppress("unused")
package me.zobrist.tichucounter
import android.os.Parcel
import android.os.Parcelable
class History() : Parcelable {
private var scores: ArrayList<Round> = ArrayList()
constructor(parcel: Parcel) : this() {
scores = parcel.readSerializable() as ArrayList<Round>
}
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 = tempHistory.plus(it.scoreA.toString()).plus("\n")
}
return tempHistory
}
fun getHistoryB(): String {
var tempHistory = String()
scores.forEach {
tempHistory = tempHistory.plus(it.scoreB.toString()).plus("\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()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeSerializable(scores)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<History> {
override fun createFromParcel(parcel: Parcel): History {
return History(parcel)
}
override fun newArray(size: Int): Array<History?> {
return arrayOfNulls(size)
}
}
}
@@ -1,451 +1,234 @@
package me.zobrist.tichucounter
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.text.InputType
import android.view.Menu
import android.view.MenuItem
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.ScrollView
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.widget.doOnTextChanged
import kotlinx.android.synthetic.main.content_main.*
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.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 invertA: Boolean = false
private var invertB: Boolean = false
@Inject
lateinit var settingsAdapter: SettingsAdapter
private var updateOnChange: Boolean = true
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?) {
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)
)
settingsAdapter.registerOnChangeListener(this)
history = savedInstanceState?.getParcelable("history") ?: History()
updateView()
inputTeamA.setOnFocusChangeListener { view, b ->
if (b) {
hideKeyboard()
}
}
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(0, 0)
}
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(0, 0)
}
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.isNotEmpty()) {
tempInt = inputTeamA.text.toString().toInt() * -1
inputTeamA.setText(tempInt.toString())
} else {
invertB = false
invertA = true
}
} else if (inputTeamB.isFocused) {
if (inputTeamB.text.isNotEmpty()) {
tempInt = inputTeamB.text.toString().toInt() * -1
inputTeamB.setText(tempInt.toString())
} else {
invertA = false
invertB = true
}
}
}
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)
setContent {
AppTheme {
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(MaterialTheme.colorScheme.background)
NavigationDrawer()
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable("history", history)
override fun onDestroy() {
super.onDestroy()
settingsAdapter.unregisterOnChangeListener(this)
}
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 onLanguageChanged(language: Language) {
AppCompatDelegate.setApplicationLocales(language.value)
}
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)
override fun onThemeChanged(theme: Theme) {
val themeValue = when (theme) {
Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
Theme.DEFAULT -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
AppCompatDelegate.setDefaultNightMode(themeValue)
}
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) {
if (invertA) {
invertA = false
inputTeamA.text.append('-')
}
inputTeamA.text.append(toAppend)
} else if (inputTeamB.isFocused) {
if (invertB) {
invertB = false
inputTeamB.text.append('-')
}
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("", 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()
dialog.show()
}
private fun updateTheme(which: Int) {
when (which) {
0 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
1 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
2 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
delegate.applyDayNight()
}
private fun keepScreenOn(keepOn: Boolean) {
if (keepOn) {
override fun onScreenOnChanged(keepOn: KeepScreenOn) {
if (keepOn.value) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
val prefs = this.getSharedPreferences("Settings", Context.MODE_PRIVATE).edit()
prefs.putBoolean("Screen_On", keepOn)
prefs.apply()
@OptIn(ExperimentalMaterial3Api::class)
@Composable
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()
}
}
}
}
}
@@ -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)
}
}
@@ -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
}
@@ -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>)
}
@@ -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()
}
}
@@ -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
}
}
@@ -0,0 +1,35 @@
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 where uid ")
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): Flow<Game>
@Query("SELECT * FROM game WHERE active is 1")
fun getActive(): Flow<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)
}
@@ -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,
@Relation(
parentColumn = "uid",
entityColumn = "gameId"
)
val rounds: List<Round>
)
@@ -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>
}
@@ -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,
var nameA: String,
var nameB: String,
val created: Date,
var modified: Date,
@PrimaryKey(autoGenerate = true) override val uid: Long = 0
) : IEntity
@@ -0,0 +1,5 @@
package me.zobrist.tichucounter.data.entity
interface IEntity {
val uid: Long
}
@@ -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
@@ -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)
@@ -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)
}
@@ -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
}
@@ -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)
}
@@ -0,0 +1,3 @@
package me.zobrist.tichucounter.domain
enum class Route { COUNTER, HISTORY, SETTINGS, ABOUT }
@@ -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)
}
}
}
@@ -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()
}
}
@@ -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 = {}
)
@@ -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 = {}
)
@@ -0,0 +1,7 @@
package me.zobrist.tichucounter.framework
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class TichuCounterApplication : Application()
@@ -0,0 +1,129 @@
package me.zobrist.tichucounter.repository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
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? = null
val activeGame: Game
get() {
return _activeGame!!
}
init {
CoroutineScope(Dispatchers.IO).launch {
gameDao.getActive().collect {
if (it == null) {
gameDao.insert(Game(true, "TeamA", "TeamB", Date(), Date()))
} 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 updateGame(game: Game) {
game.modified = Date()
withContext(Dispatchers.IO) {
gameDao.update(game)
}
}
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 {
gameDao.getGameById(uid).take(1).collect {
gameDao.delete(it)
val rounds = roundDao.getAllForGame(it.uid)
roundDao.delete(rounds)
}
} catch (_: NullPointerException) {
}
}
}
suspend fun deleteAllInactive() {
withContext(Dispatchers.IO) {
try {
gameDao.getAll().take(1).collect { games ->
val activeId = games.first { it.active }.uid
val gamesToDelete = games.filter { !it.active }
val roundsToDelete = roundDao.getAll().filter { it.gameId != activeId }
gameDao.delete(gamesToDelete)
roundDao.delete(roundsToDelete)
}
} catch (_: NullPointerException) {
}
}
}
fun getActiveGameFlow(): Flow<GameWithScores?> {
return gameDao.getActiveWithRounds()
}
fun getAllWithRoundFlow(): Flow<List<GameWithScores>> {
return gameDao.getGamesWithRounds()
}
}
@@ -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)
@@ -0,0 +1,81 @@
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
if (it != null) {
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()
}
}
}
@@ -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
)
}
@@ -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()
}
}
}
@@ -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)) },
)
}
}
}
@@ -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 enableSubmit: 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 deleteClicked() {
}
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() {
}
}
@@ -0,0 +1,289 @@
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.launch
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.Tichu
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 enableSubmit: 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 deleteClicked()
fun updateFocusStateA(state: Boolean)
fun updateFocusStateB(state: Boolean)
fun swapInputScores()
fun hideKeyboard()
fun showKeyboard()
}
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 enableSubmit 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
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() {
enableSubmit = isValidTichuRound()
}
override fun submitClicked() {
viewModelScope.launch {
gameRepository.addRoundToActiveGame(currentScoreA.toInt(), currentScoreB.toInt())
}
currentScoreA = ""
currentScoreB = ""
enableSubmit = false
}
override fun digitClicked(digit: String) {
focusLastInput()
activeValue += digit
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 deleteClicked() {
if (activeValue != "") {
activeValue = activeValue.dropLast(1)
}
updateOtherScore()
updateSubmitButton()
}
override fun updateNameA(value: String) {
viewModelScope.launch {
val game = gameRepository.activeGame
game.nameA = value
gameRepository.updateGame(game)
}
}
override fun updateNameB(value: String) {
viewModelScope.launch {
val game = gameRepository.activeGame
game.nameB = value
gameRepository.updateGame(game)
}
}
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
}
}
@@ -0,0 +1,329 @@
package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration
import androidx.compose.animation.core.*
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.enableSubmit,
viewModel.isAFocused,
viewModel.isBFocused,
{ viewModel.updateFocusStateA(it) },
{ viewModel.updateFocusStateB(it) },
{ viewModel.digitClicked(it) },
{ viewModel.addSub100Clicked(it) },
{ viewModel.deleteClicked() },
{ viewModel.negateClicked() },
{ viewModel.submitClicked() },
{ viewModel.hideKeyboard() },
{ viewModel.swapInputScores() }
)
}
@Composable
fun KeyboardView(
scoreA: String,
scoreB: String,
requestFocusA: FocusRequester,
requestFocusB: FocusRequester,
enableSubmit: Boolean,
focusStateA: Boolean,
focusStateB: Boolean,
updateFocusStateA: (Boolean) -> Unit,
updateFocusStateB: (Boolean) -> Unit,
digitClicked: (String) -> Unit,
addSub100Clicked: (Int) -> Unit,
deleteClicked: () -> Unit,
negateClicked: () -> Unit,
submitClicked: () -> Unit,
hideKeyboardClicked: () -> Unit,
onSwapClicked: () -> 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) {
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)) {
KeyboardIconButton(Icons.Outlined.Backspace) {
deleteClicked()
}
}
}
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, enableSubmit) {
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, onClicked: () -> Unit) {
ElevatedButton(
onClick = { onClicked() },
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.padding(2.dp),
enabled = enabled,
) {
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(
"1",
"3511",
FocusRequester(),
FocusRequester(),
enableSubmit = false,
focusStateA = true,
focusStateB = false,
updateFocusStateA = {},
updateFocusStateB = {},
digitClicked = {},
addSub100Clicked = {},
deleteClicked = {},
negateClicked = {},
submitClicked = {},
hideKeyboardClicked = {},
onSwapClicked = {})
}
}
}
@@ -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)
}
}
}
@@ -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", {}, {})
}
}
@@ -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)
}
}
}
@@ -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, {}, {}) {}
}
@@ -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()
}
}
}
@@ -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
) {}
}
}
}
@@ -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()
}
}
}
)
}
@@ -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,
) {}
}
}
}
@@ -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

@@ -1,319 +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="true">
<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" />
</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" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
-25
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>
-322
View File
@@ -1,322 +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"
android:clickable="true"
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" />
</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" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
-25
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>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
+21 -5
View File
@@ -1,10 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<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="keep_screen_on">Bildschirm eingeschaltet lassen</string>
<string name="confirmClear">Möchten Sie das laufende Spiel wirklich löshen?</string>
<string name="yes">Ja</string>
<string name="no">Nein</string>
<string name="choose_language_text">Sprache wählen</string>
<string name="android_default_text">Android Standard</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>
@@ -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>
+2 -3
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">@color/ic_launcher_background</color>
<color name="colorPrimaryDark">#830000</color>
<color name="colorAccent">#F57F17</color>
<color name="primaryColor">#d50000</color>
</resources>
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#DC0E00</color>
<color name="ic_launcher_background">@color/primaryColor</color>
</resources>
+23 -8
View File
@@ -1,14 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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="keep_screen_on">Keep screen on</string>
<string name="confirmClear">Do you really want to delete the current game?</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="choose_language_text">Choose language</string>
<string name="android_default_text">Android Default</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>
-17
View File
@@ -1,20 +1,3 @@
<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>
+5
View File
@@ -0,0 +1,5 @@
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar" />
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!-- Exclude specific shared preferences that contain GCM registration Id -->
</full-backup-content>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
</cloud-backup>
</data-extraction-rules>
+5
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>
@@ -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())
}
}
@@ -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())
}
}
@@ -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))
}
}
+12 -4
View File
@@ -1,12 +1,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.0"
ext {
compose_version = '1.1.1'
}
ext.kotlin_version = "1.7.20"
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
classpath 'com.android.tools.build:gradle:7.4.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// 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 {
repositories {
google()
jcenter()
mavenCentral()
}
}
+1 -1
View File
@@ -16,6 +16,6 @@ org.gradle.jvmargs=-Xmx2048m
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# 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=official
+1 -1
View File
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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