From 5a6a1cb44a332b92de74692bbd6b55daf770b5ba Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Mon, 21 Jul 2025 22:45:03 +1000 Subject: [PATCH] Feat/2412 ignored nodes (#2470) Signed-off-by: DaneEvans Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/geeksville/mesh/model/UIState.kt | 17 ++- .../com/geeksville/mesh/ui/node/NodeScreen.kt | 5 + .../ui/node/components/NodeFilterTextField.kt | 139 +++++++++++++----- app/src/main/res/values/strings.xml | 3 +- 4 files changed, 124 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 9999edaaa..5381c4f2b 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -160,6 +160,7 @@ data class NodesUiState( val distanceUnits: Int = 0, val tempInFahrenheit: Boolean = false, val showDetails: Boolean = false, + val showIgnored: Boolean = false, ) { companion object { val Empty = NodesUiState() @@ -310,6 +311,12 @@ class UIViewModel @Inject constructor( private val showPrecisionCircleOnMap = MutableStateFlow(preferences.getBoolean("show-precision-circle-on-map", true)) + private val showIgnored = MutableStateFlow(preferences.getBoolean("show-ignored", false)) + fun toggleShowIgnored() { + showIgnored.value = !showIgnored.value + preferences.edit { putBoolean("show-ignored", showIgnored.value) } + } + fun setSortOption(sort: NodeSortOption) { nodeSortOption.value = sort preferences.edit { putInt("node-sort-option", sort.ordinal) } @@ -355,6 +362,7 @@ class UIViewModel @Inject constructor( val includeUnknown: Boolean, val onlyOnline: Boolean, val onlyDirect: Boolean, + val showIgnored: Boolean, ) val nodeFilterStateFlow: Flow = combine( @@ -362,8 +370,9 @@ class UIViewModel @Inject constructor( includeUnknown, onlyOnline, onlyDirect, - ) { filterText, includeUnknown, onlyOnline, onlyDirect -> - NodeFilterState(filterText, includeUnknown, onlyOnline, onlyDirect) + showIgnored, + ) { filterText, includeUnknown, onlyOnline, onlyDirect, showIgnored -> + NodeFilterState(filterText, includeUnknown, onlyOnline, onlyDirect, showIgnored) } val nodesUiState: StateFlow = combine( @@ -382,6 +391,7 @@ class UIViewModel @Inject constructor( distanceUnits = profile.config.display.units.number, tempInFahrenheit = profile.moduleConfig.telemetry.environmentDisplayFahrenheit, showDetails = showDetails, + showIgnored = filterFlow.showIgnored, ) }.stateIn( scope = viewModelScope, @@ -397,6 +407,9 @@ class UIViewModel @Inject constructor( val nodeList: StateFlow> = nodesUiState.flatMapLatest { state -> nodeDB.getNodes(state.sort, state.filter, state.includeUnknown, state.onlyOnline, state.onlyDirect) + .map { list -> + list.filter { it.isIgnored == state.showIgnored } + } }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt index facd543e5..a10c097ad 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt @@ -65,6 +65,8 @@ fun NodeScreen( val nodes by model.nodeList.collectAsStateWithLifecycle() val ourNode by model.ourNodeInfo.collectAsStateWithLifecycle() + val unfilteredNodes by model.unfilteredNodeList.collectAsStateWithLifecycle() + val ignoredNodeCount = unfilteredNodes.count { it.isIgnored } val listState = rememberLazyListState() @@ -114,6 +116,9 @@ fun NodeScreen( onToggleOnlyDirect = model::toggleOnlyDirect, showDetails = state.showDetails, onToggleShowDetails = model::toggleShowDetails, + showIgnored = state.showIgnored, + onToggleShowIgnored = model::toggleShowIgnored, + ignoredNodeCount = ignoredNodeCount, ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeFilterTextField.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeFilterTextField.kt index 76e9673fe..d0f987aa3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeFilterTextField.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeFilterTextField.kt @@ -21,8 +21,10 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions @@ -75,29 +77,54 @@ fun NodeFilterTextField( onToggleOnlyDirect: () -> Unit, showDetails: Boolean, onToggleShowDetails: () -> Unit, + showIgnored: Boolean, + onToggleShowIgnored: () -> Unit, + ignoredNodeCount: Int, ) { - Row( - modifier = modifier.background(MaterialTheme.colorScheme.background), - ) { - NodeFilterTextField( - filterText = filterText, - onTextChange = onTextChange, - modifier = Modifier.weight(1f) - ) + Column(modifier = modifier.background(MaterialTheme.colorScheme.background)) { + Row { + NodeFilterTextField( + filterText = filterText, + onTextChange = onTextChange, + modifier = Modifier.weight(1f) + ) - NodeSortButton( - modifier = Modifier.align(Alignment.CenterVertically), - currentSortOption = currentSortOption, - onSortSelect = onSortSelect, - includeUnknown = includeUnknown, - onToggleIncludeUnknown = onToggleIncludeUnknown, - onlyOnline = onlyOnline, - onToggleOnlyOnline = onToggleOnlyOnline, - onlyDirect = onlyDirect, - onToggleOnlyDirect = onToggleOnlyDirect, - showDetails = showDetails, - onToggleShowDetails = onToggleShowDetails, - ) + NodeSortButton( + modifier = Modifier.align(Alignment.CenterVertically), + currentSortOption = currentSortOption, + onSortSelect = onSortSelect, + toggles = NodeFilterToggles( + includeUnknown = includeUnknown, + onToggleIncludeUnknown = onToggleIncludeUnknown, + onlyOnline = onlyOnline, + onToggleOnlyOnline = onToggleOnlyOnline, + onlyDirect = onlyDirect, + onToggleOnlyDirect = onToggleOnlyDirect, + showDetails = showDetails, + onToggleShowDetails = onToggleShowDetails, + showIgnored = showIgnored, + onToggleShowIgnored = onToggleShowIgnored, + ignoredNodeCount = ignoredNodeCount, + ), + ) + } + if (showIgnored) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceDim) + .clickable { onToggleShowIgnored() } + .padding(vertical = 16.dp, horizontal = 24.dp) + ) { + Text( + text = stringResource(id = R.string.node_filter_ignored), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth(), + textAlign = androidx.compose.ui.text.style.TextAlign.Center, + ) + } + } } } @@ -159,14 +186,7 @@ private fun NodeFilterTextField( private fun NodeSortButton( currentSortOption: NodeSortOption, onSortSelect: (NodeSortOption) -> Unit, - includeUnknown: Boolean, - onToggleIncludeUnknown: () -> Unit, - onlyOnline: Boolean, - onToggleOnlyOnline: () -> Unit, - onlyDirect: Boolean, - onToggleOnlyDirect: () -> Unit, - showDetails: Boolean, - onToggleShowDetails: () -> Unit, + toggles: NodeFilterToggles, modifier: Modifier = Modifier, ) = Box(modifier) { var expanded by remember { mutableStateOf(false) } @@ -202,12 +222,12 @@ private fun NodeSortButton( HorizontalDivider() DropdownMenuItem( onClick = { - onToggleIncludeUnknown() + toggles.onToggleIncludeUnknown() expanded = false }, text = { Row { - AnimatedVisibility(visible = includeUnknown) { + AnimatedVisibility(visible = toggles.includeUnknown) { Icon( imageVector = Icons.Default.Done, contentDescription = null, @@ -222,12 +242,12 @@ private fun NodeSortButton( ) DropdownMenuItem( onClick = { - onToggleOnlyOnline() + toggles.onToggleOnlyOnline() expanded = false }, text = { Row { - AnimatedVisibility(visible = onlyOnline) { + AnimatedVisibility(visible = toggles.onlyOnline) { Icon( imageVector = Icons.Default.Done, contentDescription = null, @@ -242,12 +262,12 @@ private fun NodeSortButton( ) DropdownMenuItem( onClick = { - onToggleOnlyDirect() + toggles.onToggleOnlyDirect() expanded = false }, text = { Row { - AnimatedVisibility(visible = onlyDirect) { + AnimatedVisibility(visible = toggles.onlyDirect) { Icon( imageVector = Icons.Default.Done, contentDescription = null, @@ -263,12 +283,12 @@ private fun NodeSortButton( HorizontalDivider() DropdownMenuItem( onClick = { - onToggleShowDetails() + toggles.onToggleShowDetails() expanded = false }, text = { Row { - AnimatedVisibility(visible = showDetails) { + AnimatedVisibility(visible = toggles.showDetails) { Icon( imageVector = Icons.Default.Done, contentDescription = null, @@ -281,6 +301,34 @@ private fun NodeSortButton( } } ) + HorizontalDivider() + DropdownMenuItem( + onClick = { + toggles.onToggleShowIgnored() + expanded = false + }, + text = { + Row { + AnimatedVisibility(visible = toggles.showIgnored) { + Icon( + imageVector = Icons.Default.Done, + contentDescription = null, + modifier = Modifier.padding(end = 4.dp), + ) + } + Text( + text = stringResource(id = R.string.node_filter_show_ignored), + ) + if (toggles.ignoredNodeCount > 0) { + Text( + text = " (${toggles.ignoredNodeCount})", + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 4.dp), + ) + } + } + } + ) } } @@ -302,6 +350,23 @@ private fun NodeFilterTextFieldPreview() { onToggleOnlyDirect = {}, showDetails = false, onToggleShowDetails = {}, + showIgnored = false, + onToggleShowIgnored = {}, + ignoredNodeCount = 0, ) } } + +data class NodeFilterToggles( + val includeUnknown: Boolean, + val onToggleIncludeUnknown: () -> Unit, + val onlyOnline: Boolean, + val onToggleOnlyOnline: () -> Unit, + val onlyDirect: Boolean, + val onToggleOnlyDirect: () -> Unit, + val showDetails: Boolean, + val onToggleShowDetails: () -> Unit, + val showIgnored: Boolean, + val onToggleShowIgnored: () -> Unit, + val ignoredNodeCount: Int, +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4bc1ba9b..bf93013ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,6 +43,7 @@ Include unknown Hide offline nodes Only show direct nodes + You are viewing ignored nodes,\nPress to return to the node list. Show details Node sorting options A-Z @@ -53,7 +54,7 @@ via MQTT via MQTT via Favorite - + Ignored Nodes Unrecognized Waiting to be acknowledged Queued for sending