ci: cache Robolectric SDK jars to prevent flaky SocketException failures (#5045)

This commit is contained in:
James Rich 2026-04-10 10:05:07 -05:00 committed by GitHub
parent 93e0b9ca57
commit 1390a3cd4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 374 additions and 426 deletions

View file

@ -27,6 +27,15 @@ runs:
distribution: ${{ inputs.jdk_distribution }}
token: ${{ github.token }}
# Robolectric downloads instrumented SDK jars from Maven Central at test time.
# Cache them to avoid flaky SocketException failures on CI runners.
# Update the key when bumping robolectric version in libs.versions.toml or sdk in robolectric.properties.
- name: Cache Robolectric SDK jars
uses: actions/cache@v4
with:
path: ~/.m2/repository/org/robolectric
key: robolectric-4.16.1-sdk34
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:

View file

@ -41,7 +41,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
| `core:data` | Core manager implementations and data orchestration. |
| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`, `BleRadioInterface`). |
| `core:di` | Common DI qualifiers and dispatchers. |
| `core:navigation` | Shared navigation keys/routes for Navigation 3, `DeepLinkRouter` for typed backstack synthesis, and `MeshtasticNavSavedStateConfig` for backstack persistence. |
| `core:navigation` | Shared navigation keys/routes for Navigation 3 using `@Serializable sealed interface` hierarchies per feature domain (e.g., `SettingsRoute`, `NodesRoute`). `DeepLinkRouter` for typed backstack synthesis, and `MeshtasticNavSavedStateConfig` with `subclassesOfSealed()` for automatic polymorphic backstack persistence — new routes are registered at compile time. |
| `core:ui` | Shared Compose UI components (`MeshtasticAppShell`, `MeshtasticNavDisplay`, `MeshtasticNavigationSuite`, `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `MainAppBar`, dialogs, preferences) and platform abstractions. |
| `core:service` | KMP service layer; Android bindings stay in `androidMain`. |
| `core:api` | Public AIDL/API integration module for external clients. |
@ -189,6 +189,7 @@ Always run commands in the following order to ensure reliability. Do not attempt
- **`maxParallelForks` CI logic:** ProjectExtensions.kt line ~79 checks `project.findProperty("ci") == "true"` and uses full available processors in CI (4 forks on std runners) vs. half locally. All CI invocations pass `-Pci=true` to enable this.
- **Detekt report formats:** Detekt.kt line ~44 checks `project.findProperty("ci") == "true"` and disables html, txt, md reports in CI; only xml + sarif are required for GitHub reporting.
- **KMP Smoke Compile:** Use `./gradlew kmpSmokeCompile` instead of listing individual module compile tasks. The `kmpSmokeCompile` lifecycle task (registered in `RootConventionPlugin`) auto-discovers all KMP modules and depends on their `compileKotlinJvm` + `compileKotlinIosSimulatorArm64` tasks.
- **Robolectric SDK caching:** The `gradle-setup` composite action caches `~/.m2/repository/org/robolectric` to prevent flaky `SocketException` failures when Robolectric downloads instrumented SDK jars. The cache key is `robolectric-{version}-sdk{level}` — update it when bumping the Robolectric version in `libs.versions.toml` or the SDK level in `robolectric.properties` / `@Config(sdk = ...)`.
- **`mavenLocal()` gated:** The `mavenLocal()` repository is disabled by default to prevent CI cache poisoning. For local JitPack testing, pass `-PuseMavenLocal` to Gradle.
- **Terminal Pagers:** When running shell commands like `git diff` or `git log`, ALWAYS use `--no-pager` (e.g., `git --no-pager diff`) to prevent the agent from getting stuck in an interactive prompt.
- **Text Search:** Prefer using `rg` (ripgrep) over `grep` or `find` for fast text searching across the codebase.

View file

@ -32,7 +32,7 @@ import co.touchlab.kermit.Logger
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.BuildConfig
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.core.navigation.rememberMultiBackstack
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.app_too_old
@ -53,7 +53,7 @@ import org.meshtastic.feature.wifiprovision.navigation.wifiProvisionGraph
@Composable
fun MainScreen() {
val viewModel: UIViewModel = koinViewModel()
val multiBackstack = rememberMultiBackstack(NodesRoutes.NodesGraph)
val multiBackstack = rememberMultiBackstack(NodesRoute.NodesGraph)
val backStack = multiBackstack.activeBackStack
AndroidAppVersionCheck(viewModel)

View file

@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.emptyFlow
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.feature.connections.navigation.connectionsGraph
import org.meshtastic.feature.firmware.navigation.firmwareGraph
import org.meshtastic.feature.map.navigation.mapGraph
@ -44,7 +44,7 @@ class NavigationAssemblyTest {
@Test
fun verifyNavigationGraphsAssembleWithoutCrashing() {
composeTestRule.setContent {
val backStack = rememberNavBackStack(NodesRoutes.NodesGraph)
val backStack = rememberNavBackStack(NodesRoute.NodesGraph)
entryProvider<NavKey> {
contactsGraph(backStack, emptyFlow())
nodesGraph(backStack = backStack, scrollToTopEvents = emptyFlow())

View file

@ -12,7 +12,7 @@ Contains serializable `NavKey` route classes/objects used by shared feature grap
Parses Meshtastic deep-link URIs and synthesizes a typed backstack (for example `/nodes/1234/device-metrics`).
### 3. `NavigationConfig.kt`
Defines `MeshtasticNavSavedStateConfig` so Navigation 3 backstacks can be persisted/restored safely.
Defines `MeshtasticNavSavedStateConfig` using sealed interface hierarchies so Navigation 3 backstacks can be persisted/restored safely — new routes are auto-registered at compile time.
## Features
- **Type-Safety**: Uses serializable `NavKey` routes instead of ad-hoc string routes.
@ -25,10 +25,10 @@ Feature modules depend on this module to define their entry points and navigate
```kotlin
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.NodesRoute
fun openNodeDetail(backStack: NavBackStack<NavKey>, destNum: Int) {
backStack.add(NodesRoutes.NodeDetail(destNum))
backStack.add(NodesRoute.NodeDetail(destNum))
}
```

View file

@ -59,11 +59,11 @@ object DeepLinkRouter {
"messages",
"quickchat",
-> routeContacts(uri, pathSegments)
"connections" -> listOf(ConnectionsRoutes.ConnectionsGraph)
"connections" -> listOf(ConnectionsRoute.ConnectionsGraph)
"map" -> routeMap(uri, pathSegments)
"nodes" -> routeNodes(uri, pathSegments)
"settings" -> routeSettings(pathSegments)
"channels" -> listOf(ChannelsRoutes.ChannelsGraph)
"channels" -> listOf(ChannelsRoute.ChannelsGraph)
"firmware" -> routeFirmware(pathSegments)
"wifi-provision" -> routeWifiProvision(uri)
else -> {
@ -78,31 +78,31 @@ object DeepLinkRouter {
return when (firstSegment) {
"share" -> {
val message = uri.getQueryParameter("message") ?: ""
listOf(ContactsRoutes.ContactsGraph, ContactsRoutes.Share(message))
listOf(ContactsRoute.ContactsGraph, ContactsRoute.Share(message))
}
"quickchat" -> {
listOf(ContactsRoutes.ContactsGraph, ContactsRoutes.QuickChat)
listOf(ContactsRoute.ContactsGraph, ContactsRoute.QuickChat)
}
"messages" -> {
val contactKey = if (segments.size > 1) segments[1] else uri.getQueryParameter("contactKey") ?: ""
val message = uri.getQueryParameter("message") ?: ""
if (contactKey.isNotBlank()) {
listOf(
ContactsRoutes.ContactsGraph,
ContactsRoutes.Messages(contactKey = contactKey, message = message),
ContactsRoute.ContactsGraph,
ContactsRoute.Messages(contactKey = contactKey, message = message),
)
} else {
listOf(ContactsRoutes.ContactsGraph)
listOf(ContactsRoute.ContactsGraph)
}
}
else -> listOf(ContactsRoutes.ContactsGraph)
else -> listOf(ContactsRoute.ContactsGraph)
}
}
private fun routeMap(uri: CommonUri, segments: List<String>): List<NavKey> {
val waypointIdStr = if (segments.size > 1) segments[1] else uri.getQueryParameter("waypointId")
val waypointId = waypointIdStr?.toIntOrNull()
return listOf(MapRoutes.Map(waypointId))
return listOf(MapRoute.Map(waypointId))
}
private fun routeNodes(uri: CommonUri, segments: List<String>): List<NavKey> {
@ -110,17 +110,17 @@ object DeepLinkRouter {
val destNum = destNumStr?.toIntOrNull()
return if (destNum == null) {
listOf(NodesRoutes.NodesGraph)
listOf(NodesRoute.NodesGraph)
} else if (segments.size > 2) {
val subRouteStr = segments[2].lowercase()
val detailRouteFn = nodeDetailSubRoutes[subRouteStr]
if (detailRouteFn != null) {
listOf(NodesRoutes.NodesGraph, NodesRoutes.NodeDetailGraph(destNum), detailRouteFn(destNum))
listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetailGraph(destNum), detailRouteFn(destNum))
} else {
listOf(NodesRoutes.NodesGraph, NodesRoutes.NodeDetail(destNum))
listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum))
}
} else {
listOf(NodesRoutes.NodesGraph, NodesRoutes.NodeDetail(destNum))
listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum))
}
}
@ -142,79 +142,79 @@ object DeepLinkRouter {
}
if (subRouteStr == null) {
return listOf(SettingsRoutes.SettingsGraph(destNum))
return listOf(SettingsRoute.SettingsGraph(destNum))
}
val subRoute = settingsSubRoutes[subRouteStr]
return if (subRoute != null) {
listOf(SettingsRoutes.SettingsGraph(destNum), subRoute)
listOf(SettingsRoute.SettingsGraph(destNum), subRoute)
} else {
listOf(SettingsRoutes.SettingsGraph(destNum))
listOf(SettingsRoute.SettingsGraph(destNum))
}
}
private fun routeWifiProvision(uri: CommonUri): List<NavKey> {
val address = uri.getQueryParameter("address")
return listOf(WifiProvisionRoutes.WifiProvision(address))
return listOf(WifiProvisionRoute.WifiProvision(address))
}
private fun routeFirmware(segments: List<String>): List<NavKey> {
val update = if (segments.size > 1) segments[1].lowercase() == "update" else false
return if (update) {
listOf(FirmwareRoutes.FirmwareGraph, FirmwareRoutes.FirmwareUpdate)
listOf(FirmwareRoute.FirmwareGraph, FirmwareRoute.FirmwareUpdate)
} else {
listOf(FirmwareRoutes.FirmwareGraph)
listOf(FirmwareRoute.FirmwareGraph)
}
}
private val settingsSubRoutes: Map<String, Route> =
mapOf(
"device-config" to SettingsRoutes.DeviceConfiguration,
"module-config" to SettingsRoutes.ModuleConfiguration,
"admin" to SettingsRoutes.Administration,
"user" to SettingsRoutes.User,
"channel" to SettingsRoutes.ChannelConfig,
"device" to SettingsRoutes.Device,
"position" to SettingsRoutes.Position,
"power" to SettingsRoutes.Power,
"network" to SettingsRoutes.Network,
"display" to SettingsRoutes.Display,
"lora" to SettingsRoutes.LoRa,
"bluetooth" to SettingsRoutes.Bluetooth,
"security" to SettingsRoutes.Security,
"mqtt" to SettingsRoutes.MQTT,
"serial" to SettingsRoutes.Serial,
"ext-notification" to SettingsRoutes.ExtNotification,
"store-forward" to SettingsRoutes.StoreForward,
"range-test" to SettingsRoutes.RangeTest,
"telemetry" to SettingsRoutes.Telemetry,
"canned-message" to SettingsRoutes.CannedMessage,
"audio" to SettingsRoutes.Audio,
"remote-hardware" to SettingsRoutes.RemoteHardware,
"neighbor-info" to SettingsRoutes.NeighborInfo,
"ambient-lighting" to SettingsRoutes.AmbientLighting,
"detection-sensor" to SettingsRoutes.DetectionSensor,
"paxcounter" to SettingsRoutes.Paxcounter,
"status-message" to SettingsRoutes.StatusMessage,
"traffic-management" to SettingsRoutes.TrafficManagement,
"tak" to SettingsRoutes.TAK,
"clean-node-db" to SettingsRoutes.CleanNodeDb,
"debug-panel" to SettingsRoutes.DebugPanel,
"about" to SettingsRoutes.About,
"filter-settings" to SettingsRoutes.FilterSettings,
"device-config" to SettingsRoute.DeviceConfiguration,
"module-config" to SettingsRoute.ModuleConfiguration,
"admin" to SettingsRoute.Administration,
"user" to SettingsRoute.User,
"channel" to SettingsRoute.ChannelConfig,
"device" to SettingsRoute.Device,
"position" to SettingsRoute.Position,
"power" to SettingsRoute.Power,
"network" to SettingsRoute.Network,
"display" to SettingsRoute.Display,
"lora" to SettingsRoute.LoRa,
"bluetooth" to SettingsRoute.Bluetooth,
"security" to SettingsRoute.Security,
"mqtt" to SettingsRoute.MQTT,
"serial" to SettingsRoute.Serial,
"ext-notification" to SettingsRoute.ExtNotification,
"store-forward" to SettingsRoute.StoreForward,
"range-test" to SettingsRoute.RangeTest,
"telemetry" to SettingsRoute.Telemetry,
"canned-message" to SettingsRoute.CannedMessage,
"audio" to SettingsRoute.Audio,
"remote-hardware" to SettingsRoute.RemoteHardware,
"neighbor-info" to SettingsRoute.NeighborInfo,
"ambient-lighting" to SettingsRoute.AmbientLighting,
"detection-sensor" to SettingsRoute.DetectionSensor,
"paxcounter" to SettingsRoute.Paxcounter,
"status-message" to SettingsRoute.StatusMessage,
"traffic-management" to SettingsRoute.TrafficManagement,
"tak" to SettingsRoute.TAK,
"clean-node-db" to SettingsRoute.CleanNodeDb,
"debug-panel" to SettingsRoute.DebugPanel,
"about" to SettingsRoute.About,
"filter-settings" to SettingsRoute.FilterSettings,
)
private val nodeDetailSubRoutes: Map<String, (Int) -> Route> =
mapOf(
"device-metrics" to { destNum -> NodeDetailRoutes.DeviceMetrics(destNum) },
"map" to { destNum -> NodeDetailRoutes.NodeMap(destNum) },
"position" to { destNum -> NodeDetailRoutes.PositionLog(destNum) },
"environment" to { destNum -> NodeDetailRoutes.EnvironmentMetrics(destNum) },
"signal" to { destNum -> NodeDetailRoutes.SignalMetrics(destNum) },
"power" to { destNum -> NodeDetailRoutes.PowerMetrics(destNum) },
"traceroute" to { destNum -> NodeDetailRoutes.TracerouteLog(destNum) },
"host-metrics" to { destNum -> NodeDetailRoutes.HostMetricsLog(destNum) },
"pax" to { destNum -> NodeDetailRoutes.PaxMetrics(destNum) },
"neighbors" to { destNum -> NodeDetailRoutes.NeighborInfoLog(destNum) },
"device-metrics" to { destNum -> NodeDetailRoute.DeviceMetrics(destNum) },
"map" to { destNum -> NodeDetailRoute.NodeMap(destNum) },
"position" to { destNum -> NodeDetailRoute.PositionLog(destNum) },
"environment" to { destNum -> NodeDetailRoute.EnvironmentMetrics(destNum) },
"signal" to { destNum -> NodeDetailRoute.SignalMetrics(destNum) },
"power" to { destNum -> NodeDetailRoute.PowerMetrics(destNum) },
"traceroute" to { destNum -> NodeDetailRoute.TracerouteLog(destNum) },
"host-metrics" to { destNum -> NodeDetailRoute.HostMetricsLog(destNum) },
"pax" to { destNum -> NodeDetailRoute.PaxMetrics(destNum) },
"neighbors" to { destNum -> NodeDetailRoute.NeighborInfoLog(destNum) },
)
}

View file

@ -18,99 +18,28 @@ package org.meshtastic.core.navigation
import androidx.navigation3.runtime.NavKey
import androidx.savedstate.serialization.SavedStateConfiguration
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclassesOfSealed
/**
* Shared polymorphic serialization configuration for Navigation 3 saved-state support. Registers all route types used
* across Android and Desktop navigation graphs.
* Shared polymorphic serialization configuration for Navigation 3 saved-state support. Uses sealed interface
* hierarchies so that new routes are automatically registered at compile time no manual `subclass()` calls needed.
*/
@OptIn(ExperimentalSerializationApi::class)
val MeshtasticNavSavedStateConfig = SavedStateConfiguration {
serializersModule = SerializersModule {
polymorphic(NavKey::class) {
// Nodes
subclass(NodesRoutes.NodesGraph::class, NodesRoutes.NodesGraph.serializer())
subclass(NodesRoutes.Nodes::class, NodesRoutes.Nodes.serializer())
subclass(NodesRoutes.NodeDetailGraph::class, NodesRoutes.NodeDetailGraph.serializer())
subclass(NodesRoutes.NodeDetail::class, NodesRoutes.NodeDetail.serializer())
// Node detail sub-screens
subclass(NodeDetailRoutes.DeviceMetrics::class, NodeDetailRoutes.DeviceMetrics.serializer())
subclass(NodeDetailRoutes.NodeMap::class, NodeDetailRoutes.NodeMap.serializer())
subclass(NodeDetailRoutes.PositionLog::class, NodeDetailRoutes.PositionLog.serializer())
subclass(NodeDetailRoutes.EnvironmentMetrics::class, NodeDetailRoutes.EnvironmentMetrics.serializer())
subclass(NodeDetailRoutes.SignalMetrics::class, NodeDetailRoutes.SignalMetrics.serializer())
subclass(NodeDetailRoutes.PowerMetrics::class, NodeDetailRoutes.PowerMetrics.serializer())
subclass(NodeDetailRoutes.TracerouteLog::class, NodeDetailRoutes.TracerouteLog.serializer())
subclass(NodeDetailRoutes.TracerouteMap::class, NodeDetailRoutes.TracerouteMap.serializer())
subclass(NodeDetailRoutes.HostMetricsLog::class, NodeDetailRoutes.HostMetricsLog.serializer())
subclass(NodeDetailRoutes.PaxMetrics::class, NodeDetailRoutes.PaxMetrics.serializer())
subclass(NodeDetailRoutes.NeighborInfoLog::class, NodeDetailRoutes.NeighborInfoLog.serializer())
// Conversations
subclass(ContactsRoutes.ContactsGraph::class, ContactsRoutes.ContactsGraph.serializer())
subclass(ContactsRoutes.Contacts::class, ContactsRoutes.Contacts.serializer())
subclass(ContactsRoutes.Messages::class, ContactsRoutes.Messages.serializer())
subclass(ContactsRoutes.Share::class, ContactsRoutes.Share.serializer())
subclass(ContactsRoutes.QuickChat::class, ContactsRoutes.QuickChat.serializer())
// Map
subclass(MapRoutes.Map::class, MapRoutes.Map.serializer())
// Firmware
subclass(FirmwareRoutes.FirmwareGraph::class, FirmwareRoutes.FirmwareGraph.serializer())
subclass(FirmwareRoutes.FirmwareUpdate::class, FirmwareRoutes.FirmwareUpdate.serializer())
// Settings
subclass(SettingsRoutes.SettingsGraph::class, SettingsRoutes.SettingsGraph.serializer())
subclass(SettingsRoutes.Settings::class, SettingsRoutes.Settings.serializer())
subclass(SettingsRoutes.DeviceConfiguration::class, SettingsRoutes.DeviceConfiguration.serializer())
subclass(SettingsRoutes.ModuleConfiguration::class, SettingsRoutes.ModuleConfiguration.serializer())
subclass(SettingsRoutes.Administration::class, SettingsRoutes.Administration.serializer())
// Settings - Config routes
subclass(SettingsRoutes.User::class, SettingsRoutes.User.serializer())
subclass(SettingsRoutes.ChannelConfig::class, SettingsRoutes.ChannelConfig.serializer())
subclass(SettingsRoutes.Device::class, SettingsRoutes.Device.serializer())
subclass(SettingsRoutes.Position::class, SettingsRoutes.Position.serializer())
subclass(SettingsRoutes.Power::class, SettingsRoutes.Power.serializer())
subclass(SettingsRoutes.Network::class, SettingsRoutes.Network.serializer())
subclass(SettingsRoutes.Display::class, SettingsRoutes.Display.serializer())
subclass(SettingsRoutes.LoRa::class, SettingsRoutes.LoRa.serializer())
subclass(SettingsRoutes.Bluetooth::class, SettingsRoutes.Bluetooth.serializer())
subclass(SettingsRoutes.Security::class, SettingsRoutes.Security.serializer())
// Settings - Module routes
subclass(SettingsRoutes.MQTT::class, SettingsRoutes.MQTT.serializer())
subclass(SettingsRoutes.Serial::class, SettingsRoutes.Serial.serializer())
subclass(SettingsRoutes.ExtNotification::class, SettingsRoutes.ExtNotification.serializer())
subclass(SettingsRoutes.StoreForward::class, SettingsRoutes.StoreForward.serializer())
subclass(SettingsRoutes.RangeTest::class, SettingsRoutes.RangeTest.serializer())
subclass(SettingsRoutes.Telemetry::class, SettingsRoutes.Telemetry.serializer())
subclass(SettingsRoutes.CannedMessage::class, SettingsRoutes.CannedMessage.serializer())
subclass(SettingsRoutes.Audio::class, SettingsRoutes.Audio.serializer())
subclass(SettingsRoutes.RemoteHardware::class, SettingsRoutes.RemoteHardware.serializer())
subclass(SettingsRoutes.NeighborInfo::class, SettingsRoutes.NeighborInfo.serializer())
subclass(SettingsRoutes.AmbientLighting::class, SettingsRoutes.AmbientLighting.serializer())
subclass(SettingsRoutes.DetectionSensor::class, SettingsRoutes.DetectionSensor.serializer())
subclass(SettingsRoutes.Paxcounter::class, SettingsRoutes.Paxcounter.serializer())
subclass(SettingsRoutes.StatusMessage::class, SettingsRoutes.StatusMessage.serializer())
subclass(SettingsRoutes.TrafficManagement::class, SettingsRoutes.TrafficManagement.serializer())
subclass(SettingsRoutes.TAK::class, SettingsRoutes.TAK.serializer())
// Settings - Advanced routes
subclass(SettingsRoutes.CleanNodeDb::class, SettingsRoutes.CleanNodeDb.serializer())
subclass(SettingsRoutes.DebugPanel::class, SettingsRoutes.DebugPanel.serializer())
subclass(SettingsRoutes.About::class, SettingsRoutes.About.serializer())
subclass(SettingsRoutes.FilterSettings::class, SettingsRoutes.FilterSettings.serializer())
// Channels
subclass(ChannelsRoutes.ChannelsGraph::class, ChannelsRoutes.ChannelsGraph.serializer())
subclass(ChannelsRoutes.Channels::class, ChannelsRoutes.Channels.serializer())
// Connections
subclass(ConnectionsRoutes.ConnectionsGraph::class, ConnectionsRoutes.ConnectionsGraph.serializer())
subclass(ConnectionsRoutes.Connections::class, ConnectionsRoutes.Connections.serializer())
subclassesOfSealed<ChannelsRoute>()
subclassesOfSealed<ConnectionsRoute>()
subclassesOfSealed<ContactsRoute>()
subclassesOfSealed<MapRoute>()
subclassesOfSealed<NodesRoute>()
subclassesOfSealed<NodeDetailRoute>()
subclassesOfSealed<SettingsRoute>()
subclassesOfSealed<FirmwareRoute>()
subclassesOfSealed<WifiProvisionRoute>()
}
}
}

View file

@ -25,160 +25,169 @@ interface Route : NavKey
interface Graph : Route
object ChannelsRoutes {
@Serializable data object ChannelsGraph : Graph
@Serializable
sealed interface ChannelsRoute : Route {
@Serializable data object ChannelsGraph : ChannelsRoute, Graph
@Serializable data object Channels : Route
@Serializable data object Channels : ChannelsRoute
}
object ConnectionsRoutes {
@Serializable data object ConnectionsGraph : Graph
@Serializable
sealed interface ConnectionsRoute : Route {
@Serializable data object ConnectionsGraph : ConnectionsRoute, Graph
@Serializable data object Connections : Route
@Serializable data object Connections : ConnectionsRoute
}
object ContactsRoutes {
@Serializable data object ContactsGraph : Graph
@Serializable
sealed interface ContactsRoute : Route {
@Serializable data object ContactsGraph : ContactsRoute, Graph
@Serializable data object Contacts : Route
@Serializable data object Contacts : ContactsRoute
@Serializable data class Messages(val contactKey: String, val message: String = "") : Route
@Serializable data class Messages(val contactKey: String, val message: String = "") : ContactsRoute
@Serializable data class Share(val message: String) : Route
@Serializable data class Share(val message: String) : ContactsRoute
@Serializable data object QuickChat : Route
@Serializable data object QuickChat : ContactsRoute
}
object MapRoutes {
@Serializable data class Map(val waypointId: Int? = null) : Route
@Serializable
sealed interface MapRoute : Route {
@Serializable data class Map(val waypointId: Int? = null) : MapRoute
}
object NodesRoutes {
@Serializable data object NodesGraph : Graph
@Serializable
sealed interface NodesRoute : Route {
@Serializable data object NodesGraph : NodesRoute, Graph
@Serializable data object Nodes : Route
@Serializable data object Nodes : NodesRoute
@Serializable data class NodeDetailGraph(val destNum: Int? = null) : Graph
@Serializable data class NodeDetailGraph(val destNum: Int? = null) : NodesRoute, Graph
@Serializable data class NodeDetail(val destNum: Int? = null) : Route
@Serializable data class NodeDetail(val destNum: Int? = null) : NodesRoute
}
object NodeDetailRoutes {
@Serializable data class DeviceMetrics(val destNum: Int) : Route
@Serializable
sealed interface NodeDetailRoute : Route {
@Serializable data class DeviceMetrics(val destNum: Int) : NodeDetailRoute
@Serializable data class NodeMap(val destNum: Int) : Route
@Serializable data class NodeMap(val destNum: Int) : NodeDetailRoute
@Serializable data class PositionLog(val destNum: Int) : Route
@Serializable data class PositionLog(val destNum: Int) : NodeDetailRoute
@Serializable data class EnvironmentMetrics(val destNum: Int) : Route
@Serializable data class EnvironmentMetrics(val destNum: Int) : NodeDetailRoute
@Serializable data class SignalMetrics(val destNum: Int) : Route
@Serializable data class SignalMetrics(val destNum: Int) : NodeDetailRoute
@Serializable data class PowerMetrics(val destNum: Int) : Route
@Serializable data class PowerMetrics(val destNum: Int) : NodeDetailRoute
@Serializable data class TracerouteLog(val destNum: Int) : Route
@Serializable data class TracerouteLog(val destNum: Int) : NodeDetailRoute
@Serializable data class TracerouteMap(val destNum: Int, val requestId: Int, val logUuid: String? = null) : Route
@Serializable data class TracerouteMap(val destNum: Int, val requestId: Int, val logUuid: String? = null) : NodeDetailRoute
@Serializable data class HostMetricsLog(val destNum: Int) : Route
@Serializable data class HostMetricsLog(val destNum: Int) : NodeDetailRoute
@Serializable data class PaxMetrics(val destNum: Int) : Route
@Serializable data class PaxMetrics(val destNum: Int) : NodeDetailRoute
@Serializable data class NeighborInfoLog(val destNum: Int) : Route
@Serializable data class NeighborInfoLog(val destNum: Int) : NodeDetailRoute
}
object SettingsRoutes {
@Serializable data class SettingsGraph(val destNum: Int? = null) : Graph
@Serializable
sealed interface SettingsRoute : Route {
@Serializable data class SettingsGraph(val destNum: Int? = null) : SettingsRoute, Graph
@Serializable data class Settings(val destNum: Int? = null) : Route
@Serializable data class Settings(val destNum: Int? = null) : SettingsRoute
@Serializable data object DeviceConfiguration : Route
@Serializable data object DeviceConfiguration : SettingsRoute
@Serializable data object ModuleConfiguration : Route
@Serializable data object ModuleConfiguration : SettingsRoute
@Serializable data object Administration : Route
@Serializable data object Administration : SettingsRoute
// region radio Config Routes
@Serializable data object User : Route
@Serializable data object User : SettingsRoute
@Serializable data object ChannelConfig : Route
@Serializable data object ChannelConfig : SettingsRoute
@Serializable data object Device : Route
@Serializable data object Device : SettingsRoute
@Serializable data object Position : Route
@Serializable data object Position : SettingsRoute
@Serializable data object Power : Route
@Serializable data object Power : SettingsRoute
@Serializable data object Network : Route
@Serializable data object Network : SettingsRoute
@Serializable data object Display : Route
@Serializable data object Display : SettingsRoute
@Serializable data object LoRa : Route
@Serializable data object LoRa : SettingsRoute
@Serializable data object Bluetooth : Route
@Serializable data object Bluetooth : SettingsRoute
@Serializable data object Security : Route
@Serializable data object Security : SettingsRoute
// endregion
// region module config routes
@Serializable data object MQTT : Route
@Serializable data object MQTT : SettingsRoute
@Serializable data object Serial : Route
@Serializable data object Serial : SettingsRoute
@Serializable data object ExtNotification : Route
@Serializable data object ExtNotification : SettingsRoute
@Serializable data object StoreForward : Route
@Serializable data object StoreForward : SettingsRoute
@Serializable data object RangeTest : Route
@Serializable data object RangeTest : SettingsRoute
@Serializable data object Telemetry : Route
@Serializable data object Telemetry : SettingsRoute
@Serializable data object CannedMessage : Route
@Serializable data object CannedMessage : SettingsRoute
@Serializable data object Audio : Route
@Serializable data object Audio : SettingsRoute
@Serializable data object RemoteHardware : Route
@Serializable data object RemoteHardware : SettingsRoute
@Serializable data object NeighborInfo : Route
@Serializable data object NeighborInfo : SettingsRoute
@Serializable data object AmbientLighting : Route
@Serializable data object AmbientLighting : SettingsRoute
@Serializable data object DetectionSensor : Route
@Serializable data object DetectionSensor : SettingsRoute
@Serializable data object Paxcounter : Route
@Serializable data object Paxcounter : SettingsRoute
@Serializable data object StatusMessage : Route
@Serializable data object StatusMessage : SettingsRoute
@Serializable data object TrafficManagement : Route
@Serializable data object TrafficManagement : SettingsRoute
@Serializable data object TAK : Route
@Serializable data object TAK : SettingsRoute
// endregion
// region advanced config routes
@Serializable data object CleanNodeDb : Route
@Serializable data object CleanNodeDb : SettingsRoute
@Serializable data object DebugPanel : Route
@Serializable data object DebugPanel : SettingsRoute
@Serializable data object About : Route
@Serializable data object About : SettingsRoute
@Serializable data object FilterSettings : Route
@Serializable data object FilterSettings : SettingsRoute
// endregion
}
object FirmwareRoutes {
@Serializable data object FirmwareGraph : Graph
@Serializable
sealed interface FirmwareRoute : Route {
@Serializable data object FirmwareGraph : FirmwareRoute, Graph
@Serializable data object FirmwareUpdate : Route
@Serializable data object FirmwareUpdate : FirmwareRoute
}
object WifiProvisionRoutes {
@Serializable data object WifiProvisionGraph : Graph
@Serializable
sealed interface WifiProvisionRoute : Route {
@Serializable data object WifiProvisionGraph : WifiProvisionRoute, Graph
@Serializable data class WifiProvision(val address: String? = null) : Route
@Serializable data class WifiProvision(val address: String? = null) : WifiProvisionRoute
}

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, ContactsRoutes.ContactsGraph),
Nodes(Res.string.nodes, NodesRoutes.NodesGraph),
Map(Res.string.map, MapRoutes.Map()),
Settings(Res.string.bottom_nav_settings, SettingsRoutes.SettingsGraph()),
Connections(Res.string.connections, ConnectionsRoutes.ConnectionsGraph),
Conversations(Res.string.conversations, ContactsRoute.ContactsGraph),
Nodes(Res.string.nodes, NodesRoute.NodesGraph),
Map(Res.string.map, MapRoute.Map()),
Settings(Res.string.bottom_nav_settings, SettingsRoute.SettingsGraph()),
Connections(Res.string.connections, ConnectionsRoute.ConnectionsGraph),
;
companion object {

View file

@ -29,7 +29,7 @@ class MultiBackstackTest {
val multiBackstack = MultiBackstack(startTab)
val nodesStack =
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoutes.Nodes)) }
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoute.Nodes)) }
val mapStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Map.route)) }
multiBackstack.backStacks =
@ -51,7 +51,7 @@ class MultiBackstackTest {
val multiBackstack = MultiBackstack(startTab)
val nodesStack =
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoutes.Nodes)) }
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoute.Nodes)) }
multiBackstack.backStacks = mapOf(TopLevelDestination.Nodes.route to nodesStack)
assertEquals(2, multiBackstack.activeBackStack.size)
@ -68,7 +68,7 @@ class MultiBackstackTest {
val multiBackstack = MultiBackstack(startTab)
val nodesStack =
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoutes.Nodes)) }
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoute.Nodes)) }
multiBackstack.backStacks = mapOf(TopLevelDestination.Nodes.route to nodesStack)
multiBackstack.goBack()
@ -104,11 +104,11 @@ class MultiBackstackTest {
val settingsStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Settings.route)) }
multiBackstack.backStacks = mapOf(TopLevelDestination.Settings.route to settingsStack)
val deepLinkPath = listOf(TopLevelDestination.Settings.route, SettingsRoutes.About)
val deepLinkPath = listOf(TopLevelDestination.Settings.route, SettingsRoute.About)
multiBackstack.handleDeepLink(deepLinkPath)
assertEquals(TopLevelDestination.Settings.route, multiBackstack.currentTabRoute)
assertEquals(2, multiBackstack.activeBackStack.size)
assertEquals(SettingsRoutes.About, multiBackstack.activeBackStack.last())
assertEquals(SettingsRoute.About, multiBackstack.activeBackStack.last())
}
}

