diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index b964c6c..233267d 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -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) } diff --git a/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt b/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt index 634b3cb..1f8cd32 100644 --- a/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt +++ b/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt @@ -35,4 +35,7 @@ interface GameDao : DaoBase { @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> + } \ No newline at end of file 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 78e5bb3..78e1fd3 100644 --- a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt +++ b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt @@ -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 = listOf() + init { CoroutineScope(Dispatchers.IO).launch { gameDao.getActiveAsFlow().collect { @@ -130,4 +129,8 @@ class GameRepository @Inject constructor( fun getAllWithRoundFlow(): Flow> { return gameDao.getGamesWithRounds() } + + fun getDistinctTeamNames(): Flow> { + return gameDao.getDistinctTeamNames() + } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/composables/TypeaheadTextField.kt b/app/src/main/java/me/zobrist/tichucounter/ui/composables/TypeaheadTextField.kt new file mode 100644 index 0000000..35e9902 --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/ui/composables/TypeaheadTextField.kt @@ -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, + 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) }, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt index a9b0497..d838b89 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 @@ -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 = listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long") + override val teamNameSuggestionsB: List = listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long") override fun focusLastInput() { } 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 81bed74..249c9b2 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 @@ -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 + val teamNameSuggestionsB: List 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()) + private set + + override var teamNameSuggestionsB by mutableStateOf(listOf()) + 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() + 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, currentInput: String ): List { + 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) + } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamNamesView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamNamesView.kt index 596c861..c8eae5b 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamNamesView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamNamesView.kt @@ -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, + nameSuggestionsB: List, 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"), {}, {}) } }