Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
cae95d0
feat(sensors): add search filter to sensor setting allow list dialog
danielgomezrico Mar 12, 2026
f0a1467
refactor(sensors): extract search field composable and hide for small…
danielgomezrico Mar 13, 2026
2309f97
Update dialog
danielgomezrico Mar 24, 2026
642abca
fix(sensors): sort imports in lexicographic order for ktlint
danielgomezrico Mar 29, 2026
e56435c
chore(UI): Use styles to present better data on the UI
danielgomezrico Mar 29, 2026
b6947f3
style(Sensors): Add braces to if/else in filterSettingEntries
danielgomezrico Mar 29, 2026
9eafe2c
style(Sensors): Convert short function signatures to single-line format
danielgomezrico Apr 2, 2026
dbc15c9
Cleanup
danielgomezrico May 2, 2026
5b1e1e9
refactor(Sensors): address review feedback on multi-select bottom sheet
danielgomezrico May 3, 2026
4a4b985
test(Sensors): rename filter test to match SensorDetailSettingSheet
danielgomezrico May 3, 2026
72af98e
test(Sensors): cover joinSelectedValues, label parsing, and dialog st…
danielgomezrico May 3, 2026
001d41a
test(common): cover HASearchField debounce behaviour
danielgomezrico May 3, 2026
4dbfce6
style(common): collapse debouncedSearchUpdate signature to single line
danielgomezrico May 4, 2026
33c8332
fix(Sensors): satisfy Compose stability lint and drop misplaced @Visi…
danielgomezrico May 4, 2026
93c7c0e
refactor(Sensors): tidy SensorDetailSettingSheet per PR feedback
danielgomezrico May 17, 2026
5e2dfdd
refactor(Sensors): wrap multi-select sheet in HATheme
danielgomezrico May 17, 2026
9868916
fix(Sensors): wrap SettingEntry list in @Immutable holder for Compose…
danielgomezrico May 18, 2026
7912947
fix(Sensors): top-align entry list in allow-list sheet
danielgomezrico May 19, 2026
a1352e1
refactor(Sensors): address review feedback on multi-select bottom sheet
danielgomezrico May 19, 2026
ac3db6c
refactor(common): expose HASearchField debounce constant for tests
danielgomezrico May 19, 2026
ffd0b52
refactor(common): extract bottom-sheet scroll/fling guard into shared…
danielgomezrico May 19, 2026
71cf4cb
refactor(common): drop composed{} from consumeSheetScrollFling
danielgomezrico May 24, 2026
739a352
refactor(Sensors): address review feedback on multi-select sheet
danielgomezrico May 29, 2026
beee2ae
test(common): add HASearchField Compose UI test
danielgomezrico May 29, 2026
2d36d40
test(Sensors): add multi-select bottom sheet screenshot tests
danielgomezrico May 29, 2026
5b27228
refactor(Sensors): fix ComposeUnstableCollections lint in setting sheet
danielgomezrico May 30, 2026
6a3cf6a
chore(lint): refresh ComposeUnstableCollections baseline for setting …
danielgomezrico May 30, 2026
c9e9160
feat(Sensors): show no-results placeholder in allow list sheet
danielgomezrico Jun 2, 2026
3c4300b
test(HASearchField): consolidate search field tests
danielgomezrico Jun 2, 2026
27806f4
refactor(Sensors): drop dead bracket-stripping in joinSelectedValues
danielgomezrico Jun 4, 2026
e67207d
refactor(common): trim compose composable docs and tighten HASearchField
danielgomezrico Jun 4, 2026
631611b
refactor(Sensors): render real bottom sheet in setting sheet screenshots
danielgomezrico Jun 4, 2026
34a348d
test(Sensors): reuse shared bottom sheet state in setting sheet scree…
danielgomezrico Jun 16, 2026
848be14
fix(EntityPicker): remove committed merge conflict marker
danielgomezrico Jul 9, 2026
468a0ba
docs(common): restore skipPartiallyExpanded param doc wording
danielgomezrico Jul 9, 2026
0d98dbd
refactor(Sensors): build allow list entries in view model off the mai…
danielgomezrico Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ import io.homeassistant.companion.android.database.settings.SensorUpdateFrequenc
import io.homeassistant.companion.android.database.settings.SettingsDao
import io.homeassistant.companion.android.sensors.LastAppSensorManager
import io.homeassistant.companion.android.sensors.SensorReceiver
import io.homeassistant.companion.android.settings.sensor.views.SettingEntry
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -47,8 +48,12 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import timber.log.Timber