View file

@ -20,7 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import org.meshtastic.core.navigation.MultiBackstack
import org.meshtastic.core.navigation.NodeDetailRoutes
import org.meshtastic.core.navigation.NodeDetailRoute
import org.meshtastic.core.ui.viewmodel.UIViewModel
/**
@ -44,7 +44,7 @@ fun MeshtasticAppShell(
uiViewModel = uiViewModel,
onNavigateToTracerouteMap = { destNum, requestId, logUuid ->
multiBackstack.activeBackStack.add(
NodeDetailRoutes.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid),
NodeDetailRoute.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid),
)
},
)

View file

@ -50,9 +50,9 @@ import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.ContactsRoute
import org.meshtastic.core.navigation.MultiBackstack
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.core.navigation.TopLevelDestination
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.connected
@ -141,7 +141,7 @@ private fun handleNavigation(
val currentKey = multiBackstack.activeBackStack.lastOrNull()
when (destination) {
TopLevelDestination.Nodes -> {
val onNodesList = currentKey is NodesRoutes.NodesGraph || currentKey is NodesRoutes.Nodes
val onNodesList = currentKey is NodesRoute.NodesGraph || currentKey is NodesRoute.Nodes
if (!onNodesList) {
multiBackstack.navigateTopLevel(destination.route)
} else {
@ -150,7 +150,7 @@ private fun handleNavigation(
}
TopLevelDestination.Conversations -> {
val onConversationsList =
currentKey is ContactsRoutes.ContactsGraph || currentKey is ContactsRoutes.Contacts
currentKey is ContactsRoute.ContactsGraph || currentKey is ContactsRoute.Contacts
if (!onConversationsList) {
multiBackstack.navigateTopLevel(destination.route)
} else {

View file

@ -62,7 +62,7 @@ import org.jetbrains.skia.Image
import org.koin.core.context.startKoin
import org.meshtastic.core.common.util.MeshtasticUri
import org.meshtastic.core.database.desktopDataDir
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.navigation.TopLevelDestination
import org.meshtastic.core.navigation.rememberMultiBackstack
import org.meshtastic.core.repository.UiPrefs
@ -241,7 +241,7 @@ fun main(args: Array<String>) = application(exitProcessOnExit = false) {
true
}
event.key == Key.Slash -> {
backStack.add(SettingsRoutes.About)
backStack.add(SettingsRoute.About)
true
}
else -> false

View file

@ -16,13 +16,13 @@
*/
package org.meshtastic.desktop.ui
import org.meshtastic.core.navigation.ConnectionsRoutes
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.FirmwareRoutes
import org.meshtastic.core.navigation.MapRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.ConnectionsRoute
import org.meshtastic.core.navigation.ContactsRoute
import org.meshtastic.core.navigation.FirmwareRoute
import org.meshtastic.core.navigation.MapRoute
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.navigation.TopLevelDestination
import kotlin.reflect.KClass
import kotlin.test.Test
@ -41,11 +41,11 @@ class DesktopTopLevelDestinationParityTest {
val androidParityRoutes: Set<KClass<out Route>> =
setOf(
ContactsRoutes.ContactsGraph::class,
NodesRoutes.NodesGraph::class,
MapRoutes.Map::class,
SettingsRoutes.SettingsGraph::class,
ConnectionsRoutes.ConnectionsGraph::class,
ContactsRoute.ContactsGraph::class,
NodesRoute.NodesGraph::class,
MapRoute.Map::class,
SettingsRoute.SettingsGraph::class,
ConnectionsRoute.ConnectionsGraph::class,
)
assertEquals(
@ -60,7 +60,7 @@ class DesktopTopLevelDestinationParityTest {
val desktopRoutes: Set<KClass<out Route>> = TopLevelDestination.entries.map { it.route::class }.toSet()
assertFalse(
actual = desktopRoutes.contains(FirmwareRoutes.FirmwareGraph::class),
actual = desktopRoutes.contains(FirmwareRoute.FirmwareGraph::class),
message = "Firmware must stay in-flow and not appear in the desktop top-level rail",
)
}

View file

@ -20,30 +20,30 @@ import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.navigation.ConnectionsRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.ConnectionsRoute
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.feature.connections.ScannerViewModel
import org.meshtastic.feature.connections.ui.ConnectionsScreen
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
/** Navigation graph for for the top level ConnectionsScreen - [ConnectionsRoutes.Connections]. */
/** Navigation graph for for the top level ConnectionsScreen - [ConnectionsRoute.Connections]. */
fun EntryProviderScope<NavKey>.connectionsGraph(backStack: NavBackStack<NavKey>) {
entry<ConnectionsRoutes.ConnectionsGraph> {
entry<ConnectionsRoute.ConnectionsGraph> {
ConnectionsScreen(
scanModel = koinViewModel<ScannerViewModel>(),
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetail(it)) },
onNavigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetail(it)) },
onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) },
onNavigateToNodeDetails = { backStack.add(NodesRoute.NodeDetail(it)) },
onConfigNavigate = { route -> backStack.add(route) },
)
}
entry<ConnectionsRoutes.Connections> {
entry<ConnectionsRoute.Connections> {
ConnectionsScreen(
scanModel = koinViewModel<ScannerViewModel>(),
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetail(it)) },
onNavigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetail(it)) },
onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) },
onNavigateToNodeDetails = { backStack.add(NodesRoute.NodeDetail(it)) },
onConfigNavigate = { route -> backStack.add(route) },
)
}

