Compare commits

...
29 Commits
Author SHA1 Message Date
fabian 34e64e6e50 Update Version to 2.2
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-05-16 12:07:52 +02:00
fabian 694989a917 Merge pull request 'feature/#14_team_name_suggestions' (#38) from feature/#14_team_name_suggestions into develop
continuous-integration/drone/push Build is failing
Reviewed-on: fabian/TichuCounter#38
2023-05-16 12:05:49 +02:00
fabian 1e8e72557d Move suggestion generation to ViewModel.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-05-11 23:40:17 +02:00
fabian 2e40d0315c Don't show dropdown when empty. 2023-05-11 23:37:12 +02:00
fabian 9ddbdb698c Better filtering and sorting of suggestions. 2023-05-11 23:06:29 +02:00
fabian c40d473fa5 Update value of team name immediately. 2023-05-11 23:05:47 +02:00
fabian 507944794f Clear focus on opening drawer. 2023-05-11 23:04:21 +02:00
fabian 068e36c5f6 Merge commit '7e722feb275cdb7d47e87413ee7b11efc7b4ec51' into feature/#14_team_name_suggestions
continuous-integration/drone Build is failing
2023-05-08 19:30:16 +02:00
fabian 7e722feb27 Update 'app/src/main/AndroidManifest.xml'
continuous-integration/drone/push Build is passing
2023-05-07 22:41:51 +02:00
fabian fb4c83d60a Fix display of app logo on About page.
continuous-integration/drone/push Build is passing
2023-05-07 20:50:42 +02:00
fabian 0c80e05f5c Fix logo not found.
continuous-integration/drone/push Build is passing
2023-05-05 20:10:55 +02:00
fabian 8ca82d1734 Merge branch 'develop' into feature/#14_team_name_suggestions
continuous-integration/drone/push Build was killed
2023-05-05 19:53:55 +02:00
fabian 9385deeb24 [#25] Use vector as app logo. Enable monochrom logo.
continuous-integration/drone/push Build is failing
closes #25
2023-05-05 19:50:58 +02:00
fabian fadd206e9e Update dependencies. 2023-05-05 19:44:50 +02:00
fabian 6768391caf Update dependencies. 2023-05-05 19:44:12 +02:00
fabian aa3e68dc5c Merge pull request 'release/2.1' (#35) from release/2.1 into develop
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#35
2023-03-10 17:21:32 +01:00
fabian 895264de2a [#34] Add contact button and play store button to about page.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build was killed
closes [#34]
2023-03-10 15:08:38 +01:00
fabian 3ec8c27b96 First draft
continuous-integration/drone/push Build is passing
2023-03-10 13:19:09 +01:00
fabian 343d1d8e75 [#20] Add missing translation key.
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
closes #20
2023-03-07 22:41:49 +01:00
fabian 801a17d759 Increase version to 2.1
continuous-integration/drone/push Build is passing
2023-03-07 20:53:57 +01:00
fabian 8e26f6b337 fixBuild (#30)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#30
2023-03-04 08:34:26 +01:00
fabian 9e658853eb Update Gradle and Java
continuous-integration/drone/push Build is failing
2023-03-03 15:31:36 +01:00
fabian 5a229d6c57 [#29] Disable swap button on invalid score.
continuous-integration/drone/push Build is failing
closed [#29]
2023-03-03 15:04:02 +01:00
fabian bcc3bd3848 Freeze build image version
continuous-integration/drone/push Build is failing
2023-03-03 14:28:13 +01:00
fabian 17d861403e [#24] Add long press to delete functionality.
continuous-integration/drone/push Build is running
Reformat code.
closes #24
2023-03-03 13:20:22 +01:00
fabian a1f344580d [#23] Prevent trailing zeros stacking up.
continuous-integration/drone/push Build was killed
closes #23
2023-03-03 11:49:39 +01:00
fabian b3bdbfbc05 [#23] Limit input to 5 digits. 2023-03-03 11:27:18 +01:00
fabian a611de6da4 Add instrumented test to test repository together with room database. Improve data handling on first boot.
continuous-integration/drone/push Build was killed
2023-02-17 15:19:00 +01:00
fabian 4108512139 Merge pull request 'release/2.0' (#22) from release/2.0 into develop
continuous-integration/drone/push Build is passing
Reviewed-on: fabian/TichuCounter#22
2023-01-28 23:30:57 +01:00
48 changed files with 833 additions and 187 deletions
+3 -2
View File
@@ -39,9 +39,10 @@ steps:
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
SEAFILE_REPO: 6debeef9-121e-46ba-acc7-81e109fdcbdd
commands:
- 'UPLOAD_URL=$(curl -H "Authorization: Token $SEAFILE_API_KEY" https://seafile.zobrist.me/api2/repos/$SEAFILE_REPO/upload-link/ | tr -d "\"")'
- echo $UPLOAD_URL
- '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"'
@@ -52,7 +53,7 @@ steps:
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
SEAFILE_REPO: 6debeef9-121e-46ba-acc7-81e109fdcbdd
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"'
+25 -25
View File
@@ -16,7 +16,7 @@ def keystoreProperties = new Properties()
def versionProperties = new Properties()
def versionMajor = 2
def versionMinor = 0
def versionMinor = 2
// Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
@@ -32,7 +32,7 @@ android {
targetSdkVersion 33
versionCode versionProperties["versionCode"].toInteger()
versionName "${versionMajor}.${versionMinor}.${versionProperties["versionCode"].toInteger()}"
resConfigs 'de', 'en'
resourceConfigurations += ['de', 'en']
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables {
@@ -68,15 +68,15 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
kotlinCompilerExtensionVersion = "1.4.7"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '17'
}
namespace 'me.zobrist.tichucounter'
packagingOptions {
@@ -89,7 +89,7 @@ android {
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.core:core-ktx:1.10.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'
@@ -99,32 +99,32 @@ dependencies {
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.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.fragment:fragment-ktx:1.5.7'
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 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.compose.material:material-icons-extended:1.4.3'
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.activity:activity-compose:1.7.1'
implementation "androidx.compose.ui:ui:1.4.3"
implementation "androidx.compose.ui:ui-tooling-preview:1.4.3"
implementation "androidx.compose.runtime:runtime-livedata:1.4.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'
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"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.4.3"
debugImplementation "androidx.compose.ui:ui-tooling:1.4.3"
debugImplementation "androidx.compose.ui:ui-test-manifest:1.4.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.room:room-runtime:2.5.1"
annotationProcessor "androidx.room:room-compiler:2.5.1"
kapt "androidx.room:room-compiler:2.5.1"
implementation "androidx.room:room-ktx:2.5.1"
implementation "androidx.multidex:multidex:2.0.1"
api "androidx.navigation:navigation-fragment-ktx:2.5.3"
}
@@ -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)
}
}
@@ -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 })
}
}
}
-1
View File
@@ -9,7 +9,6 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 28 KiB

@@ -12,7 +12,9 @@ import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
@@ -142,7 +144,7 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun MyScaffoldLayout(
drawerState: DrawerState,
@@ -205,7 +207,10 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener {
},
))
) { scope.launch { drawerState.open() } }
) { scope.launch {
currentFocus?.clearFocus()
drawerState.open()
} }
Counter(counterViewModel)
}
@@ -12,7 +12,7 @@ interface GameDao : DaoBase<Game> {
fun getAll(): Flow<List<Game>>
@Transaction
@Query("SELECT * FROM game where uid ")
@Query("SELECT * FROM game")
fun getGamesWithRounds(): Flow<List<GameWithScores>>
@Transaction
@@ -20,10 +20,13 @@ interface GameDao : DaoBase<Game> {
fun getActiveWithRounds(): Flow<GameWithScores?>
@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")
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;")
@@ -32,4 +35,7 @@ interface GameDao : DaoBase<Game> {
@Query("UPDATE game SET active = 0 WHERE uid is not :gameId;")
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>>
}
@@ -8,10 +8,10 @@ import me.zobrist.tichucounter.data.entity.Round
@Entity
data class GameWithScores(
@Embedded val game: Game,
@Embedded val game: Game = Game(),
@Relation(
parentColumn = "uid",
entityColumn = "gameId"
)
val rounds: List<Round>
val rounds: List<Round> = emptyList()
)
@@ -6,10 +6,10 @@ import java.util.*
@Entity
data class Game(
var active: Boolean,
var nameA: String,
var nameB: String,
val created: Date,
var modified: Date,
var active: Boolean = true,
var nameA: String = "TeamA",
var nameB: String = "TeamB",
val created: Date = Date(),
var modified: Date = Date(),
@PrimaryKey(autoGenerate = true) override val uid: Long = 0
) : IEntity
@@ -5,7 +5,7 @@ import androidx.navigation.*
import androidx.navigation.compose.composable
fun NavController.navigate(route: Route) {
this.navigate(route.name){
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
@@ -0,0 +1,12 @@
package me.zobrist.tichucounter.domain
fun String.digitCount(): Int {
var count = 0
this.forEach {
if (it.isDigit()) {
count++
}
}
return count
}
@@ -2,8 +2,7 @@ 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.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.zobrist.tichucounter.data.GameDao
@@ -19,20 +18,17 @@ class GameRepository @Inject constructor(
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!!
}
private var distinctTeamNames: List<String> = listOf()
init {
CoroutineScope(Dispatchers.IO).launch {
gameDao.getActive().collect {
gameDao.getActiveAsFlow().collect {
if (it == null) {
gameDao.insert(Game(true, "TeamA", "TeamB", Date(), Date()))
newGame()
} else {
_activeGame = it
activeGame = it
}
}
}
@@ -40,16 +36,25 @@ class GameRepository @Inject constructor(
suspend fun newGame() {
withContext(Dispatchers.IO) {
val id =
gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date()))
val id = gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date()))
setActive(id)
}
}
suspend fun updateGame(game: Game) {
game.modified = Date()
suspend fun updateActiveTeamName(nameA: String? = null, nameB: String? = null) {
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) {
gameDao.update(game)
gameDao.update(activeGame)
}
}
@@ -92,11 +97,10 @@ class GameRepository @Inject constructor(
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)
}
val game = gameDao.getGameById(uid)
gameDao.delete(game)
val rounds = roundDao.getAllForGame(game.uid)
roundDao.delete(rounds)
} catch (_: NullPointerException) {
}
}
@@ -107,9 +111,8 @@ class GameRepository @Inject constructor(
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 }
val gamesToDelete = games.filter { it.uid != activeGame.uid }
val roundsToDelete = roundDao.getAll().filter { it.gameId != activeGame.uid }
gameDao.delete(gamesToDelete)
roundDao.delete(roundsToDelete)
@@ -119,11 +122,15 @@ class GameRepository @Inject constructor(
}
}
fun getActiveGameFlow(): Flow<GameWithScores?> {
return gameDao.getActiveWithRounds()
fun getActiveGameFlow(): Flow<GameWithScores> {
return gameDao.getActiveWithRounds().filter { it != null }.map { it!! }
}
fun getAllWithRoundFlow(): Flow<List<GameWithScores>> {
return gameDao.getGamesWithRounds()
}
fun getDistinctTeamNames(): Flow<List<String>> {
return gameDao.getDistinctTeamNames()
}
}
@@ -34,17 +34,15 @@ class MainViewModel @Inject constructor(
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()) {
redoRounds.clear()
}
expectedRoundCount = it.rounds.count()
if (expectedRoundCount != it.rounds.count()) {
redoRounds.clear()
}
expectedRoundCount = it.rounds.count()
}
}
}
@@ -2,17 +2,18 @@ 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.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Mail
import androidx.compose.material.icons.outlined.Shop
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.Top
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -24,24 +25,66 @@ 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
val uriHandler = LocalUriHandler.current
Column(
modifier = Modifier
.padding(
top = 20.dp,
start = 20.dp,
end = 20.dp,
bottom = 40.dp
),
) {
Row() {
Image(
modifier = Modifier
.padding(end = 10.dp)
.align(Top)
.size(75.dp)
.background(
color = Color.Red,
shape = ShapeDefaults.Medium
),
painter = painterResource(R.drawable.tichu_logo),
contentDescription = null,
contentScale = ContentScale.None
)
Text(text = "V" + BuildConfig.VERSION_NAME)
Column {
Text(
text = stringResource(id = R.string.app_name),
style = MaterialTheme.typography.headlineMedium
)
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))
}
}
}
@@ -0,0 +1,90 @@
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.*
import androidx.compose.runtime.*
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
}
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) },
)
}
}
}
}
@@ -35,6 +35,8 @@ fun Landscape(viewModel: ICounterViewModel) {
TeamNamesView(
viewModel.teamNameA,
viewModel.teamNameB,
viewModel.teamNameSuggestionsA,
viewModel.teamNameSuggestionsB,
{ viewModel.updateNameA(it) },
{ viewModel.updateNameB(it) }
)
@@ -64,6 +66,8 @@ fun Portrait(viewModel: ICounterViewModel) {
TeamNamesView(
viewModel.teamNameA,
viewModel.teamNameB,
viewModel.teamNameSuggestionsA,
viewModel.teamNameSuggestionsB,
{ viewModel.updateNameA(it) },
{ viewModel.updateNameB(it) }
)
@@ -103,7 +107,7 @@ internal class PreviewViewModel : ICounterViewModel {
override var teamNameB: String = "Team B"
override var currentScoreA: String = ""
override var currentScoreB: String = "45"
override var enableSubmit: Boolean = false
override var isValidRound: Boolean = false
override var isAFocused: Boolean = false
override var isBFocused: Boolean = false
override var requestFocusA: FocusRequester = FocusRequester()
@@ -111,6 +115,8 @@ internal class PreviewViewModel : ICounterViewModel {
override var activeValue: String = currentScoreA
override var inactiveValue: String = currentScoreB
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() {
}
@@ -137,9 +143,6 @@ internal class PreviewViewModel : ICounterViewModel {
override fun addSub100Clicked(toAdd: Int) {
}
override fun deleteClicked() {
}
override fun updateNameA(value: String) {
}
@@ -161,4 +164,7 @@ internal class PreviewViewModel : ICounterViewModel {
override fun showKeyboard() {
}
override fun deleteState(pressed: Boolean) {
}
}
@@ -1,15 +1,21 @@
package me.zobrist.tichucounter.ui.counter
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
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.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.Tichu
import me.zobrist.tichucounter.domain.digitCount
import me.zobrist.tichucounter.domain.getTotalPoints
import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject
@@ -20,7 +26,7 @@ interface IKeyBoardViewModel {
val currentScoreA: String
val currentScoreB: String
val enableSubmit: Boolean
val isValidRound: Boolean
val isAFocused: Boolean
val isBFocused: Boolean
val requestFocusA: FocusRequester
@@ -37,12 +43,12 @@ interface IKeyBoardViewModel {
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()
fun deleteState(pressed: Boolean)
}
@@ -52,6 +58,8 @@ interface ICounterViewModel : IKeyBoardViewModel {
val totalScoreB: Int
val teamNameA: String
val teamNameB: String
val teamNameSuggestionsA: List<String>
val teamNameSuggestionsB: List<String>
fun updateNameA(value: String)
fun updateNameB(value: String)
@@ -84,7 +92,7 @@ class CounterViewModel @Inject constructor(
override var currentScoreB by mutableStateOf("")
private set
override var enableSubmit by mutableStateOf(false)
override var isValidRound by mutableStateOf(false)
private set
override var isAFocused by mutableStateOf(false)
@@ -102,6 +110,12 @@ class CounterViewModel @Inject constructor(
override var keyboardHidden by mutableStateOf(false)
private set
override var teamNameSuggestionsA by mutableStateOf(listOf<String>())
private set
override var teamNameSuggestionsB by mutableStateOf(listOf<String>())
private set
override var activeValue: String
get() {
return if (isBFocused) {
@@ -137,6 +151,12 @@ class CounterViewModel @Inject constructor(
private var lastFocused = Focused.TEAM_A
private var deletePressed = false
private var deleteJob: Job? = null
private var distinctTeamNames = listOf<String>()
init {
viewModelScope.launch {
gameRepository.getActiveGameFlow().collect {
@@ -150,9 +170,20 @@ class CounterViewModel @Inject constructor(
teamNameA = it.game.nameA
teamNameB = it.game.nameB
buildTeamNameSuggestions()
}
}
}
viewModelScope.launch {
gameRepository.getDistinctTeamNames().collect() {
distinctTeamNames = it
buildTeamNameSuggestions()
}
}
}
override fun focusLastInput() {
@@ -187,7 +218,7 @@ class CounterViewModel @Inject constructor(
}
override fun updateSubmitButton() {
enableSubmit = isValidTichuRound()
isValidRound = isValidTichuRound()
}
override fun submitClicked() {
@@ -196,13 +227,25 @@ class CounterViewModel @Inject constructor(
}
currentScoreA = ""
currentScoreB = ""
enableSubmit = false
isValidRound = false
}
override fun digitClicked(digit: String) {
focusLastInput()
activeValue += digit
if (activeValue.digitCount() >= 5) {
// 5 digits is enough
return
}
val newValue = activeValue + digit
try {
activeValue = newValue.toInt().toString()
} catch (_: NumberFormatException) {
}
updateOtherScore()
updateSubmitButton()
}
@@ -235,27 +278,17 @@ class CounterViewModel @Inject constructor(
updateSubmitButton()
}
override fun deleteClicked() {
if (activeValue != "") {
activeValue = activeValue.dropLast(1)
}
updateOtherScore()
updateSubmitButton()
}
override fun updateNameA(value: String) {
teamNameA = value
viewModelScope.launch {
val game = gameRepository.activeGame
game.nameA = value
gameRepository.updateGame(game)
gameRepository.updateActiveTeamName(nameA = value)
}
}
override fun updateNameB(value: String) {
teamNameB = value
viewModelScope.launch {
val game = gameRepository.activeGame
game.nameB = value
gameRepository.updateGame(game)
gameRepository.updateActiveTeamName(nameB = value)
}
}
@@ -286,4 +319,51 @@ class CounterViewModel @Inject constructor(
override fun showKeyboard() {
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) }
}
return filtered.sorted().sortedBy { it.length }.take(10)
}
}
@@ -2,6 +2,8 @@ package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration
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.material.icons.Icons
import androidx.compose.material.icons.outlined.Backspace
@@ -30,18 +32,18 @@ fun KeyBoardView(viewModel: IKeyBoardViewModel) {
viewModel.currentScoreB,
viewModel.requestFocusA,
viewModel.requestFocusB,
viewModel.enableSubmit,
viewModel.isValidRound,
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() }
{ viewModel.swapInputScores() },
{ viewModel.deleteState(it) }
)
}
@@ -51,18 +53,18 @@ fun KeyboardView(
scoreB: String,
requestFocusA: FocusRequester,
requestFocusB: FocusRequester,
enableSubmit: Boolean,
isValidScore: 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
onSwapClicked: () -> Unit,
deleteButtonPressedState: (Boolean) -> Unit
) {
Column {
Row(Modifier.height(IntrinsicSize.Max)) {
@@ -83,7 +85,7 @@ fun KeyboardView(
shape = MaterialTheme.shapes.extraSmall
) {
Column {
IconButton(onClick = onSwapClicked) {
IconButton(onClick = onSwapClicked, enabled = isValidScore) {
Icon(Icons.Outlined.SwapHoriz, null)
}
}
@@ -164,9 +166,16 @@ fun KeyboardView(
}
}
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)) {
KeyboardIconButton(Icons.Outlined.Check, enableSubmit) {
KeyboardIconButton(Icons.Outlined.Check, isValidScore) {
submitClicked()
}
}
@@ -219,7 +228,12 @@ fun KeyboardTextButton(text: String, onClicked: () -> Unit) {
}
@Composable
fun KeyboardIconButton(icon: ImageVector, enabled: Boolean = true, onClicked: () -> Unit) {
fun KeyboardIconButton(
icon: ImageVector,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
onClicked: () -> Unit
) {
ElevatedButton(
onClick = { onClicked() },
@@ -228,6 +242,7 @@ fun KeyboardIconButton(icon: ImageVector, enabled: Boolean = true, onClicked: ()
.height(50.dp)
.padding(2.dp),
enabled = enabled,
interactionSource = interactionSource
) {
Icon(
icon,
@@ -308,22 +323,22 @@ fun KeyboardViewPreview() {
AppTheme {
Surface {
KeyboardView(
"1",
"3511",
"10",
"190",
FocusRequester(),
FocusRequester(),
enableSubmit = false,
isValidScore = false,
focusStateA = true,
focusStateB = false,
updateFocusStateA = {},
updateFocusStateB = {},
digitClicked = {},
addSub100Clicked = {},
deleteClicked = {},
negateClicked = {},
submitClicked = {},
hideKeyboardClicked = {},
onSwapClicked = {})
onSwapClicked = {},
deleteButtonPressedState = {})
}
}
}
@@ -4,18 +4,23 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
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
import me.zobrist.tichucounter.ui.composables.TypeaheadTextField
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TeamNamesView(
nameA: String,
nameB: String,
nameSuggestionsA: List<String>,
nameSuggestionsB: List<String>,
updateA: (String) -> Unit,
updateB: (String) -> Unit
) {
@@ -25,23 +30,26 @@ fun TeamNamesView(
)
Row {
TextField(
TypeaheadTextField(
value = nameA,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
items = nameSuggestionsA,
onValueChange = { updateA(it) },
singleLine = true,
modifier = Modifier.weight(1f),
colors = color
colors = color,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center)
)
TextField(
TypeaheadTextField(
value = nameB,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
items = nameSuggestionsB,
onValueChange = { updateB(it) },
singleLine = true,
modifier = Modifier.weight(1f),
colors = color
colors = color,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center)
)
}
}
@@ -50,6 +58,6 @@ fun TeamNamesView(
@Composable
private fun TeamNamesViewPreview() {
AppTheme {
TeamNamesView("TeamA", "TeamB", {}, {})
TeamNamesView("TeamA", "TeamB", listOf("Test1", "Test3"), listOf("Test3", "Test5"), {}, {})
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,53 @@
<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: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:strokeLineJoin="miter"
android:strokeWidth="0.26458300000000001"
android:fillColor="#ffff00"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
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:strokeLineJoin="miter"
android:strokeWidth="0.26458300000000001"
android:fillColor="#ffff00"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
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:strokeLineJoin="miter"
android:strokeWidth="0.264583"
android:fillColor="#000000"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
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:strokeLineJoin="miter"
android:strokeWidth="0.264583"
android:fillColor="#000000"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
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:strokeLineJoin="miter"
android:strokeWidth="0.264583"
android:fillColor="#000000"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
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:strokeLineJoin="miter"
android:strokeWidth="0.264583"
android:fillColor="#000000"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
</group>
</vector>
+48
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: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:strokeLineJoin="miter"
android:strokeWidth="0.26458300000000001"
android:fillColor="#ffff00"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
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:strokeLineJoin="miter"
android:strokeWidth="0.26458300000000001"
android:fillColor="#ffff00"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
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:strokeLineJoin="miter"
android:strokeWidth="0.264583"
android:fillColor="#000000"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
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:strokeLineJoin="miter"
android:strokeWidth="0.264583"
android:fillColor="#000000"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
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:strokeLineJoin="miter"
android:strokeWidth="0.264583"
android:fillColor="#000000"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
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:strokeLineJoin="miter"
android:strokeWidth="0.264583"
android:fillColor="#000000"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
</vector>
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</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

+2
View File
@@ -22,5 +22,7 @@
<string name="active">Aktives Spiel</string>
<string name="inactive">Vergangene Spiele</string>
<string name="menu_counter">Counter</string>
<string name="menu_about">About</string>
<string name="contact_us">Schreib uns</string>
</resources>
-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primaryColor">#d50000</color>
</resources>
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">@color/primaryColor</color>
<color name="ic_launcher_background">#DC0E00</color>
</resources>
+3 -1
View File
@@ -19,11 +19,13 @@
<string name="delete_inactive_title">Delete history</string>
<string name="delete_inactive_text">You really want to delete the the history? This action can\'t be undone.</string>
<string name="cancel">Cancel</string>
<string name="ok">Ok</string>
<string name="ok">OK</string>
<string name="delete">Delete</string>
<string name="deleteAll">Delete all</string>
<string name="active">Current Game</string>
<string name="inactive">Old Games</string>
<string name="menu_counter">Counter</string>
<string name="menu_about">About</string>
<string name="contact_us">Contact us</string>
<string name="play_store" translatable="false">Play Store</string>
</resources>
@@ -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())
}
}
+2 -5
View File
@@ -1,15 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
compose_version = '1.1.1'
}
ext.kotlin_version = "1.7.20"
ext.kotlin_version = "1.8.21"
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.0'
classpath 'com.android.tools.build:gradle:8.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
+4 -1
View File
@@ -18,4 +18,7 @@ android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=false
# 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
+1 -1
View File
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip