release/2.0 #21

Merged
fabian merged 137 commits from release/2.0 into master 2023-01-28 23:29:27 +01:00
86 changed files with 3203 additions and 1904 deletions

View File

@@ -4,11 +4,71 @@ 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 build
- ./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:
- push
- pull_request
- tag
- name: slack notification
image: plugins/slack
settings:
webhook:
from_secret: SlackWebhook
when:
status:
- failure
- success

2
.gitignore vendored
View File

@@ -13,3 +13,5 @@
.externalNativeBuild
.cxx
.idea
keystore.properties
version.properties

1
.idea/.name generated
View File

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

View File

@@ -1,122 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<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>

6
.idea/compiler.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

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>

20
.idea/gradle.xml generated
View File

@@ -1,20 +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="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="Android Studio java home" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -1,8 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ClangTidy" enabled="true" level="WARNING" enabled_by_default="true">
<option name="clangTidyChecks" value="-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bad-signal-to-kill-thread,bugprone-branch-clone,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-dynamic-static-initializers,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-incorrect-roundings,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-parentheses,bugprone-macro-repeated-side-effects,bugprone-misplaced-operator-in-strlen-in-alloc,bugprone-misplaced-pointer-arithmetic-in-alloc,bugprone-misplaced-widening-cast,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-no-escape,bugprone-not-null-terminated-result,bugprone-parent-virtual-call,bugprone-posix-return,bugprone-reserved-identifier,bugprone-sizeof-container,bugprone-sizeof-expression,bugprone-spuriously-wake-up-functions,bugprone-string-constructor,bugprone-string-integer-assignment,bugprone-string-literal-with-embedded-nul,bugprone-suspicious-enum-usage,bugprone-suspicious-include,bugprone-suspicious-memset-usage,bugprone-suspicious-missing-comma,bugprone-suspicious-semicolon,bugprone-suspicious-string-compare,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-throw-keyword-missing,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-raii,bugprone-unused-return-value,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl21-cpp,cert-dcl58-cpp,cert-err34-c,cert-err52-cpp,cert-err58-cpp,cert-err60-cpp,cert-flp30-c,cert-msc50-cpp,cert-msc51-cpp,cert-str34-c,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-pro-type-static-cast-downcast,cppcoreguidelines-slicing,google-default-arguments,google-explicit-constructor,google-runtime-operator,hicpp-exception-baseclass,hicpp-multiway-paths-covered,misc-misplaced-const,misc-new-delete-overloads,misc-no-recursion,misc-non-copyable-objects,misc-throw-by-value-catch-by-reference,misc-unconventional-assign-operator,misc-uniqueptr-reset-release,modernize-avoid-bind,modernize-concat-nested-namespaces,modernize-deprecated-headers,modernize-deprecated-ios-base-aliases,modernize-loop-convert,modernize-make-shared,modernize-make-unique,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-auto-ptr,modernize-replace-disallow-copy-and-assign-macro,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-emplace,modernize-use-equals-default,modernize-use-equals-delete,modernize-use-nodiscard,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,mpi-buffer-deref,mpi-type-mismatch,openmp-use-default-none,performance-faster-string-find,performance-for-range-copy,performance-implicit-conversion-in-loop,performance-inefficient-algorithm,performance-inefficient-string-concatenation,performance-inefficient-vector-operation,performance-move-const-arg,performance-move-constructor-init,performance-no-automatic-move,performance-noexcept-move-constructor,performance-trivially-destructible,performance-type-promotion-in-math-fn,performance-unnecessary-copy-initialization,performance-unnecessary-value-param,portability-simd-intrinsics,readability-avoid-const-params-in-decls,readability-const-return-type,readability-container-size-empty,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-deleted-default,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-smartptr-get,readability-redundant-string-cstr,readability-redundant-string-init,readability-simplify-subscript-expr,readability-static-accessed-through-instance,readability-static-definition-in-anonymous-namespace,readability-string-compare,readability-uniqueptr-delete-release,readability-use-anyofallof,altera-id-dependent-backward-branch,altera-kernel-name-restriction,altera-single-work-item-barrier,altera-unroll-loops,bugprone-implicit-widening-of-multiplication-result,bugprone-signal-handler,bugprone-unhandled-exception-at-new,cert-pos47-c,cert-sig30-c,concurrency-mt-unsafe,concurrency-thread-canceltype-asynchronous,performance-no-int-to-ptr" />
</inspection_tool>
</profile>
</component>

View File

@@ -1,30 +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>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

64
.idea/misc.xml generated
View File