/** Threshold above which the allow-list sheet search field becomes visible. */
private const val SEARCH_VISIBILITY_THRESHOLD = 10

@HiltViewModel
class SensorDetailViewModel @Inject constructor(
state: SavedStateHandle,
Expand Down Expand Up @@ -79,12 +84,15 @@ class SensorDetailViewModel @Inject constructor(
data class SettingDialogState(
val setting: SensorSetting,
/** Indicates if this is still loading entries in the background */
val loading: Boolean,
/** List of entity ID to entity pairs */
val entries: List<Pair<String, String>>,
val isLoading: Boolean,
/** List of selectable entries */
val entries: List<SettingEntry>,
/** List of selected entity ID */
val entriesSelected: List<String>,
)
) {
/** Whether a search field should be shown to help navigate the entry list. */
val showSearch: Boolean = entries.size > SEARCH_VISIBILITY_THRESHOLD
}
}

val sensorId: String = state["id"]!!
Expand Down Expand Up @@ -256,36 +264,32 @@ class SensorDetailViewModel @Inject constructor(
* Should trigger a dialog open in view.
*/
fun onSettingWithDialogPressed(setting: SensorSetting) = viewModelScope.launch {
val dialogLoadingJob = launch {
// In case getting entries takes too long, display a temporary loading dialog
delay(1000L)
sensorSettingsDialog = SettingDialogState(
setting = setting,
loading = true,
entries = listOf(),
entriesSelected = listOf(),
)
}

val listKeys = getSettingKeys(setting)
val listEntries = getSettingEntries(setting, null)
val state = SettingDialogState(
// Open the dialog right away in a loading state; enumerating entries (for example every
// installed application) can take seconds and must not block the main thread.
sensorSettingsDialog = SettingDialogState(
setting = setting,
loading = false,
entries = when {
isLoading = true,
entries = listOf(),
entriesSelected = listOf(),
)

sensorSettingsDialog = withContext(Dispatchers.Default) {
val listKeys = getSettingKeys(setting)
val listEntries = getSettingEntries(setting, null)
val entries = when {
setting.valueType == SensorSettingType.LIST ||
setting.valueType == SensorSettingType.LIST_APPS ||
setting.valueType == SensorSettingType.LIST_BLUETOOTH ||
setting.valueType == SensorSettingType.LIST_ZONES ->
listKeys.zip(listEntries)
listKeys.zip(listEntries).map { (id, label) -> SettingEntry(id = id, label = label) }

setting.valueType.listType ->
listEntries.map { it to it }
listEntries.map { SettingEntry(id = it, label = it) }

else ->
emptyList()
},
entriesSelected = when {
}
val entriesSelected = when {
setting.valueType == SensorSettingType.LIST ||
setting.valueType == SensorSettingType.LIST_APPS ||
setting.valueType == SensorSettingType.LIST_BLUETOOTH ||
Expand All @@ -297,10 +301,30 @@ class SensorDetailViewModel @Inject constructor(

else ->
emptyList()
},
)
dialogLoadingJob.cancel()
sensorSettingsDialog = state
}
SettingDialogState(
setting = setting,
isLoading = false,
entries = sortSelectedFirst(setting = setting, entries = entries, entriesSelected = entriesSelected),
entriesSelected = entriesSelected,
)
}
}

/**
* Moves the selected entries of a multi-select setting to the top of the list, preserving the
* existing (alphabetical) order within the selected and unselected groups. Single-select and
* non-list settings keep their original order.
*/
private fun sortSelectedFirst(
setting: SensorSetting,
entries: List<SettingEntry>,
entriesSelected: List<String>,
): List<SettingEntry> {
val isMultiSelect = setting.valueType.listType && setting.valueType != SensorSettingType.LIST
if (!isMultiSelect) return entries
val selected = entriesSelected.toSet()
return entries.sortedByDescending { it.id in selected }
}

fun cancelSettingWithDialog() {
Expand All @@ -317,7 +341,14 @@ class SensorDetailViewModel @Inject constructor(

fun submitSettingWithDialog(data: SettingDialogState?) {
if (data != null) {
setSetting(data.setting)
val setting = if (data.setting.valueType.listType && data.setting.valueType != SensorSettingType.LIST) {
// Multi-select settings keep their selection as a list in the state; serialize it
// here into the comma-separated format read back by [onSettingWithDialogPressed]
data.setting.copy(value = data.entriesSelected.joinToString())
} else {
data.setting
}
setSetting(setting)
}
sensorSettingsDialog = null
}
Expand Down
Loading