refactor: migrate to Compose navigation (#1835)

Co-authored-by: andrekir <andrekir@pm.me>
This commit is contained in:
James Rich 2025-05-15 08:05:30 -05:00 committed by GitHub
parent 79c77ab1d5
commit 8cde47bdf9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 2576 additions and 3427 deletions

View file

@ -15,109 +15,306 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navDeepLink
import androidx.navigation.toRoute
import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.ui.ScreenFragment
import com.geeksville.mesh.ui.components.BaseScaffold
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.ChannelScreen
import com.geeksville.mesh.ui.ContactsScreen
import com.geeksville.mesh.ui.DebugScreen
import com.geeksville.mesh.ui.NodeScreen
import com.geeksville.mesh.ui.QuickChatScreen
import com.geeksville.mesh.ui.SettingsScreen
import com.geeksville.mesh.ui.ShareScreen
import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
import com.geeksville.mesh.ui.map.MapView
import com.geeksville.mesh.ui.message.MessageScreen
import kotlinx.serialization.Serializable
internal fun FragmentManager.navigateToNavGraph(
destNum: Int? = null,
startDestination: String = "RadioConfig",
) {
val radioConfigFragment = NavGraphFragment().apply {
arguments = bundleOf("destNum" to destNum, "startDestination" to startDestination)
}
beginTransaction()
.replace(R.id.mainActivityLayout, radioConfigFragment)
.addToBackStack(null)
.commit()
enum class AdminRoute(@StringRes val title: Int) {
REBOOT(R.string.reboot),
SHUTDOWN(R.string.shutdown),
FACTORY_RESET(R.string.factory_reset),
NODEDB_RESET(R.string.nodedb_reset),
}
@AndroidEntryPoint
class NavGraphFragment : ScreenFragment("NavGraph"), Logging {
const val DEEP_LINK_BASE_URI = "meshtastic://meshtastic"
private val model: RadioConfigViewModel by viewModels()
@Serializable
sealed interface Graph : Route {
@Serializable
data class NodeDetailGraph(val destNum: Int) : Graph
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
@Suppress("DEPRECATION")
val destNum = arguments?.getSerializable("destNum") as? Int
val startDestination: Any = when (arguments?.getString("startDestination")) {
"NodeDetails" -> Route.NodeDetail(destNum!!)
else -> Route.RadioConfig(destNum)
@Serializable
data class RadioConfigGraph(val destNum: Int? = null) : Graph
}
@Serializable
sealed interface Route {
@Serializable
data object Contacts : Route
@Serializable
data object Nodes : Route
@Serializable
data object Map : Route
@Serializable
data object Channels : Route
@Serializable
data object Settings : Route
@Serializable
data object DebugPanel : Route
@Serializable
data class Messages(val contactKey: String, val message: String = "") : Route
@Serializable
data object QuickChat : Route
@Serializable
data class Share(val message: String) : Route
@Serializable
data class RadioConfig(val destNum: Int? = null) : Route
@Serializable
data object User : Route
@Serializable
data object ChannelConfig : Route
@Serializable
data object Device : Route
@Serializable
data object Position : Route
@Serializable
data object Power : Route
@Serializable
data object Network : Route
@Serializable
data object Display : Route
@Serializable
data object LoRa : Route
@Serializable
data object Bluetooth : Route
@Serializable
data object Security : Route
@Serializable
data object MQTT : Route
@Serializable
data object Serial : Route
@Serializable
data object ExtNotification : Route
@Serializable
data object StoreForward : Route
@Serializable
data object RangeTest : Route
@Serializable
data object Telemetry : Route
@Serializable
data object CannedMessage : Route
@Serializable
data object Audio : Route
@Serializable
data object RemoteHardware : Route
@Serializable
data object NeighborInfo : Route
@Serializable
data object AmbientLighting : Route
@Serializable
data object DetectionSensor : Route
@Serializable
data object Paxcounter : Route
@Serializable
data class NodeDetail(val destNum: Int? = null) : Route
@Serializable
data object DeviceMetrics : Route
@Serializable
data object NodeMap : Route
@Serializable
data object PositionLog : Route
@Serializable
data object EnvironmentMetrics : Route
@Serializable
data object SignalMetrics : Route
@Serializable
data object PowerMetrics : Route
@Serializable
data object TracerouteLog : Route
}
fun NavDestination.isConfigRoute(): Boolean {
return ConfigRoute.entries.any { hasRoute(it.route::class) } ||
ModuleRoute.entries.any { hasRoute(it.route::class) }
}
fun NavDestination.isNodeDetailRoute(): Boolean {
return NodeDetailRoute.entries.any { hasRoute(it.route::class) }
}
fun NavDestination.showLongNameTitle(): Boolean {
return !this.isTopLevel() && (
this.hasRoute<Route.Messages>() ||
this.hasRoute<Route.RadioConfig>() ||
this.hasRoute<Route.NodeDetail>() ||
this.isConfigRoute() ||
this.isNodeDetailRoute()
)
}
@Suppress("LongMethod")
@Composable
fun NavGraph(
modifier: Modifier = Modifier,
uIViewModel: UIViewModel = hiltViewModel(),
navController: NavHostController = rememberNavController(),
) {
NavHost(
navController = navController,
startDestination = if (uIViewModel.bondedAddress.isNullOrBlank()) {
Route.Settings
} else {
Route.Contacts
},
modifier = modifier,
) {
composable<Route.Contacts> {
ContactsScreen(
uIViewModel,
onNavigate = { navController.navigate(Route.Messages(it)) }
)
}
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val node by model.destNode.collectAsStateWithLifecycle()
AppTheme {
val navController: NavHostController = rememberNavController()
BaseScaffold(
title = node?.user?.longName
?: stringResource(R.string.unknown_username),
canNavigateBack = true,
navigateUp = {
if (navController.previousBackStackEntry != null) {
navController.navigateUp()
} else {
parentFragmentManager.popBackStack()
}
},
) {
NavGraph(
navController = navController,
startDestination = startDestination,
)
composable<Route.Nodes> {
NodeScreen(
model = uIViewModel,
navigateToMessages = { navController.navigate(Route.Messages(it)) },
navigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) },
)
}
composable<Route.Map> {
MapView(uIViewModel)
}
composable<Route.Channels> {
ChannelScreen(uIViewModel)
}
composable<Route.Settings>(
deepLinks = listOf(
navDeepLink {
uriPattern = "$DEEP_LINK_BASE_URI/settings"
action = "android.intent.action.VIEW"
}
)
) { backStackEntry ->
SettingsScreen {
navController.navigate(Route.RadioConfig()) {
popUpTo(Route.Settings) {
inclusive = false
}
}
}
}
composable<Route.DebugPanel> {
DebugScreen()
}
composable<Route.Messages>(
deepLinks = listOf(
navDeepLink {
uriPattern = "$DEEP_LINK_BASE_URI/messages/{contactKey}?message={message}"
action = "android.intent.action.VIEW"
},
)
) { backStackEntry ->
val args = backStackEntry.toRoute<Route.Messages>()
MessageScreen(
contactKey = args.contactKey,
message = args.message,
viewModel = uIViewModel,
navigateToMessages = { navController.navigate(Route.Messages(it)) },
navigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) },
onNavigateBack = navController::navigateUp
)
}
composable<Route.QuickChat> {
QuickChatScreen()
}
nodeDetailGraph(navController, uIViewModel)
radioConfigGraph(navController, uIViewModel)
composable<Route.Share>(
deepLinks = listOf(
navDeepLink {
uriPattern = "$DEEP_LINK_BASE_URI/share?message={message}"
action = "android.intent.action.VIEW"
}
)
) { backStackEntry ->
val message = backStackEntry.toRoute<Route.Share>().message
ShareScreen(uIViewModel) {
navController.navigate(Route.Messages(it, message)) {
popUpTo<Route.Share> { inclusive = true }
}
}
}
}
}
@Composable
fun NavGraph(
navController: NavHostController = rememberNavController(),
startDestination: Any,
modifier: Modifier = Modifier,
) {
NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier,
) {
addNodDetailSection(navController)
addRadioConfigSection(navController)
shareScreen(
navigateUp = navController::navigateUp,
onConfirm = navController::navigateToSharedMessage,
)
}
}

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CellTower
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.PermScanWifi
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Router
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.NodeDetailScreen
import com.geeksville.mesh.ui.components.DeviceMetricsScreen
import com.geeksville.mesh.ui.components.EnvironmentMetricsScreen
import com.geeksville.mesh.ui.components.NodeMapScreen
import com.geeksville.mesh.ui.components.PositionLogScreen
import com.geeksville.mesh.ui.components.PowerMetricsScreen
import com.geeksville.mesh.ui.components.SignalMetricsScreen
import com.geeksville.mesh.ui.components.TracerouteLogScreen
fun NavGraphBuilder.nodeDetailGraph(
navController: NavHostController,
uiViewModel: UIViewModel
) {
navigation<Graph.NodeDetailGraph>(
startDestination = Route.NodeDetail(),
) {
composable<Route.NodeDetail> { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<Graph.NodeDetailGraph>()
}
NodeDetailScreen(uiViewModel = uiViewModel, viewModel = hiltViewModel(parentEntry)) {
navController.navigate(it) {
popUpTo(Route.NodeDetail()) {
inclusive = false
}
}
}
}
NodeDetailRoute.entries.forEach { nodeDetailRoute ->
composable(nodeDetailRoute.route::class) { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<Graph.NodeDetailGraph>()
}
when (nodeDetailRoute) {
NodeDetailRoute.DEVICE -> DeviceMetricsScreen(hiltViewModel(parentEntry))
NodeDetailRoute.NODE_MAP -> NodeMapScreen(hiltViewModel(parentEntry))
NodeDetailRoute.POSITION_LOG -> PositionLogScreen(hiltViewModel(parentEntry))
NodeDetailRoute.ENVIRONMENT -> EnvironmentMetricsScreen(hiltViewModel(parentEntry))
NodeDetailRoute.SIGNAL -> SignalMetricsScreen(hiltViewModel(parentEntry))
NodeDetailRoute.TRACEROUTE -> TracerouteLogScreen(hiltViewModel(parentEntry))
NodeDetailRoute.POWER -> PowerMetricsScreen(hiltViewModel(parentEntry))
}
}
}
}
}
enum class NodeDetailRoute(
@StringRes val title: Int,
val route: Route,
val icon: ImageVector?,
) {
DEVICE(R.string.device, Route.DeviceMetrics, Icons.Default.Router),
NODE_MAP(R.string.node_map, Route.NodeMap, Icons.Default.LocationOn),
POSITION_LOG(R.string.position_log, Route.PositionLog, Icons.Default.LocationOn),
ENVIRONMENT(R.string.environment, Route.EnvironmentMetrics, Icons.Default.LightMode),
SIGNAL(R.string.signal, Route.SignalMetrics, Icons.Default.CellTower),
TRACEROUTE(R.string.traceroute, Route.TracerouteLog, Icons.Default.PermScanWifi),
POWER(R.string.power, Route.PowerMetrics, Icons.Default.Power),
}

