mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-24 06:40:37 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>(),
|
||||
|
||||
Reference in New Issue
Block a user