mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-27 18:21:58 -04:00
@@ -39,6 +39,7 @@ internal const val KEY_THEME = "theme"
|
||||
// Node list filters/sort
|
||||
internal const val KEY_NODE_SORT = "node-sort-option"
|
||||
internal const val KEY_INCLUDE_UNKNOWN = "include-unknown"
|
||||
internal const val KEY_EXCLUDE_INFRASTRUCTURE = "exclude-infrastructure"
|
||||
internal const val KEY_ONLY_ONLINE = "only-online"
|
||||
internal const val KEY_ONLY_DIRECT = "only-direct"
|
||||
internal const val KEY_SHOW_IGNORED = "show-ignored"
|
||||
@@ -57,6 +58,8 @@ class UiPreferencesDataSource @Inject constructor(private val dataStore: DataSto
|
||||
|
||||
val nodeSort: StateFlow<Int> = dataStore.prefStateFlow(key = NODE_SORT, default = -1)
|
||||
val includeUnknown: StateFlow<Boolean> = dataStore.prefStateFlow(key = INCLUDE_UNKNOWN, default = false)
|
||||
val excludeInfrastructure: StateFlow<Boolean> =
|
||||
dataStore.prefStateFlow(key = EXCLUDE_INFRASTRUCTURE, default = false)
|
||||
val onlyOnline: StateFlow<Boolean> = dataStore.prefStateFlow(key = ONLY_ONLINE, default = false)
|
||||
val onlyDirect: StateFlow<Boolean> = dataStore.prefStateFlow(key = ONLY_DIRECT, default = false)
|
||||
val showIgnored: StateFlow<Boolean> = dataStore.prefStateFlow(key = SHOW_IGNORED, default = false)
|
||||
@@ -77,6 +80,10 @@ class UiPreferencesDataSource @Inject constructor(private val dataStore: DataSto
|
||||
dataStore.setPref(key = INCLUDE_UNKNOWN, value = value)
|
||||
}
|
||||
|
||||
fun setExcludeInfrastructure(value: Boolean) {
|
||||
dataStore.setPref(key = EXCLUDE_INFRASTRUCTURE, value = value)
|
||||
}
|
||||
|
||||
fun setOnlyOnline(value: Boolean) {
|
||||
dataStore.setPref(key = ONLY_ONLINE, value = value)
|
||||
}
|
||||
@@ -104,6 +111,7 @@ class UiPreferencesDataSource @Inject constructor(private val dataStore: DataSto
|
||||
val THEME = intPreferencesKey(KEY_THEME)
|
||||
val NODE_SORT = intPreferencesKey(KEY_NODE_SORT)
|
||||
val INCLUDE_UNKNOWN = booleanPreferencesKey(KEY_INCLUDE_UNKNOWN)
|
||||
val EXCLUDE_INFRASTRUCTURE = booleanPreferencesKey(KEY_EXCLUDE_INFRASTRUCTURE)
|
||||
val ONLY_ONLINE = booleanPreferencesKey(KEY_ONLY_ONLINE)
|
||||
val ONLY_DIRECT = booleanPreferencesKey(KEY_ONLY_DIRECT)
|
||||
val SHOW_IGNORED = booleanPreferencesKey(KEY_SHOW_IGNORED)
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<string name="desc_node_filter_clear">clear node filter</string>
|
||||
<string name="node_filter_title">Filter by</string>
|
||||
<string name="node_filter_include_unknown">Include unknown</string>
|
||||
<string name="node_filter_exclude_infrastructure">Exclude infrastructure</string>
|
||||
<string name="node_filter_only_online">Hide offline nodes</string>
|
||||
<string name="node_filter_only_direct">Only show direct nodes</string>
|
||||
<string name="node_filter_ignored">You are viewing ignored nodes,\nPress to return to the node list.</string>
|
||||
@@ -46,7 +47,7 @@
|
||||
<string name="node_sort_via_mqtt">via MQTT</string>
|
||||
<string name="via_mqtt">via MQTT</string>
|
||||
<string name="node_sort_via_favorite">via Favorite</string>
|
||||
<string name="node_filter_show_ignored">Ignored Nodes</string>
|
||||
<string name="node_filter_show_ignored">Only show ignored Nodes</string>
|
||||
<string name="unrecognized">Unrecognized</string>
|
||||
<string name="message_status_enroute">Waiting to be acknowledged</string>
|
||||
<string name="message_status_queued">Queued for sending</string>
|
||||
|
||||
@@ -64,6 +64,7 @@ import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.desc_node_filter_clear
|
||||
import org.meshtastic.core.strings.node_filter_exclude_infrastructure
|
||||
import org.meshtastic.core.strings.node_filter_ignored
|
||||
import org.meshtastic.core.strings.node_filter_include_unknown
|
||||
import org.meshtastic.core.strings.node_filter_only_direct
|
||||
@@ -85,6 +86,8 @@ fun NodeFilterTextField(
|
||||
onSortSelect: (NodeSortOption) -> Unit,
|
||||
includeUnknown: Boolean,
|
||||
onToggleIncludeUnknown: () -> Unit,
|
||||
excludeInfrastructure: Boolean,
|
||||
onToggleExcludeInfrastructure: () -> Unit,
|
||||
onlyOnline: Boolean,
|
||||
onToggleOnlyOnline: () -> Unit,
|
||||
onlyDirect: Boolean,
|
||||
@@ -105,6 +108,8 @@ fun NodeFilterTextField(
|
||||
NodeFilterToggles(
|
||||
includeUnknown = includeUnknown,
|
||||
onToggleIncludeUnknown = onToggleIncludeUnknown,
|
||||
excludeInfrastructure = excludeInfrastructure,
|
||||
onToggleExcludeInfrastructure = onToggleExcludeInfrastructure,
|
||||
onlyOnline = onlyOnline,
|
||||
onToggleOnlyOnline = onToggleOnlyOnline,
|
||||
onlyDirect = onlyDirect,
|
||||
@@ -212,6 +217,12 @@ private fun NodeSortButton(
|
||||
|
||||
DropdownMenuTitle(text = stringResource(Res.string.node_filter_title))
|
||||
|
||||
DropdownMenuCheck(
|
||||
text = stringResource(Res.string.node_filter_exclude_infrastructure),
|
||||
checked = toggles.excludeInfrastructure,
|
||||
onClick = toggles.onToggleExcludeInfrastructure,
|
||||
)
|
||||
|
||||
DropdownMenuCheck(
|
||||
text = stringResource(Res.string.node_filter_include_unknown),
|
||||
checked = toggles.includeUnknown,
|
||||
@@ -298,6 +309,8 @@ private fun NodeFilterTextFieldPreview() {
|
||||
onSortSelect = {},
|
||||
includeUnknown = false,
|
||||
onToggleIncludeUnknown = {},
|
||||
excludeInfrastructure = false,
|
||||
onToggleExcludeInfrastructure = {},
|
||||
onlyOnline = false,
|
||||
onToggleOnlyOnline = {},
|
||||
onlyDirect = false,
|
||||
@@ -312,6 +325,8 @@ private fun NodeFilterTextFieldPreview() {
|
||||
data class NodeFilterToggles(
|
||||
val includeUnknown: Boolean,
|
||||
val onToggleIncludeUnknown: () -> Unit,
|
||||
val excludeInfrastructure: Boolean,
|
||||
val onToggleExcludeInfrastructure: () -> Unit,
|
||||
val onlyOnline: Boolean,
|
||||
val onToggleOnlyOnline: () -> Unit,
|
||||
val onlyDirect: Boolean,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.node.list
|
||||
|
||||
import android.os.RemoteException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class NodeActions
|
||||
@Inject
|
||||
constructor(
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
) {
|
||||
suspend fun favoriteNode(node: Node) {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Favorite node error")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun ignoreNode(node: Node) {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Ignore node error")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeNode(nodeNum: Int) = withContext(Dispatchers.IO) {
|
||||
Timber.i("Removing node '$nodeNum'")
|
||||
try {
|
||||
val packetId = serviceRepository.meshService?.packetId ?: return@withContext
|
||||
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
|
||||
nodeRepository.deleteNode(nodeNum)
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e("Remove node error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.node.list
|
||||
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import javax.inject.Inject
|
||||
|
||||
class NodeFilterPreferences @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) {
|
||||
val includeUnknown = uiPreferencesDataSource.includeUnknown
|
||||
val excludeInfrastructure = uiPreferencesDataSource.excludeInfrastructure
|
||||
val onlyOnline = uiPreferencesDataSource.onlyOnline
|
||||
val onlyDirect = uiPreferencesDataSource.onlyDirect
|
||||
val showIgnored = uiPreferencesDataSource.showIgnored
|
||||
|
||||
fun toggleIncludeUnknown() {
|
||||
uiPreferencesDataSource.setIncludeUnknown(!includeUnknown.value)
|
||||
}
|
||||
|
||||
fun toggleExcludeInfrastructure() {
|
||||
uiPreferencesDataSource.setExcludeInfrastructure(!excludeInfrastructure.value)
|
||||
}
|
||||
|
||||
fun toggleOnlyOnline() {
|
||||
uiPreferencesDataSource.setOnlyOnline(!onlyOnline.value)
|
||||
}
|
||||
|
||||
fun toggleOnlyDirect() {
|
||||
uiPreferencesDataSource.setOnlyDirect(!onlyDirect.value)
|
||||
}
|
||||
|
||||
fun toggleShowIgnored() {
|
||||
uiPreferencesDataSource.setShowIgnored(!showIgnored.value)
|
||||
}
|
||||
}
|
||||
@@ -159,17 +159,21 @@ fun NodeListScreen(
|
||||
.background(MaterialTheme.colorScheme.surfaceDim)
|
||||
.padding(8.dp),
|
||||
filterText = state.filter.filterText,
|
||||
onTextChange = viewModel::setNodeFilterText,
|
||||
onTextChange = { viewModel.nodeFilterText = it },
|
||||
currentSortOption = state.sort,
|
||||
onSortSelect = viewModel::setSortOption,
|
||||
includeUnknown = state.filter.includeUnknown,
|
||||
onToggleIncludeUnknown = viewModel::toggleIncludeUnknown,
|
||||
onToggleIncludeUnknown = { viewModel.nodeFilterPreferences.toggleIncludeUnknown() },
|
||||
excludeInfrastructure = state.filter.excludeInfrastructure,
|
||||
onToggleExcludeInfrastructure = {
|
||||
viewModel.nodeFilterPreferences.toggleExcludeInfrastructure()
|
||||
},
|
||||
onlyOnline = state.filter.onlyOnline,
|
||||
onToggleOnlyOnline = viewModel::toggleOnlyOnline,
|
||||
onToggleOnlyOnline = { viewModel.nodeFilterPreferences.toggleOnlyOnline() },
|
||||
onlyDirect = state.filter.onlyDirect,
|
||||
onToggleOnlyDirect = viewModel::toggleOnlyDirect,
|
||||
onToggleOnlyDirect = { viewModel.nodeFilterPreferences.toggleOnlyDirect() },
|
||||
showIgnored = state.filter.showIgnored,
|
||||
onToggleShowIgnored = viewModel::toggleShowIgnored,
|
||||
onToggleShowIgnored = { viewModel.nodeFilterPreferences.toggleShowIgnored() },
|
||||
ignoredNodeCount = ignoredNodeCount,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
|
||||
package org.meshtastic.feature.node.list
|
||||
|
||||
import android.os.RemoteException
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -35,11 +33,11 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import timber.log.Timber
|
||||
import org.meshtastic.proto.ConfigProtos
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -50,6 +48,8 @@ constructor(
|
||||
radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val uiPreferencesDataSource: UiPreferencesDataSource,
|
||||
val nodeActions: NodeActions,
|
||||
val nodeFilterPreferences: NodeFilterPreferences,
|
||||
) : ViewModel() {
|
||||
|
||||
val ourNodeInfo: StateFlow<Node?> = nodeRepository.ourNodeInfo
|
||||
@@ -66,23 +66,24 @@ constructor(
|
||||
private val nodeSortOption =
|
||||
uiPreferencesDataSource.nodeSort.map { NodeSortOption.entries.getOrElse(it) { NodeSortOption.VIA_FAVORITE } }
|
||||
|
||||
private val nodeFilterText = MutableStateFlow("")
|
||||
private val includeUnknown = uiPreferencesDataSource.includeUnknown
|
||||
private val onlyOnline = uiPreferencesDataSource.onlyOnline
|
||||
private val onlyDirect = uiPreferencesDataSource.onlyDirect
|
||||
private val showIgnored = uiPreferencesDataSource.showIgnored
|
||||
private val _nodeFilterText = MutableStateFlow("")
|
||||
private val includeUnknown = nodeFilterPreferences.includeUnknown
|
||||
private val excludeInfrastructure = nodeFilterPreferences.excludeInfrastructure
|
||||
private val onlyOnline = nodeFilterPreferences.onlyOnline
|
||||
private val onlyDirect = nodeFilterPreferences.onlyDirect
|
||||
private val showIgnored = nodeFilterPreferences.showIgnored
|
||||
|
||||
private val nodeFilter: Flow<NodeFilterState> =
|
||||
combine(nodeFilterText, includeUnknown, onlyOnline, onlyDirect, showIgnored) {
|
||||
filterText,
|
||||
includeUnknown,
|
||||
onlyOnline,
|
||||
onlyDirect,
|
||||
showIgnored,
|
||||
->
|
||||
NodeFilterState(filterText, includeUnknown, onlyOnline, onlyDirect, showIgnored)
|
||||
combine(_nodeFilterText, includeUnknown, excludeInfrastructure, onlyOnline, onlyDirect, showIgnored) { values ->
|
||||
NodeFilterState(
|
||||
filterText = values[0] as String,
|
||||
includeUnknown = values[1] as Boolean,
|
||||
excludeInfrastructure = values[2] as Boolean,
|
||||
onlyOnline = values[3] as Boolean,
|
||||
onlyDirect = values[4] as Boolean,
|
||||
showIgnored = values[5] as Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
val nodesUiState: StateFlow<NodesUiState> =
|
||||
combine(nodeSortOption, nodeFilter, radioConfigRepository.deviceProfileFlow) { sort, nodeFilter, profile ->
|
||||
NodesUiState(
|
||||
@@ -105,32 +106,36 @@ constructor(
|
||||
onlyOnline = filter.onlyOnline,
|
||||
onlyDirect = filter.onlyDirect,
|
||||
)
|
||||
.map { list -> list.filter { it.isIgnored == filter.showIgnored } }
|
||||
.map { list ->
|
||||
list
|
||||
.filter { it.isIgnored == filter.showIgnored }
|
||||
.filter { node ->
|
||||
if (filter.excludeInfrastructure) {
|
||||
val role = node.user.role
|
||||
val infrastructureRoles =
|
||||
listOf(
|
||||
ConfigProtos.Config.DeviceConfig.Role.ROUTER,
|
||||
ConfigProtos.Config.DeviceConfig.Role.REPEATER,
|
||||
ConfigProtos.Config.DeviceConfig.Role.ROUTER_LATE,
|
||||
ConfigProtos.Config.DeviceConfig.Role.CLIENT_BASE,
|
||||
)
|
||||
role !in infrastructureRoles && !node.isEffectivelyUnmessageable
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
val unfilteredNodeList: StateFlow<List<Node>> =
|
||||
nodeRepository.getNodes().stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
fun setNodeFilterText(text: String) {
|
||||
nodeFilterText.value = text
|
||||
}
|
||||
|
||||
fun toggleIncludeUnknown() {
|
||||
uiPreferencesDataSource.setIncludeUnknown(!includeUnknown.value)
|
||||
}
|
||||
|
||||
fun toggleOnlyOnline() {
|
||||
uiPreferencesDataSource.setOnlyOnline(!onlyOnline.value)
|
||||
}
|
||||
|
||||
fun toggleOnlyDirect() {
|
||||
uiPreferencesDataSource.setOnlyDirect(!onlyDirect.value)
|
||||
}
|
||||
|
||||
fun toggleShowIgnored() {
|
||||
uiPreferencesDataSource.setShowIgnored(!showIgnored.value)
|
||||
}
|
||||
var nodeFilterText: String
|
||||
get() = _nodeFilterText.value
|
||||
set(value) {
|
||||
_nodeFilterText.value = value
|
||||
}
|
||||
|
||||
fun setSortOption(sort: NodeSortOption) {
|
||||
uiPreferencesDataSource.setNodeSort(sort.ordinal)
|
||||
@@ -140,32 +145,11 @@ constructor(
|
||||
_sharedContactRequested.value = sharedContact
|
||||
}
|
||||
|
||||
fun favoriteNode(node: Node) = viewModelScope.launch {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Favorite node error")
|
||||
}
|
||||
}
|
||||
fun favoriteNode(node: Node) = viewModelScope.launch { nodeActions.favoriteNode(node) }
|
||||
|
||||
fun ignoreNode(node: Node) = viewModelScope.launch {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Ignore node error")
|
||||
}
|
||||
}
|
||||
fun ignoreNode(node: Node) = viewModelScope.launch { nodeActions.ignoreNode(node) }
|
||||
|
||||
fun removeNode(nodeNum: Int) = viewModelScope.launch(Dispatchers.IO) {
|
||||
Timber.i("Removing node '$nodeNum'")
|
||||
try {
|
||||
val packetId = serviceRepository.meshService?.packetId ?: return@launch
|
||||
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
|
||||
nodeRepository.deleteNode(nodeNum)
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e("Remove node error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
fun removeNode(nodeNum: Int) = viewModelScope.launch { nodeActions.removeNode(nodeNum) }
|
||||
}
|
||||
|
||||
data class NodesUiState(
|
||||
@@ -178,6 +162,7 @@ data class NodesUiState(
|
||||
data class NodeFilterState(
|
||||
val filterText: String = "",
|
||||
val includeUnknown: Boolean = false,
|
||||
val excludeInfrastructure: Boolean = false,
|
||||
val onlyOnline: Boolean = false,
|
||||
val onlyDirect: Boolean = false,
|
||||
val showIgnored: Boolean = false,
|
||||
|
||||
Reference in New Issue
Block a user