114 Commits

Author SHA1 Message Date
7f904f916f Fix typo.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-01-20 23:36:55 +01:00
c4a552ad8c Move theme to top composable. Increase button size.
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-20 23:14:59 +01:00
45746aef0b Clean up.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 19:33:34 +01:00
cd39384207 Add undo redo functionality. Move new game to navigation drawer 2023-01-20 19:28:50 +01:00
f44b51c075 Show overflow on long text.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 16:54:53 +01:00
9ec2a0e465 Improve settings. 2023-01-20 16:54:32 +01:00
f7ccf46b55 Beautify settings page.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 13:55:06 +01:00
0da8a508f5 Style setting screen.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 13:01:15 +01:00
90f0b09e3d Improve navigation.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 12:20:59 +01:00
eac916d8ec Style keyboard. Add Counter previerw.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 11:33:05 +01:00
57bb4deebe Do some styling.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 08:58:51 +01:00
8b4ce20c99 Fix Theme
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 08:39:27 +01:00
bd19858834 Fix copy paste mistake
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-20 07:44:23 +01:00
395f93ca89 Enable materialYou theme usage.
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-20 07:39:24 +01:00
45f11d5caf Portrait mode for counter.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-19 21:10:16 +01:00
f947c5aeb2 Color the android app bar.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-19 20:13:59 +01:00
fb10dce89e Remove old files. Add theme and apply it
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-19 19:47:34 +01:00
e09bd26859 Update android studio. Add compose settings screen.
Some checks are pending
continuous-integration/drone/push Build is pending
2023-01-19 19:38:01 +01:00
bcc08e4605 Add actions.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-13 16:14:24 +01:00
f2cd02e130 Use composable scaffold.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-13 15:24:35 +01:00
9c653d788b Fix build. Remove unneeded import.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-10 11:40:37 +01:00
a4ccd62d72 Composify more fragments.
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-09 20:08:13 +01:00
823d1a6ca4 Simplify composables.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-08 18:47:08 +01:00
0f4d008104 Remove unused code.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 22:43:38 +01:00
2ed221a99f Replace RoundList with compose.
Some checks are pending
continuous-integration/drone/push Build is running
2023-01-07 22:42:21 +01:00
5f6da1d7d4 Move all functionality to viewmodel. Use simple button format.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 19:30:56 +01:00
ae6210073d Use compose for keyboard.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 14:31:40 +01:00
26a44dcc18 Different style for history.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 09:13:51 +01:00
b73ddbf4cc Fix app not installed bug. Delete obsolete files. Add compose preview.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 08:51:31 +01:00
b7a821b9f6 Add compose to project. display history with compose.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-07 01:15:04 +01:00
6b396dba24 Refromat code.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-06 22:15:07 +01:00
2bf0666946 Fix names and strings. Fix not all games shown in history.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-06 22:01:00 +01:00
74be455d48 Show history.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-06 19:44:02 +01:00
1e428e854e Move all fragments to ui package. Store created and modified date to game.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-06 11:09:06 +01:00
94cdbcad0b Use when statement instead of if. Change var to val.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-05 18:27:36 +01:00
f8b35bddda Reformat. Remove unneeded application restart after language change.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-05 18:18:48 +01:00
ec765b5fec Add Drawer navigation. Convert to multiple fragments shown with app drawer.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-05 17:58:20 +01:00
39b092c7c5 Fix preference listener unregistered.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-31 12:32:04 +01:00
8fb90bd6d0 Default to false for screen_on.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-31 12:02:37 +01:00
e01df3a5c7 Apply application preferences.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-31 11:54:20 +01:00
3b7b71ce77 Add preference activity.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-29 14:30:04 +01:00
968edfbb67 Optimize query. Update teamname on change.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-29 10:08:04 +01:00
6ded9efe68 Remove unneeded function calls.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-28 16:05:59 +01:00
bdc9b64c63 Extract DaoBase
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-28 15:56:05 +01:00
bdb4410638 Extract fragmentBase class
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-28 15:37:51 +01:00
f6b3f70e18 Split in more fragments. Trigger gui update trough db change
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-28 12:30:31 +01:00
70da57df9c Format all files. 2022-12-27 19:04:55 +01:00
17ed7d18f6 Implement undo last round.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 19:00:59 +01:00
30c8941bd1 Fix crash on empty database at startup. 2022-12-27 18:39:07 +01:00
bdf3c0a98e Change primary key to long. Implement newGame menu option.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 18:16:14 +01:00
09739ccc8e Fix tests.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 17:25:57 +01:00
7655d1d7a3 Add database. Write score to database and update.
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-27 17:19:55 +01:00
6edbe12fd1 Add unit test for Tichu. 2022-12-27 17:18:24 +01:00
db5384201f Add test for Tichu.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 11:58:59 +01:00
252889eff7 Fix hilt integration.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 11:40:14 +01:00
4e6193501b Use single live event to prevent false trigger after rotation. remove hilt (for the moment)
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 08:57:02 +01:00
637a34efd7 Fix app crash
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-24 15:00:41 +01:00
346ac10e68 Remove tests. need to rewrite them.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-24 12:15:27 +01:00
0e31908c7a Use viewmodels with LiveData.
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-24 12:12:19 +01:00
5e0c80be17 Split layout in Fragments.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-21 22:46:07 +01:00
a835580682 Target API33. Setup Hilt DI framework. Apply formatting
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-21 20:20:22 +01:00
479f5476e0 Merge pull request 'feature/Publish_artifacts_directly_to_seafile_#8' (#9) from feature/Publish_artifacts_directly_to_seafile_#8 into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#9
2022-12-17 21:25:34 +01:00
e825c86855 Fix file name extension
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-12-17 20:54:49 +01:00
b3793caba2 Fix more copy paste mistakes.
Some checks failed
continuous-integration/drone/push Build was killed
2022-12-17 19:32:19 +01:00
e972400313 Fix wrong merge from stash.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-17 19:10:13 +01:00
72bb963bd2 Extract variables to enviroment. Rename file after uplaod on tagged deploy
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-17 19:05:00 +01:00
beddcb7125 Fix quote mismatch in commamd
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2022-12-17 07:53:41 +01:00
f3e50dea8b Remove double quotes from upload link parameter
Some checks failed
continuous-integration/drone/push Build encountered an error
2022-12-17 07:48:12 +01:00
1cc60756d1 Remove double quotes from upload link parameter
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-17 00:24:08 +01:00
d9486dfec4 Fix file path references
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-17 00:09:54 +01:00
c3999f45d9 Use curl image for deployment
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-16 23:57:01 +01:00
5ee8b4114b put complex commands in quotes.
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-16 21:10:01 +01:00
e1634dafd9 Fix format.
Some checks failed
continuous-integration/drone/push Build encountered an error
2022-12-16 20:15:07 +01:00
1291d24ee3 Use seafile api to upload files directly
Some checks failed
continuous-integration/drone/push Build encountered an error
2022-12-16 20:13:23 +01:00
06367bb101 Merge pull request 'feature/#4-keep-tagged-builds' (#5) from feature/#4-keep-tagged-builds into develop
Some checks failed
continuous-integration/drone Build is passing
continuous-integration/drone/push Build encountered an error
Reviewed-on: fabian/TichuCounter#5
2022-12-10 19:00:10 +01:00
f9aec95547 Remove ignored files from repository.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-12-10 18:58:41 +01:00
50391c3018 Ignore release folder. 2022-12-10 18:57:40 +01:00
6f221d7880 Fix deployment for tagged commit.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-12-10 18:26:22 +01:00
a4355df9d0 Fix build again.
Some checks are pending
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is running
2022-12-10 18:14:24 +01:00
ab6bc2212d Send slack also on failure.
Some checks failed
continuous-integration/drone/push Build is failing
Fail build temporarily.
2022-12-10 18:12:31 +01:00
c16a865937 Fix versionCode not found in release build.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 18:05:55 +01:00
12de9b9ebd Generate signed release apk instead of debug variant. 2022-12-10 17:52:24 +01:00
ba337a3e0e Generate versionCode from timestamp.
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-10 17:44:42 +01:00
c2882a9751 Add step to be executed on tagged build.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 11:28:09 +01:00
15e24afe12 Merge branch 'develop' into release/1.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 10:59:10 +01:00
5ec74da139 Add slack notification
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 09:11:12 +01:00
fa24e254dc Extract prepare signing step
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 08:57:54 +01:00
5447c47d61 Fix copy paste mistake
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 08:42:37 +01:00
9986fd7565 Extract deploy step
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-10 08:24:15 +01:00
bc44dfd386 Add -p option to mkdir to fix error
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-10 08:06:52 +01:00
c895c08924 Fix file generation
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-10 07:57:25 +01:00
3022e8442b Add keystore and build on sign on release
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-09 19:33:52 +01:00
fe95de53b4 revert 383162ea3a
Some checks failed
continuous-integration/drone/push Build was killed
revert Output content of storefile temporarily
2022-12-09 19:33:31 +01:00
383162ea3a Output content of storefile temporarily
Some checks failed
continuous-integration/drone/push Build was killed
2022-12-09 19:31:48 +01:00
818965e16c Create keystore.properties before build
Some checks failed
continuous-integration/drone/push Build was killed
2022-12-09 19:29:01 +01:00
6cec709476 Revert to cp make sure directory exists
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-09 16:03:51 +01:00
78059fd187 Try rsync
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-09 15:57:38 +01:00
630f9689ad revert 531bfb42b6
Some checks failed
continuous-integration/drone/push Build was killed
revert Test gradle image
2022-12-09 15:54:47 +01:00
531bfb42b6 Test gradle image
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-09 15:52:42 +01:00
cdb00abbe3 Deploy builds to seafile library
Some checks failed
continuous-integration/drone/push Build encountered an error
continuous-integration/drone Build is failing
2022-12-09 15:31:45 +01:00
bc673b4ef7 Merge branch 'develop' into release/1.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-08 23:30:10 +01:00
f7b68c368a Merge pull request 'Add drone build script.' (#3) from feature/droneBuild into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#3
2022-12-08 23:24:50 +01:00
88c5096c7c Add conditions
All checks were successful
continuous-integration/drone Build is passing
continuous-integration/drone/pr Build is passing
2022-12-08 22:48:57 +01:00
bbab1a1eb4 Try different container 2022-12-08 22:45:36 +01:00
d4fa1483cd Fit tests.
All checks were successful
continuous-integration/drone Build is passing
2022-12-08 22:33:24 +01:00
6146219d9c add drone yml file
Some checks failed
continuous-integration/drone Build was killed
2022-12-03 14:49:38 +01:00
eec11bef81 Bump version. 2022-11-02 22:19:45 +01:00
b8ad540b57 Format all files. And fix Round unit test (that now fails...) 2022-11-01 18:35:20 +01:00
ed28d054ec Use int extensions. 2022-11-01 18:07:41 +01:00
78481d29fc Update gradle and IDE. 2022-10-30 17:12:56 +01:00
a7b3247796 Use correct binding. Remove unneeded code. 2022-06-28 10:14:50 +02:00
0ed30dc87a Update deprecated extensions. 2022-06-26 19:40:42 +02:00
cc8ae173f8 Update. Add language setting. 2022-06-26 14:08:53 +02:00
00ace8ddc0 Merge branch 'master' into develop 2020-10-04 21:50:32 +02:00
88 changed files with 2635 additions and 1712 deletions

74
.drone.yml Normal file
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
.gitignore vendored
View File

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

1
.idea/.name generated
View File

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

View File

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

View File

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

View File

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

21
.idea/gradle.xml generated
View File

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

View File

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

48
.idea/misc.xml generated
View File

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

View File

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

6
.idea/vcs.xml generated
View File

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

BIN
AndroidKey Normal file

Binary file not shown.

1
app/.gitignore vendored
View File

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

View File

@@ -1,28 +1,68 @@
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()
// 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 7
versionName "1.0.0"
minSdkVersion 21
targetSdkVersion 33
versionCode versionProperties["versionCode"].toInteger()
versionName "1.1.0Beta1"
resConfigs 'de', 'en'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables {
useSupportLibrary true
}
}
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,22 +70,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 'com.google.android.play:core:1.8.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.code.gson:gson:2.8.5'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
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.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.google.android.play:core-ktx:1.8.1'
implementation 'com.google.code.gson:gson:2.8.9'
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
}

View File

@@ -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.
*

View File

@@ -1,26 +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"
android:fullBackupContent="@xml/backup_descriptor">
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>

View File

@@ -0,0 +1,83 @@
package me.zobrist.tichucounter
import android.content.SharedPreferences
import android.os.Bundle
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager
import dagger.hilt.android.AndroidEntryPoint
import me.zobrist.tichucounter.domain.Language
import me.zobrist.tichucounter.domain.SettingsAdapter
import me.zobrist.tichucounter.domain.Theme
import javax.inject.Inject
@AndroidEntryPoint
abstract class BaseActivity : AppCompatActivity(),
SharedPreferences.OnSharedPreferenceChangeListener {
@Inject
lateinit var settingsAdapter: SettingsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
keepScreenOn(settingsAdapter.keepScreenOn)
updateTheme(settingsAdapter.theme)
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(this)
}
override fun onResume() {
super.onResume()
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
PreferenceManager.getDefaultSharedPreferences(this)
.unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
when (key) {
settingsAdapter.language::class.simpleName -> setLanguage(settingsAdapter.language)
settingsAdapter.keepScreenOn::class.simpleName -> keepScreenOn(settingsAdapter.keepScreenOn)
settingsAdapter.theme::class.simpleName -> updateTheme(settingsAdapter.theme)
}
}
private fun updateTheme(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
}
if (themeValue != AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.setDefaultNightMode(themeValue)
delegate.applyDayNight()
}
}
private fun keepScreenOn(keepOn: Boolean) {
if (keepOn) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
private fun setLanguage(language: Language) {
val currentLocale = AppCompatDelegate.getApplicationLocales()[0].toString()
if (language.value != null && language.value != currentLocale) {
val newLocale = LocaleListCompat.forLanguageTags(language.value)
AppCompatDelegate.setApplicationLocales(newLocale)
}
}
}

View File

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

View File

@@ -1,454 +1,246 @@
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.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.widget.doOnTextChanged
import com.google.gson.Gson
import kotlinx.android.synthetic.main.content_main.*
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.StringRes
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.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
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.NavigationAction
import me.zobrist.tichucounter.domain.TopBarAction
import me.zobrist.tichucounter.repository.GameRepository
import me.zobrist.tichucounter.ui.AppTheme
import me.zobrist.tichucounter.ui.MainViewModel
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.settings.SettingsView
import me.zobrist.tichucounter.ui.settings.SettingsViewModel
import javax.inject.Inject
class MainActivity : AppCompatActivity() {
@AndroidEntryPoint
class MainActivity : BaseActivity() {
private var updateOnChange: Boolean = true
@Inject
lateinit var gameRepository: GameRepository
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)
setContent {
AppTheme() {
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(MaterialTheme.colorScheme.background)
NavigationDrawer()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScaffoldLayout(
drawerState: DrawerState,
scope: CoroutineScope,
navController: NavHostController
) {
Scaffold(
topBar = {
TopBar(
mainViewModel.topBarTitle,
mainViewModel.topBarIcon,
{ mainViewModel.onNavigateClicked() },
mainViewModel.topBarActions
)
}) {
NavHost(
navController = navController,
startDestination = "counter",
modifier = Modifier.padding(it)
) {
composable("counter") {
Counter(counterViewModel)
mainViewModel.topBarActions = (listOf(
TopBarAction(
Icons.Outlined.Undo,
mainViewModel.isUndoActionActive
) { mainViewModel.undoLastRound() },
TopBarAction(
Icons.Outlined.Redo,
mainViewModel.isRedoActionActive
) { mainViewModel.redoLastRound() }
))
mainViewModel.topBarIcon = Icons.Outlined.Menu
mainViewModel.topBarTitle = stringResource(R.string.app_name)
mainViewModel.topBarNavigationAction =
NavigationAction { scope.launch { drawerState.open() } }
}
composable("history") {
HistoryList(historyViewModel)
mainViewModel.topBarActions = emptyList()
mainViewModel.topBarIcon = Icons.Outlined.ArrowBack
mainViewModel.topBarTitle = stringResource(R.string.menu_history)
mainViewModel.topBarNavigationAction =
NavigationAction { navController.navigate("counter") }
}
composable("settings") {
SettingsView(settingsViewModel)
mainViewModel.topBarActions = emptyList()
mainViewModel.topBarIcon = Icons.Outlined.ArrowBack
mainViewModel.topBarTitle = stringResource(R.string.menu_settings)
mainViewModel.topBarNavigationAction =
NavigationAction { navController.navigate("counter") }
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private 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,
)
}
}
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun NavigationDrawer() {
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
val navController = rememberNavController()
val items = listOf(
Screen("history", Icons.Outlined.List, R.string.menu_history),Screen("settings", Icons.Outlined.Settings, R.string.menu_settings)
)
val json = this.getSharedPreferences("Settings", Context.MODE_PRIVATE).getString("history", "{\"scores\":[]}")
history = Gson().fromJson(json, History::class.java)
nameTeamA.setText(this.getSharedPreferences("Settings", Context.MODE_PRIVATE).getString("nameTeamA", "TeamA"))
nameTeamB.setText(this.getSharedPreferences("Settings", Context.MODE_PRIVATE).getString("nameTeamB", "TeamB"))
updateView()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
inputTeamA.setOnFocusChangeListener { view, b ->
if (b) {
hideKeyboard()
}
}
Spacer(Modifier.height(20.dp))
inputTeamB.setOnFocusChangeListener { view, b ->
if (b) {
hideKeyboard()
NavigationDrawerItem(
icon = { Icon(Icons.Outlined.RestartAlt, contentDescription = null) },
colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = MaterialTheme.colorScheme.secondaryContainer
),
label = { Text(stringResource(R.string.newGame)) },
selected = false,
onClick = {
scope.launch { drawerState.close() }
mainViewModel.newGame()
navController.navigate("counter") {
// 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(navController.graph.findStartDestination().id) {
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
}
inputTeamA.doOnTextChanged { text, start, count, after ->
if (inputTeamA.isFocused) {
if (inputTeamA.text.isNotEmpty()) {
if (updateOnChange) {
currentRound = try {
Round(text.toString().toInt(), true)
} catch (e: java.lang.Exception) {
Round(1, 1)
}
inputTeamB.setText(currentRound.scoreB.toString())
} else {
updateOnChange = true
}
} else {
inputTeamA.text.clear()
inputTeamB.text.clear()
}
}
if (currentRound.isValidRound() && inputTeamA.text.isNotEmpty() && inputTeamB.text.isNotEmpty()) {
enableSubmitButton()
} else {
disableSubmitButton()
}
}
inputTeamB.doOnTextChanged { text, start, count, after ->
if (inputTeamB.isFocused) {
if (inputTeamB.text.isNotEmpty()) {
if (updateOnChange) {
currentRound = try {
Round(text.toString().toInt(), false)
} catch (e: java.lang.Exception) {
Round(1, 1)
}
inputTeamA.setText(currentRound.scoreA.toString())
} else {
updateOnChange = true
}
} else {
inputTeamA.text.clear()
inputTeamB.text.clear()
}
}
if (currentRound.isValidRound() && inputTeamA.text.isNotEmpty() && inputTeamB.text.isNotEmpty()) {
enableSubmitButton()
} else {
disableSubmitButton()
}
}
buttonAdd100.setOnClickListener {
giveFocusToAIfNone()
if (inputTeamA.isFocused) {
currentRound.scoreA = try {
inputTeamA.text.toString().toInt() + 100
} catch (e: Exception) {
currentRound.scoreB = 0
inputTeamB.setText(currentRound.scoreB.toString())
100
}
updateOnChange = false
inputTeamA.setText(currentRound.scoreA.toString())
}
if (inputTeamB.isFocused) {
currentRound.scoreB = try {
inputTeamB.text.toString().toInt() + 100
} catch (e: Exception) {
currentRound.scoreA = 0
inputTeamA.setText(currentRound.scoreA.toString())
100
}
updateOnChange = false
inputTeamB.setText(currentRound.scoreB.toString())
}
}
buttonSub100.setOnClickListener {
giveFocusToAIfNone()
if (inputTeamA.isFocused) {
currentRound.scoreA = try {
inputTeamA.text.toString().toInt() - 100
} catch (e: Exception) {
currentRound.scoreB = 0
inputTeamB.setText(currentRound.scoreB.toString())
-100
}
updateOnChange = false
inputTeamA.setText(currentRound.scoreA.toString())
}
if (inputTeamB.isFocused) {
currentRound.scoreB = try {
inputTeamB.text.toString().toInt() - 100
} catch (e: Exception) {
currentRound.scoreA = 0
inputTeamA.setText(currentRound.scoreA.toString())
-100
}
updateOnChange = false
inputTeamB.setText(currentRound.scoreB.toString())
}
}
button0.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('0')
}
button1.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('1')
}
button2.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('2')
}
button3.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('3')
}
button4.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('4')
}
button5.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('5')
}
button6.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('6')
}
button7.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('7')
}
button8.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('8')
}
button9.setOnClickListener {
giveFocusToAIfNone()
appendToFocusedInput('9')
}
buttonInv.setOnClickListener {
val tempInt: Int
giveFocusToAIfNone()
if (inputTeamA.isFocused) {
if (inputTeamA.text.toString().equals("-")) {
inputTeamA.text.clear()
} else if (inputTeamA.text.isNotEmpty()) {
tempInt = inputTeamA.text.toString().toInt() * -1
inputTeamA.setText(tempInt.toString())
} else {
updateOnChange = false
appendToFocusedInput('-')
currentRound = Round(1,1)
}
} else if (inputTeamB.isFocused) {
if (inputTeamB.text.toString().equals("-")) {
inputTeamB.text.clear()
} else if (inputTeamB.text.isNotEmpty()) {
tempInt = inputTeamB.text.toString().toInt() * -1
inputTeamB.setText(tempInt.toString())
} else {
updateOnChange = false
appendToFocusedInput('-')
currentRound = Round(1,1)
}
}
}
buttonBack.setOnClickListener {
giveFocusToAIfNone()
if (inputTeamA.isFocused) {
if (inputTeamA.text.isNotEmpty()) {
val string = inputTeamA.text.toString()
inputTeamA.setText(string.substring(0, string.length - 1))
}
} else if (inputTeamB.isFocused) {
if (inputTeamB.text.isNotEmpty()) {
val string = inputTeamB.text.toString()
inputTeamB.setText(string.substring(0, string.length - 1))
}
}
}
submit.setOnClickListener {
giveFocusToAIfNone()
if (inputTeamA.text.isNotEmpty() && inputTeamB.text.isNotEmpty()) {
history.logRound(
Round(
inputTeamA.text.toString().toInt(),
inputTeamB.text.toString().toInt()
)
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
updateView()
Spacer(Modifier.height(20.dp))
inputTeamA.text.clear()
inputTeamB.text.clear()
disableSubmitButton()
Divider()
scrollViewHistory.fullScroll(ScrollView.FOCUS_DOWN)
items.forEach { screen ->
NavigationDrawerItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
scope.launch { drawerState.close() }
navController.navigate(screen.route) {
// 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(navController.graph.findStartDestination().id) {
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
}
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
},
content = { MyScaffoldLayout(drawerState, scope, navController) }
)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val prefs = this.getSharedPreferences("Settings", Context.MODE_PRIVATE).edit()
prefs.putString("history", Gson().toJson(history))
prefs.putString("nameTeamA", nameTeamA.text.toString())
prefs.putString("nameTeamB", nameTeamB.text.toString())
prefs.apply()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
menu.findItem(R.id.action_screenOn).isChecked =
this.getSharedPreferences("Settings", Context.MODE_PRIVATE)
.getBoolean("Screen_On", false)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_clear -> {
val builder = AlertDialog.Builder(this)
builder.setMessage(getString(R.string.confirmClear))
.setTitle(R.string.clear)
.setCancelable(false)
.setPositiveButton(getString(R.string.yes)) { dialog, id ->
dialog.dismiss()
clearAll()
}
.setNegativeButton(getString(R.string.no)) { dialog, id ->
dialog.cancel()
}
builder.create().show()
true
}
R.id.action_undo -> {
undoLastRound()
true
}
R.id.action_theme -> {
chooseThemeDialog()
true
}
R.id.action_screenOn -> {
item.isChecked = !item.isChecked
keepScreenOn(item.isChecked)
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun hideKeyboard() {
val imm: InputMethodManager =
getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(currentFocus!!.windowToken, 0)
}
private fun giveFocusToAIfNone() {
if (!inputTeamA.isFocused && !inputTeamB.isFocused) {
inputTeamA.requestFocus()
}
}
private fun undoLastRound() {
history.revertLastRound()
updateView()
}
private fun updateView() {
scoreA.text = history.getScoreA().toString()
scoreB.text = history.getScoreB().toString()
historyA.text = history.getHistoryA()
historyB.text = history.getHistoryB()
}
private fun clearAll() {
historyA.text = ""
historyB.text = ""
inputTeamA.text.clear()
inputTeamB.text.clear()
scoreA.text = "0"
scoreB.text = "0"
history.clearAll()
}
private fun appendToFocusedInput(toAppend: Char) {
if (inputTeamA.isFocused) {
inputTeamA.text.append(toAppend)
} else if (inputTeamB.isFocused) {
inputTeamB.text.append(toAppend)
}
}
private fun enableSubmitButton() {
submit.imageAlpha = 255 // 0 being transparent and 255 being opaque
submit.isEnabled = true
}
private fun disableSubmitButton() {
submit.imageAlpha = 60 // 0 being transparent and 255 being opaque
submit.isEnabled = false
}
private fun chooseThemeDialog() {
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.choose_theme_text))
val styles = arrayOf("Light", "Dark", "System default")
val checkedItem =
this.getSharedPreferences("Settings", Context.MODE_PRIVATE).getInt("Theme", 2)
val prefs = this.getSharedPreferences("Settings", Context.MODE_PRIVATE).edit()
builder.setSingleChoiceItems(styles, checkedItem) { dialog, which ->
prefs.putInt("Theme", which)
prefs.apply()
updateTheme(which)
dialog.dismiss()
}
val dialog = builder.create()
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) {
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()
}
private class Screen(val route: String, val icon: ImageVector, @StringRes val resourceId: Int)
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
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)
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
package me.zobrist.tichucounter.data
import androidx.room.Entity
import java.util.*
@Entity
data class GameAndScore(
override var active: Boolean,
override var nameA: String,
override var nameB: String,
override val created: Date,
override var modified: Date,
override var gameId: Long,
override var scoreA: Int,
override var scoreB: Int,
) : IGame, IRound {
}

