/* * 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 . */ /* * 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 . */ package com.geeksville.mesh.navigation import androidx.annotation.StringRes import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier 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.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 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), } const val DEEP_LINK_BASE_URI = "meshtastic://meshtastic" @Serializable sealed interface Graph : Route { @Serializable data class NodeDetailGraph(val destNum: Int) : Graph @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() || this.hasRoute() || this.isConfigRoute() || this.isNodeDetailRoute() ) } @OptIn(ExperimentalSharedTransitionApi::class) @Suppress("LongMethod") @Composable fun NavGraph( modifier: Modifier = Modifier, uIViewModel: UIViewModel = hiltViewModel(), navController: NavHostController = rememberNavController(), ) { SharedTransitionLayout { NavHost( navController = navController, startDestination = if (uIViewModel.bondedAddress.isNullOrBlank()) { Route.Settings } else { Route.Contacts }, modifier = modifier, ) { composable { ContactsScreen( uIViewModel, onNavigate = { navController.navigate(Route.Messages(it)) } ) } composable { NodeScreen( model = uIViewModel, navigateToMessages = { navController.navigate(Route.Messages(it)) }, navigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) }, sharedTransitionScope = this@SharedTransitionLayout, this@composable, ) } composable { MapView(uIViewModel) } composable { ChannelScreen(uIViewModel) } composable( deepLinks = listOf( navDeepLink { uriPattern = "$DEEP_LINK_BASE_URI/settings" action = "android.intent.action.VIEW" } ) ) { backStackEntry -> SettingsScreen( uIViewModel, onNavigateToRadioConfig = { navController.navigate(Route.RadioConfig()) { popUpTo(Route.Settings) { inclusive = false } } }, onNavigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) } ) } composable { DebugScreen() } composable( deepLinks = listOf( navDeepLink { uriPattern = "$DEEP_LINK_BASE_URI/messages/{contactKey}?message={message}" action = "android.intent.action.VIEW" }, ) ) { backStackEntry -> val args = backStackEntry.toRoute() MessageScreen( contactKey = args.contactKey, message = args.message, viewModel = uIViewModel, navigateToMessages = { navController.navigate(Route.Messages(it)) }, navigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) }, onNavigateBack = navController::navigateUp, sharedTransitionScope = this@SharedTransitionLayout, animatedContentScope = this@composable, ) } composable { QuickChatScreen() } nodeDetailGraph(navController, uIViewModel, sharedTransitionScope = this@SharedTransitionLayout) radioConfigGraph(navController, uIViewModel) composable( deepLinks = listOf( navDeepLink { uriPattern = "$DEEP_LINK_BASE_URI/share?message={message}" action = "android.intent.action.VIEW" } ) ) { backStackEntry -> val message = backStackEntry.toRoute().message ShareScreen(uIViewModel) { navController.navigate(Route.Messages(it, message)) { popUpTo { inclusive = true } } } } } } }