View file

@ -1,83 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
import androidx.compose.runtime.remember
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.ui.NodeDetailScreen
import com.geeksville.mesh.ui.components.DeviceMetricsScreen
import com.geeksville.mesh.ui.components.EnvironmentMetricsScreen
import com.geeksville.mesh.ui.components.NodeMapScreen
import com.geeksville.mesh.ui.components.PositionLogScreen
import com.geeksville.mesh.ui.components.PowerMetricsScreen
import com.geeksville.mesh.ui.components.SignalMetricsScreen
import com.geeksville.mesh.ui.components.TracerouteLogScreen
fun NavGraphBuilder.addNodDetailSection(navController: NavController) {
composable<Route.NodeDetail> {
NodeDetailScreen(
onNavigate = navController::navigate,
)
}
composable<Route.DeviceMetrics> {
val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() }
DeviceMetricsScreen(
viewModel = hiltViewModel<MetricsViewModel>(parentEntry),
)
}
composable<Route.NodeMap> {
val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() }
NodeMapScreen(
viewModel = hiltViewModel<MetricsViewModel>(parentEntry),
)
}
composable<Route.PositionLog> {
val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() }
PositionLogScreen(
viewModel = hiltViewModel<MetricsViewModel>(parentEntry),
)
}
composable<Route.EnvironmentMetrics> {
val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() }
EnvironmentMetricsScreen(
viewModel = hiltViewModel<MetricsViewModel>(parentEntry),
)
}
composable<Route.SignalMetrics> {
val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() }
SignalMetricsScreen(
viewModel = hiltViewModel<MetricsViewModel>(parentEntry),
)
}
composable<Route.PowerMetrics> {
val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() }
PowerMetricsScreen(
viewModel = hiltViewModel<MetricsViewModel>(parentEntry),
)
}
composable<Route.TracerouteLog> {
val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() }
TracerouteLogScreen(
viewModel = hiltViewModel<MetricsViewModel>(parentEntry),
)
}
}

View file

@ -0,0 +1,256 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Forward
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.Message
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.CellTower
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.DataUsage
import androidx.compose.material.icons.filled.DisplaySettings
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.PermScanWifi
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Router
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Sensors
import androidx.compose.material.icons.filled.SettingsRemote
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Usb
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import com.geeksville.mesh.MeshProtos.DeviceMetadata
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.radioconfig.RadioConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.AmbientLightingConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.AudioConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.BluetoothConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.CannedMessageConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.ChannelConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.DetectionSensorConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.DeviceConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.DisplayConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.ExternalNotificationConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.LoRaConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.MQTTConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.NeighborInfoConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.NetworkConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.PaxcounterConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.PositionConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.PowerConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.RangeTestConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.RemoteHardwareConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.SecurityConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.SerialConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.StoreForwardConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.TelemetryConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.UserConfigScreen
fun NavGraphBuilder.radioConfigGraph(navController: NavHostController, uiViewModel: UIViewModel) {
navigation<Graph.RadioConfigGraph>(
startDestination = Route.RadioConfig(),
) {
composable<Route.RadioConfig> { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<Graph.RadioConfigGraph>()
}
RadioConfigScreen(
uiViewModel = uiViewModel,
viewModel = hiltViewModel(parentEntry)
) {
navController.navigate(it) {
popUpTo(Route.RadioConfig()) {
inclusive = false
}
}
}
}
configRoutes(navController)
moduleRoutes(navController)
}
}
private fun NavGraphBuilder.configRoutes(
navController: NavHostController,
) {
ConfigRoute.entries.forEach { configRoute ->
composable(configRoute.route::class) { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<Graph.RadioConfigGraph>()
}
when (configRoute) {
ConfigRoute.USER -> UserConfigScreen(hiltViewModel(parentEntry))
ConfigRoute.CHANNELS -> ChannelConfigScreen(hiltViewModel(parentEntry))
ConfigRoute.DEVICE -> DeviceConfigScreen(hiltViewModel(parentEntry))
ConfigRoute.POSITION -> PositionConfigScreen(hiltViewModel(parentEntry))
ConfigRoute.POWER -> PowerConfigScreen(hiltViewModel(parentEntry))
ConfigRoute.NETWORK -> NetworkConfigScreen(hiltViewModel(parentEntry))
ConfigRoute.DISPLAY -> DisplayConfigScreen(hiltViewModel(parentEntry))
ConfigRoute.LORA -> LoRaConfigScreen(hiltViewModel(parentEntry))
ConfigRoute.BLUETOOTH -> BluetoothConfigScreen(hiltViewModel(parentEntry))
ConfigRoute.SECURITY -> SecurityConfigScreen(hiltViewModel(parentEntry))
}
}
}
}
@Suppress("CyclomaticComplexMethod")
private fun NavGraphBuilder.moduleRoutes(
navController: NavHostController,
) {
ModuleRoute.entries.forEach { moduleRoute ->
composable(moduleRoute.route::class) { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<Graph.RadioConfigGraph>()
}
when (moduleRoute) {
ModuleRoute.MQTT -> MQTTConfigScreen(hiltViewModel(parentEntry))
ModuleRoute.SERIAL -> SerialConfigScreen(hiltViewModel(parentEntry))
ModuleRoute.EXT_NOTIFICATION -> ExternalNotificationConfigScreen(
hiltViewModel(parentEntry)
)
ModuleRoute.STORE_FORWARD -> StoreForwardConfigScreen(hiltViewModel(parentEntry))
ModuleRoute.RANGE_TEST -> RangeTestConfigScreen(hiltViewModel(parentEntry))
ModuleRoute.TELEMETRY -> TelemetryConfigScreen(hiltViewModel(parentEntry))
ModuleRoute.CANNED_MESSAGE -> CannedMessageConfigScreen(
hiltViewModel(parentEntry)
)
ModuleRoute.AUDIO -> AudioConfigScreen(hiltViewModel(parentEntry))
ModuleRoute.REMOTE_HARDWARE -> RemoteHardwareConfigScreen(
hiltViewModel(parentEntry)
)
ModuleRoute.NEIGHBOR_INFO -> NeighborInfoConfigScreen(hiltViewModel(parentEntry))
ModuleRoute.AMBIENT_LIGHTING -> AmbientLightingConfigScreen(
hiltViewModel(parentEntry)
)
ModuleRoute.DETECTION_SENSOR -> DetectionSensorConfigScreen(
hiltViewModel(parentEntry)
)
ModuleRoute.PAXCOUNTER -> PaxcounterConfigScreen(hiltViewModel(parentEntry))
}
}
}
}
// Config (type = AdminProtos.AdminMessage.ConfigType)
@Suppress("MagicNumber")
enum class ConfigRoute(
@StringRes val title: Int,
val route: Route,
val icon: ImageVector?,
val type: Int = 0
) {
USER(R.string.user, Route.User, Icons.Default.Person, 0),
CHANNELS(R.string.channels, Route.ChannelConfig, Icons.AutoMirrored.Default.List, 0),
DEVICE(R.string.device, Route.Device, Icons.Default.Router, 0),
POSITION(R.string.position, Route.Position, Icons.Default.LocationOn, 1),
POWER(R.string.power, Route.Power, Icons.Default.Power, 2),
NETWORK(R.string.network, Route.Network, Icons.Default.Wifi, 3),
DISPLAY(R.string.display, Route.Display, Icons.Default.DisplaySettings, 4),
LORA(R.string.lora, Route.LoRa, Icons.Default.CellTower, 5),
BLUETOOTH(R.string.bluetooth, Route.Bluetooth, Icons.Default.Bluetooth, 6),
SECURITY(R.string.security, Route.Security, Icons.Default.Security, 7),
;
companion object {
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ConfigRoute> = entries.filter {
when {
metadata == null -> true
it == BLUETOOTH -> metadata.hasBluetooth
it == NETWORK -> metadata.hasWifi || metadata.hasEthernet
else -> true // Include all other routes by default
}
}
}
}
// ModuleConfig (type = AdminProtos.AdminMessage.ModuleConfigType)
@Suppress("MagicNumber")
enum class ModuleRoute(
@StringRes val title: Int,
val route: Route,
val icon: ImageVector?,
val type: Int = 0
) {
MQTT(R.string.mqtt, Route.MQTT, Icons.Default.Cloud, 0),
SERIAL(R.string.serial, Route.Serial, Icons.Default.Usb, 1),
EXT_NOTIFICATION(
R.string.external_notification,
Route.ExtNotification,
Icons.Default.Notifications,
2
),
STORE_FORWARD(
R.string.store_forward,
Route.StoreForward,
Icons.AutoMirrored.Default.Forward,
3
),
RANGE_TEST(R.string.range_test, Route.RangeTest, Icons.Default.Speed, 4),
TELEMETRY(R.string.telemetry, Route.Telemetry, Icons.Default.DataUsage, 5),
CANNED_MESSAGE(
R.string.canned_message,
Route.CannedMessage,
Icons.AutoMirrored.Default.Message,
6
),
AUDIO(R.string.audio, Route.Audio, Icons.AutoMirrored.Default.VolumeUp, 7),
REMOTE_HARDWARE(
R.string.remote_hardware,
Route.RemoteHardware,
Icons.Default.SettingsRemote,
8
),
NEIGHBOR_INFO(R.string.neighbor_info, Route.NeighborInfo, Icons.Default.People, 9),
AMBIENT_LIGHTING(R.string.ambient_lighting, Route.AmbientLighting, Icons.Default.LightMode, 10),
DETECTION_SENSOR(R.string.detection_sensor, Route.DetectionSensor, Icons.Default.Sensors, 11),
PAXCOUNTER(R.string.paxcounter, Route.Paxcounter, Icons.Default.PermScanWifi, 12),
;
val bitfield: Int get() = 1 shl ordinal
companion object {
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ModuleRoute> = entries.filter {
when (metadata) {
null -> true
else -> metadata.excludedModules and it.bitfield == 0
}
}
}
}

