From 17d861403edd9ffe65530daddad6205d2063f0f9 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 3 Mar 2023 13:20:22 +0100 Subject: [PATCH] [#24] Add long press to delete functionality. Reformat code. closes #24 --- .../RepositoryInstrumentedTest.kt | 63 +++++++++---------- .../me/zobrist/tichucounter/data/DaoBase.kt | 5 +- .../tichucounter/data/GameWithScores.kt | 3 +- .../tichucounter/domain/NavExtensions.kt | 2 +- .../tichucounter/repository/GameRepository.kt | 12 +++- .../tichucounter/ui/counter/CounterView.kt | 6 +- .../ui/counter/CounterViewModel.kt | 53 ++++++++++++---- .../tichucounter/ui/counter/KeyboardView.kt | 35 ++++++++--- .../tichucounter/StringExtensionTest.kt | 2 +- 9 files changed, 113 insertions(+), 68 deletions(-) diff --git a/app/src/androidTest/java/me/zobrist/tichucounter/RepositoryInstrumentedTest.kt b/app/src/androidTest/java/me/zobrist/tichucounter/RepositoryInstrumentedTest.kt index 7620a3b..0373232 100644 --- a/app/src/androidTest/java/me/zobrist/tichucounter/RepositoryInstrumentedTest.kt +++ b/app/src/androidTest/java/me/zobrist/tichucounter/RepositoryInstrumentedTest.kt @@ -1,18 +1,13 @@ package me.zobrist.tichucounter import android.content.Context -import androidx.compose.runtime.collectAsState -import androidx.lifecycle.asLiveData import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withContext import me.zobrist.tichucounter.data.AppDatabase import me.zobrist.tichucounter.data.GameDao -import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.data.RoundDao import me.zobrist.tichucounter.repository.GameRepository import org.junit.After @@ -39,7 +34,8 @@ class RepositoryInstrumentedTest { fun createDb() { val context = ApplicationProvider.getApplicationContext() db = Room.inMemoryDatabaseBuilder( - context, AppDatabase::class.java).build() + context, AppDatabase::class.java + ).build() roundDao = db.roundDao() gameDao = db.gameDao() @@ -70,14 +66,14 @@ class RepositoryInstrumentedTest { repository.getActiveGameFlow().take(1).collect { } - repository.updateActiveTeamName(nameA ="aaa") + repository.updateActiveTeamName(nameA = "aaa") repository.getActiveGameFlow().take(1).collect { assertEquals("aaa", it.game.nameA) assertEquals("TeamB", it.game.nameB) } - repository.updateActiveTeamName(nameB ="bbb") + repository.updateActiveTeamName(nameB = "bbb") repository.getActiveGameFlow().take(1).collect { assertEquals("aaa", it.game.nameA) @@ -88,7 +84,6 @@ class RepositoryInstrumentedTest { } - @Test @Throws(Exception::class) fun newGame() = runTest { @@ -107,7 +102,7 @@ class RepositoryInstrumentedTest { assertEquals(6, it.count()) var uid: Long = 1 - it.forEach {game -> + it.forEach { game -> assertEquals(uid++, game.game.uid) assertEquals(0, game.rounds.count()) } @@ -157,12 +152,12 @@ class RepositoryInstrumentedTest { 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.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 -> @@ -187,12 +182,12 @@ class RepositoryInstrumentedTest { 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) + 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) @@ -225,12 +220,12 @@ class RepositoryInstrumentedTest { 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) + 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()) @@ -253,12 +248,12 @@ class RepositoryInstrumentedTest { 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) + 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 diff --git a/app/src/main/java/me/zobrist/tichucounter/data/DaoBase.kt b/app/src/main/java/me/zobrist/tichucounter/data/DaoBase.kt index 74cad1c..5f4a9c3 100644 --- a/app/src/main/java/me/zobrist/tichucounter/data/DaoBase.kt +++ b/app/src/main/java/me/zobrist/tichucounter/data/DaoBase.kt @@ -1,6 +1,9 @@ package me.zobrist.tichucounter.data -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update @Dao interface DaoBase { diff --git a/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt b/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt index 463f6ac..3b184c3 100644 --- a/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt +++ b/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt @@ -5,7 +5,6 @@ import androidx.room.Entity import androidx.room.Relation import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Round -import java.util.* @Entity data class GameWithScores( @@ -14,5 +13,5 @@ data class GameWithScores( parentColumn = "uid", entityColumn = "gameId" ) - val rounds: List = emptyList() + val rounds: List = emptyList() ) \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt b/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt index 68771fb..31b0240 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt @@ -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 diff --git a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt index 47082b6..78e5bb3 100644 --- a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt +++ b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt @@ -1,7 +1,13 @@ package me.zobrist.tichucounter.repository -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import me.zobrist.tichucounter.data.GameDao import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.data.RoundDao @@ -41,7 +47,7 @@ class GameRepository @Inject constructor( val newA = nameA ?: activeGame.nameA val newB = nameB ?: activeGame.nameB - if(newA == activeGame.nameA && newB == activeGame.nameB) { + if (newA == activeGame.nameA && newB == activeGame.nameB) { return } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt index 1113d76..a4cd96c 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt @@ -137,9 +137,6 @@ internal class PreviewViewModel : ICounterViewModel { override fun addSub100Clicked(toAdd: Int) { } - override fun deleteClicked() { - } - override fun updateNameA(value: String) { } @@ -161,4 +158,7 @@ internal class PreviewViewModel : ICounterViewModel { override fun showKeyboard() { } + override fun deleteState(pressed: Boolean) { + + } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index 5362fbf..6f58c27 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -7,6 +7,8 @@ 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.launch import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.domain.Tichu @@ -38,12 +40,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) } @@ -138,6 +140,10 @@ class CounterViewModel @Inject constructor( private var lastFocused = Focused.TEAM_A + private var deletePressed = false + + private var deleteJob: Job? = null + init { viewModelScope.launch { gameRepository.getActiveGameFlow().collect { @@ -204,8 +210,7 @@ class CounterViewModel @Inject constructor( focusLastInput() - if(activeValue.digitCount() >= 5) - { + if (activeValue.digitCount() >= 5) { // 5 digits is enough return } @@ -214,8 +219,7 @@ class CounterViewModel @Inject constructor( try { activeValue = newValue.toInt().toString() - } catch (_: NumberFormatException) - { + } catch (_: NumberFormatException) { } updateOtherScore() @@ -250,14 +254,6 @@ class CounterViewModel @Inject constructor( updateSubmitButton() } - override fun deleteClicked() { - if (activeValue != "") { - activeValue = activeValue.dropLast(1) - } - updateOtherScore() - updateSubmitButton() - } - override fun updateNameA(value: String) { viewModelScope.launch { gameRepository.updateActiveTeamName(nameA = value) @@ -297,4 +293,35 @@ 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) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt index 273cd8b..2716e99 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt @@ -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 @@ -37,11 +39,11 @@ fun KeyBoardView(viewModel: IKeyBoardViewModel) { { viewModel.updateFocusStateB(it) }, { viewModel.digitClicked(it) }, { viewModel.addSub100Clicked(it) }, - { viewModel.deleteClicked() }, { viewModel.negateClicked() }, { viewModel.submitClicked() }, { viewModel.hideKeyboard() }, - { viewModel.swapInputScores() } + { viewModel.swapInputScores() }, + { viewModel.deleteState(it) } ) } @@ -58,11 +60,11 @@ fun KeyboardView( 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)) { @@ -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 + ) {} } } @@ -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, @@ -319,11 +334,11 @@ fun KeyboardViewPreview() { updateFocusStateB = {}, digitClicked = {}, addSub100Clicked = {}, - deleteClicked = {}, negateClicked = {}, submitClicked = {}, hideKeyboardClicked = {}, - onSwapClicked = {}) + onSwapClicked = {}, + deleteButtonPressedState = {}) } } } \ No newline at end of file diff --git a/app/src/test/java/me/zobrist/tichucounter/StringExtensionTest.kt b/app/src/test/java/me/zobrist/tichucounter/StringExtensionTest.kt index ec3ac64..70a6650 100644 --- a/app/src/test/java/me/zobrist/tichucounter/StringExtensionTest.kt +++ b/app/src/test/java/me/zobrist/tichucounter/StringExtensionTest.kt @@ -1,7 +1,7 @@ package me.zobrist.tichucounter import me.zobrist.tichucounter.domain.digitCount -import org.junit.Assert.* +import org.junit.Assert.assertEquals import org.junit.Test /**