From e4313e0bd3bd48a184dcb6925610cd18f98b0da5 Mon Sep 17 00:00:00 2001 From: Robert-0410 <62630290+Robert-0410@users.noreply.github.com> Date: Tue, 3 Jun 2025 08:31:08 -0700 Subject: [PATCH] feat: Provide Navigation to Input Timezone (#2003) --- .../{ChannelsGraph.kt => ChannelsRoutes.kt} | 22 +++-- .../mesh/navigation/ConnectionsRoutes.kt | 85 +++++++++++++++++++ .../geeksville/mesh/navigation/NavGraph.kt | 34 +------- .../main/java/com/geeksville/mesh/ui/Main.kt | 7 +- .../mesh/ui/connections/Connections.kt | 58 +++++++++---- .../com/geeksville/mesh/ui/sharing/Channel.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 7 files changed, 153 insertions(+), 56 deletions(-) rename app/src/main/java/com/geeksville/mesh/navigation/{ChannelsGraph.kt => ChannelsRoutes.kt} (78%) create mode 100644 app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ChannelsGraph.kt b/app/src/main/java/com/geeksville/mesh/navigation/ChannelsRoutes.kt similarity index 78% rename from app/src/main/java/com/geeksville/mesh/navigation/ChannelsGraph.kt rename to app/src/main/java/com/geeksville/mesh/navigation/ChannelsRoutes.kt index 2de3722ec..49607e463 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/ChannelsGraph.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/ChannelsRoutes.kt @@ -27,17 +27,27 @@ import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.ui.sharing.ChannelScreen import com.geeksville.mesh.ui.radioconfig.components.ChannelConfigScreen import com.geeksville.mesh.ui.radioconfig.components.LoRaConfigScreen +import kotlinx.serialization.Serializable + +@Serializable +sealed interface ChannelsRoutes { + @Serializable + data object ChannelsGraph : Graph + + @Serializable + data object Channels : Route +} /** - * Navigation graph for for the top level ChannelScreen - [Route.Channels]. + * Navigation graph for for the top level ChannelScreen - [ChannelsRoutes.Channels]. */ fun NavGraphBuilder.channelsGraph(navController: NavHostController, uiViewModel: UIViewModel) { - navigation( - startDestination = Route.Channels, + navigation( + startDestination = ChannelsRoutes.Channels, ) { - composable { backStackEntry -> + composable { backStackEntry -> val parentEntry = remember(backStackEntry) { - navController.getBackStackEntry() + navController.getBackStackEntry() } ChannelScreen( viewModel = uiViewModel, @@ -55,7 +65,7 @@ private fun NavGraphBuilder.configRoutes( ConfigRoute.entries.forEach { configRoute -> composable(configRoute.route::class) { backStackEntry -> val parentEntry = remember(backStackEntry) { - navController.getBackStackEntry() + navController.getBackStackEntry() } when (configRoute) { ConfigRoute.CHANNELS -> ChannelConfigScreen(hiltViewModel(parentEntry)) diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt new file mode 100644 index 000000000..718f60efe --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt @@ -0,0 +1,85 @@ +/* + * 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.compose.runtime.remember +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import androidx.navigation.navDeepLink +import androidx.navigation.navigation +import com.geeksville.mesh.model.UIViewModel +import com.geeksville.mesh.ui.connections.ConnectionsScreen +import com.geeksville.mesh.ui.radioconfig.components.LoRaConfigScreen +import kotlinx.serialization.Serializable + +@Serializable +sealed interface ConnectionsRoutes { + @Serializable + data object ConnectionsGraph : Graph + + @Serializable + data object Connections : Route +} + +/** + * Navigation graph for for the top level ConnectionsScreen - [ConnectionsRoutes.Connections]. + */ +fun NavGraphBuilder.connectionsGraph(navController: NavHostController, uiViewModel: UIViewModel) { + navigation( + startDestination = ConnectionsRoutes.Connections, + ) { + composable( + deepLinks = listOf( + navDeepLink { + uriPattern = "$DEEP_LINK_BASE_URI/connections" + action = "android.intent.action.VIEW" + } + ) + ) { backStackEntry -> + val parentEntry = remember(backStackEntry) { + navController.getBackStackEntry() + } + ConnectionsScreen( + uiViewModel, + radioConfigViewModel = hiltViewModel(parentEntry), + onNavigateToRadioConfig = { navController.navigate(Route.RadioConfig()) }, + onNavigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) }, + onConfigNavigate = { route -> navController.navigate(route) } + ) + } + configRoutes(navController) + } +} + +private fun NavGraphBuilder.configRoutes( + navController: NavHostController, +) { + ConfigRoute.entries.forEach { configRoute -> + composable(configRoute.route::class) { backStackEntry -> + val parentEntry = remember(backStackEntry) { + navController.getBackStackEntry() + } + when (configRoute) { + ConfigRoute.LORA -> LoRaConfigScreen(hiltViewModel(parentEntry)) + else -> Unit + } + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt b/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt index 89c1770f9..28dc42d05 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt @@ -32,7 +32,6 @@ import androidx.navigation.toRoute import com.geeksville.mesh.R import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel -import com.geeksville.mesh.ui.connections.ConnectionsScreen import com.geeksville.mesh.ui.contact.ContactsScreen import com.geeksville.mesh.ui.debug.DebugScreen import com.geeksville.mesh.ui.map.MapView @@ -53,9 +52,6 @@ const val DEEP_LINK_BASE_URI = "meshtastic://meshtastic" @Serializable sealed interface Graph : Route { - @Serializable - data class ChannelsGraph(val destNum: Int?) - @Serializable data class NodeDetailGraph(val destNum: Int) : Graph @@ -74,12 +70,6 @@ sealed interface Route { @Serializable data object Map : Route - @Serializable - data object Channels : Route - - @Serializable - data object Connections : Route - @Serializable data object DebugPanel : Route @@ -221,7 +211,7 @@ fun NavGraph( NavHost( navController = navController, startDestination = if (uIViewModel.bondedAddress.isNullOrBlank()) { - Route.Connections + ConnectionsRoutes.ConnectionsGraph } else { Route.Contacts }, @@ -246,26 +236,8 @@ fun NavGraph( channelsGraph(navController, uIViewModel) - composable( - deepLinks = listOf( - navDeepLink { - uriPattern = "$DEEP_LINK_BASE_URI/connections" - action = "android.intent.action.VIEW" - } - ) - ) { backStackEntry -> - ConnectionsScreen( - uIViewModel, - onNavigateToRadioConfig = { - navController.navigate(Route.RadioConfig()) { - popUpTo(Route.Connections) { - inclusive = false - } - } - }, - onNavigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) } - ) - } + connectionsGraph(navController, uIViewModel) + composable { DebugScreen() } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index 2197bea60..8cc9f7f79 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -74,6 +74,8 @@ import com.geeksville.mesh.BuildConfig import com.geeksville.mesh.R import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.UIViewModel +import com.geeksville.mesh.navigation.ChannelsRoutes +import com.geeksville.mesh.navigation.ConnectionsRoutes import com.geeksville.mesh.navigation.NavGraph import com.geeksville.mesh.navigation.Route import com.geeksville.mesh.navigation.showLongNameTitle @@ -88,8 +90,8 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, Contacts(R.string.contacts, Icons.AutoMirrored.TwoTone.Chat, Route.Contacts), Nodes(R.string.nodes, Icons.TwoTone.People, Route.Nodes), Map(R.string.map, Icons.TwoTone.Map, Route.Map), - Channels(R.string.channels, Icons.TwoTone.Contactless, Route.Channels), - Connections(R.string.connections, Icons.TwoTone.CloudOff, Route.Connections), + Channels(R.string.channels, Icons.TwoTone.Contactless, ChannelsRoutes.Channels), + Connections(R.string.connections, Icons.TwoTone.CloudOff, ConnectionsRoutes.Connections), ; companion object { @@ -323,7 +325,6 @@ private fun MainAppBar( ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun MainMenuActions( isManaged: Boolean, diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt index 24aefd162..b749b0354 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt @@ -42,6 +42,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Checkbox @@ -93,10 +94,16 @@ import com.geeksville.mesh.model.BluetoothViewModel import com.geeksville.mesh.model.NO_DEVICE_SELECTED import com.geeksville.mesh.model.Node import com.geeksville.mesh.model.UIViewModel +import com.geeksville.mesh.navigation.ConfigRoute +import com.geeksville.mesh.navigation.Route +import com.geeksville.mesh.navigation.getNavRouteFrom import com.geeksville.mesh.repository.network.NetworkRepository import com.geeksville.mesh.service.MeshService +import com.geeksville.mesh.ui.node.NodeActionButton import com.geeksville.mesh.ui.node.components.NodeChip import com.geeksville.mesh.ui.node.components.NodeMenuAction +import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel +import com.geeksville.mesh.ui.radioconfig.components.PacketResponseStateDialog import com.geeksville.mesh.ui.sharing.SharedContactDialog import kotlinx.coroutines.delay @@ -115,9 +122,12 @@ fun ConnectionsScreen( uiViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanModel = hiltViewModel(), bluetoothViewModel: BluetoothViewModel = hiltViewModel(), + radioConfigViewModel: RadioConfigViewModel = hiltViewModel(), onNavigateToRadioConfig: () -> Unit, onNavigateToNodeDetails: (Int) -> Unit, + onConfigNavigate: (Route) -> Unit ) { + val radioConfigState by radioConfigViewModel.radioConfigState.collectAsStateWithLifecycle() val config by uiViewModel.localConfig.collectAsState() val currentRegion = config.lora.region val scrollState = rememberScrollState() @@ -134,6 +144,25 @@ fun ConnectionsScreen( val regionUnset = currentRegion == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET && connectionState == MeshService.ConnectionState.CONNECTED + /* Animate waiting for the configurations */ + var isWaiting by remember { mutableStateOf(false) } + if (isWaiting) { + PacketResponseStateDialog( + state = radioConfigState.responseState, + onDismiss = { + isWaiting = false + radioConfigViewModel.clearPacketResponse() + }, + onComplete = { + getNavRouteFrom(radioConfigState.route)?.let { route -> + isWaiting = false + radioConfigViewModel.clearPacketResponse() + onConfigNavigate(route) + } + }, + ) + } + val isGpsDisabled = context.gpsDisabled() LaunchedEffect(isGpsDisabled) { if (isGpsDisabled) { @@ -285,28 +314,27 @@ fun ConnectionsScreen( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "${node.user.longName}", + text = node.user.longName, style = MaterialTheme.typography.titleLarge ) } } - Button( - modifier = Modifier.fillMaxWidth(), + NodeActionButton( + title = stringResource(id = R.string.radio_configuration), + icon = Icons.Default.Settings, + enabled = true, onClick = onNavigateToRadioConfig - - ) { - Text(stringResource(R.string.radio_configuration)) - } + ) Spacer(modifier = Modifier.height(8.dp)) if (regionUnset && selectedDevice != "m") { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - text = stringResource(R.string.must_set_region), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.titleLarge, - textAlign = TextAlign.Center + NodeActionButton( + title = stringResource(id = R.string.set_your_region), + icon = ConfigRoute.LORA.icon, + enabled = true, + onClick = { + isWaiting = true + radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA) + } ) Spacer(modifier = Modifier.height(8.dp)) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt index 4ce002ddc..549b46cb4 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt @@ -138,7 +138,7 @@ fun ChannelScreen( var showResetDialog by remember { mutableStateOf(false) } var showScanDialog by remember { mutableStateOf(false) } - /* Animate waiting for the channel configurations */ + /* Animate waiting for the configurations */ var isWaiting by remember { mutableStateOf(false) } if (isWaiting) { PacketResponseStateDialog( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 58da37a5b..9048bd7b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -660,5 +660,6 @@ Map Contacts Nodes + Set your region Reply