View File

@@ -0,0 +1,39 @@
package me.zobrist.tichucounter.data
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface GameDao : DaoBase<Game> {
@Query("SELECT * FROM game")
fun getAll(): Flow<List<Game>>
@Query(
"SELECT active, " +
"nameA, " +
"nameB, " +
"created, " +
"modified, " +
"game.uid as gameId, " +
"COALESCE(SUM(round.scoreA), 0) as scoreA, " +
"COALESCE(SUM(round.scoreB), 0) as scoreB " +
"FROM game " +
"LEFT JOIN round ON round.gameId = game.uid GROUP BY game.uid ORDER BY modified DESC"
)
fun getAllWithPoints(): Flow<List<GameAndScore>>
@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)
}

View File

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

View File

@@ -0,0 +1,11 @@
package me.zobrist.tichucounter.data
import java.util.*
interface IGame {
var active: Boolean
var nameA: String
var nameB: String
val created: Date
var modified: Date
}

View File

@@ -0,0 +1,7 @@
package me.zobrist.tichucounter.data
interface IRound {
var gameId: Long
var scoreA: Int
var scoreB: Int
}

View File

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

View File

@@ -0,0 +1,27 @@
package me.zobrist.tichucounter.data
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface RoundDao : DaoBase<Round> {
@Query("SELECT * FROM round WHERE gameId is :gameId")
fun getAllForGame(gameId: Long?): List<Round>
@Query(
"SELECT gameId, SUM(scoreA) as scoreA, SUM(scoreB) as scoreB " +
"FROM round " +
"LEFT JOIN game ON game.uid = round.gameId " +
"WHERE game.active == 1"
)
fun getRoundSumForActiveGame(): Flow<Round>
@Query(
"SELECT gameId, scoreA, scoreB, round.uid " +
"FROM round " +
"LEFT JOIN game ON game.uid = round.gameId " +
"WHERE game.active == 1"
)
fun getForActiveGame(): Flow<List<Round>>
}

View File

@@ -0,0 +1,16 @@
package me.zobrist.tichucounter.domain
import kotlin.math.abs
fun Int.isMultipleOf5(): Boolean {
return (this % 5) == 0
}
fun Int.isMultipleOf100(): Boolean {
return (this % 100) == 0
}
fun Int.getAbsoluteDifference(other: Int): Int {
return abs(this - other)
}

View File

@@ -0,0 +1,3 @@
package me.zobrist.tichucounter.domain
class NavigationAction(val aciton: () -> Unit)

View File

@@ -0,0 +1,65 @@
package me.zobrist.tichucounter.domain
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate.getApplicationLocales
import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
enum class Theme { DEFAULT, DARK, LIGHT }
enum class Language(val value: String) { ENGLISH("en"), GERMAN("de") }
class SettingsAdapter @Inject constructor(@ApplicationContext private val context: Context) {
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val language: Language
get() {
var setting = sharedPreferences.getString(Language::class.simpleName, null)
if (setting == null) {
setCurrentLanguage()
setting =
sharedPreferences.getString(Language::class.simpleName, Language.ENGLISH.name)
}
return enumValueOf(setting!!)
}
val theme: Theme
get() {
val setting = sharedPreferences.getString(Theme::class.simpleName, Theme.DEFAULT.name)
return enumValueOf(setting!!)
}
val keepScreenOn: Boolean
get() {
return sharedPreferences.getBoolean("keep_screen_on", false)
}
private fun setCurrentLanguage() {
var setting = when (getApplicationLocales()[0].toString()) {
"de" -> Language.GERMAN
else -> Language.ENGLISH
}
setLanguage(setting)
}
fun setLanguage(language: Language) {
val editor = sharedPreferences.edit()
editor.putString(Language::class.simpleName, language.name)
editor.commit()
}
fun setTheme(theme: Theme) {
val editor = sharedPreferences.edit()
editor.putString(Theme::class.simpleName, theme.name)
editor.commit()
}
fun setKeepScreenOn(setting: Boolean) {
val editor = sharedPreferences.edit()
editor.putBoolean("keep_screen_on", setting)
editor.commit()
}
}

