mirror of
https://github.com/home-assistant/android
synced 2024-07-22 10:54:12 +00:00
Implement a frontend-like single entity picker (#3653)
* Implement a frontend-like single entity picker - Adds a new Composable `SingleEntityPicker` which aims to provide a text input with autocomplete search for entities like the frontend, where you can pick one * Improve filtering to allow suggestions with space, searching for entity IDs - Trim search input so the space automatically added after an IME suggestion is ignored - Also search entity ID but sort those results second, friendly name keeps priority
This commit is contained in:
parent
dbae8d2613
commit
9d64260e1f
|
@ -38,6 +38,7 @@ import androidx.compose.ui.unit.sp
|
|||
import io.homeassistant.companion.android.common.R
|
||||
import io.homeassistant.companion.android.settings.qs.ManageTilesViewModel
|
||||
import io.homeassistant.companion.android.util.compose.ServerDropdownButton
|
||||
import io.homeassistant.companion.android.util.compose.SingleEntityPicker
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
|
@ -128,25 +129,16 @@ fun ManageTilesView(
|
|||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.tile_entity),
|
||||
fontSize = 15.sp
|
||||
SingleEntityPicker(
|
||||
entities = viewModel.sortedEntities,
|
||||
currentEntity = viewModel.selectedEntityId,
|
||||
onEntityCleared = { viewModel.selectEntityId("") },
|
||||
onEntitySelected = viewModel::selectEntityId,
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.fillMaxWidth(),
|
||||
label = { Text(stringResource(R.string.tile_entity)) }
|
||||
)
|
||||
Box {
|
||||
OutlinedButton(onClick = { expandedEntity = true }) {
|
||||
Text(text = viewModel.selectedEntityId)
|
||||
}
|
||||
DropdownMenu(expanded = expandedEntity, onDismissRequest = { expandedEntity = false }) {
|
||||
for (item in viewModel.sortedEntities) {
|
||||
DropdownMenuItem(onClick = {
|
||||
viewModel.selectEntityId(item.entityId)
|
||||
expandedEntity = false
|
||||
}) {
|
||||
Text(text = item.entityId, fontSize = 15.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
package io.homeassistant.companion.android.util.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ExposedDropdownMenuBox
|
||||
import androidx.compose.material.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.friendlyName
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun SingleEntityPicker(
|
||||
entities: List<Entity<*>>,
|
||||
currentEntity: String?,
|
||||
onEntityCleared: () -> Unit,
|
||||
onEntitySelected: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
label: @Composable (() -> Unit) = { Text(stringResource(commonR.string.select_entity_to_display)) }
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var inputValue by remember(currentEntity, entities.size) {
|
||||
mutableStateOf(
|
||||
if (currentEntity == null) {
|
||||
""
|
||||
} else {
|
||||
entities.firstOrNull { it.entityId == currentEntity }?.friendlyName ?: currentEntity
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var list by remember { mutableStateOf(entities) }
|
||||
var listTooLarge by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(entities.size, inputValue) {
|
||||
list = withContext(Dispatchers.IO) {
|
||||
val query = inputValue.trim()
|
||||
val items = if (inputValue.isBlank() || inputValue == (entities.firstOrNull { it.entityId == currentEntity }?.friendlyName ?: currentEntity)) {
|
||||
entities
|
||||
} else {
|
||||
entities.filter {
|
||||
it.friendlyName.contains(query, ignoreCase = true) ||
|
||||
it.entityId.contains(query.replace(" ", "_"), ignoreCase = true)
|
||||
}
|
||||
}
|
||||
// The amount of items is limited because Compose ExposedDropdownMenu isn't lazy
|
||||
listTooLarge = items.size > 150
|
||||
items.sortedWith(
|
||||
compareBy(
|
||||
{ !it.friendlyName.startsWith(query, ignoreCase = true) },
|
||||
{ !it.entityId.split(".")[1].startsWith(query.replace(" ", "_"), ignoreCase = true) },
|
||||
{ it.friendlyName.lowercase() }
|
||||
)
|
||||
).take(150)
|
||||
}
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded },
|
||||
modifier = modifier
|
||||
) {
|
||||
TextField(
|
||||
value = inputValue,
|
||||
onValueChange = {
|
||||
expanded = true
|
||||
inputValue = it
|
||||
},
|
||||
label = label,
|
||||
trailingIcon =
|
||||
{
|
||||
Row {
|
||||
if (currentEntity?.isNotBlank() == true) {
|
||||
IconButton(onClick = onEntityCleared, modifier = Modifier.clearAndSetSemantics { }) {
|
||||
Icon(
|
||||
Icons.Filled.Clear,
|
||||
stringResource(commonR.string.search_clear_selection)
|
||||
)
|
||||
}
|
||||
}
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
||||
}
|
||||
},
|
||||
colors = ExposedDropdownMenuDefaults.textFieldColors(),
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
if (list.isNotEmpty()) {
|
||||
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
list.forEach {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onEntitySelected(it.entityId)
|
||||
inputValue = it.friendlyName
|
||||
focusManager.clearFocus()
|
||||
expanded = false
|
||||
},
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = it.friendlyName,
|
||||
style = MaterialTheme.typography.body1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
|
||||
Text(
|
||||
text = it.entityId,
|
||||
style = MaterialTheme.typography.body2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (listTooLarge) {
|
||||
DropdownMenuItem(onClick = { /* No-op */ }, enabled = false) {
|
||||
Text(
|
||||
text = stringResource(commonR.string.search_refine_for_more),
|
||||
style = MaterialTheme.typography.body2,
|
||||
fontStyle = FontStyle.Italic
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -488,9 +488,11 @@
|
|||
<string name="scene">Scene</string>
|
||||
<string name="scenes">Scenes</string>
|
||||
<string name="scripts">Scripts</string>
|
||||
<string name="search_clear_selection">Clear selection</string>
|
||||
<string name="search_icons">Search icons</string>
|
||||
<string name="search_icons_in_english">Search icons (in English)</string>
|
||||
<string name="search_notifications">Search notifications</string>
|
||||
<string name="search_refine_for_more">Refine your search to view more…</string>
|
||||
<string name="search_results">Search Results</string>
|
||||
<string name="search_sensors">Search sensors</string>
|
||||
<string name="security_vulnerably_message">Your version of Home Assistant does not protect for recently discovered security vulnerabilities in custom components. Please view the details via the bulletin and update as soon as possible.</string>
|
||||
|
|
Loading…
Reference in a new issue