Refactor map layer management and navigation infrastructure (#4921)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-25 19:29:24 -05:00 committed by GitHub
parent b608a04ca4
commit a005231d94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
142 changed files with 5408 additions and 3090 deletions

View file

@ -19,7 +19,6 @@ package org.meshtastic.desktop.navigation
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.meshtastic.desktop.ui.map.KmpMapPlaceholder
import org.meshtastic.feature.connections.navigation.connectionsGraph
import org.meshtastic.feature.firmware.navigation.firmwareGraph
import org.meshtastic.feature.map.navigation.mapGraph
@ -43,12 +42,12 @@ fun EntryProviderScope<NavKey>.desktopNavGraph(
// Nodes — real composables from feature:node
nodesGraph(
backStack = backStack,
scrollToTopEvents = uiViewModel.scrollToTopEventFlow,
onHandleDeepLink = uiViewModel::handleDeepLink,
nodeMapScreen = { destNum, _ -> KmpMapPlaceholder(title = "Node Map ($destNum)") },
)
// Conversations — real composables from feature:messaging
contactsGraph(backStack)
contactsGraph(backStack, uiViewModel.scrollToTopEventFlow)
// Map — placeholder for now, will be replaced with feature:map real implementation
mapGraph(backStack)

View file

@ -16,35 +16,20 @@
*/
package org.meshtastic.desktop.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.navigation.TopLevelDestination
import org.meshtastic.core.navigation.navigateTopLevel
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.ui.component.MeshtasticAppShell
import org.meshtastic.core.ui.navigation.icon
import org.meshtastic.core.ui.viewmodel.UIViewModel
import org.meshtastic.desktop.navigation.desktopNavGraph
@ -56,74 +41,25 @@ import org.meshtastic.desktop.navigation.desktopNavGraph
*/
@Composable
@Suppress("LongMethod")
fun DesktopMainScreen(
backStack: NavBackStack<NavKey>,
radioService: RadioInterfaceService = koinInject(),
uiViewModel: UIViewModel = koinViewModel(),
) {
val currentKey = backStack.lastOrNull()
val selected = TopLevelDestination.fromNavKey(currentKey)
LaunchedEffect(uiViewModel) {
uiViewModel.navigationDeepLink.collect { uri ->
val commonUri = org.meshtastic.core.common.util.CommonUri.parse(uri.uriString)
org.meshtastic.core.navigation.DeepLinkRouter.route(commonUri)?.let { navKeys ->
backStack.clear()
backStack.addAll(navKeys)
}
}
}
val connectionState by radioService.connectionState.collectAsStateWithLifecycle()
val selectedDevice by radioService.currentDeviceAddressFlow.collectAsStateWithLifecycle()
val colorScheme = MaterialTheme.colorScheme
fun DesktopMainScreen(backStack: NavBackStack<NavKey>, uiViewModel: UIViewModel = koinViewModel()) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
MeshtasticAppShell(
backStack = backStack,
uiViewModel = uiViewModel,
hostModifier = Modifier.padding(bottom = 24.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Row(modifier = Modifier.fillMaxSize()) {
NavigationRail {
TopLevelDestination.entries.forEach { destination ->
NavigationRailItem(
selected = destination == selected,
onClick = {
if (destination != selected) {
backStack.navigateTopLevel(destination.route)
}
},
icon = {
if (destination == TopLevelDestination.Connections) {
org.meshtastic.feature.connections.ui.components.AnimatedConnectionsNavIcon(
connectionState = connectionState,
deviceType = DeviceType.fromAddress(selectedDevice ?: "NoDevice"),
meshActivityFlow = radioService.meshActivity,
colorScheme = colorScheme,
)
} else {
Icon(
imageVector = destination.icon,
contentDescription = stringResource(destination.label),
)
}
},
label = { Text(stringResource(destination.label)) },
)
}
}
org.meshtastic.core.ui.component.MeshtasticNavigationSuite(
backStack = backStack,
uiViewModel = uiViewModel,
) {
val provider = entryProvider<NavKey> { desktopNavGraph(backStack, uiViewModel) }
val provider = entryProvider<NavKey> { desktopNavGraph(backStack, uiViewModel) }
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = provider,
modifier = Modifier.weight(1f).fillMaxSize(),
)
}
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = provider,
modifier = Modifier.fillMaxSize(),
)
}
}
}

View file

@ -1,78 +0,0 @@
/*
* Copyright (c) 2026 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.desktop.ui.map
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.map
import org.meshtastic.core.resources.map_coming_soon
import org.meshtastic.core.ui.icon.Map
import org.meshtastic.core.ui.icon.MeshtasticIcons
/**
* A placeholder screen used on Desktop and other non-Android KMP targets where a full mapping library (like osmdroid or
* Google Maps) is not yet available.
*/
@Composable
fun KmpMapPlaceholder(
title: String = stringResource(Res.string.map),
description: String = stringResource(Res.string.map_coming_soon),
modifier: Modifier = Modifier,
) {
Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Column(
modifier = Modifier.fillMaxSize().padding(32.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = MeshtasticIcons.Map,
contentDescription = title,
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
)
Text(
text = title,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp),
)
Text(
text = description,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
}
}