View File

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

View File

@@ -0,0 +1,5 @@
package me.zobrist.tichucounter.domain
import androidx.compose.ui.graphics.vector.ImageVector
class TopBarAction(val imageVector: ImageVector, val isActive: Boolean, val action: () -> Unit)

View File

@@ -0,0 +1,55 @@
package me.zobrist.tichucounter.framework
import android.util.Log
import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
*
* Note that only one observer is going to be notified of changes.
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner) { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
@MainThread
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
companion object {
private const val TAG = "me.zobrist.tichucounter.framework.SingleLiveEvent"
}
}

View File

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

View File

@@ -0,0 +1,88 @@
package me.zobrist.tichucounter.repository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.zobrist.tichucounter.data.Game
import me.zobrist.tichucounter.data.GameDao
import me.zobrist.tichucounter.data.Round
import me.zobrist.tichucounter.data.RoundDao
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)
}
}
private 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)
}
}
}

View File

@@ -0,0 +1,68 @@
package me.zobrist.tichucounter.ui
import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFFBE0034)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFFFDADA)
val md_theme_light_onPrimaryContainer = Color(0xFF40000B)
val md_theme_light_secondary = Color(0xFF6E5D00)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFFFE261)
val md_theme_light_onSecondaryContainer = Color(0xFF221B00)
val md_theme_light_tertiary = Color(0xFF76592F)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFFFDDB1)
val md_theme_light_onTertiaryContainer = Color(0xFF291800)
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(0xFFF4DDDD)
val md_theme_light_onSurfaceVariant = Color(0xFF524343)
val md_theme_light_outline = Color(0xFF857373)
val md_theme_light_inverseOnSurface = Color(0xFFFBEEED)
val md_theme_light_inverseSurface = Color(0xFF362F2F)
val md_theme_light_inversePrimary = Color(0xFFFFB3B5)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFFBE0034)
val md_theme_light_outlineVariant = Color(0xFFD7C1C1)
val md_theme_light_scrim = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFFFFB3B5)
val md_theme_dark_onPrimary = Color(0xFF680018)
val md_theme_dark_primaryContainer = Color(0xFF920026)
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDADA)
val md_theme_dark_secondary = Color(0xFFE6C500)
val md_theme_dark_onSecondary = Color(0xFF3A3000)
val md_theme_dark_secondaryContainer = Color(0xFF534600)
val md_theme_dark_onSecondaryContainer = Color(0xFFFFE261)
val md_theme_dark_tertiary = Color(0xFFE6C18D)
val md_theme_dark_onTertiary = Color(0xFF422C05)
val md_theme_dark_tertiaryContainer = Color(0xFF5C421A)
val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDB1)
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(0xFF524343)
val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C1)
val md_theme_dark_outline = Color(0xFF9F8C8C)
val md_theme_dark_inverseOnSurface = Color(0xFF201A1A)
val md_theme_dark_inverseSurface = Color(0xFFECE0DF)
val md_theme_dark_inversePrimary = Color(0xFFBE0034)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFFFFB3B5)
val md_theme_dark_outlineVariant = Color(0xFF524343)
val md_theme_dark_scrim = Color(0xFF000000)
val seed = Color(0xFFED0043)