View file

@ -51,7 +51,7 @@ import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.connected
import org.meshtastic.core.resources.connected_device
@ -125,8 +125,8 @@ fun ConnectionsScreen(
getNavRouteFrom(radioConfigState.route)?.let { route ->
isWaiting = false
radioConfigViewModel.clearPacketResponse()
if (route == SettingsRoutes.LoRa) {
onConfigNavigate(SettingsRoutes.LoRa)
if (route == SettingsRoute.LoRa) {
onConfigNavigate(SettingsRoute.LoRa)
}
}
},

View file

@ -21,14 +21,14 @@ import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.navigation.FirmwareRoutes
import org.meshtastic.core.navigation.FirmwareRoute
import org.meshtastic.feature.firmware.FirmwareUpdateScreen
import org.meshtastic.feature.firmware.FirmwareUpdateViewModel
/** Registers the firmware update screen entries into the Navigation 3 entry provider. */
fun EntryProviderScope<NavKey>.firmwareGraph(backStack: NavBackStack<NavKey>) {
entry<FirmwareRoutes.FirmwareGraph> { FirmwareScreen(onNavigateUp = { backStack.removeLastOrNull() }) }
entry<FirmwareRoutes.FirmwareUpdate> { FirmwareScreen(onNavigateUp = { backStack.removeLastOrNull() }) }
entry<FirmwareRoute.FirmwareGraph> { FirmwareScreen(onNavigateUp = { backStack.removeLastOrNull() }) }
entry<FirmwareRoute.FirmwareUpdate> { FirmwareScreen(onNavigateUp = { backStack.removeLastOrNull() }) }
}
@Composable

View file

@ -19,15 +19,15 @@ package org.meshtastic.feature.map.navigation
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.meshtastic.core.navigation.MapRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.MapRoute
import org.meshtastic.core.navigation.NodesRoute
fun EntryProviderScope<NavKey>.mapGraph(backStack: NavBackStack<NavKey>) {
entry<MapRoutes.Map> { args ->
entry<MapRoute.Map> { args ->
val mapScreen = org.meshtastic.core.ui.util.LocalMapMainScreenProvider.current
mapScreen(
{ backStack.add(NodesRoutes.NodeDetail(it)) }, // onClickNodeChip
{ backStack.add(NodesRoutes.NodeDetail(it)) }, // navigateToNodeDetails
{ backStack.add(NodesRoute.NodeDetail(it)) }, // onClickNodeChip
{ backStack.add(NodesRoute.NodeDetail(it)) }, // navigateToNodeDetails
args.waypointId,
)
}

View file

@ -27,8 +27,8 @@ import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.ContactsRoute
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.core.navigation.replaceLast
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.feature.messaging.QuickChatScreen
@ -43,15 +43,15 @@ fun EntryProviderScope<NavKey>.contactsGraph(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent> = MutableSharedFlow(),
) {
entry<ContactsRoutes.ContactsGraph>(metadata = { ListDetailSceneStrategy.listPane() }) {
entry<ContactsRoute.ContactsGraph>(metadata = { ListDetailSceneStrategy.listPane() }) {
ContactsEntryContent(backStack = backStack, scrollToTopEvents = scrollToTopEvents)
}
entry<ContactsRoutes.Contacts>(metadata = { ListDetailSceneStrategy.listPane() }) {
entry<ContactsRoute.Contacts>(metadata = { ListDetailSceneStrategy.listPane() }) {
ContactsEntryContent(backStack = backStack, scrollToTopEvents = scrollToTopEvents)
}
entry<ContactsRoutes.Messages>(metadata = { ListDetailSceneStrategy.detailPane() }) { args ->
entry<ContactsRoute.Messages>(metadata = { ListDetailSceneStrategy.detailPane() }) { args ->
val contactKey = args.contactKey
val messageViewModel: org.meshtastic.feature.messaging.MessageViewModel =
koinViewModel(key = "messages-$contactKey")
@ -61,23 +61,23 @@ fun EntryProviderScope<NavKey>.contactsGraph(
contactKey = contactKey,
message = args.message,
viewModel = messageViewModel,
navigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetail(it)) },
navigateToQuickChatOptions = { backStack.add(org.meshtastic.core.navigation.ContactsRoutes.QuickChat) },
navigateToNodeDetails = { backStack.add(NodesRoute.NodeDetail(it)) },
navigateToQuickChatOptions = { backStack.add(org.meshtastic.core.navigation.ContactsRoute.QuickChat) },
onNavigateBack = { backStack.removeLastOrNull() },
)
}
entry<ContactsRoutes.Share>(metadata = { ListDetailSceneStrategy.extraPane() }) { args ->
entry<ContactsRoute.Share>(metadata = { ListDetailSceneStrategy.extraPane() }) { args ->
val message = args.message
val viewModel = koinViewModel<ContactsViewModel>()
ShareScreen(
viewModel = viewModel,
onConfirm = { contactKey -> backStack.replaceLast(ContactsRoutes.Messages(contactKey, message)) },
onConfirm = { contactKey -> backStack.replaceLast(ContactsRoute.Messages(contactKey, message)) },
onNavigateUp = { backStack.removeLastOrNull() },
)
}
entry<ContactsRoutes.QuickChat>(metadata = { ListDetailSceneStrategy.extraPane() }) {
entry<ContactsRoute.QuickChat>(metadata = { ListDetailSceneStrategy.extraPane() }) {
val viewModel = koinViewModel<QuickChatViewModel>()
QuickChatScreen(viewModel = viewModel, onNavigateUp = { backStack.removeLastOrNull() })
}

View file

@ -21,9 +21,9 @@ import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.flow.Flow
import org.meshtastic.core.common.util.MeshtasticUri
import org.meshtastic.core.navigation.ChannelsRoutes
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.ChannelsRoute
import org.meshtastic.core.navigation.ContactsRoute
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.SharedContact
@ -40,16 +40,16 @@ fun AdaptiveContactsScreen(
onClearRequestChannelUrl: () -> Unit,
) {
ContactsScreen(
onNavigateToShare = { backStack.add(ChannelsRoutes.ChannelsGraph) },
onNavigateToShare = { backStack.add(ChannelsRoute.ChannelsGraph) },
sharedContactRequested = sharedContactRequested,
requestChannelSet = requestChannelSet,
onHandleDeepLink = onHandleDeepLink,
onClearSharedContactRequested = onClearSharedContactRequested,
onClearRequestChannelUrl = onClearRequestChannelUrl,
viewModel = contactsViewModel,
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetail(it)) },
onNavigateToMessages = { contactKey -> backStack.add(ContactsRoutes.Messages(contactKey)) },
onNavigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetail(it)) },
onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) },
onNavigateToMessages = { contactKey -> backStack.add(ContactsRoute.Messages(contactKey)) },
onNavigateToNodeDetails = { backStack.add(NodesRoute.NodeDetail(it)) },
scrollToTopEvents = scrollToTopEvents,
activeContactKey = null,
)

View file

@ -27,7 +27,7 @@ import org.meshtastic.core.database.entity.asDeviceVersion
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.administration
import org.meshtastic.core.resources.firmware
@ -77,7 +77,7 @@ fun AdministrationSection(
leadingIcon = MeshtasticIcons.Settings,
enabled = metricsState.isLocal || node.metadata != null,
) {
onAction(NodeDetailAction.Navigate(SettingsRoutes.Settings(node.num)))
onAction(NodeDetailAction.Navigate(SettingsRoute.Settings(node.num)))
}
}
}

View file

@ -18,7 +18,7 @@ package org.meshtastic.feature.node.model
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.navigation.NodeDetailRoutes
import org.meshtastic.core.navigation.NodeDetailRoute
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.device_metrics_log
@ -43,14 +43,14 @@ import org.meshtastic.core.resources.signal_quality
import org.meshtastic.core.resources.traceroute_log
enum class LogsType(val titleRes: StringResource, val icon: DrawableResource, val routeFactory: (Int) -> Route) {
DEVICE(Res.string.device_metrics_log, Res.drawable.ic_charging_station, { NodeDetailRoutes.DeviceMetrics(it) }),
NODE_MAP(Res.string.node_map, Res.drawable.ic_map, { NodeDetailRoutes.NodeMap(it) }),
POSITIONS(Res.string.position_log, Res.drawable.ic_location_on, { NodeDetailRoutes.PositionLog(it) }),
ENVIRONMENT(Res.string.env_metrics_log, Res.drawable.ic_thermostat, { NodeDetailRoutes.EnvironmentMetrics(it) }),
SIGNAL(Res.string.signal_quality, Res.drawable.ic_signal_cellular_alt, { NodeDetailRoutes.SignalMetrics(it) }),
POWER(Res.string.power_metrics_log, Res.drawable.ic_power, { NodeDetailRoutes.PowerMetrics(it) }),
TRACEROUTE(Res.string.traceroute_log, Res.drawable.ic_route, { NodeDetailRoutes.TracerouteLog(it) }),
NEIGHBOR_INFO(Res.string.neighbor_info, Res.drawable.ic_groups, { NodeDetailRoutes.NeighborInfoLog(it) }),
HOST(Res.string.host_metrics_log, Res.drawable.ic_memory, { NodeDetailRoutes.HostMetricsLog(it) }),
PAX(Res.string.pax_metrics_log, Res.drawable.ic_people, { NodeDetailRoutes.PaxMetrics(it) }),
DEVICE(Res.string.device_metrics_log, Res.drawable.ic_charging_station, { NodeDetailRoute.DeviceMetrics(it) }),
NODE_MAP(Res.string.node_map, Res.drawable.ic_map, { NodeDetailRoute.NodeMap(it) }),
POSITIONS(Res.string.position_log, Res.drawable.ic_location_on, { NodeDetailRoute.PositionLog(it) }),
ENVIRONMENT(Res.string.env_metrics_log, Res.drawable.ic_thermostat, { NodeDetailRoute.EnvironmentMetrics(it) }),
SIGNAL(Res.string.signal_quality, Res.drawable.ic_signal_cellular_alt, { NodeDetailRoute.SignalMetrics(it) }),
POWER(Res.string.power_metrics_log, Res.drawable.ic_power, { NodeDetailRoute.PowerMetrics(it) }),
TRACEROUTE(Res.string.traceroute_log, Res.drawable.ic_route, { NodeDetailRoute.TracerouteLog(it) }),
NEIGHBOR_INFO(Res.string.neighbor_info, Res.drawable.ic_groups, { NodeDetailRoute.NeighborInfoLog(it) }),
HOST(Res.string.host_metrics_log, Res.drawable.ic_memory, { NodeDetailRoute.HostMetricsLog(it) }),
PAX(Res.string.pax_metrics_log, Res.drawable.ic_people, { NodeDetailRoute.PaxMetrics(it) }),
}

