diff --git a/.drone.yml b/.drone.yml
index 82e296e..a2c0734 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -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
\ No newline at end of file
+ event:
+ - tag
+
+- name: slack notification
+ image: plugins/slack
+ settings:
+ webhook:
+ from_secret: SlackWebhook
+ when:
+ status:
+ - failure
+ - success
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f0f71e2..5f99499 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,5 @@
.externalNativeBuild
.cxx
.idea
+keystore.properties
+version.properties
diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index 7dcb52c..0000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-Tichu Counter
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
deleted file mode 100644
index 88ea3aa..0000000
--- a/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,122 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- xmlns:android
-
- ^$
-
-
-
-
-
-
-
-
- xmlns:.*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*:id
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:name
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- name
-
- ^$
-
-
-
-
-
-
-
-
- style
-
- ^$
-
-
-
-
-
-
-
-
- .*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*
-
- http://schemas.android.com/apk/res/android
-
-
- ANDROID_ATTRIBUTE_ORDER
-
-
-
-
-
-
- .*
-
- .*
-
-
- BY_NAME
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 79ee123..0000000
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index fb7f4a8..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/dictionaries/fabian.xml b/.idea/dictionaries/fabian.xml
deleted file mode 100644
index 0fad81e..0000000
--- a/.idea/dictionaries/fabian.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
- checkmark
- tichu
- tichucounter
- zobrist
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 66ff961..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index bbc6cd7..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
deleted file mode 100644
index e34606c..0000000
--- a/.idea/jarRepositories.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 9f83b5d..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/AndroidKey b/AndroidKey
new file mode 100644
index 0000000..5d88afa
Binary files /dev/null and b/AndroidKey differ
diff --git a/app/.gitignore b/app/.gitignore
index 42afabf..956c004 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1 +1,2 @@
-/build
\ No newline at end of file
+/build
+/release
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index a93790f..6628138 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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
}
\ No newline at end of file
diff --git a/app/release/app-release.aab b/app/release/app-release.aab
deleted file mode 100644
index f803bcf..0000000
Binary files a/app/release/app-release.aab and /dev/null differ
diff --git a/app/schemas/me.zobrist.tichucounter.data.AppDatabase/1.json b/app/schemas/me.zobrist.tichucounter.data.AppDatabase/1.json
new file mode 100644
index 0000000..324b84a
--- /dev/null
+++ b/app/schemas/me.zobrist.tichucounter.data.AppDatabase/1.json
@@ -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')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/me.zobrist.tichucounter.data.AppDatabase/2.json b/app/schemas/me.zobrist.tichucounter.data.AppDatabase/2.json
new file mode 100644
index 0000000..914f81c
--- /dev/null
+++ b/app/schemas/me.zobrist.tichucounter.data.AppDatabase/2.json
@@ -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')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/me/zobrist/tichucounter/ExampleInstrumentedTest.kt b/app/src/androidTest/java/me/zobrist/tichucounter/ExampleInstrumentedTest.kt
index 20588ed..ef6f3a2 100644
--- a/app/src/androidTest/java/me/zobrist/tichucounter/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/me/zobrist/tichucounter/ExampleInstrumentedTest.kt
@@ -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.
*
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3f3aab9..df25535 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,25 +2,31 @@
+ android:theme="@style/AppTheme">
+ android:exported="true"
+ android:windowSoftInputMode="adjustPan">
+
+
+
()
-
- 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()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt
index 21dd10d..b964c6c 100644
--- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt
+++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt
@@ -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())
- }
- 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)
+ setContent {
+ AppTheme {
+ val systemUiController = rememberSystemUiController()
+ systemUiController.setStatusBarColor(MaterialTheme.colorScheme.background)
+ NavigationDrawer()
}
}
}
- 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 onDestroy() {
+ super.onDestroy()
+ settingsAdapter.unregisterOnChangeListener(this)
}
- override fun onCreateOptionsMenu(menu: Menu): Boolean
- {
- // Inflate the menu; this adds items to the action bar if it is present.
- menuInflater.inflate(R.menu.menu_main , menu)
-
- menu.findItem(R.id.action_screenOn).isChecked=
- this.getSharedPreferences("Settings" , MODE_PRIVATE)
- .getBoolean("Screen_On" , false)
- return true
+ override fun onLanguageChanged(language: Language) {
+ AppCompatDelegate.setApplicationLocales(language.value)
}
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean
- {
- return when (item.itemId)
- {
- R.id.action_clear ->
- {
- val builder=AlertDialog.Builder(this)
- builder.setMessage(getString(R.string.confirmClear))
- .setTitle(R.string.clear)
- .setCancelable(false)
- .setPositiveButton(getString(R.string.yes)) { dialog , _ ->
- 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)
+ override fun onThemeChanged(theme: Theme) {
+ val themeValue = when (theme) {
+ Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
+ Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
+ Theme.DEFAULT -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
+ AppCompatDelegate.setDefaultNightMode(themeValue)
}
- private fun hideKeyboard()
- {
- val imm: InputMethodManager=
- getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
- imm.hideSoftInputFromWindow(currentFocus!!.windowToken , 0)
- }
-
- private fun giveFocusToAIfNone()
- {
- if (!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()
- }
-
- val dialog=builder.create()
- dialog.show()
- }
-
- private fun updateTheme(which: Int)
- {
- when (which)
- {
- 0 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
- 1 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
- 2 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
- }
- delegate.applyDayNight()
- }
-
- private fun keepScreenOn(keepOn: Boolean)
- {
- if (keepOn)
- {
+ override fun onScreenOnChanged(keepOn: KeepScreenOn) {
+ if (keepOn.value) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
- }
- else
- {
+ } 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()
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/Round.kt b/app/src/main/java/me/zobrist/tichucounter/Round.kt
deleted file mode 100644
index 93188e3..0000000
--- a/app/src/main/java/me/zobrist/tichucounter/Round.kt
+++ /dev/null
@@ -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()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/data/AppDatabase.kt b/app/src/main/java/me/zobrist/tichucounter/data/AppDatabase.kt
new file mode 100644
index 0000000..06eca31
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/data/AppDatabase.kt
@@ -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
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/data/DaoBase.kt b/app/src/main/java/me/zobrist/tichucounter/data/DaoBase.kt
new file mode 100644
index 0000000..5f4a9c3
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/data/DaoBase.kt
@@ -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 {
+
+ @Insert
+ fun insert(entity: T): Long
+
+ @Update
+ fun update(entity: T)
+
+ @Delete
+ fun delete(entity: T)
+
+ @Delete
+ fun delete(entity: List)
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/data/DatabaseModule.kt b/app/src/main/java/me/zobrist/tichucounter/data/DatabaseModule.kt
new file mode 100644
index 0000000..4ac90d2
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/data/DatabaseModule.kt
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/data/DateConverter.kt b/app/src/main/java/me/zobrist/tichucounter/data/DateConverter.kt
new file mode 100644
index 0000000..be2a9db
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/data/DateConverter.kt
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt b/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt
new file mode 100644
index 0000000..00a9855
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt
@@ -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 {
+
+ @Query("SELECT * FROM game")
+ fun getAll(): Flow>
+
+ @Transaction
+ @Query("SELECT * FROM game where uid ")
+ fun getGamesWithRounds(): Flow>
+
+ @Transaction
+ @Query("SELECT * FROM game WHERE active is 1")
+ fun getActiveWithRounds(): Flow
+
+ @Query("SELECT * FROM game WHERE uid is :gameId")
+ fun getGameById(gameId: Long): Flow
+
+ @Query("SELECT * FROM game WHERE active is 1")
+ fun getActive(): Flow
+
+
+ @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)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt b/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt
new file mode 100644
index 0000000..44d2d54
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt
@@ -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
+)
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/data/RoundDao.kt b/app/src/main/java/me/zobrist/tichucounter/data/RoundDao.kt
new file mode 100644
index 0000000..2da3a09
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/data/RoundDao.kt
@@ -0,0 +1,15 @@
+package me.zobrist.tichucounter.data
+
+import androidx.room.*
+import me.zobrist.tichucounter.data.entity.Round
+
+@Dao
+interface RoundDao : DaoBase {
+
+ @Query("SELECT * FROM round")
+ fun getAll(): List
+
+ @Query("SELECT * FROM round WHERE gameId is :gameId")
+ fun getAllForGame(gameId: Long?): List
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/data/entity/Game.kt b/app/src/main/java/me/zobrist/tichucounter/data/entity/Game.kt
new file mode 100644
index 0000000..e70a2de
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/data/entity/Game.kt
@@ -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
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/data/entity/IEntity.kt b/app/src/main/java/me/zobrist/tichucounter/data/entity/IEntity.kt
new file mode 100644
index 0000000..23b9c73
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/data/entity/IEntity.kt
@@ -0,0 +1,5 @@
+package me.zobrist.tichucounter.data.entity
+
+interface IEntity {
+ val uid: Long
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/data/entity/Round.kt b/app/src/main/java/me/zobrist/tichucounter/data/entity/Round.kt
new file mode 100644
index 0000000..2e21660
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/data/entity/Round.kt
@@ -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
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/DrawerItem.kt b/app/src/main/java/me/zobrist/tichucounter/domain/DrawerItem.kt
new file mode 100644
index 0000000..b3757ef
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/domain/DrawerItem.kt
@@ -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)
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/GameWithScoresExtension.kt b/app/src/main/java/me/zobrist/tichucounter/domain/GameWithScoresExtension.kt
new file mode 100644
index 0000000..5094493
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/domain/GameWithScoresExtension.kt
@@ -0,0 +1,14 @@
+package me.zobrist.tichucounter.domain
+
+import me.zobrist.tichucounter.data.GameWithScores
+
+fun GameWithScores.getTotalPoints(): Pair {
+ var scoreA = 0
+ var scoreB = 0
+
+ this.rounds.forEach {
+ scoreA += it.scoreA
+ scoreB += it.scoreB
+ }
+ return Pair(scoreA, scoreB)
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/IntExtensions.kt b/app/src/main/java/me/zobrist/tichucounter/domain/IntExtensions.kt
new file mode 100644
index 0000000..12e7864
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/domain/IntExtensions.kt
@@ -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
+}
+
diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt b/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt
new file mode 100644
index 0000000..68771fb
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt
@@ -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 = emptyList(),
+ deepLinks: List = emptyList(),
+ content: @Composable (NavBackStackEntry) -> Unit
+) {
+ this.composable(route.name, arguments, deepLinks, content)
+}
diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/Route.kt b/app/src/main/java/me/zobrist/tichucounter/domain/Route.kt
new file mode 100644
index 0000000..5ae62a8
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/domain/Route.kt
@@ -0,0 +1,3 @@
+package me.zobrist.tichucounter.domain
+
+enum class Route { COUNTER, HISTORY, SETTINGS, ABOUT }
diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt
new file mode 100644
index 0000000..452e678
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt
@@ -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()
+
+ 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)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/Tichu.kt b/app/src/main/java/me/zobrist/tichucounter/domain/Tichu.kt
new file mode 100644
index 0000000..95599eb
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/domain/Tichu.kt
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/TopBarAction.kt b/app/src/main/java/me/zobrist/tichucounter/domain/TopBarAction.kt
new file mode 100644
index 0000000..0a9eb39
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/domain/TopBarAction.kt
@@ -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 = {}
+)
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/TopBarState.kt b/app/src/main/java/me/zobrist/tichucounter/domain/TopBarState.kt
new file mode 100644
index 0000000..46bbd46
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/domain/TopBarState.kt
@@ -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 = emptyList(),
+ var onNavigate: () -> Unit = {}
+)
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/framework/TichuCounterApplication.kt b/app/src/main/java/me/zobrist/tichucounter/framework/TichuCounterApplication.kt
new file mode 100644
index 0000000..786dfd8
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/framework/TichuCounterApplication.kt
@@ -0,0 +1,7 @@
+package me.zobrist.tichucounter.framework
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class TichuCounterApplication : Application()
diff --git a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt
new file mode 100644
index 0000000..5716266
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt
@@ -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 {
+ return gameDao.getActiveWithRounds()
+ }
+
+ fun getAllWithRoundFlow(): Flow> {
+ return gameDao.getGamesWithRounds()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/Color.kt b/app/src/main/java/me/zobrist/tichucounter/ui/Color.kt
new file mode 100644
index 0000000..4dea1c7
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/Color.kt
@@ -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)
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt
new file mode 100644
index 0000000..53fde44
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt
@@ -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()
+ 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()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/Theme.kt b/app/src/main/java/me/zobrist/tichucounter/ui/Theme.kt
new file mode 100644
index 0000000..bb0a5c7
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/Theme.kt
@@ -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
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/about/AboutView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/about/AboutView.kt
new file mode 100644
index 0000000..9202b49
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/about/AboutView.kt
@@ -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()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/composables/DropDownMenu.kt b/app/src/main/java/me/zobrist/tichucounter/ui/composables/DropDownMenu.kt
new file mode 100644
index 0000000..12a9700
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/composables/DropDownMenu.kt
@@ -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 DropDownMenu(map: Map, 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)) },
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt
new file mode 100644
index 0000000..1113d76
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt
@@ -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 =
+ 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() {
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt
new file mode 100644
index 0000000..e6ae019
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt
@@ -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
+ 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())
+ 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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt
new file mode 100644
index 0000000..273cd8b
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt
@@ -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 = {})
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/RoundListView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/RoundListView.kt
new file mode 100644
index 0000000..e9d3dfb
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/RoundListView.kt
@@ -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, 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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamNamesView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamNamesView.kt
new file mode 100644
index 0000000..596c861
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamNamesView.kt
@@ -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", {}, {})
+ }
+}
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamScoresView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamScoresView.kt
new file mode 100644
index 0000000..30adac4
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamScoresView.kt
@@ -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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt
new file mode 100644
index 0000000..2dfc42e
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt
@@ -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,
+ 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, {}, {}) {}
+}
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt
new file mode 100644
index 0000000..ed3f587
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt
@@ -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())
+ 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()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/layout/DrawerContent.kt b/app/src/main/java/me/zobrist/tichucounter/ui/layout/DrawerContent.kt
new file mode 100644
index 0000000..e025a4a
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/layout/DrawerContent.kt
@@ -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,
+ 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
+ ) {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/layout/TopBar.kt b/app/src/main/java/me/zobrist/tichucounter/ui/layout/TopBar.kt
new file mode 100644
index 0000000..a4dbd23
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/layout/TopBar.kt
@@ -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
+) {
+ 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()
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsView.kt
new file mode 100644
index 0000000..38840be
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsView.kt
@@ -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 StringSetting(name: String, map: Map, 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,
+ ) {}
+ }
+ }
+}
+
diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt
new file mode 100644
index 0000000..b37e1bd
--- /dev/null
+++ b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt
@@ -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
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-mdpi/back.png b/app/src/main/res/drawable-mdpi/back.png
deleted file mode 100644
index 72b8bd7..0000000
Binary files a/app/src/main/res/drawable-mdpi/back.png and /dev/null differ
diff --git a/app/src/main/res/drawable-mdpi/checkmark.png b/app/src/main/res/drawable-mdpi/checkmark.png
deleted file mode 100644
index 75d2263..0000000
Binary files a/app/src/main/res/drawable-mdpi/checkmark.png and /dev/null differ
diff --git a/app/src/main/res/drawable-night/back.png b/app/src/main/res/drawable-night/back.png
deleted file mode 100644
index 770c83c..0000000
Binary files a/app/src/main/res/drawable-night/back.png and /dev/null differ
diff --git a/app/src/main/res/drawable/app_logo.png b/app/src/main/res/drawable/app_logo.png
new file mode 100644
index 0000000..7681534
Binary files /dev/null and b/app/src/main/res/drawable/app_logo.png differ
diff --git a/app/src/main/res/layout-land/content_main.xml b/app/src/main/res/layout-land/content_main.xml
deleted file mode 100644
index a205dec..0000000
--- a/app/src/main/res/layout-land/content_main.xml
+++ /dev/null
@@ -1,321 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 4c98b99..0000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml
deleted file mode 100644
index fac6752..0000000
--- a/app/src/main/res/layout/content_main.xml
+++ /dev/null
@@ -1,338 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
deleted file mode 100644
index e534389..0000000
--- a/app/src/main/res/menu/menu_main.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 036d09b..c9ad5f9 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 036d09b..c9ad5f9 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 06e21fc..be60463 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -1,17 +1,26 @@
- Neues Spiel starten
- Letzte Runde löschen
Theme auswählen
Bildschirm eingeschaltet lassen
- Möchten Sie das laufende Spiel wirklich löschen?
- Ja
- Nein
- Zurück
Sprache wählen
Android Standard
Englisch
- Detusch
+ Deutsch
Hell
Dunkel
+ Verlauf
+ Einstellungen
+ Ein
+ Aus
+ Neues Spiel
+ Verlauf löschen
+ Wirklich den gesamten Verlauf löschen? Diese Aktion kann nicht rückgängig gemacht werden.
+ Abbrechen
+ Ok
+ Löschen
+ Alle löschen
+ Aktives Spiel
+ Vergangene Spiele
+ Counter
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index c2e1ee9..ad9de19 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,6 +1,5 @@
- @color/ic_launcher_background
- #830000
- #F57F17
+ #d50000
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
index 939714f..d8bfdb7 100644
--- a/app/src/main/res/values/ic_launcher_background.xml
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -1,4 +1,4 @@
- #DC0E00
+ @color/primaryColor
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f00f5be..e0d8623 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,21 +1,29 @@
Tichu Counter
- Team A
- Team B
- Start new game
- Undo last round
Choose theme
Keep screen on
- Do you really want to delete the current game?
- Yes
- No
- Back
Choose language
Android Default
English
German
Light
Dark
+
+ History
+ Settings
+ On
+ Off
+ New Game
+ Delete history
+ You really want to delete the the history? This action can\'t be undone.
+ Cancel
+ Ok
+ Delete
+ Delete all
+ Current Game
+ Old Games
+ Counter
+ About
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 752e91f..6b30d8d 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,20 +1,3 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..181c3c9
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..fe7a40d
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
index e04caba..cb544e7 100644
--- a/app/src/main/res/xml/locales_config.xml
+++ b/app/src/main/res/xml/locales_config.xml
@@ -1,5 +1,5 @@
-
-
+
+
diff --git a/app/src/test/java/me/zobrist/tichucounter/HistoryUnitTest.kt b/app/src/test/java/me/zobrist/tichucounter/HistoryUnitTest.kt
deleted file mode 100644
index 65ea151..0000000
--- a/app/src/test/java/me/zobrist/tichucounter/HistoryUnitTest.kt
+++ /dev/null
@@ -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())
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/me/zobrist/tichucounter/RoundUnitTest.kt b/app/src/test/java/me/zobrist/tichucounter/RoundUnitTest.kt
deleted file mode 100644
index ba0c920..0000000
--- a/app/src/test/java/me/zobrist/tichucounter/RoundUnitTest.kt
+++ /dev/null
@@ -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())
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/me/zobrist/tichucounter/TichuUnitTest.kt b/app/src/test/java/me/zobrist/tichucounter/TichuUnitTest.kt
new file mode 100644
index 0000000..c42f7e5
--- /dev/null
+++ b/app/src/test/java/me/zobrist/tichucounter/TichuUnitTest.kt
@@ -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))
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index f970681..b2dbe00 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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()
diff --git a/gradle.properties b/gradle.properties
index 4d15d01..30dbeb4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -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
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 7e135b5..f9d4a26 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -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