From 8b34a69d620eb4d084c0a334a8431e55575a11f5 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Thu, 18 Sep 2025 12:31:17 -0400 Subject: [PATCH] Add `MapScreen` (#3142) --- .../geeksville/mesh/ui/map/MapViewModel.kt | 4 +- .../geeksville/mesh/ui/map/MapViewModel.kt | 2 +- .../mesh/navigation/MapNavigation.kt | 54 +++----------- .../mesh/ui/map/BaseMapViewModel.kt | 9 +++ .../com/geeksville/mesh/ui/map/MapScreen.kt | 74 +++++++++++++++++++ 5 files changed, 97 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/map/MapScreen.kt diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewModel.kt b/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewModel.kt index 0f4d3fc1a..b51c9b7d9 100644 --- a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewModel.kt +++ b/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewModel.kt @@ -20,6 +20,7 @@ package com.geeksville.mesh.ui.map import com.geeksville.mesh.android.prefs.MapPrefs import com.geeksville.mesh.database.NodeRepository import com.geeksville.mesh.database.PacketRepository +import com.geeksville.mesh.repository.datastore.RadioConfigRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -30,7 +31,8 @@ constructor( mapPrefs: MapPrefs, packetRepository: PacketRepository, nodeRepository: NodeRepository, -) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository) { + radioConfigRepository: RadioConfigRepository, +) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioConfigRepository) { var mapStyleId: Int get() = mapPrefs.mapStyle diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt index 30b1a557e..dd7905380 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt @@ -85,7 +85,7 @@ constructor( packetRepository: PacketRepository, radioConfigRepository: RadioConfigRepository, private val customTileProviderRepository: CustomTileProviderRepository, -) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository) { +) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioConfigRepository) { private val _errorFlow = MutableSharedFlow() val errorFlow: SharedFlow = _errorFlow.asSharedFlow() diff --git a/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt index 845a42a58..e6394b3e8 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt @@ -17,58 +17,24 @@ package com.geeksville.mesh.navigation -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.navDeepLink -import com.geeksville.mesh.R import com.geeksville.mesh.model.UIViewModel -import com.geeksville.mesh.ui.common.components.MainAppBar -import com.geeksville.mesh.ui.map.MapView -import com.geeksville.mesh.ui.node.components.NodeMenuAction +import com.geeksville.mesh.ui.map.MapScreen fun NavGraphBuilder.mapGraph(navController: NavHostController, uiViewModel: UIViewModel) { composable(deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/map"))) { - val ourNodeInfo by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle() - val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle() - - Scaffold( - topBar = { - MainAppBar( - title = stringResource(R.string.map), - ourNode = ourNodeInfo, - isConnected = isConnected, - showNodeChip = ourNodeInfo != null && isConnected, - canNavigateUp = false, - onNavigateUp = {}, - actions = {}, - onAction = { action -> - when (action) { - is NodeMenuAction.MoreDetails -> { - navController.navigate(NodesRoutes.NodeDetailGraph(action.node.num)) { - launchSingleTop = true - restoreState = true - } - } - else -> {} - } - }, - ) + MapScreen( + uiViewModel = uiViewModel, + onClickNodeChip = { + navController.navigate(NodesRoutes.NodeDetailGraph(it)) { + launchSingleTop = true + restoreState = true + } }, - ) { paddingValues -> - Box(modifier = Modifier.padding(paddingValues)) { - MapView( - uiViewModel = uiViewModel, - navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) }, - ) - } - } + navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) }, + ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/BaseMapViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/map/BaseMapViewModel.kt index 4de3fe2e3..aafb5f67d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/map/BaseMapViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/map/BaseMapViewModel.kt @@ -24,6 +24,7 @@ import com.geeksville.mesh.database.NodeRepository import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.model.Node +import com.geeksville.mesh.repository.datastore.RadioConfigRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -37,6 +38,7 @@ abstract class BaseMapViewModel( protected val mapPrefs: MapPrefs, nodeRepository: NodeRepository, packetRepository: PacketRepository, + radioConfigRepository: RadioConfigRepository, ) : ViewModel() { val nodes: StateFlow> = @@ -67,6 +69,13 @@ abstract class BaseMapViewModel( private val showPrecisionCircleOnMap = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap) + val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo + + val isConnected = + radioConfigRepository.connectionState + .map { it.isConnected() } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false) + fun toggleOnlyFavorites() { val current = showOnlyFavorites.value mapPrefs.showOnlyFavorites = !current diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/MapScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/map/MapScreen.kt new file mode 100644 index 000000000..81f013df7 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/map/MapScreen.kt @@ -0,0 +1,74 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.ui.map + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.geeksville.mesh.R +import com.geeksville.mesh.model.UIViewModel +import com.geeksville.mesh.ui.common.components.MainAppBar +import com.geeksville.mesh.ui.node.components.NodeMenuAction + +@Composable +fun MapScreen( + onClickNodeChip: (Int) -> Unit, + navigateToNodeDetails: (Int) -> Unit, + uiViewModel: UIViewModel, + modifier: Modifier = Modifier, + mapViewModel: MapViewModel = hiltViewModel(), +) { + val ourNodeInfo by mapViewModel.ourNodeInfo.collectAsStateWithLifecycle() + val isConnected by mapViewModel.isConnected.collectAsStateWithLifecycle() + + @Suppress("ViewModelForwarding") + Scaffold( + modifier = modifier, + topBar = { + MainAppBar( + title = stringResource(R.string.map), + ourNode = ourNodeInfo, + isConnected = isConnected, + showNodeChip = ourNodeInfo != null && isConnected, + canNavigateUp = false, + onNavigateUp = {}, + actions = {}, + onAction = { action -> + when (action) { + is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num) + else -> {} + } + }, + ) + }, + ) { paddingValues -> + Box(modifier = Modifier.padding(paddingValues)) { + MapView( + uiViewModel = uiViewModel, + mapViewModel = mapViewModel, + navigateToNodeDetails = navigateToNodeDetails, + ) + } + } +}