View file

@ -21,8 +21,8 @@ import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.flow.Flow
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.navigation.ChannelsRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.ChannelsRoute
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.feature.node.list.NodeListScreen
import org.meshtastic.feature.node.list.NodeListViewModel
@ -37,8 +37,8 @@ fun AdaptiveNodeListScreen(
NodeListScreen(
viewModel = nodeListViewModel,
navigateToNodeDetails = { nodeId -> backStack.add(NodesRoutes.NodeDetail(nodeId)) },
onNavigateToChannels = { backStack.add(ChannelsRoutes.ChannelsGraph) },
navigateToNodeDetails = { nodeId -> backStack.add(NodesRoute.NodeDetail(nodeId)) },
onNavigateToChannels = { backStack.add(ChannelsRoute.ChannelsGraph) },
scrollToTopEvents = scrollToTopEvents,
activeNodeId = null,
onHandleDeepLink = onHandleDeepLink,

View file

@ -28,9 +28,9 @@ import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.NodeDetailRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.ContactsRoute
import org.meshtastic.core.navigation.NodeDetailRoute
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.device
@ -74,7 +74,7 @@ fun EntryProviderScope<NavKey>.nodesGraph(
scrollToTopEvents: Flow<ScrollToTopEvent> = MutableSharedFlow(),
onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
) {
entry<NodesRoutes.NodesGraph>(metadata = { ListDetailSceneStrategy.listPane() }) {
entry<NodesRoute.NodesGraph>(metadata = { ListDetailSceneStrategy.listPane() }) {
AdaptiveNodeListScreen(
backStack = backStack,
scrollToTopEvents = scrollToTopEvents,
@ -82,7 +82,7 @@ fun EntryProviderScope<NavKey>.nodesGraph(
)
}
entry<NodesRoutes.Nodes>(metadata = { ListDetailSceneStrategy.listPane() }) {
entry<NodesRoute.Nodes>(metadata = { ListDetailSceneStrategy.listPane() }) {
AdaptiveNodeListScreen(
backStack = backStack,
scrollToTopEvents = scrollToTopEvents,
@ -100,7 +100,7 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
scrollToTopEvents: Flow<ScrollToTopEvent>,
onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
) {
entry<NodesRoutes.NodeDetailGraph>(metadata = { ListDetailSceneStrategy.listPane() }) { args ->
entry<NodesRoute.NodeDetailGraph>(metadata = { ListDetailSceneStrategy.listPane() }) { args ->
AdaptiveNodeListScreen(
backStack = backStack,
scrollToTopEvents = scrollToTopEvents,
@ -108,7 +108,7 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
)
}
entry<NodesRoutes.NodeDetail>(metadata = { ListDetailSceneStrategy.detailPane() }) { args ->
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
@ -116,18 +116,18 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
nodeId = destNum,
viewModel = nodeDetailViewModel,
compassViewModel = compassViewModel,
navigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) },
navigateToMessages = { backStack.add(ContactsRoute.Messages(it)) },
onNavigate = { backStack.add(it) },
onNavigateUp = { backStack.removeLastOrNull() },
)
}
entry<NodeDetailRoutes.NodeMap>(metadata = { ListDetailSceneStrategy.extraPane() }) { args ->
entry<NodeDetailRoute.NodeMap>(metadata = { ListDetailSceneStrategy.extraPane() }) { args ->
val mapScreen = org.meshtastic.core.ui.util.LocalNodeMapScreenProvider.current
mapScreen(args.destNum) { backStack.removeLastOrNull() }
}
entry<NodeDetailRoutes.TracerouteLog>(metadata = { ListDetailSceneStrategy.extraPane() }) { args ->
entry<NodeDetailRoute.TracerouteLog>(metadata = { ListDetailSceneStrategy.extraPane() }) { args ->
val metricsViewModel = koinViewModel<MetricsViewModel> { parametersOf(args.destNum) }
metricsViewModel.setNodeId(args.destNum)
@ -136,7 +136,7 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
onNavigateUp = { backStack.removeLastOrNull() },
onViewOnMap = { requestId, responseLogUuid ->
backStack.add(
NodeDetailRoutes.TracerouteMap(
NodeDetailRoute.TracerouteMap(
destNum = args.destNum,
requestId = requestId,
logUuid = responseLogUuid,
@ -146,40 +146,40 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
)
}
entry<NodeDetailRoutes.TracerouteMap>(metadata = { ListDetailSceneStrategy.extraPane() }) { args ->
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() }
}
NodeDetailRoute.entries.forEach { routeInfo ->
NodeDetailScreen.entries.forEach { routeInfo ->
when (routeInfo.routeClass) {
NodeDetailRoutes.DeviceMetrics::class ->
addNodeDetailScreenComposable<NodeDetailRoutes.DeviceMetrics>(backStack, routeInfo) { it.destNum }
NodeDetailRoutes.PositionLog::class ->
addNodeDetailScreenComposable<NodeDetailRoutes.PositionLog>(backStack, routeInfo) { it.destNum }
NodeDetailRoutes.EnvironmentMetrics::class ->
addNodeDetailScreenComposable<NodeDetailRoutes.EnvironmentMetrics>(backStack, routeInfo) { it.destNum }
NodeDetailRoutes.SignalMetrics::class ->
addNodeDetailScreenComposable<NodeDetailRoutes.SignalMetrics>(backStack, routeInfo) { it.destNum }
NodeDetailRoutes.PowerMetrics::class ->
addNodeDetailScreenComposable<NodeDetailRoutes.PowerMetrics>(backStack, routeInfo) { it.destNum }
NodeDetailRoutes.HostMetricsLog::class ->
addNodeDetailScreenComposable<NodeDetailRoutes.HostMetricsLog>(backStack, routeInfo) { it.destNum }
NodeDetailRoutes.PaxMetrics::class ->
addNodeDetailScreenComposable<NodeDetailRoutes.PaxMetrics>(backStack, routeInfo) { it.destNum }
NodeDetailRoutes.NeighborInfoLog::class ->
addNodeDetailScreenComposable<NodeDetailRoutes.NeighborInfoLog>(backStack, routeInfo) { it.destNum }
NodeDetailRoute.DeviceMetrics::class ->
addNodeDetailScreenComposable<NodeDetailRoute.DeviceMetrics>(backStack, routeInfo) { it.destNum }
NodeDetailRoute.PositionLog::class ->
addNodeDetailScreenComposable<NodeDetailRoute.PositionLog>(backStack, routeInfo) { it.destNum }
NodeDetailRoute.EnvironmentMetrics::class ->
addNodeDetailScreenComposable<NodeDetailRoute.EnvironmentMetrics>(backStack, routeInfo) { it.destNum }
NodeDetailRoute.SignalMetrics::class ->
addNodeDetailScreenComposable<NodeDetailRoute.SignalMetrics>(backStack, routeInfo) { it.destNum }
NodeDetailRoute.PowerMetrics::class ->
addNodeDetailScreenComposable<NodeDetailRoute.PowerMetrics>(backStack, routeInfo) { it.destNum }
NodeDetailRoute.HostMetricsLog::class ->
addNodeDetailScreenComposable<NodeDetailRoute.HostMetricsLog>(backStack, routeInfo) { it.destNum }
NodeDetailRoute.PaxMetrics::class ->
addNodeDetailScreenComposable<NodeDetailRoute.PaxMetrics>(backStack, routeInfo) { it.destNum }
NodeDetailRoute.NeighborInfoLog::class ->
addNodeDetailScreenComposable<NodeDetailRoute.NeighborInfoLog>(backStack, routeInfo) { it.destNum }
else -> Unit
}
}
}
fun NavKey.isNodeDetailRoute(): Boolean = NodeDetailRoute.entries.any { this::class == it.routeClass }
fun NavKey.isNodeDetailRoute(): Boolean = NodeDetailScreen.entries.any { this::class == it.routeClass }
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private inline fun <reified R : Route> EntryProviderScope<NavKey>.addNodeDetailScreenComposable(
backStack: NavBackStack<NavKey>,
routeInfo: NodeDetailRoute,
routeInfo: NodeDetailScreen,
crossinline getDestNum: (R) -> Int,
) {
entry<R>(metadata = { ListDetailSceneStrategy.extraPane() }) { args ->
@ -192,7 +192,7 @@ private inline fun <reified R : Route> EntryProviderScope<NavKey>.addNodeDetailS
}
/** Expect declaration for the platform-specific traceroute map screen. */
enum class NodeDetailRoute(
enum class NodeDetailScreen(
val title: StringResource,
val routeClass: KClass<out Route>,
val icon: DrawableResource? = null,
@ -200,55 +200,55 @@ enum class NodeDetailRoute(
) {
DEVICE(
Res.string.device,
NodeDetailRoutes.DeviceMetrics::class,
NodeDetailRoute.DeviceMetrics::class,
Res.drawable.ic_router,
{ metricsVM, onNavigateUp -> DeviceMetricsScreen(metricsVM, onNavigateUp) },
),
POSITION_LOG(
Res.string.position_log,
NodeDetailRoutes.PositionLog::class,
NodeDetailRoute.PositionLog::class,
Res.drawable.ic_location_on,
{ metricsVM, onNavigateUp -> PositionLogScreen(metricsVM, onNavigateUp) },
),
ENVIRONMENT(
Res.string.environment,
NodeDetailRoutes.EnvironmentMetrics::class,
NodeDetailRoute.EnvironmentMetrics::class,
Res.drawable.ic_light_mode,
{ metricsVM, onNavigateUp -> EnvironmentMetricsScreen(metricsVM, onNavigateUp) },
),
SIGNAL(
Res.string.signal,
NodeDetailRoutes.SignalMetrics::class,
NodeDetailRoute.SignalMetrics::class,
Res.drawable.ic_cell_tower,
{ metricsVM, onNavigateUp -> SignalMetricsScreen(metricsVM, onNavigateUp) },
),
TRACEROUTE(
Res.string.traceroute,
NodeDetailRoutes.TracerouteLog::class,
NodeDetailRoute.TracerouteLog::class,
Res.drawable.ic_perm_scan_wifi,
{ metricsVM, onNavigateUp -> TracerouteLogScreen(viewModel = metricsVM, onNavigateUp = onNavigateUp) },
),
NEIGHBOR_INFO(
Res.string.neighbor_info,
NodeDetailRoutes.NeighborInfoLog::class,
NodeDetailRoute.NeighborInfoLog::class,
Res.drawable.ic_groups,
{ metricsVM, onNavigateUp -> NeighborInfoLogScreen(viewModel = metricsVM, onNavigateUp = onNavigateUp) },
),
POWER(
Res.string.power,
NodeDetailRoutes.PowerMetrics::class,
NodeDetailRoute.PowerMetrics::class,
Res.drawable.ic_power,
{ metricsVM, onNavigateUp -> PowerMetricsScreen(metricsVM, onNavigateUp) },
),
HOST(
Res.string.host,
NodeDetailRoutes.HostMetricsLog::class,
NodeDetailRoute.HostMetricsLog::class,
Res.drawable.ic_memory,
{ metricsVM, onNavigateUp -> HostMetricsLogScreen(metricsVM, onNavigateUp) },
),
PAX(
Res.string.pax,
NodeDetailRoutes.PaxMetrics::class,
NodeDetailRoute.PaxMetrics::class,
Res.drawable.ic_people,
{ metricsVM, onNavigateUp -> PaxMetricsScreen(metricsVM, onNavigateUp) },
),

View file

@ -41,8 +41,8 @@ import org.meshtastic.core.common.util.toDate
import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.common.util.toMeshtasticUri
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.WifiProvisionRoutes
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.navigation.WifiProvisionRoute
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.bottom_nav_settings
import org.meshtastic.core.resources.export_configuration
@ -233,7 +233,7 @@ fun SettingsScreen(
ExpressiveSection(title = stringResource(Res.string.wifi_devices)) {
ListItem(text = stringResource(Res.string.wifi_devices), leadingIcon = MeshtasticIcons.Wifi) {
onNavigate(WifiProvisionRoutes.WifiProvision())
onNavigate(WifiProvisionRoute.WifiProvision())
}
}
@ -249,7 +249,7 @@ fun SettingsScreen(
excludedModulesUnlocked = excludedModulesUnlocked,
onUnlockExcludedModules = { settingsViewModel.unlockExcludedModules() },
onShowAppIntro = { settingsViewModel.showAppIntro() },
onNavigateToAbout = { onNavigate(SettingsRoutes.About) },
onNavigateToAbout = { onNavigate(SettingsRoute.About) },
)
}
}

View file

@ -16,7 +16,7 @@
*/
package org.meshtastic.feature.settings.navigation
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.SettingsRoute
actual fun getAboutLibrariesJson(): String =
SettingsRoutes::class.java.getResource("/aboutlibraries.json")?.readText() ?: ""
SettingsRoute::class.java.getResource("/aboutlibraries.json")?.readText() ?: ""

View file

@ -19,7 +19,7 @@ package org.meshtastic.feature.settings.navigation
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.bluetooth
import org.meshtastic.core.resources.channels
@ -50,43 +50,43 @@ enum class ConfigRoute(
val icon: DrawableResource? = null,
val type: Int = 0,
) {
USER(Res.string.user, SettingsRoutes.User, Res.drawable.ic_person, 0),
CHANNELS(Res.string.channels, SettingsRoutes.ChannelConfig, Res.drawable.ic_list, 0),
USER(Res.string.user, SettingsRoute.User, Res.drawable.ic_person, 0),
CHANNELS(Res.string.channels, SettingsRoute.ChannelConfig, Res.drawable.ic_list, 0),
DEVICE(
Res.string.device,
SettingsRoutes.Device,
SettingsRoute.Device,
Res.drawable.ic_router,
AdminMessage.ConfigType.DEVICE_CONFIG.value,
),
POSITION(
Res.string.position,
SettingsRoutes.Position,
SettingsRoute.Position,
Res.drawable.ic_location_on,
AdminMessage.ConfigType.POSITION_CONFIG.value,
),
POWER(Res.string.power, SettingsRoutes.Power, Res.drawable.ic_power, AdminMessage.ConfigType.POWER_CONFIG.value),
POWER(Res.string.power, SettingsRoute.Power, Res.drawable.ic_power, AdminMessage.ConfigType.POWER_CONFIG.value),
NETWORK(
Res.string.network,
SettingsRoutes.Network,
SettingsRoute.Network,
Res.drawable.ic_wifi,
AdminMessage.ConfigType.NETWORK_CONFIG.value,
),
DISPLAY(
Res.string.display,
SettingsRoutes.Display,
SettingsRoute.Display,
Res.drawable.ic_display_settings,
AdminMessage.ConfigType.DISPLAY_CONFIG.value,
),
LORA(Res.string.lora, SettingsRoutes.LoRa, Res.drawable.ic_cell_tower, AdminMessage.ConfigType.LORA_CONFIG.value),
LORA(Res.string.lora, SettingsRoute.LoRa, Res.drawable.ic_cell_tower, AdminMessage.ConfigType.LORA_CONFIG.value),
BLUETOOTH(
Res.string.bluetooth,
SettingsRoutes.Bluetooth,
SettingsRoute.Bluetooth,
Res.drawable.ic_bluetooth,
AdminMessage.ConfigType.BLUETOOTH_CONFIG.value,
),
SECURITY(
Res.string.security,
SettingsRoutes.Security,
SettingsRoute.Security,
Res.drawable.ic_security,
AdminMessage.ConfigType.SECURITY_CONFIG.value,
),

View file

@ -20,7 +20,7 @@ import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.model.Capabilities
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.ambient_lighting
import org.meshtastic.core.resources.audio
@ -64,96 +64,96 @@ enum class ModuleRoute(
val isSupported: (Capabilities) -> Boolean = { true },
val isApplicable: (Config.DeviceConfig.Role?) -> Boolean = { true },
) {
MQTT(Res.string.mqtt, SettingsRoutes.MQTT, Res.drawable.ic_cloud, AdminMessage.ModuleConfigType.MQTT_CONFIG.value),
MQTT(Res.string.mqtt, SettingsRoute.MQTT, Res.drawable.ic_cloud, AdminMessage.ModuleConfigType.MQTT_CONFIG.value),
SERIAL(
Res.string.serial,
SettingsRoutes.Serial,
SettingsRoute.Serial,
Res.drawable.ic_usb,
AdminMessage.ModuleConfigType.SERIAL_CONFIG.value,
),
EXT_NOTIFICATION(
Res.string.external_notification,
SettingsRoutes.ExtNotification,
SettingsRoute.ExtNotification,
Res.drawable.ic_notifications,
AdminMessage.ModuleConfigType.EXTNOTIF_CONFIG.value,
),
STORE_FORWARD(
Res.string.store_forward,
SettingsRoutes.StoreForward,
SettingsRoute.StoreForward,
Res.drawable.ic_terminal,
AdminMessage.ModuleConfigType.STOREFORWARD_CONFIG.value,
),
RANGE_TEST(
Res.string.range_test,
SettingsRoutes.RangeTest,
SettingsRoute.RangeTest,
Res.drawable.ic_speed,
AdminMessage.ModuleConfigType.RANGETEST_CONFIG.value,
),
TELEMETRY(
Res.string.telemetry,
SettingsRoutes.Telemetry,
SettingsRoute.Telemetry,
Res.drawable.ic_data_usage,
AdminMessage.ModuleConfigType.TELEMETRY_CONFIG.value,
),
CANNED_MESSAGE(
Res.string.canned_message,
SettingsRoutes.CannedMessage,
SettingsRoute.CannedMessage,
Res.drawable.ic_message,
AdminMessage.ModuleConfigType.CANNEDMSG_CONFIG.value,
),
AUDIO(
Res.string.audio,
SettingsRoutes.Audio,
SettingsRoute.Audio,
Res.drawable.ic_volume_up,
AdminMessage.ModuleConfigType.AUDIO_CONFIG.value,
),
REMOTE_HARDWARE(
Res.string.remote_hardware,
SettingsRoutes.RemoteHardware,
SettingsRoute.RemoteHardware,
Res.drawable.ic_settings_remote,
AdminMessage.ModuleConfigType.REMOTEHARDWARE_CONFIG.value,
),
NEIGHBOR_INFO(
Res.string.neighbor_info,
SettingsRoutes.NeighborInfo,
SettingsRoute.NeighborInfo,
Res.drawable.ic_people,
AdminMessage.ModuleConfigType.NEIGHBORINFO_CONFIG.value,
),
AMBIENT_LIGHTING(
Res.string.ambient_lighting,
SettingsRoutes.AmbientLighting,
SettingsRoute.AmbientLighting,
Res.drawable.ic_light_mode,
AdminMessage.ModuleConfigType.AMBIENTLIGHTING_CONFIG.value,
),
DETECTION_SENSOR(
Res.string.detection_sensor,
SettingsRoutes.DetectionSensor,
SettingsRoute.DetectionSensor,
Res.drawable.ic_sensors,
AdminMessage.ModuleConfigType.DETECTIONSENSOR_CONFIG.value,
),
PAXCOUNTER(
Res.string.paxcounter,
SettingsRoutes.Paxcounter,
SettingsRoute.Paxcounter,
Res.drawable.ic_perm_scan_wifi,
AdminMessage.ModuleConfigType.PAXCOUNTER_CONFIG.value,
),
STATUS_MESSAGE(
Res.string.status_message,
SettingsRoutes.StatusMessage,
SettingsRoute.StatusMessage,
Res.drawable.ic_message,
AdminMessage.ModuleConfigType.STATUSMESSAGE_CONFIG.value,
isSupported = { it.supportsStatusMessage },
),
TRAFFIC_MANAGEMENT(
Res.string.traffic_management,
SettingsRoutes.TrafficManagement,
SettingsRoute.TrafficManagement,
Res.drawable.ic_alt_route,
AdminMessage.ModuleConfigType.TRAFFICMANAGEMENT_CONFIG.value,
isSupported = { it.supportsTrafficManagementConfig },
),
TAK(
Res.string.tak,
SettingsRoutes.TAK,
SettingsRoute.TAK,
Res.drawable.ic_people,
AdminMessage.ModuleConfigType.TAK_CONFIG.value,
isSupported = { it.supportsTakConfig },

View file

@ -26,9 +26,9 @@ import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.feature.settings.AboutScreen
import org.meshtastic.feature.settings.AdministrationScreen
import org.meshtastic.feature.settings.DeviceConfigurationScreen
@ -74,10 +74,10 @@ fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): RadioConfigViewMod
val viewModel = koinViewModel<RadioConfigViewModel>()
val destNum =
remember(backStack.toList()) {
backStack.lastOrNull { it is SettingsRoutes.Settings }?.let { (it as SettingsRoutes.Settings).destNum }
backStack.lastOrNull { it is SettingsRoute.Settings }?.let { (it as SettingsRoute.Settings).destNum }
?: backStack
.lastOrNull { it is SettingsRoutes.SettingsGraph }
?.let { (it as SettingsRoutes.SettingsGraph).destNum }
.lastOrNull { it is SettingsRoute.SettingsGraph }
?.let { (it as SettingsRoute.SettingsGraph).destNum }
}
SideEffect { viewModel.initDestNum(destNum) }
return viewModel
@ -85,25 +85,25 @@ fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): RadioConfigViewMod
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
entry<SettingsRoutes.SettingsGraph> {
entry<SettingsRoute.SettingsGraph> {
SettingsMainScreen(
settingsViewModel = koinViewModel(),
radioConfigViewModel = getRadioConfigViewModel(backStack),
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetail(it)) },
onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) },
onNavigate = { backStack.add(it) },
)
}
entry<SettingsRoutes.Settings> {
entry<SettingsRoute.Settings> {
SettingsMainScreen(
settingsViewModel = koinViewModel(),
radioConfigViewModel = getRadioConfigViewModel(backStack),
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetail(it)) },
onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) },
onNavigate = { backStack.add(it) },
)
}
entry<SettingsRoutes.DeviceConfiguration> {
entry<SettingsRoute.DeviceConfiguration> {
DeviceConfigurationScreen(
viewModel = getRadioConfigViewModel(backStack),
onBack = { backStack.removeLastOrNull() },
@ -111,7 +111,7 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
)
}
entry<SettingsRoutes.ModuleConfiguration> {
entry<SettingsRoute.ModuleConfiguration> {
val settingsViewModel: SettingsViewModel = koinViewModel()
val excludedModulesUnlocked by settingsViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
ModuleConfigurationScreen(
@ -122,11 +122,11 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
)
}
entry<SettingsRoutes.Administration> {
entry<SettingsRoute.Administration> {
AdministrationScreen(viewModel = getRadioConfigViewModel(backStack), onBack = { backStack.removeLastOrNull() })
}
entry<SettingsRoutes.CleanNodeDb> {
entry<SettingsRoute.CleanNodeDb> {
val viewModel: CleanNodeDatabaseViewModel = koinViewModel()
CleanNodeDatabaseScreen(viewModel = viewModel)
}
@ -185,16 +185,16 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
}
}
entry<SettingsRoutes.DebugPanel> {
entry<SettingsRoute.DebugPanel> {
val viewModel: DebugViewModel = koinViewModel()
DebugScreen(viewModel = viewModel, onNavigateUp = { backStack.removeLastOrNull() })
}
entry<SettingsRoutes.About> {
entry<SettingsRoute.About> {
AboutScreen(onNavigateUp = { backStack.removeLastOrNull() }, jsonProvider = { getAboutLibrariesJson() })
}
entry<SettingsRoutes.FilterSettings> {
entry<SettingsRoute.FilterSettings> {
val viewModel: FilterSettingsViewModel = koinViewModel()
FilterSettingsScreen(viewModel = viewModel, onBack = { backStack.removeLastOrNull() })
}

View file

@ -28,9 +28,9 @@ import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
import org.meshtastic.core.navigation.FirmwareRoutes
import org.meshtastic.core.navigation.FirmwareRoute
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.administration
import org.meshtastic.core.resources.advanced_title
@ -125,7 +125,7 @@ private fun DeviceConfigSection(isManaged: Boolean, enabled: Boolean, onNavigate
trailingIcon = MeshtasticIcons.KeyboardArrowRight,
enabled = enabled,
) {
onNavigate(SettingsRoutes.DeviceConfiguration)
onNavigate(SettingsRoute.DeviceConfiguration)
}
}
}
@ -142,7 +142,7 @@ private fun ModuleSettingsSection(isManaged: Boolean, enabled: Boolean, onNaviga
trailingIcon = MeshtasticIcons.KeyboardArrowRight,
enabled = enabled,
) {
onNavigate(SettingsRoutes.ModuleConfiguration)
onNavigate(SettingsRoute.ModuleConfiguration)
}
}
}
@ -181,7 +181,7 @@ private fun AdministrationSection(enabled: Boolean, onNavigate: (Route) -> Unit)
trailingIconTint = MaterialTheme.colorScheme.error,
enabled = enabled,
) {
onNavigate(SettingsRoutes.Administration)
onNavigate(SettingsRoute.Administration)
}
}
}
@ -198,7 +198,7 @@ private fun AdvancedSection(isManaged: Boolean, isOtaCapable: Boolean, enabled:
text = stringResource(Res.string.firmware_update_title),
leadingIcon = MeshtasticIcons.SystemUpdate,
enabled = enabled,
onClick = { onNavigate(FirmwareRoutes.FirmwareUpdate) },
onClick = { onNavigate(FirmwareRoute.FirmwareUpdate) },
)
}
@ -206,14 +206,14 @@ private fun AdvancedSection(isManaged: Boolean, isOtaCapable: Boolean, enabled:
text = stringResource(Res.string.clean_node_database_title),
leadingIcon = MeshtasticIcons.CleaningServices,
enabled = enabled,
onClick = { onNavigate(SettingsRoutes.CleanNodeDb) },
onClick = { onNavigate(SettingsRoute.CleanNodeDb) },
)
ListItem(
text = stringResource(Res.string.debug_panel),
leadingIcon = MeshtasticIcons.BugReport,
enabled = enabled,
onClick = { onNavigate(SettingsRoutes.DebugPanel) },
onClick = { onNavigate(SettingsRoute.DebugPanel) },
)
}
}

View file

@ -20,12 +20,12 @@ import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.navigation.ChannelsRoutes
import org.meshtastic.core.navigation.ChannelsRoute
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
/** Navigation graph for for the top level ChannelScreen - [ChannelsRoutes.Channels]. */
/** Navigation graph for for the top level ChannelScreen - [ChannelsRoute.Channels]. */
fun EntryProviderScope<NavKey>.channelsGraph(backStack: NavBackStack<NavKey>) {
entry<ChannelsRoutes.ChannelsGraph> {
entry<ChannelsRoute.ChannelsGraph> {
ChannelScreen(
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
onNavigate = { route -> backStack.add(route) },
@ -33,7 +33,7 @@ fun EntryProviderScope<NavKey>.channelsGraph(backStack: NavBackStack<NavKey>) {
)
}
entry<ChannelsRoutes.Channels> {
entry<ChannelsRoute.Channels> {
ChannelScreen(
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
onNavigate = { route -> backStack.add(route) },

View file

@ -39,8 +39,8 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.DatabaseConstants
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.WifiProvisionRoutes
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.navigation.WifiProvisionRoute
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.acknowledgements
import org.meshtastic.core.resources.app_settings
@ -202,7 +202,7 @@ fun DesktopSettingsScreen(
ExpressiveSection(title = stringResource(Res.string.wifi_devices)) {
ListItem(text = stringResource(Res.string.wifi_devices), leadingIcon = MeshtasticIcons.Wifi) {
onNavigate(WifiProvisionRoutes.WifiProvision())
onNavigate(WifiProvisionRoute.WifiProvision())
}
}
@ -219,7 +219,7 @@ fun DesktopSettingsScreen(
appVersionName = settingsViewModel.appVersionName,
excludedModulesUnlocked = excludedModulesUnlocked,
onUnlockExcludedModules = { settingsViewModel.unlockExcludedModules() },
onNavigateToAbout = { onNavigate(SettingsRoutes.About) },
onNavigateToAbout = { onNavigate(SettingsRoute.About) },
)
}
}

View file

@ -16,7 +16,7 @@
*/
package org.meshtastic.feature.settings.navigation
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.SettingsRoute
actual fun getAboutLibrariesJson(): String =
SettingsRoutes::class.java.getResource("/aboutlibraries.json")?.readText() ?: ""
SettingsRoute::class.java.getResource("/aboutlibraries.json")?.readText() ?: ""

View file

@ -19,21 +19,21 @@ package org.meshtastic.feature.wifiprovision.navigation
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.meshtastic.core.navigation.WifiProvisionRoutes
import org.meshtastic.core.navigation.WifiProvisionRoute
import org.meshtastic.feature.wifiprovision.ui.WifiProvisionScreen
/**
* Registers the WiFi provisioning graph entries into the host navigation provider.
*
* Both the graph sentinel ([WifiProvisionRoutes.WifiProvisionGraph]) and the primary screen
* ([WifiProvisionRoutes.WifiProvision]) navigate to the same composable so that the feature can be reached via either a
* Both the graph sentinel ([WifiProvisionRoute.WifiProvisionGraph]) and the primary screen
* ([WifiProvisionRoute.WifiProvision]) navigate to the same composable so that the feature can be reached via either a
* top-level push or a deep-link graph push.
*/
fun EntryProviderScope<NavKey>.wifiProvisionGraph(backStack: NavBackStack<NavKey>) {
entry<WifiProvisionRoutes.WifiProvisionGraph> {
entry<WifiProvisionRoute.WifiProvisionGraph> {
WifiProvisionScreen(onNavigateUp = { backStack.removeLastOrNull() })
}
entry<WifiProvisionRoutes.WifiProvision> { key ->
entry<WifiProvisionRoute.WifiProvision> { key ->
WifiProvisionScreen(onNavigateUp = { backStack.removeLastOrNull() }, address = key.address)
}
}