mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Decouple NodeScreen from UiViewModel (#3207)
This commit is contained in:
parent
1ba8c536e2
commit
01290278e9
6 changed files with 299 additions and 189 deletions
|
|
@ -58,7 +58,6 @@ import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
|||
import com.geeksville.mesh.util.safeNumber
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
|
@ -81,7 +80,6 @@ import org.meshtastic.core.database.entity.QuickChatAction
|
|||
import org.meshtastic.core.database.entity.asDeviceVersion
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
|
|
@ -149,22 +147,6 @@ internal fun getChannelList(new: List<ChannelSettings>, old: List<ChannelSetting
|
|||
}
|
||||
}
|
||||
|
||||
data class NodesUiState(
|
||||
val sort: NodeSortOption = NodeSortOption.LAST_HEARD,
|
||||
val filter: String = "",
|
||||
val includeUnknown: Boolean = false,
|
||||
val onlyOnline: Boolean = false,
|
||||
val onlyDirect: Boolean = false,
|
||||
val distanceUnits: Int = 0,
|
||||
val tempInFahrenheit: Boolean = false,
|
||||
val showDetails: Boolean = false,
|
||||
val showIgnored: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
val Empty = NodesUiState()
|
||||
}
|
||||
}
|
||||
|
||||
data class Contact(
|
||||
val contactKey: String,
|
||||
val shortName: String,
|
||||
|
|
@ -299,37 +281,11 @@ constructor(
|
|||
.getAllActions()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
|
||||
private val nodeFilterText = MutableStateFlow("")
|
||||
private val nodeSortOption =
|
||||
MutableStateFlow(NodeSortOption.entries.getOrElse(uiPrefs.nodeSortOption) { NodeSortOption.VIA_FAVORITE })
|
||||
private val includeUnknown = MutableStateFlow(uiPrefs.includeUnknown)
|
||||
private val showDetails = MutableStateFlow(uiPrefs.showDetails)
|
||||
private val onlyOnline = MutableStateFlow(uiPrefs.onlyOnline)
|
||||
private val onlyDirect = MutableStateFlow(uiPrefs.onlyDirect)
|
||||
|
||||
private val _showIgnored = MutableStateFlow(uiPrefs.showIgnored)
|
||||
val showIgnored: StateFlow<Boolean> = _showIgnored
|
||||
|
||||
private val _showQuickChat = MutableStateFlow(uiPrefs.showQuickChat)
|
||||
val showQuickChat: StateFlow<Boolean> = _showQuickChat
|
||||
|
||||
fun toggleShowIgnored() = toggle(_showIgnored) { uiPrefs.showIgnored = it }
|
||||
|
||||
fun toggleShowQuickChat() = toggle(_showQuickChat) { uiPrefs.showQuickChat = it }
|
||||
|
||||
fun setSortOption(sort: NodeSortOption) {
|
||||
nodeSortOption.value = sort
|
||||
uiPrefs.nodeSortOption = sort.ordinal
|
||||
}
|
||||
|
||||
fun toggleShowDetails() = toggle(showDetails) { uiPrefs.showDetails = it }
|
||||
|
||||
fun toggleIncludeUnknown() = toggle(includeUnknown) { uiPrefs.includeUnknown = it }
|
||||
|
||||
fun toggleOnlyOnline() = toggle(onlyOnline) { uiPrefs.onlyOnline = it }
|
||||
|
||||
fun toggleOnlyDirect() = toggle(onlyDirect) { uiPrefs.onlyDirect = it }
|
||||
|
||||
private fun toggle(state: MutableStateFlow<Boolean>, onChanged: (newValue: Boolean) -> Unit) {
|
||||
(!state.value).let { toggled ->
|
||||
state.update { toggled }
|
||||
|
|
@ -337,51 +293,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
data class NodeFilterState(
|
||||
val filterText: String,
|
||||
val includeUnknown: Boolean,
|
||||
val onlyOnline: Boolean,
|
||||
val onlyDirect: Boolean,
|
||||
val showIgnored: Boolean,
|
||||
)
|
||||
|
||||
val nodeFilterStateFlow: Flow<NodeFilterState> =
|
||||
combine(nodeFilterText, includeUnknown, onlyOnline, onlyDirect, showIgnored) {
|
||||
filterText,
|
||||
includeUnknown,
|
||||
onlyOnline,
|
||||
onlyDirect,
|
||||
showIgnored,
|
||||
->
|
||||
NodeFilterState(filterText, includeUnknown, onlyOnline, onlyDirect, showIgnored)
|
||||
}
|
||||
|
||||
val nodesUiState: StateFlow<NodesUiState> =
|
||||
combine(nodeFilterStateFlow, nodeSortOption, showDetails, radioConfigRepository.deviceProfileFlow) {
|
||||
filterFlow,
|
||||
sort,
|
||||
showDetails,
|
||||
profile,
|
||||
->
|
||||
NodesUiState(
|
||||
sort = sort,
|
||||
filter = filterFlow.filterText,
|
||||
includeUnknown = filterFlow.includeUnknown,
|
||||
onlyOnline = filterFlow.onlyOnline,
|
||||
onlyDirect = filterFlow.onlyDirect,
|
||||
distanceUnits = profile.config.display.units.number,
|
||||
tempInFahrenheit = profile.moduleConfig.telemetry.environmentDisplayFahrenheit,
|
||||
showDetails = showDetails,
|
||||
showIgnored = filterFlow.showIgnored,
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = NodesUiState.Empty,
|
||||
)
|
||||
|
||||
val unfilteredNodeList: StateFlow<List<Node>> =
|
||||
val nodeList: StateFlow<List<Node>> =
|
||||
nodeDB
|
||||
.getNodes()
|
||||
.stateIn(
|
||||
|
|
@ -390,33 +302,6 @@ constructor(
|
|||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
val nodeList: StateFlow<List<Node>> =
|
||||
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),
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
val onlineNodeCount =
|
||||
nodeDB.onlineNodeCount.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
val totalNodeCount =
|
||||
nodeDB.totalNodeCount.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
// hardware info about our local device (can be null)
|
||||
val myNodeInfo: StateFlow<MyNodeEntity?>
|
||||
get() = nodeDB.myNodeInfo
|
||||
|
|
@ -603,9 +488,6 @@ constructor(
|
|||
_sharedContactRequested.value = sharedContact
|
||||
}
|
||||
|
||||
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
|
||||
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.AddSharedContact(sharedContact)) }
|
||||
|
||||
fun requestTraceroute(destNum: Int) {
|
||||
info("Requesting traceroute for '$destNum'")
|
||||
try {
|
||||
|
|
@ -749,9 +631,6 @@ constructor(
|
|||
val myNodeNum
|
||||
get() = myNodeInfo.value?.myNodeNum
|
||||
|
||||
val maxChannels
|
||||
get() = myNodeInfo.value?.maxChannels ?: 8
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
debug("ViewModel cleared")
|
||||
|
|
@ -788,21 +667,6 @@ constructor(
|
|||
if (config.lora != newConfig.lora) setConfig(newConfig)
|
||||
}
|
||||
|
||||
fun setOwner(name: String) {
|
||||
val user =
|
||||
ourNodeInfo.value?.user?.copy {
|
||||
longName = name
|
||||
shortName = getInitials(name)
|
||||
} ?: return
|
||||
|
||||
try {
|
||||
// Note: we use ?. here because we might be running in the emulator
|
||||
meshService?.setRemoteOwner(myNodeNum ?: return, user.toByteArray())
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("Can't set username on device, is device offline? ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun addQuickChatAction(action: QuickChatAction) =
|
||||
viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.upsert(action) }
|
||||
|
||||
|
|
@ -824,10 +688,6 @@ constructor(
|
|||
serviceRepository.clearTracerouteResponse()
|
||||
}
|
||||
|
||||
fun setNodeFilterText(text: String) {
|
||||
nodeFilterText.value = text
|
||||
}
|
||||
|
||||
val appIntroCompleted: StateFlow<Boolean> = uiPreferencesDataSource.appIntroCompleted
|
||||
|
||||
fun onAppIntroCompleted() {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ fun NavGraphBuilder.nodesGraph(navController: NavHostController, uiViewModel: UI
|
|||
deepLinks = listOf(navDeepLink<NodesRoutes.Nodes>(basePath = "$DEEP_LINK_BASE_URI/nodes")),
|
||||
) {
|
||||
NodeScreen(
|
||||
model = uiViewModel,
|
||||
navigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) },
|
||||
navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.AdminProtos
|
||||
import com.geeksville.mesh.service.ConnectionState
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.node.components.NodeFilterTextField
|
||||
|
|
@ -65,23 +65,23 @@ import org.meshtastic.core.ui.component.rememberTimeTickWithLifecycle
|
|||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun NodeScreen(
|
||||
model: UIViewModel = hiltViewModel(),
|
||||
nodesViewModel: NodesViewModel = hiltViewModel(),
|
||||
navigateToMessages: (String) -> Unit,
|
||||
navigateToNodeDetails: (Int) -> Unit,
|
||||
) {
|
||||
val state by model.nodesUiState.collectAsStateWithLifecycle()
|
||||
val state by nodesViewModel.nodesUiState.collectAsStateWithLifecycle()
|
||||
|
||||
val nodes by model.nodeList.collectAsStateWithLifecycle()
|
||||
val ourNode by model.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val onlineNodeCount by model.onlineNodeCount.collectAsStateWithLifecycle(0)
|
||||
val totalNodeCount by model.totalNodeCount.collectAsStateWithLifecycle(0)
|
||||
val unfilteredNodes by model.unfilteredNodeList.collectAsStateWithLifecycle()
|
||||
val nodes by nodesViewModel.nodeList.collectAsStateWithLifecycle()
|
||||
val ourNode by nodesViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val onlineNodeCount by nodesViewModel.onlineNodeCount.collectAsStateWithLifecycle(0)
|
||||
val totalNodeCount by nodesViewModel.totalNodeCount.collectAsStateWithLifecycle(0)
|
||||
val unfilteredNodes by nodesViewModel.unfilteredNodeList.collectAsStateWithLifecycle()
|
||||
val ignoredNodeCount = unfilteredNodes.count { it.isIgnored }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val currentTimeMillis = rememberTimeTickWithLifecycle()
|
||||
val connectionState by model.connectionState.collectAsStateWithLifecycle()
|
||||
val connectionState by nodesViewModel.connectionState.collectAsStateWithLifecycle()
|
||||
|
||||
var showSharedContact: Node? by remember { mutableStateOf(null) }
|
||||
if (showSharedContact != null) {
|
||||
|
|
@ -106,15 +106,18 @@ fun NodeScreen(
|
|||
floatingActionButton = {
|
||||
val firmwareVersion = DeviceVersion(ourNode?.metadata?.firmwareVersion ?: "0.0.0")
|
||||
val shareCapable = firmwareVersion.supportsQrCodeSharing()
|
||||
|
||||
val scannedContact: AdminProtos.SharedContact? by
|
||||
nodesViewModel.sharedContactRequested.collectAsStateWithLifecycle(null)
|
||||
AddContactFAB(
|
||||
unfilteredNodes = unfilteredNodes,
|
||||
scannedContact = scannedContact,
|
||||
modifier =
|
||||
Modifier.animateFloatingActionButton(
|
||||
visible = !isScrollInProgress && connectionState == ConnectionState.CONNECTED && shareCapable,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
model = model,
|
||||
onSharedContactImport = { contact -> model.addSharedContact(contact) },
|
||||
onSharedContactImport = { contact -> nodesViewModel.addSharedContact(contact) },
|
||||
onSharedContactRequested = { contact -> nodesViewModel.setSharedContactRequested(contact) },
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
|
|
@ -129,20 +132,20 @@ fun NodeScreen(
|
|||
.graphicsLayer(alpha = animatedAlpha)
|
||||
.background(MaterialTheme.colorScheme.surfaceDim)
|
||||
.padding(8.dp),
|
||||
filterText = state.filter,
|
||||
onTextChange = model::setNodeFilterText,
|
||||
filterText = state.filter.filterText,
|
||||
onTextChange = nodesViewModel::setNodeFilterText,
|
||||
currentSortOption = state.sort,
|
||||
onSortSelect = model::setSortOption,
|
||||
includeUnknown = state.includeUnknown,
|
||||
onToggleIncludeUnknown = model::toggleIncludeUnknown,
|
||||
onlyOnline = state.onlyOnline,
|
||||
onToggleOnlyOnline = model::toggleOnlyOnline,
|
||||
onlyDirect = state.onlyDirect,
|
||||
onToggleOnlyDirect = model::toggleOnlyDirect,
|
||||
onSortSelect = nodesViewModel::setSortOption,
|
||||
includeUnknown = state.filter.includeUnknown,
|
||||
onToggleIncludeUnknown = nodesViewModel::toggleIncludeUnknown,
|
||||
onlyOnline = state.filter.onlyOnline,
|
||||
onToggleOnlyOnline = nodesViewModel::toggleOnlyOnline,
|
||||
onlyDirect = state.filter.onlyDirect,
|
||||
onToggleOnlyDirect = nodesViewModel::toggleOnlyDirect,
|
||||
showDetails = state.showDetails,
|
||||
onToggleShowDetails = model::toggleShowDetails,
|
||||
showIgnored = state.showIgnored,
|
||||
onToggleShowIgnored = model::toggleShowIgnored,
|
||||
onToggleShowDetails = nodesViewModel::toggleShowDetails,
|
||||
showIgnored = state.filter.showIgnored,
|
||||
onToggleShowIgnored = nodesViewModel::toggleShowIgnored,
|
||||
ignoredNodeCount = ignoredNodeCount,
|
||||
)
|
||||
}
|
||||
|
|
@ -156,18 +159,18 @@ fun NodeScreen(
|
|||
tempInFahrenheit = state.tempInFahrenheit,
|
||||
onAction = { menuItem ->
|
||||
when (menuItem) {
|
||||
is NodeMenuAction.Remove -> model.removeNode(node.num)
|
||||
is NodeMenuAction.Ignore -> model.ignoreNode(node)
|
||||
is NodeMenuAction.Favorite -> model.favoriteNode(node)
|
||||
is NodeMenuAction.Remove -> nodesViewModel.removeNode(node.num)
|
||||
is NodeMenuAction.Ignore -> nodesViewModel.ignoreNode(node)
|
||||
is NodeMenuAction.Favorite -> nodesViewModel.favoriteNode(node)
|
||||
is NodeMenuAction.DirectMessage -> {
|
||||
val hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC
|
||||
val hasPKC = nodesViewModel.ourNodeInfo.value?.hasPKC == true && node.hasPKC
|
||||
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
|
||||
navigateToMessages("$channel${node.user.id}")
|
||||
}
|
||||
|
||||
is NodeMenuAction.RequestUserInfo -> model.requestUserInfo(node.num)
|
||||
is NodeMenuAction.RequestPosition -> model.requestPosition(node.num)
|
||||
is NodeMenuAction.TraceRoute -> model.requestTraceroute(node.num)
|
||||
is NodeMenuAction.RequestUserInfo -> nodesViewModel.requestUserInfo(node.num)
|
||||
is NodeMenuAction.RequestPosition -> nodesViewModel.requestPosition(node.num)
|
||||
is NodeMenuAction.TraceRoute -> nodesViewModel.requestTraceroute(node.num)
|
||||
is NodeMenuAction.MoreDetails -> navigateToNodeDetails(node.num)
|
||||
is NodeMenuAction.Share -> showSharedContact = node
|
||||
}
|
||||
|
|
|
|||
254
app/src/main/java/com/geeksville/mesh/ui/node/NodesViewModel.kt
Normal file
254
app/src/main/java/com/geeksville/mesh/ui/node/NodesViewModel.kt
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* 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 com.geeksville.mesh.ui.node
|
||||
|
||||
import android.os.RemoteException
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.AdminProtos
|
||||
import com.geeksville.mesh.database.NodeRepository
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import com.geeksville.mesh.service.ServiceAction
|
||||
import com.geeksville.mesh.service.ServiceRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NodesViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val nodeRepository: NodeRepository,
|
||||
radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val uiPrefs: UiPrefs,
|
||||
) : ViewModel() {
|
||||
|
||||
val ourNodeInfo: StateFlow<Node?> = nodeRepository.ourNodeInfo
|
||||
|
||||
val onlineNodeCount =
|
||||
nodeRepository.onlineNodeCount.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
val totalNodeCount =
|
||||
nodeRepository.totalNodeCount.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
val connectionState = serviceRepository.connectionState
|
||||
|
||||
private val _sharedContactRequested: MutableStateFlow<AdminProtos.SharedContact?> = MutableStateFlow(null)
|
||||
val sharedContactRequested = _sharedContactRequested.asStateFlow()
|
||||
|
||||
private val nodeSortOption =
|
||||
MutableStateFlow(NodeSortOption.entries.getOrElse(uiPrefs.nodeSortOption) { NodeSortOption.VIA_FAVORITE })
|
||||
|
||||
private val nodeFilterText = MutableStateFlow("")
|
||||
private val includeUnknown = MutableStateFlow(uiPrefs.includeUnknown)
|
||||
private val onlyOnline = MutableStateFlow(uiPrefs.onlyOnline)
|
||||
private val onlyDirect = MutableStateFlow(uiPrefs.onlyDirect)
|
||||
private val _showIgnored = MutableStateFlow(uiPrefs.showIgnored)
|
||||
val showIgnored: StateFlow<Boolean> = _showIgnored
|
||||
|
||||
private val nodeFilter: Flow<NodeFilterState> =
|
||||
combine(nodeFilterText, includeUnknown, onlyOnline, onlyDirect, showIgnored) {
|
||||
filterText,
|
||||
includeUnknown,
|
||||
onlyOnline,
|
||||
onlyDirect,
|
||||
showIgnored,
|
||||
->
|
||||
NodeFilterState(filterText, includeUnknown, onlyOnline, onlyDirect, showIgnored)
|
||||
}
|
||||
|
||||
private val showDetails = MutableStateFlow(uiPrefs.showDetails)
|
||||
|
||||
val nodesUiState: StateFlow<NodesUiState> =
|
||||
combine(nodeSortOption, nodeFilter, showDetails, radioConfigRepository.deviceProfileFlow) {
|
||||
sort,
|
||||
nodeFilter,
|
||||
showDetails,
|
||||
profile,
|
||||
->
|
||||
NodesUiState(
|
||||
sort = sort,
|
||||
filter = nodeFilter,
|
||||
distanceUnits = profile.config.display.units.number,
|
||||
tempInFahrenheit = profile.moduleConfig.telemetry.environmentDisplayFahrenheit,
|
||||
showDetails = showDetails,
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = NodesUiState(),
|
||||
)
|
||||
|
||||
val nodeList: StateFlow<List<Node>> =
|
||||
combine(nodeFilter, nodeSortOption, ::Pair)
|
||||
.flatMapLatest { (filter, sort) ->
|
||||
nodeRepository
|
||||
.getNodes(
|
||||
sort = sort,
|
||||
filter = filter.filterText,
|
||||
includeUnknown = filter.includeUnknown,
|
||||
onlyOnline = filter.onlyOnline,
|
||||
onlyDirect = filter.onlyDirect,
|
||||
)
|
||||
.map { list -> list.filter { it.isIgnored == filter.showIgnored } }
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
val unfilteredNodeList: StateFlow<List<Node>> =
|
||||
nodeRepository
|
||||
.getNodes()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
fun setNodeFilterText(text: String) {
|
||||
nodeFilterText.value = text
|
||||
}
|
||||
|
||||
fun toggleIncludeUnknown() = toggle(includeUnknown) { uiPrefs.includeUnknown = it }
|
||||
|
||||
fun toggleOnlyOnline() = toggle(onlyOnline) { uiPrefs.onlyOnline = it }
|
||||
|
||||
fun toggleOnlyDirect() = toggle(onlyDirect) { uiPrefs.onlyDirect = it }
|
||||
|
||||
fun toggleShowIgnored() = toggle(_showIgnored) { uiPrefs.showIgnored = it }
|
||||
|
||||
fun setSortOption(sort: NodeSortOption) {
|
||||
nodeSortOption.value = sort
|
||||
uiPrefs.nodeSortOption = sort.ordinal
|
||||
}
|
||||
|
||||
fun toggleShowDetails() = toggle(showDetails) { uiPrefs.showDetails = it }
|
||||
|
||||
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
|
||||
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.AddSharedContact(sharedContact)) }
|
||||
|
||||
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 ignoreNode(node: Node) = viewModelScope.launch {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Ignore node error")
|
||||
}
|
||||
}
|
||||
|
||||
fun favoriteNode(node: Node) = viewModelScope.launch {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Favorite node error")
|
||||
}
|
||||
}
|
||||
|
||||
fun requestUserInfo(destNum: Int) {
|
||||
Timber.i("Requesting UserInfo for '$destNum'")
|
||||
try {
|
||||
serviceRepository.meshService?.requestUserInfo(destNum)
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e("Request NodeInfo error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPosition(destNum: Int, position: Position = Position(0.0, 0.0, 0)) {
|
||||
Timber.i("Requesting position for '$destNum'")
|
||||
try {
|
||||
serviceRepository.meshService?.requestPosition(destNum, position)
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e("Request position error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun requestTraceroute(destNum: Int) {
|
||||
Timber.i("Requesting traceroute for '$destNum'")
|
||||
try {
|
||||
val packetId = serviceRepository.meshService?.packetId ?: return
|
||||
serviceRepository.meshService?.requestTraceroute(packetId, destNum)
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e("Request traceroute error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
|
||||
_sharedContactRequested.value = sharedContact
|
||||
}
|
||||
|
||||
private fun toggle(state: MutableStateFlow<Boolean>, onChanged: (newValue: Boolean) -> Unit) {
|
||||
(!state.value).let { toggled ->
|
||||
state.update { toggled }
|
||||
onChanged(toggled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class NodesUiState(
|
||||
val sort: NodeSortOption = NodeSortOption.LAST_HEARD,
|
||||
val filter: NodeFilterState = NodeFilterState(),
|
||||
val distanceUnits: Int = 0,
|
||||
val tempInFahrenheit: Boolean = false,
|
||||
val showDetails: Boolean = false,
|
||||
)
|
||||
|
||||
data class NodeFilterState(
|
||||
val filterText: String = "",
|
||||
val includeUnknown: Boolean = false,
|
||||
val onlyOnline: Boolean = false,
|
||||
val onlyDirect: Boolean = false,
|
||||
val showIgnored: Boolean = false,
|
||||
)
|
||||
|
|
@ -36,8 +36,6 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
|
|
@ -48,13 +46,10 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.AdminProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.BuildUtils.errormsg
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
|
@ -79,19 +74,18 @@ import java.net.MalformedURLException
|
|||
* requests using Accompanist Permissions.
|
||||
*
|
||||
* @param modifier Modifier for this composable.
|
||||
* @param model UIViewModel for interacting with application state.
|
||||
* @param onSharedContactImport Callback invoked when a shared contact is successfully imported.
|
||||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun AddContactFAB(
|
||||
unfilteredNodes: List<Node>,
|
||||
scannedContact: AdminProtos.SharedContact?,
|
||||
modifier: Modifier = Modifier,
|
||||
model: UIViewModel = hiltViewModel(),
|
||||
onSharedContactImport: (AdminProtos.SharedContact) -> Unit = {},
|
||||
onSharedContactRequested: (AdminProtos.SharedContact?) -> Unit = {},
|
||||
) {
|
||||
val scannedContact: AdminProtos.SharedContact? by model.sharedContactRequested.collectAsStateWithLifecycle(null)
|
||||
|
||||
val barcodeLauncher =
|
||||
rememberLauncherForActivityResult(ScanContract()) { result ->
|
||||
if (result.contents != null) {
|
||||
|
|
@ -104,15 +98,14 @@ fun AddContactFAB(
|
|||
null
|
||||
}
|
||||
if (sharedContact != null) {
|
||||
model.setSharedContactRequested(sharedContact)
|
||||
onSharedContactRequested(sharedContact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scannedContact?.let { contactToImport ->
|
||||
val nodeNum = scannedContact?.nodeNum
|
||||
val nodes by model.unfilteredNodeList.collectAsState()
|
||||
val node = nodes.find { it.num == nodeNum }
|
||||
val nodeNum = contactToImport.nodeNum
|
||||
val node = unfilteredNodes.find { it.num == nodeNum }
|
||||
SimpleAlertDialog(
|
||||
title = R.string.import_shared_contact,
|
||||
text = {
|
||||
|
|
@ -133,11 +126,11 @@ fun AddContactFAB(
|
|||
}
|
||||
},
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
onDismiss = { model.setSharedContactRequested(null) },
|
||||
onDismiss = { onSharedContactRequested(null) },
|
||||
confirmText = stringResource(R.string.import_label),
|
||||
onConfirm = {
|
||||
onSharedContactImport(contactToImport)
|
||||
model.setSharedContactRequested(null)
|
||||
onSharedContactRequested(null)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue