38 Commits

Author SHA1 Message Date
1819802748 Merge pull request 'release/2.2' (#41) from release/2.2 into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #41
2023-06-16 11:11:22 +02:00
a099659b2c Try to make DRONE_TAG work.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-06-02 22:16:40 +02:00
61745c95a4 Fix tagged upload url
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-06-02 16:58:36 +02:00
164cf6900f fix tagged upload link
All checks were successful
continuous-integration/drone/push Build is passing
2023-06-02 15:03:13 +02:00
89ab0afaf5 upload to nextcloud instead of seafile
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
fix nextcloud upload
2023-06-02 14:40:23 +02:00
9d8de2dd3c Update dependencies. Code clean up.
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is passing
2023-05-25 18:09:16 +02:00
fcb8c12454 [#39] Case insensitive team name suggestions.
Some checks failed
continuous-integration/drone Build is failing
closes #39
2023-05-25 17:37:30 +02:00
34e64e6e50 Update Version to 2.2
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-05-16 12:07:52 +02:00
694989a917 Merge pull request 'feature/#14_team_name_suggestions' (#38) from feature/#14_team_name_suggestions into develop
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: fabian/TichuCounter#38
2023-05-16 12:05:49 +02:00
1e8e72557d Move suggestion generation to ViewModel.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-05-11 23:40:17 +02:00
2e40d0315c Don't show dropdown when empty. 2023-05-11 23:37:12 +02:00
9ddbdb698c Better filtering and sorting of suggestions. 2023-05-11 23:06:29 +02:00
c40d473fa5 Update value of team name immediately. 2023-05-11 23:05:47 +02:00
507944794f Clear focus on opening drawer. 2023-05-11 23:04:21 +02:00
068e36c5f6 Merge commit '7e722feb275cdb7d47e87413ee7b11efc7b4ec51' into feature/#14_team_name_suggestions
Some checks failed
continuous-integration/drone Build is failing
2023-05-08 19:30:16 +02:00
7e722feb27 Update 'app/src/main/AndroidManifest.xml'
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-07 22:41:51 +02:00
fb4c83d60a Fix display of app logo on About page.
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-07 20:50:42 +02:00
0c80e05f5c Fix logo not found.
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-05 20:10:55 +02:00
8ca82d1734 Merge branch 'develop' into feature/#14_team_name_suggestions
Some checks failed
continuous-integration/drone/push Build was killed
2023-05-05 19:53:55 +02:00
9385deeb24 [#25] Use vector as app logo. Enable monochrom logo.
Some checks failed
continuous-integration/drone/push Build is failing
closes #25
2023-05-05 19:50:58 +02:00
fadd206e9e Update dependencies. 2023-05-05 19:44:50 +02:00
6768391caf Update dependencies. 2023-05-05 19:44:12 +02:00
aa3e68dc5c Merge pull request 'release/2.1' (#35) from release/2.1 into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#35
2023-03-10 17:21:32 +01:00
a2b84640b1 Merge pull request 'release/2.1' (#36) from release/2.1 into master
Some checks are pending
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is running
Reviewed-on: fabian/TichuCounter#36
2023-03-10 17:21:17 +01:00
895264de2a [#34] Add contact button and play store button to about page.
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build was killed
closes [#34]
2023-03-10 15:08:38 +01:00
3ec8c27b96 First draft
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-10 13:19:09 +01:00
343d1d8e75 [#20] Add missing translation key.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
closes #20
2023-03-07 22:41:49 +01:00
801a17d759 Increase version to 2.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-07 20:53:57 +01:00
8e26f6b337 fixBuild (#30)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: fabian/TichuCounter#30
2023-03-04 08:34:26 +01:00
9e658853eb Update Gradle and Java
Some checks failed
continuous-integration/drone/push Build is failing
2023-03-03 15:31:36 +01:00
5a229d6c57 [#29] Disable swap button on invalid score.
Some checks failed
continuous-integration/drone/push Build is failing
closed [#29]
2023-03-03 15:04:02 +01:00
bcc3bd3848 Freeze build image version
Some checks failed
continuous-integration/drone/push Build is failing
2023-03-03 14:28:13 +01:00
17d861403e [#24] Add long press to delete functionality.
Some checks are pending
continuous-integration/drone/push Build is running
Reformat code.
closes #24
2023-03-03 13:20:22 +01:00
a1f344580d [#23] Prevent trailing zeros stacking up.
Some checks failed
continuous-integration/drone/push Build was killed
closes #23
2023-03-03 11:49:39 +01:00
b3bdbfbc05 [#23] Limit input to 5 digits. 2023-03-03 11:27:18 +01:00
a611de6da4 Add instrumented test to test repository together with room database. Improve data handling on first boot.
Some checks failed
continuous-integration/drone/push Build was killed
2023-02-17 15:19:00 +01:00
4108512139 Merge pull request 'release/2.0' (#22) from release/2.0 into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#22
2023-01-28 23:30:57 +01:00
30723f7862 Merge pull request 'release/2.0' (#21) from release/2.0 into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: fabian/TichuCounter#21
2023-01-28 23:29:25 +01:00
55 changed files with 944 additions and 226 deletions

View File

@@ -32,33 +32,48 @@ steps:
- ./gradlew assembleRelease - ./gradlew assembleRelease
- ./gradlew bundleRelease - ./gradlew bundleRelease
- name: deploy latest build - name: upload latest apk
image: curlimages/curl image: vividboarder/drone-webdav
environment: settings:
SEAFILE_API_KEY: file: app/build/outputs/apk/release/app-release.apk
from_secret: SeafileApiKey destination: https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/latest/app-release.apk
APK_FILE: app/build/outputs/apk/release/app-release.apk username:
BUNDLE_FILE: app/build/outputs/bundle/release/app-release.aab from_secret: NextCloudUser
SEAFILE_REPO: daffda8b-5840-4a65-b6d0-73b991facfb6 password:
commands: from_secret: NextCloudPassword
- '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 - name: upload latest bundle
image: curlimages/curl image: vividboarder/drone-webdav
environment: settings:
SEAFILE_API_KEY: file: app/build/outputs/bundle/release/app-release.aab
from_secret: SeafileApiKey destination: https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/latest/app-release.aab
APK_FILE: app/build/outputs/apk/release/app-release.apk username:
BUNDLE_FILE: app/build/outputs/bundle/release/app-release.aab from_secret: NextCloudUser
SEAFILE_REPO: daffda8b-5840-4a65-b6d0-73b991facfb6 password:
commands: from_secret: NextCloudPassword
- '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"' - name: upload tagged apk
- '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"' image: vividboarder/drone-webdav
- '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' settings:
- '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' file: app/build/outputs/apk/release/app-release.apk
destination: 'https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/tagged/app-release$DRONE_TAG.apk'
username:
from_secret: NextCloudUser
password:
from_secret: NextCloudPassword
when:
event:
- tag
- name: upload tagged bundle
image: vividboarder/drone-webdav
settings:
file: app/build/outputs/bundle/release/app-release.aab
destination: 'https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/tagged/app-release$DRONE_TAG.aab'
username:
from_secret: NextCloudUser
password:
from_secret: NextCloudPassword
when: when:
event: event:
- tag - tag

View File

@@ -16,7 +16,7 @@ def keystoreProperties = new Properties()
def versionProperties = new Properties() def versionProperties = new Properties()
def versionMajor = 2 def versionMajor = 2
def versionMinor = 0 def versionMinor = 2
// Load your keystore.properties file into the keystoreProperties object. // Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
@@ -32,7 +32,7 @@ android {
targetSdkVersion 33 targetSdkVersion 33
versionCode versionProperties["versionCode"].toInteger() versionCode versionProperties["versionCode"].toInteger()
versionName "${versionMajor}.${versionMinor}.${versionProperties["versionCode"].toInteger()}" versionName "${versionMajor}.${versionMinor}.${versionProperties["versionCode"].toInteger()}"
resConfigs 'de', 'en' resourceConfigurations += ['de', 'en']
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables { vectorDrawables {
@@ -68,15 +68,15 @@ android {
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.3.2" kotlinCompilerExtensionVersion = "1.4.7"
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '17'
} }
namespace 'me.zobrist.tichucounter' namespace 'me.zobrist.tichucounter'
packagingOptions { packagingOptions {
@@ -89,9 +89,9 @@ android {
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.0-rc01' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation "androidx.compose.material3:material3:1.0.1" implementation "androidx.compose.material3:material3:1.1.0"
implementation 'com.google.android.play:core-ktx:1.8.1' implementation 'com.google.android.play:core-ktx:1.8.1'
implementation 'com.google.android.play:core-ktx:1.8.1' implementation 'com.google.android.play:core-ktx:1.8.1'
implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.code.gson:gson:2.9.0'
@@ -99,32 +99,32 @@ dependencies {
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.fragment:fragment-ktx:1.5.5' implementation 'androidx.fragment:fragment-ktx:1.5.7'
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.compose.material:material-icons-extended:1.3.1' implementation 'androidx.compose.material:material-icons-extended:1.4.3'
implementation "com.google.accompanist:accompanist-systemuicontroller:0.27.0" implementation "com.google.accompanist:accompanist-systemuicontroller:0.27.0"
implementation 'androidx.activity:activity-compose:1.6.1' implementation 'androidx.activity:activity-compose:1.7.2'
implementation "androidx.compose.ui:ui:1.3.3" implementation "androidx.compose.ui:ui:1.4.3"
implementation "androidx.compose.ui:ui-tooling-preview:1.3.3" implementation "androidx.compose.ui:ui-tooling-preview:1.4.3"
implementation "androidx.compose.runtime:runtime-livedata:1.3.3" implementation "androidx.compose.runtime:runtime-livedata:1.4.3"
implementation "androidx.navigation:navigation-compose:2.5.3" implementation "androidx.navigation:navigation-compose:2.5.3"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation "com.google.dagger:hilt-android:2.44" implementation "com.google.dagger:hilt-android:2.44"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.3.3" androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.4.3"
debugImplementation "androidx.compose.ui:ui-tooling:1.3.3" debugImplementation "androidx.compose.ui:ui-tooling:1.4.3"
debugImplementation "androidx.compose.ui:ui-test-manifest:1.3.3" debugImplementation "androidx.compose.ui:ui-test-manifest:1.4.3"
kapt "com.google.dagger:hilt-compiler:2.44" kapt "com.google.dagger:hilt-compiler:2.44"
implementation "androidx.room:room-runtime:2.5.0" implementation "androidx.room:room-runtime:2.5.1"
annotationProcessor "androidx.room:room-compiler:2.5.0" annotationProcessor "androidx.room:room-compiler:2.5.1"
kapt "androidx.room:room-compiler:2.5.0" kapt "androidx.room:room-compiler:2.5.1"
implementation "androidx.room:room-ktx:2.5.0" implementation "androidx.room:room-ktx:2.5.1"
implementation "androidx.multidex:multidex:2.0.1" implementation "androidx.multidex:multidex:2.0.1"
api "androidx.navigation:navigation-fragment-ktx:2.5.3" api "androidx.navigation:navigation-fragment-ktx:2.5.3"
} }

View File

@@ -1,22 +0,0 @@
package me.zobrist.tichucounter
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
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("me.zobrist.tichucounter", appContext.packageName)
}
}

View File

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

View File

@@ -9,7 +9,6 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity <activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -88,7 +88,6 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener {
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun NavigationDrawer() { private fun NavigationDrawer() {
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)
@@ -142,7 +141,6 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener {
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MyScaffoldLayout( fun MyScaffoldLayout(
drawerState: DrawerState, drawerState: DrawerState,
@@ -205,7 +203,12 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener {
}, },
)) ))
) { scope.launch { drawerState.open() } } ) {
scope.launch {
currentFocus?.clearFocus()
drawerState.open()
}
}
Counter(counterViewModel) Counter(counterViewModel)
} }

View File

@@ -2,7 +2,7 @@ package me.zobrist.tichucounter.data
import androidx.room.ProvidedTypeConverter import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter import androidx.room.TypeConverter
import java.util.* import java.util.Date
@ProvidedTypeConverter @ProvidedTypeConverter
object DateConverter { object DateConverter {

View File

@@ -12,7 +12,7 @@ interface GameDao : DaoBase<Game> {
fun getAll(): Flow<List<Game>> fun getAll(): Flow<List<Game>>
@Transaction @Transaction
@Query("SELECT * FROM game where uid ") @Query("SELECT * FROM game")
fun getGamesWithRounds(): Flow<List<GameWithScores>> fun getGamesWithRounds(): Flow<List<GameWithScores>>
@Transaction @Transaction
@@ -20,10 +20,13 @@ interface GameDao : DaoBase<Game> {
fun getActiveWithRounds(): Flow<GameWithScores?> fun getActiveWithRounds(): Flow<GameWithScores?>
@Query("SELECT * FROM game WHERE uid is :gameId") @Query("SELECT * FROM game WHERE uid is :gameId")
fun getGameById(gameId: Long): Flow<Game> fun getGameById(gameId: Long): Game
@Query("SELECT * FROM game WHERE active is 1") @Query("SELECT * FROM game WHERE active is 1")
fun getActive(): Flow<Game?> fun getActiveAsFlow(): Flow<Game?>
@Query("SELECT * FROM game WHERE active is 1")
fun getActive(): Game?
@Query("UPDATE game SET active = 1 WHERE uid is :gameId;") @Query("UPDATE game SET active = 1 WHERE uid is :gameId;")
@@ -32,4 +35,7 @@ interface GameDao : DaoBase<Game> {
@Query("UPDATE game SET active = 0 WHERE uid is not :gameId;") @Query("UPDATE game SET active = 0 WHERE uid is not :gameId;")
fun setOthersInactive(gameId: Long) fun setOthersInactive(gameId: Long)
@Query("SELECT names FROM (SELECT nameA AS names FROM game UNION ALL SELECT nameB AS names FROM game) GROUP BY names")
fun getDistinctTeamNames(): Flow<List<String>>
} }

View File

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

View File

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

View File

@@ -1,11 +1,15 @@
package me.zobrist.tichucounter.domain package me.zobrist.tichucounter.domain
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.navigation.* import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavDeepLink
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
fun NavController.navigate(route: Route) { fun NavController.navigate(route: Route) {
this.navigate(route.name){ this.navigate(route.name) {
// Pop up to the start destination of the graph to // Pop up to the start destination of the graph to
// avoid building up a large stack of destinations // avoid building up a large stack of destinations
// on the back stack as users select items // on the back stack as users select items

View File

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

View File

@@ -3,6 +3,8 @@ package me.zobrist.tichucounter.repository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -11,7 +13,7 @@ import me.zobrist.tichucounter.data.GameWithScores
import me.zobrist.tichucounter.data.RoundDao import me.zobrist.tichucounter.data.RoundDao
import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.data.entity.Round
import java.util.* import java.util.Date
import javax.inject.Inject import javax.inject.Inject
class GameRepository @Inject constructor( class GameRepository @Inject constructor(
@@ -19,20 +21,15 @@ class GameRepository @Inject constructor(
private val roundDao: RoundDao private val roundDao: RoundDao
) { ) {
private var _activeGame: Game? = null private var activeGame: Game = Game(true, "TeamA", "TeamB", Date(), Date())
val activeGame: Game
get() {
return _activeGame!!
}
init { init {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
gameDao.getActive().collect { gameDao.getActiveAsFlow().collect {
if (it == null) { if (it == null) {
gameDao.insert(Game(true, "TeamA", "TeamB", Date(), Date())) newGame()
} else { } else {
_activeGame = it activeGame = it
} }
} }
} }
@@ -40,16 +37,25 @@ class GameRepository @Inject constructor(
suspend fun newGame() { suspend fun newGame() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val id = val id = gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date()))
gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date()))
setActive(id) setActive(id)
} }
} }
suspend fun updateGame(game: Game) { suspend fun updateActiveTeamName(nameA: String? = null, nameB: String? = null) {
game.modified = Date()
val newA = nameA ?: activeGame.nameA
val newB = nameB ?: activeGame.nameB
if (newA == activeGame.nameA && newB == activeGame.nameB) {
return
}
activeGame.modified = Date()
activeGame.nameA = newA
activeGame.nameB = newB
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
gameDao.update(game) gameDao.update(activeGame)
} }
} }
@@ -92,11 +98,10 @@ class GameRepository @Inject constructor(
suspend fun deleteGame(uid: Long) { suspend fun deleteGame(uid: Long) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
gameDao.getGameById(uid).take(1).collect { val game = gameDao.getGameById(uid)
gameDao.delete(it) gameDao.delete(game)
val rounds = roundDao.getAllForGame(it.uid) val rounds = roundDao.getAllForGame(game.uid)
roundDao.delete(rounds) roundDao.delete(rounds)
}
} catch (_: NullPointerException) { } catch (_: NullPointerException) {
} }
} }
@@ -107,9 +112,8 @@ class GameRepository @Inject constructor(
try { try {
gameDao.getAll().take(1).collect { games -> gameDao.getAll().take(1).collect { games ->
val activeId = games.first { it.active }.uid val gamesToDelete = games.filter { it.uid != activeGame.uid }
val gamesToDelete = games.filter { !it.active } val roundsToDelete = roundDao.getAll().filter { it.gameId != activeGame.uid }
val roundsToDelete = roundDao.getAll().filter { it.gameId != activeId }
gameDao.delete(gamesToDelete) gameDao.delete(gamesToDelete)
roundDao.delete(roundsToDelete) roundDao.delete(roundsToDelete)
@@ -119,11 +123,15 @@ class GameRepository @Inject constructor(
} }
} }
fun getActiveGameFlow(): Flow<GameWithScores?> { fun getActiveGameFlow(): Flow<GameWithScores> {
return gameDao.getActiveWithRounds() return gameDao.getActiveWithRounds().filter { it != null }.map { it!! }
} }
fun getAllWithRoundFlow(): Flow<List<GameWithScores>> { fun getAllWithRoundFlow(): Flow<List<GameWithScores>> {
return gameDao.getGamesWithRounds() return gameDao.getGamesWithRounds()
} }
fun getDistinctTeamNames(): Flow<List<String>> {
return gameDao.getDistinctTeamNames()
}
} }

View File

@@ -34,9 +34,8 @@ class MainViewModel @Inject constructor(
gameRepository.getActiveGameFlow().collect { gameRepository.getActiveGameFlow().collect {
activeGameHasRounds = it?.rounds?.isNotEmpty() == true activeGameHasRounds = it.rounds.isNotEmpty() == true
if (it != null) {
isUndoActionActive = it.rounds.isNotEmpty() isUndoActionActive = it.rounds.isNotEmpty()
if (expectedRoundCount != it.rounds.count()) { if (expectedRoundCount != it.rounds.count()) {
@@ -47,7 +46,6 @@ class MainViewModel @Inject constructor(
} }
} }
} }
}
fun undoLastRound() { fun undoLastRound() {
viewModelScope.launch { viewModelScope.launch {

View File

@@ -2,7 +2,11 @@ package me.zobrist.tichucounter.ui
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.* import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext

View File

@@ -2,17 +2,27 @@ package me.zobrist.tichucounter.ui.about
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Mail
import androidx.compose.material.icons.outlined.Shop
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.Top import androidx.compose.ui.Alignment.Companion.Top
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -24,15 +34,33 @@ import me.zobrist.tichucounter.ui.AppTheme
@Composable @Composable
fun AboutView() { fun AboutView() {
Row(Modifier.padding(20.dp)) {
val uriHandler = LocalUriHandler.current
Column(
modifier = Modifier
.padding(
top = 20.dp,
start = 20.dp,
end = 20.dp,
bottom = 40.dp
),
) {
Row {
Image( Image(
modifier = Modifier modifier = Modifier
.height(80.dp)
.padding(end = 10.dp) .padding(end = 10.dp)
.align(Top), .align(Top)
painter = painterResource(R.drawable.app_logo), .size(75.dp)
.background(
color = Color.Red,
shape = ShapeDefaults.Medium
),
painter = painterResource(R.drawable.tichu_logo),
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Fit contentScale = ContentScale.None
) )
Column { Column {
@@ -43,13 +71,37 @@ fun AboutView() {
Text(text = "V" + BuildConfig.VERSION_NAME) Text(text = "V" + BuildConfig.VERSION_NAME)
} }
} }
Button(
modifier = Modifier
.fillMaxWidth()
.padding(top = 30.dp),
onClick = { uriHandler.openUri("market://details?id=me.zobrist.tichucounter") }
) {
Icon(imageVector = Icons.Outlined.Shop, contentDescription = null)
Text(stringResource(id = R.string.play_store))
}
Button(
modifier = Modifier
.fillMaxWidth()
.padding(top = 30.dp),
onClick = { uriHandler.openUri("mailto:app@zobrist.me") }
) {
Icon(imageVector = Icons.Outlined.Mail, contentDescription = null)
Text(stringResource(id = R.string.contact_us))
}
}
} }
@Preview(name = "Light Mode") @Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true) @Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable @Composable
fun AboutViewPreview() { fun AboutViewPreview() {
AppTheme() { AppTheme {
Surface { Surface {
AboutView() AboutView()
} }

View File

@@ -0,0 +1,100 @@
package me.zobrist.tichucounter.ui.composables
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun TypeaheadTextField(
value: String,
items: List<String>,
onValueChange: (String) -> Unit,
modifier: Modifier,
colors: TextFieldColors,
textStyle: TextStyle
) {
var isFocused by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
ExposedDropdownMenuBox(
expanded = isFocused,
modifier = modifier,
onExpandedChange = {}
) {
var dropDownWidth by remember { mutableStateOf(0) }
TextField(
value = value,
textStyle = textStyle,
onValueChange = {
onValueChange(it)
},
singleLine = true,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
}
),
modifier = Modifier
.menuAnchor()
.onFocusChanged {
isFocused = it.isFocused
}
.onSizeChanged { dropDownWidth = it.width }
.onKeyEvent { event ->
if (event.key == Key.Back || event.key == Key.Enter) {
focusManager.clearFocus()
true
} else {
false
}
},
colors = colors
)
ExposedDropdownMenu(
expanded = isFocused && items.isNotEmpty(),
modifier = Modifier
.width(with(LocalDensity.current) { dropDownWidth.toDp() }),
onDismissRequest = { }
) {
items.forEach {
DropdownMenuItem(
onClick = {
onValueChange(it)
focusManager.clearFocus()
},
text = { Text(it) },
)
}
}
}
}

View File

@@ -4,7 +4,11 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
@@ -35,6 +39,8 @@ fun Landscape(viewModel: ICounterViewModel) {
TeamNamesView( TeamNamesView(
viewModel.teamNameA, viewModel.teamNameA,
viewModel.teamNameB, viewModel.teamNameB,
viewModel.teamNameSuggestionsA,
viewModel.teamNameSuggestionsB,
{ viewModel.updateNameA(it) }, { viewModel.updateNameA(it) },
{ viewModel.updateNameB(it) } { viewModel.updateNameB(it) }
) )
@@ -64,6 +70,8 @@ fun Portrait(viewModel: ICounterViewModel) {
TeamNamesView( TeamNamesView(
viewModel.teamNameA, viewModel.teamNameA,
viewModel.teamNameB, viewModel.teamNameB,
viewModel.teamNameSuggestionsA,
viewModel.teamNameSuggestionsB,
{ viewModel.updateNameA(it) }, { viewModel.updateNameA(it) },
{ viewModel.updateNameB(it) } { viewModel.updateNameB(it) }
) )
@@ -103,7 +111,7 @@ internal class PreviewViewModel : ICounterViewModel {
override var teamNameB: String = "Team B" override var teamNameB: String = "Team B"
override var currentScoreA: String = "" override var currentScoreA: String = ""
override var currentScoreB: String = "45" override var currentScoreB: String = "45"
override var enableSubmit: Boolean = false override var isValidRound: Boolean = false
override var isAFocused: Boolean = false override var isAFocused: Boolean = false
override var isBFocused: Boolean = false override var isBFocused: Boolean = false
override var requestFocusA: FocusRequester = FocusRequester() override var requestFocusA: FocusRequester = FocusRequester()
@@ -111,6 +119,10 @@ internal class PreviewViewModel : ICounterViewModel {
override var activeValue: String = currentScoreA override var activeValue: String = currentScoreA
override var inactiveValue: String = currentScoreB override var inactiveValue: String = currentScoreB
override var keyboardHidden: Boolean = false override var keyboardHidden: Boolean = false
override val teamNameSuggestionsA: List<String> =
listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long")
override val teamNameSuggestionsB: List<String> =
listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long")
override fun focusLastInput() { override fun focusLastInput() {
} }
@@ -137,9 +149,6 @@ internal class PreviewViewModel : ICounterViewModel {
override fun addSub100Clicked(toAdd: Int) { override fun addSub100Clicked(toAdd: Int) {
} }
override fun deleteClicked() {
}
override fun updateNameA(value: String) { override fun updateNameA(value: String) {
} }
@@ -161,4 +170,7 @@ internal class PreviewViewModel : ICounterViewModel {
override fun showKeyboard() { override fun showKeyboard() {
} }
override fun deleteState(pressed: Boolean) {
}
} }

View File

@@ -7,9 +7,12 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.Tichu import me.zobrist.tichucounter.domain.Tichu
import me.zobrist.tichucounter.domain.digitCount
import me.zobrist.tichucounter.domain.getTotalPoints import me.zobrist.tichucounter.domain.getTotalPoints
import me.zobrist.tichucounter.repository.GameRepository import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject import javax.inject.Inject
@@ -20,7 +23,7 @@ interface IKeyBoardViewModel {
val currentScoreA: String val currentScoreA: String
val currentScoreB: String val currentScoreB: String
val enableSubmit: Boolean val isValidRound: Boolean
val isAFocused: Boolean val isAFocused: Boolean
val isBFocused: Boolean val isBFocused: Boolean
val requestFocusA: FocusRequester val requestFocusA: FocusRequester
@@ -37,12 +40,12 @@ interface IKeyBoardViewModel {
fun digitClicked(digit: String) fun digitClicked(digit: String)
fun negateClicked() fun negateClicked()
fun addSub100Clicked(toAdd: Int) fun addSub100Clicked(toAdd: Int)
fun deleteClicked()
fun updateFocusStateA(state: Boolean) fun updateFocusStateA(state: Boolean)
fun updateFocusStateB(state: Boolean) fun updateFocusStateB(state: Boolean)
fun swapInputScores() fun swapInputScores()
fun hideKeyboard() fun hideKeyboard()
fun showKeyboard() fun showKeyboard()
fun deleteState(pressed: Boolean)
} }
@@ -52,6 +55,8 @@ interface ICounterViewModel : IKeyBoardViewModel {
val totalScoreB: Int val totalScoreB: Int
val teamNameA: String val teamNameA: String
val teamNameB: String val teamNameB: String
val teamNameSuggestionsA: List<String>
val teamNameSuggestionsB: List<String>
fun updateNameA(value: String) fun updateNameA(value: String)
fun updateNameB(value: String) fun updateNameB(value: String)
@@ -84,7 +89,7 @@ class CounterViewModel @Inject constructor(
override var currentScoreB by mutableStateOf("") override var currentScoreB by mutableStateOf("")
private set private set
override var enableSubmit by mutableStateOf(false) override var isValidRound by mutableStateOf(false)
private set private set
override var isAFocused by mutableStateOf(false) override var isAFocused by mutableStateOf(false)
@@ -102,6 +107,12 @@ class CounterViewModel @Inject constructor(
override var keyboardHidden by mutableStateOf(false) override var keyboardHidden by mutableStateOf(false)
private set private set
override var teamNameSuggestionsA by mutableStateOf(listOf<String>())
private set
override var teamNameSuggestionsB by mutableStateOf(listOf<String>())
private set
override var activeValue: String override var activeValue: String
get() { get() {
return if (isBFocused) { return if (isBFocused) {
@@ -137,6 +148,12 @@ class CounterViewModel @Inject constructor(
private var lastFocused = Focused.TEAM_A private var lastFocused = Focused.TEAM_A
private var deletePressed = false
private var deleteJob: Job? = null
private var distinctTeamNames = listOf<String>()
init { init {
viewModelScope.launch { viewModelScope.launch {
gameRepository.getActiveGameFlow().collect { gameRepository.getActiveGameFlow().collect {
@@ -150,9 +167,20 @@ class CounterViewModel @Inject constructor(
teamNameA = it.game.nameA teamNameA = it.game.nameA
teamNameB = it.game.nameB teamNameB = it.game.nameB
buildTeamNameSuggestions()
} }
} }
} }
viewModelScope.launch {
gameRepository.getDistinctTeamNames().collect {
distinctTeamNames = it
buildTeamNameSuggestions()
}
}
} }
override fun focusLastInput() { override fun focusLastInput() {
@@ -187,7 +215,7 @@ class CounterViewModel @Inject constructor(
} }
override fun updateSubmitButton() { override fun updateSubmitButton() {
enableSubmit = isValidTichuRound() isValidRound = isValidTichuRound()
} }
override fun submitClicked() { override fun submitClicked() {
@@ -196,13 +224,25 @@ class CounterViewModel @Inject constructor(
} }
currentScoreA = "" currentScoreA = ""
currentScoreB = "" currentScoreB = ""
enableSubmit = false isValidRound = false
} }
override fun digitClicked(digit: String) { override fun digitClicked(digit: String) {
focusLastInput() focusLastInput()
activeValue += digit
if (activeValue.digitCount() >= 5) {
// 5 digits is enough
return
}
val newValue = activeValue + digit
try {
activeValue = newValue.toInt().toString()
} catch (_: NumberFormatException) {
}
updateOtherScore() updateOtherScore()
updateSubmitButton() updateSubmitButton()
} }
@@ -235,27 +275,17 @@ class CounterViewModel @Inject constructor(
updateSubmitButton() updateSubmitButton()
} }
override fun deleteClicked() {
if (activeValue != "") {
activeValue = activeValue.dropLast(1)
}
updateOtherScore()
updateSubmitButton()
}
override fun updateNameA(value: String) { override fun updateNameA(value: String) {
teamNameA = value
viewModelScope.launch { viewModelScope.launch {
val game = gameRepository.activeGame gameRepository.updateActiveTeamName(nameA = value)
game.nameA = value
gameRepository.updateGame(game)
} }
} }
override fun updateNameB(value: String) { override fun updateNameB(value: String) {
teamNameB = value
viewModelScope.launch { viewModelScope.launch {
val game = gameRepository.activeGame gameRepository.updateActiveTeamName(nameB = value)
game.nameB = value
gameRepository.updateGame(game)
} }
} }
@@ -286,4 +316,50 @@ class CounterViewModel @Inject constructor(
override fun showKeyboard() { override fun showKeyboard() {
keyboardHidden = false keyboardHidden = false
} }
override fun deleteState(pressed: Boolean) {
deletePressed = pressed
if (deletePressed) {
if (deleteJob?.isActive != true) {
deleteJob = deleteRepeatedlyUntilRelease()
}
} else {
deleteJob?.cancel()
}
}
private fun deleteLastDigitActive() {
if (activeValue != "") {
activeValue = activeValue.dropLast(1)
}
updateOtherScore()
updateSubmitButton()
}
private fun deleteRepeatedlyUntilRelease(): Job {
return viewModelScope.launch {
deleteLastDigitActive()
delay(500)
while (deletePressed) {
deleteLastDigitActive()
delay(100)
}
}
}
private fun buildTeamNameSuggestions() {
teamNameSuggestionsA = buildTypeaheadList(distinctTeamNames, teamNameA)
teamNameSuggestionsB = buildTypeaheadList(distinctTeamNames, teamNameB)
}
private fun buildTypeaheadList(rawList: List<String>, currentInput: String): List<String> {
var filtered = rawList.filter { it.isNotEmpty() && it != currentInput }
if (currentInput.isNotEmpty()) {
filtered = filtered.filter { it.contains(currentInput, ignoreCase = true) }
}
return filtered.sorted().sortedBy { it.length }.take(10)
}
} }

View File

@@ -2,6 +2,8 @@ package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Backspace import androidx.compose.material.icons.outlined.Backspace
@@ -30,18 +32,18 @@ fun KeyBoardView(viewModel: IKeyBoardViewModel) {
viewModel.currentScoreB, viewModel.currentScoreB,
viewModel.requestFocusA, viewModel.requestFocusA,
viewModel.requestFocusB, viewModel.requestFocusB,
viewModel.enableSubmit, viewModel.isValidRound,
viewModel.isAFocused, viewModel.isAFocused,
viewModel.isBFocused, viewModel.isBFocused,
{ viewModel.updateFocusStateA(it) }, { viewModel.updateFocusStateA(it) },
{ viewModel.updateFocusStateB(it) }, { viewModel.updateFocusStateB(it) },
{ viewModel.digitClicked(it) }, { viewModel.digitClicked(it) },
{ viewModel.addSub100Clicked(it) }, { viewModel.addSub100Clicked(it) },
{ viewModel.deleteClicked() },
{ viewModel.negateClicked() }, { viewModel.negateClicked() },
{ viewModel.submitClicked() }, { viewModel.submitClicked() },
{ viewModel.hideKeyboard() }, { viewModel.hideKeyboard() },
{ viewModel.swapInputScores() } { viewModel.swapInputScores() },
{ viewModel.deleteState(it) }
) )
} }
@@ -51,18 +53,18 @@ fun KeyboardView(
scoreB: String, scoreB: String,
requestFocusA: FocusRequester, requestFocusA: FocusRequester,
requestFocusB: FocusRequester, requestFocusB: FocusRequester,
enableSubmit: Boolean, isValidScore: Boolean,
focusStateA: Boolean, focusStateA: Boolean,
focusStateB: Boolean, focusStateB: Boolean,
updateFocusStateA: (Boolean) -> Unit, updateFocusStateA: (Boolean) -> Unit,
updateFocusStateB: (Boolean) -> Unit, updateFocusStateB: (Boolean) -> Unit,
digitClicked: (String) -> Unit, digitClicked: (String) -> Unit,
addSub100Clicked: (Int) -> Unit, addSub100Clicked: (Int) -> Unit,
deleteClicked: () -> Unit,
negateClicked: () -> Unit, negateClicked: () -> Unit,
submitClicked: () -> Unit, submitClicked: () -> Unit,
hideKeyboardClicked: () -> Unit, hideKeyboardClicked: () -> Unit,
onSwapClicked: () -> Unit onSwapClicked: () -> Unit,
deleteButtonPressedState: (Boolean) -> Unit
) { ) {
Column { Column {
Row(Modifier.height(IntrinsicSize.Max)) { Row(Modifier.height(IntrinsicSize.Max)) {
@@ -83,7 +85,7 @@ fun KeyboardView(
shape = MaterialTheme.shapes.extraSmall shape = MaterialTheme.shapes.extraSmall
) { ) {
Column { Column {
IconButton(onClick = onSwapClicked) { IconButton(onClick = onSwapClicked, enabled = isValidScore) {
Icon(Icons.Outlined.SwapHoriz, null) Icon(Icons.Outlined.SwapHoriz, null)
} }
} }
@@ -164,9 +166,16 @@ fun KeyboardView(
} }
} }
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
KeyboardIconButton(Icons.Outlined.Backspace) {
deleteClicked() val interactionSource = remember { MutableInteractionSource() }
} val deletePressed by interactionSource.collectIsPressedAsState()
deleteButtonPressedState(deletePressed)
KeyboardIconButton(
icon = Icons.Outlined.Backspace,
interactionSource = interactionSource
) {}
} }
} }
@@ -188,7 +197,7 @@ fun KeyboardView(
} }
} }
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
KeyboardIconButton(Icons.Outlined.Check, enableSubmit) { KeyboardIconButton(Icons.Outlined.Check, isValidScore) {
submitClicked() submitClicked()
} }
} }
@@ -219,7 +228,12 @@ fun KeyboardTextButton(text: String, onClicked: () -> Unit) {
} }
@Composable @Composable
fun KeyboardIconButton(icon: ImageVector, enabled: Boolean = true, onClicked: () -> Unit) { fun KeyboardIconButton(
icon: ImageVector,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
onClicked: () -> Unit
) {
ElevatedButton( ElevatedButton(
onClick = { onClicked() }, onClick = { onClicked() },
@@ -228,6 +242,7 @@ fun KeyboardIconButton(icon: ImageVector, enabled: Boolean = true, onClicked: ()
.height(50.dp) .height(50.dp)
.padding(2.dp), .padding(2.dp),
enabled = enabled, enabled = enabled,
interactionSource = interactionSource
) { ) {
Icon( Icon(
icon, icon,
@@ -236,7 +251,6 @@ fun KeyboardIconButton(icon: ImageVector, enabled: Boolean = true, onClicked: ()
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun CenteredTextField( fun CenteredTextField(
value: String, value: String,
@@ -308,22 +322,22 @@ fun KeyboardViewPreview() {
AppTheme { AppTheme {
Surface { Surface {
KeyboardView( KeyboardView(
"1", "10",
"3511", "190",
FocusRequester(), FocusRequester(),
FocusRequester(), FocusRequester(),
enableSubmit = false, isValidScore = false,
focusStateA = true, focusStateA = true,
focusStateB = false, focusStateB = false,
updateFocusStateA = {}, updateFocusStateA = {},
updateFocusStateB = {}, updateFocusStateB = {},
digitClicked = {}, digitClicked = {},
addSub100Clicked = {}, addSub100Clicked = {},
deleteClicked = {},
negateClicked = {}, negateClicked = {},
submitClicked = {}, submitClicked = {},
hideKeyboardClicked = {}, hideKeyboardClicked = {},
onSwapClicked = {}) onSwapClicked = {},
deleteButtonPressedState = {})
} }
} }
} }

View File

@@ -2,46 +2,53 @@ package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.AppTheme
import me.zobrist.tichucounter.ui.composables.TypeaheadTextField
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TeamNamesView( fun TeamNamesView(
nameA: String, nameA: String,
nameB: String, nameB: String,
nameSuggestionsA: List<String>,
nameSuggestionsB: List<String>,
updateA: (String) -> Unit, updateA: (String) -> Unit,
updateB: (String) -> Unit updateB: (String) -> Unit
) { ) {
val color = TextFieldDefaults.textFieldColors( val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp) val color = TextFieldDefaults.colors(
focusedContainerColor = containerColor,
unfocusedContainerColor = containerColor,
disabledContainerColor = containerColor,
) )
Row { Row {
TextField(
TypeaheadTextField(
value = nameA, value = nameA,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), items = nameSuggestionsA,
onValueChange = { updateA(it) }, onValueChange = { updateA(it) },
singleLine = true,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = color colors = color,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center)
) )
TextField( TypeaheadTextField(
value = nameB, value = nameB,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), items = nameSuggestionsB,
onValueChange = { updateB(it) }, onValueChange = { updateB(it) },
singleLine = true,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = color colors = color,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center)
) )
} }
} }
@@ -50,6 +57,6 @@ fun TeamNamesView(
@Composable @Composable
private fun TeamNamesViewPreview() { private fun TeamNamesViewPreview() {
AppTheme { AppTheme {
TeamNamesView("TeamA", "TeamB", {}, {}) TeamNamesView("TeamA", "TeamB", listOf("Test1", "Test3"), listOf("Test3", "Test5"), {}, {})
} }
} }

View File

@@ -4,7 +4,11 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.* import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign

View File

@@ -2,14 +2,31 @@ package me.zobrist.tichucounter.ui.history
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DeleteForever import androidx.compose.material.icons.outlined.DeleteForever
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.* import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.* import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -23,7 +40,8 @@ import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.getTotalPoints import me.zobrist.tichucounter.domain.getTotalPoints
import me.zobrist.tichucounter.ui.composables.DropDownMenu import me.zobrist.tichucounter.ui.composables.DropDownMenu
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.Date
import java.util.Locale
@Composable @Composable

View File

@@ -16,7 +16,6 @@ import me.zobrist.tichucounter.domain.*
import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.AppTheme
import me.zobrist.tichucounter.ui.counter.* import me.zobrist.tichucounter.ui.counter.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DrawerContent( fun DrawerContent(
drawerItems: List<DrawerItem>, drawerItems: List<DrawerItem>,

View File

@@ -8,8 +8,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material3.* import androidx.compose.material3.Icon
import androidx.compose.runtime.* import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.End import androidx.compose.ui.Alignment.Companion.End
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,54 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="50.684"
android:viewportHeight="39.77">
<group
android:scaleX="0.46444446"
android:scaleY="0.36443365"
android:translateX="13.572049"
android:translateY="12.638237">
<path
android:fillColor="#ffff00"
android:pathData="m25.885,0.371 l-25.752,0.047 0.094,9.167 8.174,0.094 -0.094,29.957 9.592,-0.094 -0.047,-29.957h8.08z"
android:strokeWidth="0.26458300000000001"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#ffff00"
android:pathData="m50.503,39.494 l-0.047,-9.214h-10.773c-0.738,-0.009 -2.147,-0.64 -2.139,-1.95 0.009,-1.415 0.012,-16.903 0.012,-16.903 -0.001,-1.41 1.227,-2.055 1.89,-2.079l11.009,-0.142 0.094,-9.072 -11.86,0.189c-2.609,0.059 -10.536,2.804 -10.537,13.23 -0,10.079 0.218,15.921 0.218,15.921 0.223,4.386 5.741,9.979 10.125,9.99l12.006,0.03z"
android:strokeWidth="0.26458300000000001"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#000000"
android:pathData="m8.403,10.083c0.049,-0.713 -0.112,-0.88 -0.029,-2.095 0.366,-1.278 9.609,-5.62 16.985,-2.65 0.403,0.162 0.5,-0.375 0.299,-0.467 -2.964,-2.692 -13.318,-8.128 -25.314,0.286l8.059,4.521"
android:strokeWidth="0.264583"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#000000"
android:pathData="m13.09,39.45 l-4.651,-7.072 1.436,-0.155c1.716,-3.55 8.698,-15.304 2.46,-24.75 -0.307,-0.496 0.802,-2.297 1.512,-0.563 5.772,10.547 5.161,23.028 -0.757,32.54z"
android:strokeWidth="0.264583"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#000000"
android:pathData="m42.812,9.194 l7.51,-4.144c-4.938,-5.973 -14.39,-7.27 -20.302,4.385 -0.2,0.447 0.901,0.888 1.106,0.649 5.387,-7.535 11.325,-3.968 11.686,-2.476 0.158,0.461 0.006,1.054 0.001,1.585z"
android:strokeWidth="0.264583"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#000000"
android:pathData="m42.429,30.41 l7.935,4.743c-13.982,13.838 -26.059,-8.724 -21.01,-24.194 0.143,-0.438 1.306,-0.12 1.201,0.408 -1.411,6.97 1.621,22.234 11.726,20.975z"
android:strokeWidth="0.264583"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
</group>
</vector>

View File

@@ -0,0 +1,48 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50.684dp"
android:height="39.77dp"
android:viewportWidth="50.684"
android:viewportHeight="39.77">
<path
android:fillColor="#ffff00"
android:pathData="m25.885,0.371 l-25.752,0.047 0.094,9.167 8.174,0.094 -0.094,29.957 9.592,-0.094 -0.047,-29.957h8.08z"
android:strokeWidth="0.26458300000000001"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#ffff00"
android:pathData="m50.503,39.494 l-0.047,-9.214h-10.773c-0.738,-0.009 -2.147,-0.64 -2.139,-1.95 0.009,-1.415 0.012,-16.903 0.012,-16.903 -0.001,-1.41 1.227,-2.055 1.89,-2.079l11.009,-0.142 0.094,-9.072 -11.86,0.189c-2.609,0.059 -10.536,2.804 -10.537,13.23 -0,10.079 0.218,15.921 0.218,15.921 0.223,4.386 5.741,9.979 10.125,9.99l12.006,0.03z"
android:strokeWidth="0.26458300000000001"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#000000"
android:pathData="m8.403,10.083c0.049,-0.713 -0.112,-0.88 -0.029,-2.095 0.366,-1.278 9.609,-5.62 16.985,-2.65 0.403,0.162 0.5,-0.375 0.299,-0.467 -2.964,-2.692 -13.318,-8.128 -25.314,0.286l8.059,4.521"
android:strokeWidth="0.264583"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#000000"
android:pathData="m13.09,39.45 l-4.651,-7.072 1.436,-0.155c1.716,-3.55 8.698,-15.304 2.46,-24.75 -0.307,-0.496 0.802,-2.297 1.512,-0.563 5.772,10.547 5.161,23.028 -0.757,32.54z"
android:strokeWidth="0.264583"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#000000"
android:pathData="m42.812,9.194 l7.51,-4.144c-4.938,-5.973 -14.39,-7.27 -20.302,4.385 -0.2,0.447 0.901,0.888 1.106,0.649 5.387,-7.535 11.325,-3.968 11.686,-2.476 0.158,0.461 0.006,1.054 0.001,1.585z"
android:strokeWidth="0.264583"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#000000"
android:pathData="m42.429,30.41 l7.935,4.743c-13.982,13.838 -26.059,-8.724 -21.01,-24.194 0.143,-0.438 1.306,-0.12 1.201,0.408 -1.411,6.97 1.621,22.234 11.726,20.975z"
android:strokeWidth="0.264583"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
</vector>

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -22,5 +22,7 @@
<string name="active">Aktives Spiel</string> <string name="active">Aktives Spiel</string>
<string name="inactive">Vergangene Spiele</string> <string name="inactive">Vergangene Spiele</string>
<string name="menu_counter">Counter</string> <string name="menu_counter">Counter</string>
<string name="menu_about">About</string>
<string name="contact_us">Schreib uns</string>
</resources> </resources>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primaryColor">#d50000</color>
</resources>

View File

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

View File

@@ -19,11 +19,13 @@
<string name="delete_inactive_title">Delete history</string> <string name="delete_inactive_title">Delete history</string>
<string name="delete_inactive_text">You really want to delete the the history? This action can\'t be undone.</string> <string name="delete_inactive_text">You really want to delete the the history? This action can\'t be undone.</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="ok">Ok</string> <string name="ok">OK</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="deleteAll">Delete all</string> <string name="deleteAll">Delete all</string>
<string name="active">Current Game</string> <string name="active">Current Game</string>
<string name="inactive">Old Games</string> <string name="inactive">Old Games</string>
<string name="menu_counter">Counter</string> <string name="menu_counter">Counter</string>
<string name="menu_about">About</string> <string name="menu_about">About</string>
<string name="contact_us">Contact us</string>
<string name="play_store" translatable="false">Play Store</string>
</resources> </resources>

View File

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

View File

@@ -1,7 +1,9 @@
package me.zobrist.tichucounter package me.zobrist.tichucounter
import me.zobrist.tichucounter.domain.Tichu import me.zobrist.tichucounter.domain.Tichu
import org.junit.Assert.* import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
/** /**

View File

@@ -1,15 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext { ext.kotlin_version = "1.8.21"
compose_version = '1.1.1'
}
ext.kotlin_version = "1.7.20"
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.4.0' classpath 'com.android.tools.build:gradle:8.0.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

View File

@@ -19,3 +19,6 @@ android.useAndroidX=true
android.enableJetifier=false android.enableJetifier=false
# Kotlin code style for this project: "official" or "obsolete": # Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=true
android.nonFinalResIds=false

View File

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