@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="app/src/main/res/layout/activity_main.xml" value="0.1" />
<entry key="app/src/main/res/layout/content_main.xml" value="0.1" />
<entry key="app/src/main/res/menu/menu_main.xml" value="0.1390625" />
</map>
</option>
</component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="androidx.annotation.Nullable" />
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="15">
<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" />
<item index="12" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="13" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
<item index="14" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="15">
<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" />
<item index="11" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="12" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
<item index="13" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
<item index="14" class="java.lang.String" itemvalue="lombok.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" 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>

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,29 +1,74 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.google.dagger.hilt.android'
id 'kotlin-kapt'
}
// Create a variable called keystorePropertiesFile, and initialize it to your
// keystore.properties file, in the rootProject folder.
def keystorePropertiesFile = rootProject.file("keystore.properties")
def versionPropertiesFile = rootProject.file("version.properties")
// Initialize a new Properties() object called keystoreProperties.
def keystoreProperties = new Properties()
def versionProperties = new Properties()
def versionMajor = 2
def versionMinor = 0
// Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
versionProperties.load(new FileInputStream(versionPropertiesFile))
android {
compileSdkVersion 32
compileSdkVersion 33
defaultConfig {
applicationId "me.zobrist.tichucounter"
minSdkVersion 16
targetSdkVersion 32
versionCode 7
versionName "1.0.0"
minSdkVersion 21
targetSdkVersion 33
versionCode versionProperties["versionCode"].toInteger()
versionName "${versionMajor}.${versionMinor}.${versionProperties["versionCode"].toInteger()}"
resConfigs 'de', 'en'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables {
useSupportLibrary true
}
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"]
keyPassword = keystoreProperties["keyPassword"]
storeFile = file(keystoreProperties["storeFile"])
storePassword = keystoreProperties["storePassword"]
}
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig = signingConfigs.getByName("release")
}
}
buildFeatures {
viewBinding = true
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
}
compileOptions {
@@ -34,22 +79,57 @@ android {
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.8.0'
implementation 'androidx.appcompat:appcompat:1.6.0-alpha05'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation "androidx.compose.material3:material3:1.0.1"
implementation 'com.google.android.play:core-ktx:1.8.1'
implementation 'com.google.android.play:core-ktx:1.8.1'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
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
}

Binary file not shown.

View File

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

View File

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

View File

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

@@ -2,25 +2,31 @@
<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:theme="@style/AppTheme.NoActionBar"
android:exported="true">
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"

View File

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

View File

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

View File

@@ -1,47 +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 (score.isMultipleOf100() && score != 0)
{
return 0
}
if (score in 101 ..125)
{
return 0 - (score % 100)
}
return 100 - (score % 100)
}
fun isValidRound(): Boolean
{
return (scoreA.isMultipleOf5()) && scoreB.isMultipleOf5() && (scoreA + scoreB).isMultipleOf100()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

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

View File

@@ -1,27 +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
android:id="@+id/contentMain"
layout="@layout/content_main" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,338 +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"
tools:ignore="HardcodedText" />
<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"
tools:ignore="HardcodedText" />
</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"
tools:ignore="HardcodedText" />
<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"
tools:ignore="HardcodedText" />
</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"
tools:ignore="HardcodedText" />
<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"
tools:ignore="HardcodedText" />
<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"
tools:ignore="HardcodedText" />
<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"
tools:ignore="HardcodedText" />
</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"
tools:ignore="HardcodedText" />
<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"
tools:ignore="HardcodedText" />
<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"
tools:ignore="HardcodedText" />
<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"
tools:ignore="HardcodedText" />
</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"
tools:ignore="HardcodedText" />
<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"
tools:ignore="HardcodedText" />
<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"
tools:ignore="HardcodedText" />
<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="@string/back" />
</LinearLayout>
<LinearLayout
android:id="@+id/ButtonRow4"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="horizontal"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
tools:layout_editor_absoluteX="1dp">
<Button
android:id="@+id/buttonInv"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:text="+/-" />
<Button
android:id="@+id/button0"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:text="0" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0" />
<ImageButton
android:id="@+id/submit"
style='style="?android:attr/buttonBarButtonStyle'
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:scaleType="fitCenter"
app:srcCompat="@drawable/checkmark"
android:contentDescription="TODO" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,29 +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_language"
android:orderInCategory="16"
android:title="@string/choose_language_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

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

View File

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

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,21 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Tichu Counter</string>
<string name="team_a" translatable="false">Team A</string>
<string name="team_b" translatable="false">Team B</string>
<string name="clear">Start new game</string>
<string name="undo">Undo last round</string>
<string name="choose_theme_text">Choose theme</string>
<string name="keep_screen_on">Keep screen on</string>
<string name="confirmClear">Do you really want to delete the current game?</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="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="menu_history">History</string>
<string name="menu_settings">Settings</string>
<string name="on">On</string>
<string name="off">Off</string>
<string name="newGame">New Game</string>
<string name="delete_inactive_title">Delete history</string>
<string name="delete_inactive_text">You really want to delete the the history? This action can\'t be undone.</string>
<string name="cancel">Cancel</string>
<string name="ok">Ok</string>
<string name="delete">Delete</string>
<string name="deleteAll">Delete all</string>
<string name="active">Current Game</string>
<string name="inactive">Old Games</string>
<string name="menu_counter">Counter</string>
<string name="menu_about">About</string>
</resources>

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,5 @@
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar" />
</resources>

View File

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

View File

@@ -1,5 +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 android:name="en" />
<locale android:name="de" />
</locale-config>

View File

@@ -1,55 +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,77 +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
}
// 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.7.0"
ext {
compose_version = '1.1.1'
}
ext.kotlin_version = "1.7.20"
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.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,6 +17,11 @@ 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()

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-7.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip