fix(nav): remote admin nodenum + Nav3 consolidation and improvements (#5478)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-18 12:29:27 -05:00
committed by GitHub
parent f6587a1236
commit df4f10c4d6
23 changed files with 196 additions and 249 deletions

View File

@@ -60,7 +60,7 @@ fun MainScreen() {
if (viewModel.currentDeviceAddressFlow.value.isNullOrSelectedNone()) {
TopLevelDestination.Connections.route
} else {
NodesRoute.NodesGraph
NodesRoute.Nodes
}
val multiBackstack = rememberMultiBackstack(initialTab)
val backStack = multiBackstack.activeBackStack

View File

@@ -43,7 +43,7 @@ class NavigationAssemblyTest {
@Test
fun verifyNavigationGraphsAssembleWithoutCrashing() = runComposeUiTest {
setContent {
val backStack = rememberNavBackStack(NodesRoute.NodesGraph)
val backStack = rememberNavBackStack(NodesRoute.Nodes)
entryProvider<NavKey> {
contactsGraph(backStack, emptyFlow())
nodesGraph(backStack = backStack, scrollToTopEvents = emptyFlow())

View File

@@ -60,7 +60,7 @@ object DeepLinkRouter {
"quickchat",
-> routeContacts(uri, pathSegments)
"connections" -> listOf(ConnectionsRoute.ConnectionsGraph)
"connections" -> listOf(ConnectionsRoute.Connections)
"map" -> routeMap(uri, pathSegments)
@@ -68,7 +68,7 @@ object DeepLinkRouter {
"settings" -> routeSettings(pathSegments)
"channels" -> listOf(ChannelsRoute.ChannelsGraph)
"channels" -> listOf(ChannelsRoute.Channels)
"firmware" -> routeFirmware(pathSegments)
@@ -86,27 +86,24 @@ object DeepLinkRouter {
return when (firstSegment) {
"share" -> {
val message = uri.getQueryParameter("message") ?: ""
listOf(ContactsRoute.ContactsGraph, ContactsRoute.Share(message))
listOf(ContactsRoute.Contacts, ContactsRoute.Share(message))
}
"quickchat" -> {
listOf(ContactsRoute.ContactsGraph, ContactsRoute.QuickChat)
listOf(ContactsRoute.Contacts, ContactsRoute.QuickChat)
}
"messages" -> {
val contactKey = if (segments.size > 1) segments[1] else uri.getQueryParameter("contactKey") ?: ""
val message = uri.getQueryParameter("message") ?: ""
if (contactKey.isNotBlank()) {
listOf(
ContactsRoute.ContactsGraph,
ContactsRoute.Messages(contactKey = contactKey, message = message),
)
listOf(ContactsRoute.Contacts, ContactsRoute.Messages(contactKey = contactKey, message = message))
} else {
listOf(ContactsRoute.ContactsGraph)
listOf(ContactsRoute.Contacts)
}
}
else -> listOf(ContactsRoute.ContactsGraph)
else -> listOf(ContactsRoute.Contacts)
}
}
@@ -121,17 +118,17 @@ object DeepLinkRouter {
val destNum = destNumStr?.toIntOrNull()
return if (destNum == null) {
listOf(NodesRoute.NodesGraph)
listOf(NodesRoute.Nodes)
} else if (segments.size > 2) {
val subRouteStr = segments[2].lowercase()
val detailRouteFn = nodeDetailSubRoutes[subRouteStr]
if (detailRouteFn != null) {
listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetailGraph(destNum), detailRouteFn(destNum))
listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum), detailRouteFn(destNum))
} else {
listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum))
listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum))
}
} else {
listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum))
listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum))
}
}
@@ -153,14 +150,14 @@ object DeepLinkRouter {
}
if (subRouteStr == null) {
return listOf(SettingsRoute.SettingsGraph(destNum))
return listOf(SettingsRoute.Settings(destNum))
}
val subRoute = settingsSubRoutes[subRouteStr]
return if (subRoute != null) {
listOf(SettingsRoute.SettingsGraph(destNum), subRoute)
listOf(SettingsRoute.Settings(destNum), subRoute)
} else {
listOf(SettingsRoute.SettingsGraph(destNum))
listOf(SettingsRoute.Settings(destNum))
}
}

View File

@@ -17,20 +17,23 @@
package org.meshtastic.core.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberNavBackStack
/** Manages independent backstacks for multiple tabs. */
class MultiBackstack(val startTab: NavKey) {
class MultiBackstack(val startTab: NavKey, private val currentTabState: MutableState<NavKey>) {
var backStacks: Map<NavKey, NavBackStack<NavKey>> = emptyMap()
var currentTabRoute: NavKey by mutableStateOf(TopLevelDestination.fromNavKey(startTab)?.route ?: startTab)
var currentTabRoute: NavKey by currentTabState
private set
val activeBackStack: NavBackStack<NavKey>
@@ -73,6 +76,19 @@ class MultiBackstack(val startTab: NavKey) {
}
}
/** Saver that persists the active tab ordinal across process death. */
private val CurrentTabSaver =
Saver<MutableState<NavKey>, Int>(
save = { state ->
TopLevelDestination.entries.indexOfFirst { it.route::class == state.value::class }.takeIf { it >= 0 }
},
restore = { ordinal ->
mutableStateOf(
TopLevelDestination.entries.getOrNull(ordinal)?.route ?: TopLevelDestination.Connections.route,
)
},
)
/** Remembers a [MultiBackstack] for managing independent tab navigation histories with Navigation 3. */
@Composable
fun rememberMultiBackstack(initialTab: NavKey = TopLevelDestination.Connections.route): MultiBackstack {
@@ -82,7 +98,12 @@ fun rememberMultiBackstack(initialTab: NavKey = TopLevelDestination.Connections.
key(dest.route) { stacks[dest.route] = rememberNavBackStack(MeshtasticNavSavedStateConfig, dest.route) }
}
val multiBackstack = remember { MultiBackstack(initialTab) }
val currentTabState =
rememberSaveable(saver = CurrentTabSaver) {
mutableStateOf(TopLevelDestination.fromNavKey(initialTab)?.route ?: initialTab)
}
val multiBackstack = remember { MultiBackstack(initialTab, currentTabState) }
multiBackstack.backStacks = stacks
return multiBackstack

View File

@@ -27,23 +27,17 @@ interface Graph : Route
@Serializable
sealed interface ChannelsRoute : Route {
@Serializable data object ChannelsGraph : ChannelsRoute, Graph
@Serializable data object Channels : ChannelsRoute
@Serializable data object Channels : ChannelsRoute, Graph
}
@Serializable
sealed interface ConnectionsRoute : Route {
@Serializable data object ConnectionsGraph : ConnectionsRoute, Graph
@Serializable data object Connections : ConnectionsRoute
@Serializable data object Connections : ConnectionsRoute, Graph
}
@Serializable
sealed interface ContactsRoute : Route {
@Serializable data object ContactsGraph : ContactsRoute, Graph
@Serializable data object Contacts : ContactsRoute
@Serializable data object Contacts : ContactsRoute, Graph
@Serializable data class Messages(val contactKey: String, val message: String = "") : ContactsRoute
@@ -59,13 +53,7 @@ sealed interface MapRoute : Route {
@Serializable
sealed interface NodesRoute : Route {
@Serializable data object NodesGraph : NodesRoute, Graph
@Serializable data object Nodes : NodesRoute
@Serializable data class NodeDetailGraph(val destNum: Int? = null) :
NodesRoute,
Graph
@Serializable data object Nodes : NodesRoute, Graph
@Serializable data class NodeDetail(val destNum: Int? = null) : NodesRoute
}
@@ -96,12 +84,10 @@ sealed interface NodeDetailRoute : Route {
@Serializable
sealed interface SettingsRoute : Route {
@Serializable data class SettingsGraph(val destNum: Int? = null) :
@Serializable data class Settings(val destNum: Int? = null) :
SettingsRoute,
Graph
@Serializable data class Settings(val destNum: Int? = null) : SettingsRoute
@Serializable data object DeviceConfiguration : SettingsRoute
@Serializable data object ModuleConfiguration : SettingsRoute

View File

@@ -32,11 +32,11 @@ import org.meshtastic.core.resources.nodes
* and Desktop navigation shells.
*/
enum class TopLevelDestination(val label: StringResource, val route: Route) {
Conversations(Res.string.conversations, ContactsRoute.ContactsGraph),
Nodes(Res.string.nodes, NodesRoute.NodesGraph),
Conversations(Res.string.conversations, ContactsRoute.Contacts),
Nodes(Res.string.nodes, NodesRoute.Nodes),
Map(Res.string.map, MapRoute.Map()),
Settings(Res.string.bottom_nav_settings, SettingsRoute.SettingsGraph()),
Connections(Res.string.connections, ConnectionsRoute.ConnectionsGraph),
Settings(Res.string.bottom_nav_settings, SettingsRoute.Settings()),
Connections(Res.string.connections, ConnectionsRoute.Connections),
;
companion object {

View File

@@ -47,25 +47,25 @@ class DeepLinkRouterTest {
@Test
fun `share with message`() {
assertEquals(
listOf(ContactsRoute.ContactsGraph, ContactsRoute.Share("hello world")),
listOf(ContactsRoute.Contacts, ContactsRoute.Share("hello world")),
route("/share?message=hello%20world"),
)
}
@Test
fun `share without message defaults to empty string`() {
assertEquals(listOf(ContactsRoute.ContactsGraph, ContactsRoute.Share("")), route("/share"))
assertEquals(listOf(ContactsRoute.Contacts, ContactsRoute.Share("")), route("/share"))
}
@Test
fun `quickchat routes to QuickChat`() {
assertEquals(listOf(ContactsRoute.ContactsGraph, ContactsRoute.QuickChat), route("/quickchat"))
assertEquals(listOf(ContactsRoute.Contacts, ContactsRoute.QuickChat), route("/quickchat"))
}
@Test
fun `messages with contactKey path segment`() {
assertEquals(
listOf(ContactsRoute.ContactsGraph, ContactsRoute.Messages(contactKey = "abc123", message = "")),
listOf(ContactsRoute.Contacts, ContactsRoute.Messages(contactKey = "abc123", message = "")),
route("/messages/abc123"),
)
}
@@ -73,7 +73,7 @@ class DeepLinkRouterTest {
@Test
fun `messages with contactKey query param`() {
assertEquals(
listOf(ContactsRoute.ContactsGraph, ContactsRoute.Messages(contactKey = "contact1", message = "")),
listOf(ContactsRoute.Contacts, ContactsRoute.Messages(contactKey = "contact1", message = "")),
route("/messages?contactKey=contact1"),
)
}
@@ -81,14 +81,14 @@ class DeepLinkRouterTest {
@Test
fun `messages with contactKey and message`() {
assertEquals(
listOf(ContactsRoute.ContactsGraph, ContactsRoute.Messages(contactKey = "contact1", message = "hi")),
listOf(ContactsRoute.Contacts, ContactsRoute.Messages(contactKey = "contact1", message = "hi")),
route("/messages/contact1?message=hi"),
)
}
@Test
fun `messages without contactKey returns graph only`() {
assertEquals(listOf(ContactsRoute.ContactsGraph), route("/messages"))
assertEquals(listOf(ContactsRoute.Contacts), route("/messages"))
}
// endregion
@@ -96,8 +96,8 @@ class DeepLinkRouterTest {
// region connections
@Test
fun `connections routes to ConnectionsGraph`() {
assertEquals(listOf(ConnectionsRoute.ConnectionsGraph), route("/connections"))
fun `connections routes to Connections`() {
assertEquals(listOf(ConnectionsRoute.Connections), route("/connections"))
}
// endregion
@@ -129,21 +129,21 @@ class DeepLinkRouterTest {
// region nodes
@Test
fun `nodes root returns NodesGraph`() {
assertEquals(listOf(NodesRoute.NodesGraph), route("/nodes"))
fun `nodes root returns Nodes`() {
assertEquals(listOf(NodesRoute.Nodes), route("/nodes"))
}
@Test
fun `nodes with destNum returns NodeDetail`() {
assertEquals(listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum = 1234)), route("/nodes/1234"))
assertEquals(listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 1234)), route("/nodes/1234"))
}
@Test
fun `nodes with destNum and device-metrics sub-route`() {
assertEquals(
listOf(
NodesRoute.NodesGraph,
NodesRoute.NodeDetailGraph(destNum = 1234),
NodesRoute.Nodes,
NodesRoute.NodeDetail(destNum = 1234),
NodeDetailRoute.DeviceMetrics(destNum = 1234),
),
route("/nodes/1234/device-metrics"),
@@ -154,8 +154,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and map sub-route`() {
assertEquals(
listOf(
NodesRoute.NodesGraph,
NodesRoute.NodeDetailGraph(destNum = 5678),
NodesRoute.Nodes,
NodesRoute.NodeDetail(destNum = 5678),
NodeDetailRoute.PositionLog(destNum = 5678),
),
route("/nodes/5678/map"),
@@ -165,11 +165,7 @@ class DeepLinkRouterTest {
@Test
fun `nodes with destNum and position sub-route`() {
assertEquals(
listOf(
NodesRoute.NodesGraph,
NodesRoute.NodeDetailGraph(destNum = 100),
NodeDetailRoute.PositionLog(destNum = 100),
),
listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 100), NodeDetailRoute.PositionLog(destNum = 100)),
route("/nodes/100/position"),
)
}
@@ -178,8 +174,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and environment sub-route`() {
assertEquals(
listOf(
NodesRoute.NodesGraph,
NodesRoute.NodeDetailGraph(destNum = 100),
NodesRoute.Nodes,
NodesRoute.NodeDetail(destNum = 100),
NodeDetailRoute.EnvironmentMetrics(destNum = 100),
),
route("/nodes/100/environment"),
@@ -190,8 +186,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and signal sub-route`() {
assertEquals(
listOf(
NodesRoute.NodesGraph,
NodesRoute.NodeDetailGraph(destNum = 100),
NodesRoute.Nodes,
NodesRoute.NodeDetail(destNum = 100),
NodeDetailRoute.SignalMetrics(destNum = 100),
),
route("/nodes/100/signal"),
@@ -201,11 +197,7 @@ class DeepLinkRouterTest {
@Test
fun `nodes with destNum and power sub-route`() {
assertEquals(
listOf(
NodesRoute.NodesGraph,
NodesRoute.NodeDetailGraph(destNum = 100),
NodeDetailRoute.PowerMetrics(destNum = 100),
),
listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 100), NodeDetailRoute.PowerMetrics(destNum = 100)),
route("/nodes/100/power"),
)
}
@@ -214,8 +206,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and traceroute sub-route`() {
assertEquals(
listOf(
NodesRoute.NodesGraph,
NodesRoute.NodeDetailGraph(destNum = 100),
NodesRoute.Nodes,
NodesRoute.NodeDetail(destNum = 100),
NodeDetailRoute.TracerouteLog(destNum = 100),
),
route("/nodes/100/traceroute"),
@@ -226,8 +218,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and host-metrics sub-route`() {
assertEquals(
listOf(
NodesRoute.NodesGraph,
NodesRoute.NodeDetailGraph(destNum = 100),
NodesRoute.Nodes,
NodesRoute.NodeDetail(destNum = 100),
NodeDetailRoute.HostMetricsLog(destNum = 100),
),
route("/nodes/100/host-metrics"),
@@ -237,11 +229,7 @@ class DeepLinkRouterTest {
@Test
fun `nodes with destNum and pax sub-route`() {
assertEquals(
listOf(
NodesRoute.NodesGraph,
NodesRoute.NodeDetailGraph(destNum = 100),
NodeDetailRoute.PaxMetrics(destNum = 100),
),
listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 100), NodeDetailRoute.PaxMetrics(destNum = 100)),
route("/nodes/100/pax"),
)
}
@@ -250,8 +238,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and neighbors sub-route`() {
assertEquals(
listOf(
NodesRoute.NodesGraph,
NodesRoute.NodeDetailGraph(destNum = 100),
NodesRoute.Nodes,
NodesRoute.NodeDetail(destNum = 100),
NodeDetailRoute.NeighborInfoLog(destNum = 100),
),
route("/nodes/100/neighbors"),
@@ -260,20 +248,17 @@ class DeepLinkRouterTest {
@Test
fun `nodes with destNum and unknown sub-route falls back to NodeDetail`() {
assertEquals(
listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum = 1234)),
route("/nodes/1234/unknown-sub"),
)
assertEquals(listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 1234)), route("/nodes/1234/unknown-sub"))
}
@Test
fun `nodes with non-numeric destNum returns NodesGraph only`() {
assertEquals(listOf(NodesRoute.NodesGraph), route("/nodes/not-a-number"))
fun `nodes with non-numeric destNum returns Nodes only`() {
assertEquals(listOf(NodesRoute.Nodes), route("/nodes/not-a-number"))
}
@Test
fun `nodes with destNum query param`() {
assertEquals(listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum = 9999)), route("/nodes?destNum=9999"))
assertEquals(listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 9999)), route("/nodes?destNum=9999"))
}
// endregion
@@ -281,31 +266,28 @@ class DeepLinkRouterTest {
// region settings
@Test
fun `settings root returns SettingsGraph`() {
assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = null)), route("/settings"))
fun `settings root returns Settings`() {
assertEquals(listOf(SettingsRoute.Settings(destNum = null)), route("/settings"))
}
@Test
fun `settings with destNum`() {
assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = 1234)), route("/settings/1234"))
assertEquals(listOf(SettingsRoute.Settings(destNum = 1234)), route("/settings/1234"))
}
@Test
fun `settings with destNum and sub-route`() {
assertEquals(
listOf(SettingsRoute.SettingsGraph(destNum = 1234), SettingsRoute.About),
route("/settings/1234/about"),
)
assertEquals(listOf(SettingsRoute.Settings(destNum = 1234), SettingsRoute.About), route("/settings/1234/about"))
}
@Test
fun `settings with sub-route without destNum`() {
assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = null), SettingsRoute.LoRa), route("/settings/lora"))
assertEquals(listOf(SettingsRoute.Settings(destNum = null), SettingsRoute.LoRa), route("/settings/lora"))
}
@Test
fun `settings with unknown sub-route returns SettingsGraph only`() {
assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = null)), route("/settings/nonexistent-page"))
fun `settings with unknown sub-route returns Settings only`() {
assertEquals(listOf(SettingsRoute.Settings(destNum = null)), route("/settings/nonexistent-page"))
}
@Test
@@ -349,7 +331,7 @@ class DeepLinkRouterTest {
expectedSubRoutes.forEach { (slug, expectedRoute) ->
assertEquals(
listOf(SettingsRoute.SettingsGraph(destNum = null), expectedRoute),
listOf(SettingsRoute.Settings(destNum = null), expectedRoute),
route("/settings/$slug"),
"Settings sub-route '$slug' did not resolve to $expectedRoute",
)
@@ -361,8 +343,8 @@ class DeepLinkRouterTest {
// region channels
@Test
fun `channels routes to ChannelsGraph`() {
assertEquals(listOf(ChannelsRoute.ChannelsGraph), route("/channels"))
fun `channels routes to Channels`() {
assertEquals(listOf(ChannelsRoute.Channels), route("/channels"))
}
// endregion
@@ -402,8 +384,8 @@ class DeepLinkRouterTest {
@Test
fun `route segments are case insensitive`() {
assertEquals(listOf(NodesRoute.NodesGraph), route("/Nodes"))
assertEquals(listOf(ConnectionsRoute.ConnectionsGraph), route("/CONNECTIONS"))
assertEquals(listOf(NodesRoute.Nodes), route("/Nodes"))
assertEquals(listOf(ConnectionsRoute.Connections), route("/CONNECTIONS"))
}
// endregion

View File

@@ -16,6 +16,7 @@
*/
package org.meshtastic.core.navigation
import androidx.compose.runtime.mutableStateOf
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import kotlin.test.Test
@@ -23,10 +24,15 @@ import kotlin.test.assertEquals
class MultiBackstackTest {
private fun createMultiBackstack(startTab: NavKey): MultiBackstack {
val tabRoute = TopLevelDestination.fromNavKey(startTab)?.route ?: startTab
return MultiBackstack(startTab, mutableStateOf(tabRoute))
}
@Test
fun `navigateTopLevel to different tab preserves previous tab stack and activates new tab stack`() {
val startTab = TopLevelDestination.Nodes.route
val multiBackstack = MultiBackstack(startTab)
val multiBackstack = createMultiBackstack(startTab)
val nodesStack =
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoute.Nodes)) }
@@ -48,7 +54,7 @@ class MultiBackstackTest {
@Test
fun `navigateTopLevel to same tab resets stack to root`() {
val startTab = TopLevelDestination.Nodes.route
val multiBackstack = MultiBackstack(startTab)
val multiBackstack = createMultiBackstack(startTab)
val nodesStack =
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoute.Nodes)) }
@@ -65,7 +71,7 @@ class MultiBackstackTest {
@Test
fun `goBack pops current stack if size is greater than 1`() {
val startTab = TopLevelDestination.Nodes.route
val multiBackstack = MultiBackstack(startTab)
val multiBackstack = createMultiBackstack(startTab)
val nodesStack =
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoute.Nodes)) }
@@ -80,7 +86,7 @@ class MultiBackstackTest {
@Test
fun `goBack on root of non-start tab returns to start tab`() {
val startTab = TopLevelDestination.Connections.route
val multiBackstack = MultiBackstack(startTab)
val multiBackstack = createMultiBackstack(startTab)
val mapStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Map.route)) }
val connectionsStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Connections.route)) }
@@ -99,7 +105,7 @@ class MultiBackstackTest {
@Test
fun `handleDeepLink sets target tab and populates stack`() {
val startTab = TopLevelDestination.Nodes.route
val multiBackstack = MultiBackstack(startTab)
val multiBackstack = createMultiBackstack(startTab)
val settingsStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Settings.route)) }
multiBackstack.backStacks = mapOf(TopLevelDestination.Settings.route to settingsStack)
@@ -116,7 +122,7 @@ class MultiBackstackTest {
fun `handleDeepLink from different tab switches tab and sets stack`() {
// Start on Connections tab
val startTab = TopLevelDestination.Connections.route
val multiBackstack = MultiBackstack(startTab)
val multiBackstack = createMultiBackstack(startTab)
val connectionsStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Connections.route)) }
val nodesStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route)) }
@@ -133,13 +139,13 @@ class MultiBackstackTest {
// Deep-link to a TracerouteMap on the Nodes tab (this is the exact pattern
// MeshtasticAppShell uses for traceroute alert "View on Map")
val tracerouteMap = NodeDetailRoute.TracerouteMap(destNum = 100, requestId = 42, logUuid = "abc")
multiBackstack.handleDeepLink(listOf(NodesRoute.NodesGraph, tracerouteMap))
multiBackstack.handleDeepLink(listOf(NodesRoute.Nodes, tracerouteMap))
// Should have switched to the Nodes tab
assertEquals(TopLevelDestination.Nodes.route, multiBackstack.currentTabRoute)
// Stack should contain the graph root + the traceroute map route
assertEquals(2, multiBackstack.activeBackStack.size)
assertEquals(NodesRoute.NodesGraph, multiBackstack.activeBackStack.first())
assertEquals(NodesRoute.Nodes, multiBackstack.activeBackStack.first())
assertEquals(tracerouteMap, multiBackstack.activeBackStack.last())
}
}

View File

@@ -26,21 +26,21 @@ class NavBackStackExtTest {
@Test
fun `replaceLast on non-empty list replaces the last element`() {
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
val stack = mutableListOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes)
stack.replaceLast(NodesRoute.NodeDetail(destNum = 42))
assertEquals(2, stack.size)
assertEquals(NodesRoute.NodesGraph, stack[0])
assertEquals(NodesRoute.Nodes, stack[0])
assertEquals(NodesRoute.NodeDetail(destNum = 42), stack[1])
}
@Test
fun `replaceLast on single-element list replaces that element`() {
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph)
stack.replaceLast(SettingsRoute.SettingsGraph())
val stack = mutableListOf<NavKey>(NodesRoute.Nodes)
stack.replaceLast(SettingsRoute.Settings())
assertEquals(1, stack.size)
assertEquals(SettingsRoute.SettingsGraph(), stack[0])
assertEquals(SettingsRoute.Settings(), stack[0])
}
@Test
@@ -55,7 +55,7 @@ class NavBackStackExtTest {
@Test
fun `replaceLast with same element does not mutate`() {
val route = NodesRoute.Nodes
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, route)
val stack = mutableListOf<NavKey>(NodesRoute.Nodes, route)
stack.replaceLast(route)
assertEquals(2, stack.size)
@@ -68,8 +68,8 @@ class NavBackStackExtTest {
@Test
fun `replaceAll replaces entire stack with new routes`() {
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
val newRoutes = listOf<NavKey>(SettingsRoute.SettingsGraph(), SettingsRoute.About)
val stack = mutableListOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes)
val newRoutes = listOf<NavKey>(SettingsRoute.Settings(), SettingsRoute.About)
stack.replaceAll(newRoutes)
@@ -78,19 +78,19 @@ class NavBackStackExtTest {
@Test
fun `replaceAll with shorter list trims excess elements`() {
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 42))
val newRoutes = listOf<NavKey>(SettingsRoute.SettingsGraph())
val stack = mutableListOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 42))
val newRoutes = listOf<NavKey>(SettingsRoute.Settings())
stack.replaceAll(newRoutes)
assertEquals(1, stack.size)
assertEquals(SettingsRoute.SettingsGraph(), stack[0])
assertEquals(SettingsRoute.Settings(), stack[0])
}
@Test
fun `replaceAll with longer list appends new elements`() {
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph)
val newRoutes = listOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 99))
val stack = mutableListOf<NavKey>(NodesRoute.Nodes)
val newRoutes = listOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 99))
stack.replaceAll(newRoutes)
@@ -99,7 +99,7 @@ class NavBackStackExtTest {
@Test
fun `replaceAll with empty list clears the stack`() {
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
val stack = mutableListOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes)
stack.replaceAll(emptyList())
@@ -109,7 +109,7 @@ class NavBackStackExtTest {
@Test
fun `replaceAll on empty stack with new routes populates it`() {
val stack = mutableListOf<NavKey>()
val newRoutes = listOf<NavKey>(ContactsRoute.ContactsGraph, ContactsRoute.Contacts)
val newRoutes = listOf<NavKey>(ContactsRoute.Contacts, ContactsRoute.Contacts)
stack.replaceAll(newRoutes)
@@ -118,7 +118,7 @@ class NavBackStackExtTest {
@Test
fun `replaceAll with identical routes does not mutate entries`() {
val routes = listOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
val routes = listOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes)
val stack = routes.toMutableList()
stack.replaceAll(routes)
@@ -128,17 +128,17 @@ class NavBackStackExtTest {
@Test
fun `replaceAll with partial overlap only changes differing elements`() {
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 1))
val stack = mutableListOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 1))
val newRoutes =
listOf<NavKey>(
NodesRoute.NodesGraph, // same
NodesRoute.Nodes, // same
SettingsRoute.About, // different
)
stack.replaceAll(newRoutes)
assertEquals(2, stack.size)
assertEquals(NodesRoute.NodesGraph, stack[0])
assertEquals(NodesRoute.Nodes, stack[0])
assertEquals(SettingsRoute.About, stack[1])
}

View File

@@ -38,13 +38,10 @@ class NavigationConfigTest {
private val allRouteInstances: List<NavKey> =
listOf(
// ChannelsRoute
ChannelsRoute.ChannelsGraph,
ChannelsRoute.Channels,
// ConnectionsRoute
ConnectionsRoute.ConnectionsGraph,
ConnectionsRoute.Connections,
// ContactsRoute
ContactsRoute.ContactsGraph,
ContactsRoute.Contacts,
ContactsRoute.Messages(contactKey = "test-contact", message = "hello"),
ContactsRoute.Messages(contactKey = "test-contact"),
@@ -54,10 +51,7 @@ class NavigationConfigTest {
MapRoute.Map(),
MapRoute.Map(waypointId = 42),
// NodesRoute
NodesRoute.NodesGraph,
NodesRoute.Nodes,
NodesRoute.NodeDetailGraph(destNum = 1234),
NodesRoute.NodeDetailGraph(),
NodesRoute.NodeDetail(destNum = 5678),
NodesRoute.NodeDetail(),
// NodeDetailRoute
@@ -73,8 +67,6 @@ class NavigationConfigTest {
NodeDetailRoute.PaxMetrics(destNum = 100),
NodeDetailRoute.NeighborInfoLog(destNum = 100),
// SettingsRoute
SettingsRoute.SettingsGraph(),
SettingsRoute.SettingsGraph(destNum = 999),
SettingsRoute.Settings(),
SettingsRoute.Settings(destNum = 999),
SettingsRoute.DeviceConfiguration,
@@ -183,9 +175,7 @@ class NavigationConfigTest {
val routesWithDefaults: List<Pair<NavKey, NavKey>> =
listOf(
MapRoute.Map() to MapRoute.Map(waypointId = null),
NodesRoute.NodeDetailGraph() to NodesRoute.NodeDetailGraph(destNum = null),
NodesRoute.NodeDetail() to NodesRoute.NodeDetail(destNum = null),
SettingsRoute.SettingsGraph() to SettingsRoute.SettingsGraph(destNum = null),
SettingsRoute.Settings() to SettingsRoute.Settings(destNum = null),
WifiProvisionRoute.WifiProvision() to WifiProvisionRoute.WifiProvision(address = null),
)

View File

@@ -127,7 +127,7 @@
<ID>PreviewPublic:SwitchPreference.kt:@Preview(showBackground = true) @Composable fun SwitchPreferencePreview</ID>
<ID>PreviewPublic:TextDividerPreference.kt:@Preview(showBackground = true) @Composable fun TextDividerPreferencePreview</ID>
<ID>PreviewPublic:TitledCard.kt:@PreviewLightDark @Composable fun TitledCardPreview</ID>
<ID>ViewModelForwarding:MeshtasticAppShell.kt:MeshtasticCommonAppSetup( uiViewModel = uiViewModel, onNavigateToTracerouteMap = { destNum, requestId, logUuid -> multiBackstack.handleDeepLink( listOf( NodesRoute.NodesGraph, NodeDetailRoute.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid), ), ) }, )</ID>
<ID>ViewModelForwarding:MeshtasticAppShell.kt:MeshtasticCommonAppSetup( uiViewModel = uiViewModel, onNavigateToTracerouteMap = { destNum, requestId, logUuid -> multiBackstack.handleDeepLink( listOf( NodesRoute.Nodes, NodeDetailRoute.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid), ), ) }, )</ID>
<ID>ViewModelForwarding:MeshtasticCommonAppSetup.kt:FirmwareVersionCheck(viewModel = uiViewModel)</ID>
<ID>ViewModelForwarding:MeshtasticCommonAppSetup.kt:SharedDialogs(uiViewModel = uiViewModel)</ID>
<ID>ViewModelForwarding:MeshtasticCommonAppSetup.kt:TracerouteAlertHandler(uiViewModel = uiViewModel, onNavigateToMap = onNavigateToTracerouteMap)</ID>

View File

@@ -46,7 +46,7 @@ fun MeshtasticAppShell(
onNavigateToTracerouteMap = { destNum, requestId, logUuid ->
multiBackstack.handleDeepLink(
listOf(
NodesRoute.NodesGraph,
NodesRoute.Nodes,
NodeDetailRoute.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid),
),
)

View File

@@ -18,6 +18,8 @@ package org.meshtastic.core.ui.component
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -68,7 +70,7 @@ fun MeshtasticNavDisplay(
/** Shared [NavDisplay] wrapper for a single backstack. */
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalSharedTransitionApi::class)
@Composable
fun MeshtasticNavDisplay(
backStack: NavBackStack<NavKey>,
@@ -115,28 +117,32 @@ fun MeshtasticNavDisplay(
val activeDecorators =
remember(backStack, saveableDecorator, vmStoreDecorator) { listOf(saveableDecorator, vmStoreDecorator) }
NavDisplay(
backStack = backStack,
entryProvider = entryProvider,
entryDecorators = activeDecorators,
onBack =
onBack
?: {
if (backStack.size > 1) {
backStack.removeLastOrNull()
}
},
sceneStrategies =
listOf(
DialogSceneStrategy(),
listDetailSceneStrategy,
supportingPaneSceneStrategy,
SinglePaneSceneStrategy(),
),
transitionSpec = meshtasticTransitionSpec(),
popTransitionSpec = meshtasticTransitionSpec(),
modifier = modifier,
)
SharedTransitionLayout {
NavDisplay(
backStack = backStack,
entryProvider = entryProvider,
entryDecorators = activeDecorators,
onBack =
onBack
?: {
if (backStack.size > 1) {
backStack.removeLastOrNull()
}
},
sceneStrategies =
listOf(
DialogSceneStrategy(),
listDetailSceneStrategy,
supportingPaneSceneStrategy,
SinglePaneSceneStrategy(),
),
sharedTransitionScope = this@SharedTransitionLayout,
transitionSpec = meshtasticTransitionSpec(),
popTransitionSpec = meshtasticTransitionSpec(),
predictivePopTransitionSpec = meshtasticPredictivePopTransitionSpec(),
modifier = modifier,
)
}
}
/** Shared crossfade [ContentTransform] used for both forward and pop navigation. */
@@ -146,3 +152,13 @@ private fun meshtasticTransitionSpec(): AnimatedContentTransitionScope<Scene<Nav
fadeOut(animationSpec = tween(TRANSITION_DURATION_MS)),
)
}
/** Crossfade transition for predictive back gestures (Android 14+). */
private fun meshtasticPredictivePopTransitionSpec():
AnimatedContentTransitionScope<Scene<NavKey>>.(Int) -> ContentTransform =
{
ContentTransform(
fadeIn(animationSpec = tween(TRANSITION_DURATION_MS)),
fadeOut(animationSpec = tween(TRANSITION_DURATION_MS)),
)
}

View File

@@ -141,7 +141,7 @@ private fun handleNavigation(
val currentKey = multiBackstack.activeBackStack.lastOrNull()
when (destination) {
TopLevelDestination.Nodes -> {
val onNodesList = currentKey is NodesRoute.NodesGraph || currentKey is NodesRoute.Nodes
val onNodesList = currentKey is NodesRoute.Nodes
if (!onNodesList) {
multiBackstack.navigateTopLevel(destination.route)
} else {
@@ -150,8 +150,7 @@ private fun handleNavigation(
}
TopLevelDestination.Conversations -> {
val onConversationsList =
currentKey is ContactsRoute.ContactsGraph || currentKey is ContactsRoute.Contacts
val onConversationsList = currentKey is ContactsRoute.Contacts
if (!onConversationsList) {
multiBackstack.navigateTopLevel(destination.route)
} else {

View File

@@ -41,11 +41,11 @@ class DesktopTopLevelDestinationParityTest {
val androidParityRoutes: Set<KClass<out Route>> =
setOf(
ContactsRoute.ContactsGraph::class,
NodesRoute.NodesGraph::class,
ContactsRoute.Contacts::class,
NodesRoute.Nodes::class,
MapRoute.Map::class,
SettingsRoute.SettingsGraph::class,
ConnectionsRoute.ConnectionsGraph::class,
SettingsRoute.Settings::class,
ConnectionsRoute.Connections::class,
)
assertEquals(

View File

@@ -28,16 +28,6 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
/** Navigation graph for for the top level ConnectionsScreen - [ConnectionsRoute.Connections]. */
fun EntryProviderScope<NavKey>.connectionsGraph(backStack: NavBackStack<NavKey>) {
entry<ConnectionsRoute.ConnectionsGraph> {
ConnectionsScreen(
scanModel = koinViewModel<ScannerViewModel>(),
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
onClickNodeChip = { id -> backStack.add(NodesRoute.NodeDetail(id)) },
onNavigateToNodeDetails = { id -> backStack.add(NodesRoute.NodeDetail(id)) },
onConfigNavigate = { route -> backStack.add(route) },
)
}
entry<ConnectionsRoute.Connections> {
ConnectionsScreen(
scanModel = koinViewModel<ScannerViewModel>(),

View File

@@ -35,6 +35,6 @@
<ID>PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun EditQuickChatDialogPreview</ID>
<ID>PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun QuickChatItemPreview</ID>
<ID>PreviewPublic:ReactionPreviews.kt:@PreviewLightDark @Composable fun ReactionItemPreview</ID>
<ID>ViewModelForwarding:AdaptiveContactsScreen.kt:ContactsScreen( onNavigateToShare = { backStack.add(ChannelsRoute.ChannelsGraph) }, sharedContactRequested = sharedContactRequested, requestChannelSet = requestChannelSet, onHandleDeepLink = onHandleDeepLink, onClearSharedContactRequested = onClearSharedContactRequested, onClearRequestChannelUrl = onClearRequestChannelUrl, viewModel = contactsViewModel, onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) }, onNavigateToMessages = { contactKey -> backStack.add(ContactsRoute.Messages(contactKey)) }, onNavigateToNodeDetails = { backStack.add(NodesRoute.NodeDetail(it)) }, scrollToTopEvents = scrollToTopEvents, activeContactKey = null, )</ID>
<ID>ViewModelForwarding:AdaptiveContactsScreen.kt:ContactsScreen( onNavigateToShare = { backStack.add(ChannelsRoute.Channels) }, sharedContactRequested = sharedContactRequested, requestChannelSet = requestChannelSet, onHandleDeepLink = onHandleDeepLink, onClearSharedContactRequested = onClearSharedContactRequested, onClearRequestChannelUrl = onClearRequestChannelUrl, viewModel = contactsViewModel, onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) }, onNavigateToMessages = { contactKey -> backStack.add(ContactsRoute.Messages(contactKey)) }, onNavigateToNodeDetails = { backStack.add(NodesRoute.NodeDetail(it)) }, scrollToTopEvents = scrollToTopEvents, activeContactKey = null, )</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -45,10 +45,6 @@ fun EntryProviderScope<NavKey>.contactsGraph(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent> = MutableSharedFlow(),
) {
entry<ContactsRoute.ContactsGraph>(metadata = { ListDetailSceneStrategy.listPane() }) {
ContactsEntryContent(backStack = backStack, scrollToTopEvents = scrollToTopEvents)
}
entry<ContactsRoute.Contacts>(metadata = { ListDetailSceneStrategy.listPane() }) {
ContactsEntryContent(backStack = backStack, scrollToTopEvents = scrollToTopEvents)
}

View File

@@ -40,7 +40,7 @@ fun AdaptiveContactsScreen(
onClearRequestChannelUrl: () -> Unit,
) {
ContactsScreen(
onNavigateToShare = { backStack.add(ChannelsRoute.ChannelsGraph) },
onNavigateToShare = { backStack.add(ChannelsRoute.Channels) },
sharedContactRequested = sharedContactRequested,
requestChannelSet = requestChannelSet,
onHandleDeepLink = onHandleDeepLink,

View File

@@ -39,7 +39,7 @@ fun AdaptiveNodeListScreen(
NodeListScreen(
viewModel = nodeListViewModel,
navigateToNodeDetails = { nodeId -> backStack.add(NodesRoute.NodeDetail(nodeId)) },
onNavigateToChannels = { backStack.add(ChannelsRoute.ChannelsGraph) },
onNavigateToChannels = { backStack.add(ChannelsRoute.Channels) },
scrollToTopEvents = scrollToTopEvents,
activeNodeId = null,
onHandleDeepLink = onHandleDeepLink,

View File

@@ -76,15 +76,6 @@ fun EntryProviderScope<NavKey>.nodesGraph(
onHandleDeepLink: (org.meshtastic.core.common.util.CommonUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
onNavigateToConnections: () -> Unit = {},
) {
entry<NodesRoute.NodesGraph>(metadata = { ListDetailSceneStrategy.listPane() }) {
AdaptiveNodeListScreen(
backStack = backStack,
scrollToTopEvents = scrollToTopEvents,
onHandleDeepLink = onHandleDeepLink,
onNavigateToConnections = onNavigateToConnections,
)
}
entry<NodesRoute.Nodes>(metadata = { ListDetailSceneStrategy.listPane() }) {
AdaptiveNodeListScreen(
backStack = backStack,
@@ -94,30 +85,16 @@ fun EntryProviderScope<NavKey>.nodesGraph(
)
}
nodeDetailGraph(backStack, scrollToTopEvents, onHandleDeepLink, onNavigateToConnections)
nodeDetailGraph(backStack)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Suppress("LongMethod")
fun EntryProviderScope<NavKey>.nodeDetailGraph(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent>,
onHandleDeepLink: (org.meshtastic.core.common.util.CommonUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
onNavigateToConnections: () -> Unit = {},
) {
entry<NodesRoute.NodeDetailGraph>(metadata = { ListDetailSceneStrategy.listPane() }) { args ->
AdaptiveNodeListScreen(
backStack = backStack,
scrollToTopEvents = scrollToTopEvents,
onHandleDeepLink = onHandleDeepLink,
onNavigateToConnections = onNavigateToConnections,
)
}
fun EntryProviderScope<NavKey>.nodeDetailGraph(backStack: NavBackStack<NavKey>) {
entry<NodesRoute.NodeDetail>(metadata = { ListDetailSceneStrategy.detailPane() }) { args ->
val nodeDetailViewModel: NodeDetailViewModel = koinViewModel()
val compassViewModel: CompassViewModel = koinViewModel()
val destNum = args.destNum ?: 0 // Handle nullable destNum if needed
val destNum = args.destNum ?: 0
NodeDetailScreen(
nodeId = destNum,
viewModel = nodeDetailViewModel,
@@ -149,7 +126,12 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
entry<NodeDetailRoute.TracerouteMap>(metadata = { ListDetailSceneStrategy.extraPane() }) { args ->
val tracerouteMapScreen = org.meshtastic.core.ui.util.LocalTracerouteMapScreenProvider.current
tracerouteMapScreen(args.destNum, args.requestId, args.logUuid) { backStack.removeLastOrNull() }
tracerouteMapScreen(
args.destNum,
args.requestId,
args.logUuid,
dropUnlessResumed { backStack.removeLastOrNull() },
)
}
NodeDetailScreen.entries.forEach { routeInfo ->

View File

@@ -73,14 +73,12 @@ import org.meshtastic.feature.settings.radio.component.UserConfigScreen
import kotlin.reflect.KClass
@Composable
fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): RadioConfigViewModel {
fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>, destNumOverride: Int? = null): RadioConfigViewModel {
val destNum =
remember(backStack.toList()) {
backStack.lastOrNull { it is SettingsRoute.Settings }?.let { (it as SettingsRoute.Settings).destNum }
?: backStack
.lastOrNull { it is SettingsRoute.SettingsGraph }
?.let { (it as SettingsRoute.SettingsGraph).destNum }
}
destNumOverride
?: remember(backStack.toList()) {
backStack.lastOrNull { it is SettingsRoute.Settings }?.let { (it as SettingsRoute.Settings).destNum }
}
return koinViewModel<RadioConfigViewModel>(key = destNum?.toString()) {
parametersOf(SavedStateHandle(mapOf("destNum" to destNum)))
}
@@ -88,22 +86,14 @@ fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): RadioConfigViewMod
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
entry<SettingsRoute.SettingsGraph> {
entry<SettingsRoute.Settings> { args ->
val isTabRoot = backStack.firstOrNull() == args
SettingsMainScreen(
settingsViewModel = koinViewModel(),
radioConfigViewModel = getRadioConfigViewModel(backStack),
radioConfigViewModel = getRadioConfigViewModel(backStack, destNumOverride = args.destNum),
onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) },
onNavigate = { backStack.add(it) },
)
}
entry<SettingsRoute.Settings> {
SettingsMainScreen(
settingsViewModel = koinViewModel(),
radioConfigViewModel = getRadioConfigViewModel(backStack),
onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) },
onNavigate = { backStack.add(it) },
onBack = dropUnlessResumed { backStack.removeLastOrNull() },
onBack = if (isTabRoot) null else dropUnlessResumed { backStack.removeLastOrNull() },
)
}
@@ -236,7 +226,7 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
}
}
entry<SettingsRoute.TakServer> { TakServerScreen(onBack = { backStack.removeLastOrNull() }) }
entry<SettingsRoute.TakServer> { TakServerScreen(onBack = dropUnlessResumed { backStack.removeLastOrNull() }) }
entry<SettingsRoute.DebugPanel> {
val viewModel: DebugViewModel = koinViewModel()

View File

@@ -26,14 +26,6 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
/** Navigation graph for for the top level ChannelScreen - [ChannelsRoute.Channels]. */
fun EntryProviderScope<NavKey>.channelsGraph(backStack: NavBackStack<NavKey>) {
entry<ChannelsRoute.ChannelsGraph> {
ChannelScreen(
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
onNavigate = { route -> backStack.add(route) },
onNavigateUp = dropUnlessResumed { backStack.removeLastOrNull() },
)
}
entry<ChannelsRoute.Channels> {
ChannelScreen(
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),