View File

@@ -0,0 +1,90 @@
package me.zobrist.tichucounter.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
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.Round
import me.zobrist.tichucounter.data.RoundDao
import me.zobrist.tichucounter.domain.NavigationAction
import me.zobrist.tichucounter.domain.TopBarAction
import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val gameRepository: GameRepository,
roundDao: RoundDao
) : ViewModel() {
private var redoRounds = mutableStateListOf<Round>()
private var expectedRoundCount = 0
var topBarTitle by mutableStateOf("")
var topBarActions by mutableStateOf(emptyList<TopBarAction>())
var topBarIcon by mutableStateOf(Icons.Filled.Menu)
var isUndoActionActive by mutableStateOf(false)
var topBarNavigationAction by mutableStateOf(NavigationAction {})
val isRedoActionActive: Boolean
get() = redoRounds.isNotEmpty()
init {
viewModelScope.launch {
roundDao.getForActiveGame().collect() {
isUndoActionActive = it.isNotEmpty()
if (expectedRoundCount != it.count()) {
redoRounds.clear()
}
expectedRoundCount = it.count()
}
}
}
fun onNavigateClicked() {
topBarNavigationAction.aciton()
}
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 clearRedoList() {
redoRounds.clear()
}
fun newGame() {
viewModelScope.launch {
redoRounds.clear()
gameRepository.newGame()
}
}
}

View File

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

View File

@@ -0,0 +1,173 @@
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.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)
)
}
Column(Modifier.weight(1f)) {
KeyboardView(
viewModel.currentScoreA,
viewModel.currentScoreB,
viewModel.requestFocusA,
viewModel.enableSubmit,
{ viewModel.updateFocusStateA(it) },
{ viewModel.updateFocusStateB(it) },
{ viewModel.digitClicked(it) },
{ viewModel.addSub100Clicked(it) },
{ viewModel.deleteClicked() },
{ viewModel.negateClicked() }
) { viewModel.submitClicked() }
}
}
}
@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)
)
KeyboardView(
viewModel.currentScoreA,
viewModel.currentScoreB,
viewModel.requestFocusA,
viewModel.enableSubmit,
{ viewModel.updateFocusStateA(it) },
{ viewModel.updateFocusStateB(it) },
{ viewModel.digitClicked(it) },
{ viewModel.addSub100Clicked(it) },
{ viewModel.deleteClicked() },
{ viewModel.negateClicked() }
) { viewModel.submitClicked() }
}
}
@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 activeValue: String = currentScoreA
override var inactiveValue: String = currentScoreB
override fun giveFocusToAIfNone() {
}
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) {
}
}

View File

@@ -0,0 +1,244 @@
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.GameDao
import me.zobrist.tichucounter.data.Round
import me.zobrist.tichucounter.data.RoundDao
import me.zobrist.tichucounter.domain.Tichu
import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject
interface ICounterViewModel {
var roundScoreList: List<Round>
var totalScoreA: Int
var totalScoreB: Int
var teamNameA: String
var teamNameB: String
var currentScoreA: String
var currentScoreB: String
var enableSubmit: Boolean
var isAFocused: Boolean
var isBFocused: Boolean
var requestFocusA: FocusRequester
var activeValue: String
var inactiveValue: String
fun giveFocusToAIfNone()
fun updateOtherScore()
fun isValidTichuRound(): Boolean
fun updateSubmitButton()
fun submitClicked()
fun digitClicked(digit: String)
fun negateClicked()
fun addSub100Clicked(toAdd: Int)
fun deleteClicked()
fun updateNameA(value: String)
fun updateNameB(value: String)
fun updateFocusStateA(state: Boolean)
fun updateFocusStateB(state: Boolean)
}
@HiltViewModel
class CounterViewModel @Inject constructor(
private val gameRepository: GameRepository,
private val roundDao: RoundDao,
private val gameDao: GameDao
) :
ViewModel(), ICounterViewModel {
override var roundScoreList by mutableStateOf(emptyList<Round>())
override var totalScoreA by mutableStateOf(0)
override var totalScoreB by mutableStateOf(0)
override var teamNameA by mutableStateOf("")
override var teamNameB by mutableStateOf("")
override var currentScoreA by mutableStateOf("")
override var currentScoreB by mutableStateOf("")
override var enableSubmit by mutableStateOf(false)
override var isAFocused by mutableStateOf(false)
override var isBFocused by mutableStateOf(false)
override var requestFocusA by mutableStateOf(FocusRequester())
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
}
}
init {
viewModelScope.launch {
roundDao.getForActiveGame().collect {
roundScoreList = it
}
}
viewModelScope.launch {
gameDao.getActive().collect {
if (it != null) {
teamNameA = it.nameA
teamNameB = it.nameB
}
}
}
viewModelScope.launch {
roundDao.getRoundSumForActiveGame().collect { score ->
totalScoreA = score.scoreA
totalScoreB = score.scoreB
}
}
}
override fun giveFocusToAIfNone() {
if (!isAFocused && !isBFocused) {
requestFocusA.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) {
giveFocusToAIfNone()
activeValue += digit
updateOtherScore()
updateSubmitButton()
}
override fun negateClicked() {
giveFocusToAIfNone()
activeValue = if (activeValue.contains("-")) {
activeValue.replace("-", "")
} else {
"-$activeValue"
}
updateOtherScore()
updateSubmitButton()
}
override fun addSub100Clicked(toAdd: Int) {
giveFocusToAIfNone()
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
}
override fun updateFocusStateB(state: Boolean) {
isBFocused = state
}
}

View File

@@ -0,0 +1,226 @@
package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration
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.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
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(ExperimentalComposeUiApi::class)
@Composable
fun KeyboardView(
scoreA: String,
scoreB: String,
requestFocus: FocusRequester,
enableSubmit: Boolean,
updateFocusStateA: (Boolean) -> Unit,
updateFocusStateB: (Boolean) -> Unit,
digitClicked: (String) -> Unit,
addSub100Clicked: (Int) -> Unit,
deleteClicked: () -> Unit,
negateClicked: () -> Unit,
submitClicked: () -> Unit
) {
val keyboardController = LocalSoftwareKeyboardController.current
Column {
Row {
Column(Modifier.weight(1f)) {
CenteredTextField(
scoreA,
"0",
Modifier
.focusRequester(requestFocus)
.onFocusChanged {
keyboardController?.hide()
updateFocusStateA(it.isFocused)
}
)
}
Column(Modifier.weight(1f)) {
CenteredTextField(
scoreB,
"0",
Modifier
.onFocusChanged {
keyboardController?.hide()
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)) {
KeyboardTextButton("+/-") {
negateClicked()
}
}
Column(Modifier.weight(1f)) {
KeyboardTextButton("0") {
digitClicked("0")
}
}
Column(Modifier.weight(2f)) {
KeyboardIconButton(Icons.Outlined.Check, enableSubmit) {
submitClicked()
}
}
}
}
}
@Composable
fun KeyboardTextButton(text: String, onClicked: () -> Unit) {
ElevatedButton(
modifier = Modifier.fillMaxWidth().height(50.dp).padding(2.dp),
onClick = { onClicked() },
) { Text(text) }
}
@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,
modifier: Modifier,
) {
TextField(
value = value,
onValueChange = { },
placeholder = {
Text(
placeholder,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
},
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
singleLine = true,
readOnly = true,
modifier = modifier.fillMaxWidth()
)
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun KeyboardViewPreview() {
AppTheme {
Surface {
KeyboardView(
"",
"350",
FocusRequester(),
false,
{},
{},
{},
{},
{},
{},
{})
}
}
}

View File

@@ -0,0 +1,85 @@
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.LazyListState
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.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, lazyListState)
}
scope.launch {
lazyListState.animateScrollToItem(rounds.size)
}
}
}
@Composable
private fun RoundListItem(round: Round, index: Int, lazyListState: LazyListState) {
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.toString(),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center
)
Text(
text = round.scoreB.toString(),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(5f),
textAlign = TextAlign.Center
)
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun RoundListViewPreview() {
val rounds = listOf(
Round(1, 10, 90),
Round(1, 5, 95),
Round(1, 100, 0),
Round(1, 125, -25),
Round(1, 50, 50)
)
AppTheme {
Surface {
RoundListView(rounds, Modifier)
}
}
}

View File

