/* * Copyright (c) 2025-2026 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 . */ @file:Suppress("Wrapping", "SpacingAroundColon") package com.geeksville.mesh.navigation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.navDeepLink import androidx.navigation.navigation import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI import org.meshtastic.core.navigation.Graph import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.navigation.Route import org.meshtastic.core.navigation.SettingsRoutes import org.meshtastic.feature.settings.AboutScreen import org.meshtastic.feature.settings.AdministrationScreen import org.meshtastic.feature.settings.DeviceConfigurationScreen import org.meshtastic.feature.settings.ModuleConfigurationScreen import org.meshtastic.feature.settings.SettingsScreen import org.meshtastic.feature.settings.SettingsViewModel import org.meshtastic.feature.settings.debugging.DebugScreen import org.meshtastic.feature.settings.filter.FilterSettingsScreen import org.meshtastic.feature.settings.navigation.ConfigRoute import org.meshtastic.feature.settings.navigation.ModuleRoute import org.meshtastic.feature.settings.radio.CleanNodeDatabaseScreen import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.radio.channel.ChannelConfigScreen import org.meshtastic.feature.settings.radio.component.AmbientLightingConfigScreen import org.meshtastic.feature.settings.radio.component.AudioConfigScreen import org.meshtastic.feature.settings.radio.component.BluetoothConfigScreen import org.meshtastic.feature.settings.radio.component.CannedMessageConfigScreen import org.meshtastic.feature.settings.radio.component.DetectionSensorConfigScreen import org.meshtastic.feature.settings.radio.component.DeviceConfigScreen import org.meshtastic.feature.settings.radio.component.DisplayConfigScreen import org.meshtastic.feature.settings.radio.component.ExternalNotificationConfigScreen import org.meshtastic.feature.settings.radio.component.LoRaConfigScreen import org.meshtastic.feature.settings.radio.component.MQTTConfigScreen import org.meshtastic.feature.settings.radio.component.NeighborInfoConfigScreen import org.meshtastic.feature.settings.radio.component.NetworkConfigScreen import org.meshtastic.feature.settings.radio.component.PaxcounterConfigScreen import org.meshtastic.feature.settings.radio.component.PositionConfigScreen import org.meshtastic.feature.settings.radio.component.PowerConfigScreen import org.meshtastic.feature.settings.radio.component.RangeTestConfigScreen import org.meshtastic.feature.settings.radio.component.RemoteHardwareConfigScreen import org.meshtastic.feature.settings.radio.component.SecurityConfigScreen import org.meshtastic.feature.settings.radio.component.SerialConfigScreen import org.meshtastic.feature.settings.radio.component.StatusMessageConfigScreen import org.meshtastic.feature.settings.radio.component.StoreForwardConfigScreen import org.meshtastic.feature.settings.radio.component.TAKConfigScreen import org.meshtastic.feature.settings.radio.component.TelemetryConfigScreen import org.meshtastic.feature.settings.radio.component.TrafficManagementConfigScreen import org.meshtastic.feature.settings.radio.component.UserConfigScreen import kotlin.reflect.KClass @Suppress("LongMethod") fun NavGraphBuilder.settingsGraph(navController: NavHostController) { navigation(startDestination = SettingsRoutes.Settings()) { composable( deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/settings")), ) { backStackEntry -> val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) } SettingsScreen( settingsViewModel = hiltViewModel(parentEntry), viewModel = hiltViewModel(parentEntry), onClickNodeChip = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) { launchSingleTop = true restoreState = true } }, ) { navController.navigate(it) } } composable { backStackEntry -> val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) } DeviceConfigurationScreen( viewModel = hiltViewModel(parentEntry), onBack = navController::popBackStack, onNavigate = { route -> navController.navigate(route) }, ) } composable { backStackEntry -> val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) } val settingsViewModel: SettingsViewModel = hiltViewModel(parentEntry) val excludedModulesUnlocked by settingsViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle() ModuleConfigurationScreen( viewModel = hiltViewModel(parentEntry), excludedModulesUnlocked = excludedModulesUnlocked, onBack = navController::popBackStack, onNavigate = { route -> navController.navigate(route) }, ) } composable { backStackEntry -> val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) } AdministrationScreen(viewModel = hiltViewModel(parentEntry), onBack = navController::popBackStack) } composable( deepLinks = listOf( navDeepLink( basePath = "$DEEP_LINK_BASE_URI/settings/radio/clean_node_db", ), ), ) { CleanNodeDatabaseScreen() } ConfigRoute.entries.forEach { entry -> navController.configComposable( route = entry.route::class, parentGraphRoute = SettingsRoutes.SettingsGraph::class, ) { viewModel -> LaunchedEffect(Unit) { viewModel.setResponseStateLoading(entry) } when (entry) { ConfigRoute.USER -> UserConfigScreen(viewModel, onBack = navController::popBackStack) ConfigRoute.CHANNELS -> ChannelConfigScreen(viewModel, onBack = navController::popBackStack) ConfigRoute.DEVICE -> DeviceConfigScreen(viewModel, onBack = navController::popBackStack) ConfigRoute.POSITION -> PositionConfigScreen(viewModel, onBack = navController::popBackStack) ConfigRoute.POWER -> PowerConfigScreen(viewModel, onBack = navController::popBackStack) ConfigRoute.NETWORK -> NetworkConfigScreen(viewModel, onBack = navController::popBackStack) ConfigRoute.DISPLAY -> DisplayConfigScreen(viewModel, onBack = navController::popBackStack) ConfigRoute.LORA -> LoRaConfigScreen(viewModel, onBack = navController::popBackStack) ConfigRoute.BLUETOOTH -> BluetoothConfigScreen(viewModel, onBack = navController::popBackStack) ConfigRoute.SECURITY -> SecurityConfigScreen(viewModel, onBack = navController::popBackStack) } } } ModuleRoute.entries.forEach { entry -> navController.configComposable( route = entry.route::class, parentGraphRoute = SettingsRoutes.SettingsGraph::class, ) { viewModel -> LaunchedEffect(Unit) { viewModel.setResponseStateLoading(entry) } when (entry) { ModuleRoute.MQTT -> MQTTConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.SERIAL -> SerialConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.EXT_NOTIFICATION -> ExternalNotificationConfigScreen(viewModel = viewModel, onBack = navController::popBackStack) ModuleRoute.STORE_FORWARD -> StoreForwardConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.RANGE_TEST -> RangeTestConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.TELEMETRY -> TelemetryConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.CANNED_MESSAGE -> CannedMessageConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.AUDIO -> AudioConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.REMOTE_HARDWARE -> RemoteHardwareConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.NEIGHBOR_INFO -> NeighborInfoConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.AMBIENT_LIGHTING -> AmbientLightingConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.DETECTION_SENSOR -> DetectionSensorConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.PAXCOUNTER -> PaxcounterConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.STATUS_MESSAGE -> StatusMessageConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.TRAFFIC_MANAGEMENT -> TrafficManagementConfigScreen(viewModel, onBack = navController::popBackStack) ModuleRoute.TAK -> TAKConfigScreen(viewModel, onBack = navController::popBackStack) } } } composable( deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/settings/debug_panel")), ) { DebugScreen(onNavigateUp = navController::navigateUp) } composable { AboutScreen(onNavigateUp = navController::navigateUp) } composable { FilterSettingsScreen(onBack = navController::navigateUp) } } } context(_: NavGraphBuilder) inline fun NavHostController.configComposable( noinline content: @Composable (RadioConfigViewModel) -> Unit, ) { configComposable(route = R::class, parentGraphRoute = G::class, content = content) } context(navGraphBuilder: NavGraphBuilder) fun NavHostController.configComposable( route: KClass, parentGraphRoute: KClass, content: @Composable (RadioConfigViewModel) -> Unit, ) { navGraphBuilder.composable(route = route) { backStackEntry -> val parentEntry = remember(backStackEntry) { getBackStackEntry(parentGraphRoute) } content(hiltViewModel(parentEntry)) } }