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
This commit was merged in pull request #38.
This commit is contained in:
2023-05-16 12:05:49 +02:00
7 changed files with 173 additions and 16 deletions

View File

@@ -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)
}

View File

@@ -35,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>>
}

View File

@@ -2,10 +2,7 @@ package me.zobrist.tichucounter.repository
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.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.zobrist.tichucounter.data.GameDao
@@ -23,6 +20,8 @@ class GameRepository @Inject constructor(
private var activeGame: Game = Game(true, "TeamA", "TeamB", Date(), Date())
private var distinctTeamNames: List<String> = listOf()
init {
CoroutineScope(Dispatchers.IO).launch {
gameDao.getActiveAsFlow().collect {
@@ -130,4 +129,8 @@ class GameRepository @Inject constructor(
fun getAllWithRoundFlow(): Flow<List<GameWithScores>> {
return gameDao.getGamesWithRounds()
}
fun getDistinctTeamNames(): Flow<List<String>> {
return gameDao.getDistinctTeamNames()
}
}

View File

@@ -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) },
)
}
}
}
}

View File

@@ -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) }
)
@@ -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() {
}

View File

@@ -1,6 +1,7 @@
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
@@ -9,6 +10,8 @@ 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
@@ -55,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)
@@ -105,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) {
@@ -144,6 +155,8 @@ class CounterViewModel @Inject constructor(
private var deleteJob: Job? = null
private var distinctTeamNames = listOf<String>()
init {
viewModelScope.launch {
gameRepository.getActiveGameFlow().collect {
@@ -157,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() {
@@ -255,12 +279,14 @@ class CounterViewModel @Inject constructor(
}
override fun updateNameA(value: String) {
teamNameA = value
viewModelScope.launch {
gameRepository.updateActiveTeamName(nameA = value)
}
}
override fun updateNameB(value: String) {
teamNameB = value
viewModelScope.launch {
gameRepository.updateActiveTeamName(nameB = value)
}
@@ -324,4 +350,20 @@ class CounterViewModel @Inject constructor(
}
}
}
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)
}
}

View File

@@ -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"), {}, {})
}
}