@@ -0,0 +1,47 @@
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 me.zobrist.tichucounter.ui.AppTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TeamNamesView(
nameA: String,
nameB: String,
updateA: (String) -> Unit,
updateB: (String) -> Unit
) {
Row() {
TextField(
value = nameA,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
onValueChange = { updateA(it) },
singleLine = true,
modifier = Modifier.weight(1f)
)
TextField(
value = nameB,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
onValueChange = { updateB(it) },
singleLine = true,
modifier = Modifier.weight(1f)
)
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
private fun TeamNamesViewPreview() {
AppTheme {
TeamNamesView("TeamA", "TeamB", {}, {})
}
}

View File

@@ -0,0 +1,56 @@
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.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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() {
Row() {
Text(
style = MaterialTheme.typography.headlineSmall,
text = scoreA.toString(),
modifier = Modifier
.weight(5f)
.padding(6.dp),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.weight(1f))
Text(
style = MaterialTheme.typography.headlineSmall,
text = scoreB.toString(),
modifier = Modifier
.weight(5f)
.padding(6.dp),
textAlign = TextAlign.Center
)
}
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
private fun TeamScoresViewPreview() {
AppTheme {
Surface {
TeamScoresView(10, 90)
}
}
}

View File

@@ -0,0 +1,91 @@
package me.zobrist.tichucounter.ui.history
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.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.data.GameAndScore
import java.text.DateFormat
import java.util.*
@Composable
fun HistoryList(viewModel: HistoryViewModel) {
HistoryList(viewModel.gameAndHistory)
}
@Composable
fun HistoryList(games: List<GameAndScore>) {
LazyColumn {
items(games) {
HistoryListItem(it)
}
}
}
@Composable
fun HistoryListItem(game: GameAndScore) {
val format =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault())
Card(
modifier = Modifier
.fillMaxWidth()
.padding(all = 4.dp),
) {
Column(
Modifier
.padding(all = 12.dp),
) {
Row() {
Text(
text = game.nameA,
style = MaterialTheme.typography.headlineSmall
)
Text(
text = game.scoreA.toString(),
style = MaterialTheme.typography.headlineSmall
)
}
Row() {
Text(
text = game.nameB,
style = MaterialTheme.typography.headlineSmall
)
Text(
text = game.scoreB.toString(),
style = MaterialTheme.typography.headlineSmall
)
}
Row() {
Text(
text = format.format(game.modified),
style = MaterialTheme.typography.labelSmall
)
}
}
}
}
@Preview
@Composable
private fun HistoryListPreview() {
val tempData = listOf<GameAndScore>(
GameAndScore(false, "abc", "def", Date(), Date(), 1, 10, 50),
GameAndScore(true, "ADTH", "dogfg", Date(), Date(), 2, 20, 60),
GameAndScore(false, "TeamA3", "TeamB3", Date(), Date(), 3, 30, 70),
GameAndScore(false, "TeamA4", "TeamB4", Date(), Date(), 4, 40, 80),
GameAndScore(false, "TeamA5", "TeamB5", Date(), Date(), 5, 50, 90)
)
HistoryList(tempData)
}

View File

@@ -0,0 +1,30 @@
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.GameAndScore
import me.zobrist.tichucounter.data.GameDao
import javax.inject.Inject
@HiltViewModel
class HistoryViewModel @Inject constructor(
private val gameDao: GameDao
) : ViewModel() {
var gameAndHistory by mutableStateOf(emptyList<GameAndScore>())
private set
init {
viewModelScope.launch {
gameDao.getAllWithPoints().collect { games ->
gameAndHistory = games
}
}
}
}

View File

@@ -0,0 +1,161 @@
package me.zobrist.tichucounter.ui.settings
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.Check
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.Language
import me.zobrist.tichucounter.domain.Theme
import me.zobrist.tichucounter.ui.AppTheme
val languageMap = mapOf(
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,
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: (Boolean) -> Unit = {},
updateLanguage: (Language) -> Unit = {},
updateTheme: (Theme) -> Unit = {}
) {
Column() {
BooleanSetting(
stringResource(R.string.keep_screen_on),
valueScreenOn
) { updateScreenOn(it) }
StringSetting(
stringResource(R.string.choose_language_text),
languageMap,
valueLanguage,
) { updateLanguage(it) }
StringSetting(
stringResource(R.string.choose_theme_text),
themeMap,
valueTheme,
) { updateTheme(it) }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@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)
Text(stringResource(map[selected]!!), style = MaterialTheme.typography.labelLarge)
}
Column(Modifier.weight(1f)) {
Icon(
Icons.Outlined.ArrowDropDown,
contentDescription = null,
modifier = Modifier.align(End)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
map.forEach {
DropdownMenuItem(
onClick = {
onSelected(it.key)
expanded = false
},
text = { Text(stringResource(it.value)) },
trailingIcon = {
if (it.key == selected) {
Icon(Icons.Outlined.Check, contentDescription = null)
}
})
}
}
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun SettingsViewPreview() {
AppTheme() {
Surface() {
SettingsView()
}
}
}

View File

@@ -0,0 +1,41 @@
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.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(false)
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: Boolean) {
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

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0" />
<path
android:fillColor="#FF000000"
android:pathData="M9,2L7.17,4H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2H9zm3,15c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M22,16V4c0,-1.1 -0.9,-2 -2,-2H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zm-11,-4l2.03,2.71L16,11l4,5H8l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2H4V6H2z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6zm16,-4H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zm-8,12.5v-9l6,4.5 -6,4.5z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/primaryColor"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/primaryColor"
android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z" />
</vector>

View File

@@ -0,0 +1,9 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="135"
android:centerColor="#009688"
android:endColor="#00695C"
android:startColor="#4DB6AC"
android:type="linear" />
</shape>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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>

View File

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

View File

@@ -4,7 +4,26 @@
<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öschen?</string>
<string name="confirmNew">Möchten Sie wirklich ein neues Spiel starten?</string>
<string name="yes">Ja</string>
<string name="no">Nein</string>
<string name="back">Zurück</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="settings">Einstellungen</string>
<string name="display">Anzeige</string>
<string name="activate">Aktivieren</string>
<string name="delete">Löschen</string>
<string name="menu_history">Verlauf</string>
<string name="menu_settings">Einstellungen</string>
<string name="menu_counter">Counter</string>
<string name="submit">Übermitteln</string>
<string name="on">Ein</string>
<string name="off">Aus</string>
<string name="newGame">Neues Spiel</string>
</resources>

View File

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

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">200dp</dimen>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@@ -1,6 +1,12 @@
<?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>
<color name="primaryLightColor">#ff5131</color>
<color name="primaryDarkColor">#9b0000</color>
<color name="secondaryColor">#ffccbc</color>
<color name="secondaryLightColor">#ffffee</color>
<color name="secondaryDarkColor">#cb9b8c</color>
<color name="primaryTextColor">#ffffff</color>
<color name="secondaryTextColor">#000000</color>
</resources>

View File

@@ -0,0 +1,9 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="text_margin">16dp</dimen>
</resources>

View File

@@ -0,0 +1,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<item name="ic_menu_camera" type="drawable">@android:drawable/ic_menu_camera</item>
<item name="ic_menu_gallery" type="drawable">@android:drawable/ic_menu_gallery</item>
<item name="ic_menu_slideshow" type="drawable">@android:drawable/ic_menu_slideshow</item>
<item name="ic_menu_manage" type="drawable">@android:drawable/ic_menu_manage</item>
<item name="ic_menu_share" type="drawable">@android:drawable/ic_menu_share</item>
<item name="ic_menu_send" type="drawable">@android:drawable/ic_menu_send</item>
</resources>

View File

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

View File

@@ -1,14 +1,34 @@
<?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="title_activity_settings" translatable="false">SettingsActivity</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="confirmNew">Do you really want to start a new Game?</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="back">Back</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="display">Display</string>
<string name="settings">Settings</string>
<string name="menu_counter">Counter</string>
<string name="menu_history">History</string>
<string name="menu_settings">Settings</string>
<string name="activate">Activate</string>
<string name="delete">Delete</string>
<string name="submit">Submit</string>
<string name="on">On</string>
<string name="off">Off</string>
<string name="newGame">New Game</string>
</resources>

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>

View File

@@ -0,0 +1,8 @@
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"></style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.MaterialComponents.Light" />
</resources>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.10"
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()
}
}

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

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