View file

@ -1,196 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
import androidx.compose.runtime.remember
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.geeksville.mesh.ui.radioconfig.RadioConfigScreen
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.radioconfig.components.AmbientLightingConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.AudioConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.BluetoothConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.CannedMessageConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.ChannelConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.DetectionSensorConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.DeviceConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.DisplayConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.ExternalNotificationConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.LoRaConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.MQTTConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.NeighborInfoConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.NetworkConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.PaxcounterConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.PositionConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.PowerConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.RangeTestConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.RemoteHardwareConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.SecurityConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.SerialConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.StoreForwardConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.TelemetryConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.UserConfigScreen
@Suppress("LongMethod")
fun NavGraphBuilder.addRadioConfigSection(navController: NavController) {
composable<Route.RadioConfig> {
RadioConfigScreen(
onNavigate = navController::navigate,
)
}
composable<Route.User> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
UserConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.ChannelConfig> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
ChannelConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.Device> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
DeviceConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.Position> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
PositionConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.Power> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
PowerConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.Network> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
NetworkConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.Display> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
DisplayConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.LoRa> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
LoRaConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.Bluetooth> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
BluetoothConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.Security> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
SecurityConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.MQTT> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
MQTTConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.Serial> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
SerialConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.ExtNotification> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
ExternalNotificationConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.StoreForward> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
StoreForwardConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.RangeTest> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
RangeTestConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.Telemetry> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
TelemetryConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.CannedMessage> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
CannedMessageConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.Audio> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
AudioConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.RemoteHardware> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
RemoteHardwareConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.NeighborInfo> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
NeighborInfoConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.AmbientLighting> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
AmbientLightingConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.DetectionSensor> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
DetectionSensorConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
composable<Route.Paxcounter> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
PaxcounterConfigScreen(
viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry),
)
}
}

View file

@ -1,71 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
import kotlinx.serialization.Serializable
sealed interface Route {
@Serializable data object Contacts : Route
@Serializable data object Nodes : Route
@Serializable data object Map : Route
@Serializable data object Channels : Route
@Serializable data object Settings : Route
@Serializable data object DebugPanel : Route
@Serializable
data class Messages(val contactKey: String, val message: String = "") : Route
@Serializable data object QuickChat : Route
@Serializable
data class Share(val message: String) : Route
@Serializable
data class RadioConfig(val destNum: Int? = null) : Route
@Serializable data object User : Route
@Serializable data object ChannelConfig : Route
@Serializable data object Device : Route
@Serializable data object Position : Route
@Serializable data object Power : Route
@Serializable data object Network : Route
@Serializable data object Display : Route
@Serializable data object LoRa : Route
@Serializable data object Bluetooth : Route
@Serializable data object Security : Route
@Serializable data object MQTT : Route
@Serializable data object Serial : Route
@Serializable data object ExtNotification : Route
@Serializable data object StoreForward : Route
@Serializable data object RangeTest : Route
@Serializable data object Telemetry : Route
@Serializable data object CannedMessage : Route
@Serializable data object Audio : Route
@Serializable data object RemoteHardware : Route
@Serializable data object NeighborInfo : Route
@Serializable data object AmbientLighting : Route
@Serializable data object DetectionSensor : Route
@Serializable data object Paxcounter : Route
@Serializable data class NodeDetail(val destNum: Int) : Route
@Serializable data object DeviceMetrics : Route
@Serializable data object NodeMap : Route
@Serializable data object PositionLog : Route
@Serializable data object EnvironmentMetrics : Route
@Serializable data object SignalMetrics : Route
@Serializable data object PowerMetrics : Route
@Serializable data object TracerouteLog : Route
}

View file

@ -1,42 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import com.geeksville.mesh.ui.ShareScreen
fun NavController.navigateToSharedMessage(contactKey: String, message: String) {
navigate(Route.Messages(contactKey, message)) {
popUpTo<Route.Share> { inclusive = true }
}
}
fun NavGraphBuilder.shareScreen(
navigateUp: () -> Unit,
onConfirm: (String, String) -> Unit
) {
composable<Route.Share> { backStackEntry ->
val message = backStackEntry.toRoute<Route.Share>().message
ShareScreen(
navigateUp = navigateUp,
) { contactKey -> onConfirm(contactKey, message) }
}
}