diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt index d5cd956b8..1eb87a65d 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt @@ -57,29 +57,22 @@ fun MainScreen() { AndroidAppVersionCheck(viewModel) - MeshtasticAppShell( - backStack = backStack, - uiViewModel = viewModel, - hostModifier = Modifier, - ) { - MeshtasticNavigationSuite( - backStack = backStack, - uiViewModel = viewModel, - modifier = Modifier.fillMaxSize(), - ) { - val provider = entryProvider { - contactsGraph(backStack, viewModel.scrollToTopEventFlow) - nodesGraph( - backStack = backStack, - scrollToTopEvents = viewModel.scrollToTopEventFlow, - onHandleDeepLink = viewModel::handleDeepLink, - ) - mapGraph(backStack) - channelsGraph(backStack) - connectionsGraph(backStack) - settingsGraph(backStack) - firmwareGraph(backStack) - } + MeshtasticAppShell(backStack = backStack, uiViewModel = viewModel, hostModifier = Modifier) { + MeshtasticNavigationSuite(backStack = backStack, uiViewModel = viewModel, modifier = Modifier.fillMaxSize()) { + val provider = + entryProvider { + contactsGraph(backStack, viewModel.scrollToTopEventFlow) + nodesGraph( + backStack = backStack, + scrollToTopEvents = viewModel.scrollToTopEventFlow, + onHandleDeepLink = viewModel::handleDeepLink, + ) + mapGraph(backStack) + channelsGraph(backStack) + connectionsGraph(backStack) + settingsGraph(backStack) + firmwareGraph(backStack) + } MeshtasticNavDisplay( backStack = backStack, entryProvider = provider, @@ -115,4 +108,4 @@ private fun AndroidAppVersionCheck(viewModel: UIViewModel) { } } } -} \ No newline at end of file +} diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 81ed465c6..a50d13d44 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -57,6 +57,7 @@ kotlin { implementation(libs.jetbrains.compose.material3.adaptive.navigation.suite) implementation(libs.jetbrains.navigationevent.compose) implementation(libs.jetbrains.navigation3.ui) + implementation(libs.jetbrains.compose.material3.adaptive.navigation3) implementation(libs.jetbrains.lifecycle.viewmodel.navigation3) implementation(libs.jetbrains.lifecycle.viewmodel.compose) } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AdaptiveListDetailScaffold.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AdaptiveListDetailScaffold.kt deleted file mode 100644 index 415937ccc..000000000 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AdaptiveListDetailScaffold.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 . - */ -package org.meshtastic.core.ui.component - -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.layout.AnimatedPane -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole -import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior -import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.key -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalFocusManager -import androidx.navigationevent.NavigationEventInfo -import androidx.navigationevent.compose.NavigationBackHandler -import androidx.navigationevent.compose.rememberNavigationEventState -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun AdaptiveListDetailScaffold( - navigator: ThreePaneScaffoldNavigator, - scrollToTopEvents: Flow, - onBackToGraph: () -> Unit, - onTabPressedEvent: (ScrollToTopEvent) -> Boolean, - initialKey: T? = null, - listPane: @Composable (isActive: Boolean, contentKey: T?) -> Unit, - detailPane: @Composable (contentKey: T, handleBack: () -> Unit) -> Unit, - emptyDetailPane: @Composable () -> Unit, -) { - val scope = rememberCoroutineScope() - val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange - - val handleBack: () -> Unit = { - if (navigator.canNavigateBack(backNavigationBehavior)) { - scope.launch { navigator.navigateBack(backNavigationBehavior) } - } else { - onBackToGraph() - } - } - - val navState = rememberNavigationEventState(NavigationEventInfo.None) - NavigationBackHandler( - state = navState, - isBackEnabled = navigator.currentDestination?.pane == ListDetailPaneScaffoldRole.Detail, - onBackCancelled = {}, - onBackCompleted = { handleBack() }, - ) - - LaunchedEffect(initialKey) { - if (initialKey != null) { - navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, initialKey) - } - } - - LaunchedEffect(scrollToTopEvents) { - scrollToTopEvents.collect { event -> - if (onTabPressedEvent(event) && navigator.currentDestination?.pane == ListDetailPaneScaffoldRole.Detail) { - if (navigator.canNavigateBack(backNavigationBehavior)) { - navigator.navigateBack(backNavigationBehavior) - } else { - navigator.navigateTo(ListDetailPaneScaffoldRole.List) - } - } - } - } - - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, - listPane = { - AnimatedPane { - val focusManager = LocalFocusManager.current - // Prevent TextFields from auto-focusing when pane animates in - LaunchedEffect(Unit) { focusManager.clearFocus() } - - listPane( - navigator.currentDestination?.pane == ListDetailPaneScaffoldRole.List, - navigator.currentDestination?.contentKey, - ) - } - }, - detailPane = { - AnimatedPane { - val focusManager = LocalFocusManager.current - - navigator.currentDestination?.contentKey?.let { contentKey -> - key(contentKey) { - LaunchedEffect(contentKey) { focusManager.clearFocus() } - detailPane(contentKey, handleBack) - } - } ?: emptyDetailPane() - } - }, - ) -} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt index c7aab787f..96d5aa472 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt @@ -21,6 +21,8 @@ import androidx.compose.animation.ContentTransform import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator @@ -52,6 +54,8 @@ private const val TRANSITION_DURATION_MS = 350 * **Scene strategies** (evaluated in order): * - [DialogSceneStrategy] — entries annotated with `metadata = DialogSceneStrategy.dialog()` render as overlay * [Dialog][androidx.compose.ui.window.Dialog] windows with proper backstack lifecycle. + * - [ListDetailSceneStrategy] — entries annotated with `listPane()` / `detailPane()` render in adaptive list-detail + * layout on wider screens. * - [SinglePaneSceneStrategy] — default single-pane fallback. * * **Transitions**: A uniform 350 ms crossfade for both forward and pop navigation. @@ -60,18 +64,20 @@ private const val TRANSITION_DURATION_MS = 350 * @param entryProvider the entry provider built from feature navigation graphs. * @param modifier modifier applied to the underlying [NavDisplay]. */ +@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable fun MeshtasticNavDisplay( backStack: List, entryProvider: (key: NavKey) -> NavEntry, modifier: Modifier = Modifier, ) { + val listDetailSceneStrategy = rememberListDetailSceneStrategy() NavDisplay( backStack = backStack, entryProvider = entryProvider, entryDecorators = listOf(rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator()), - sceneStrategies = listOf(DialogSceneStrategy(), SinglePaneSceneStrategy()), + sceneStrategies = listOf(DialogSceneStrategy(), listDetailSceneStrategy, SinglePaneSceneStrategy()), transitionSpec = meshtasticTransitionSpec(), popTransitionSpec = meshtasticTransitionSpec(), modifier = modifier, diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt index 0280c193f..0a8082226 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt @@ -32,12 +32,14 @@ import org.meshtastic.core.ui.component.MeshtasticNavigationSuite import org.meshtastic.core.ui.viewmodel.UIViewModel import org.meshtastic.desktop.navigation.desktopNavGraph -/** - * Desktop main screen — uses shared navigation components. - */ +/** Desktop main screen — uses shared navigation components. */ @Composable fun DesktopMainScreen(uiViewModel: UIViewModel) { - val backStack = androidx.navigation3.runtime.rememberNavBackStack(MeshtasticNavSavedStateConfig, NodesRoutes.NodesGraph as NavKey) + val backStack = + androidx.navigation3.runtime.rememberNavBackStack( + MeshtasticNavSavedStateConfig, + NodesRoutes.NodesGraph as NavKey, + ) Surface(modifier = Modifier.fillMaxSize()) { MeshtasticAppShell( @@ -45,10 +47,12 @@ fun DesktopMainScreen(uiViewModel: UIViewModel) { uiViewModel = uiViewModel, hostModifier = Modifier.padding(bottom = 24.dp), ) { - MeshtasticNavigationSuite(backStack = backStack, uiViewModel = uiViewModel, modifier = Modifier.fillMaxSize()) { - val provider = entryProvider { - desktopNavGraph(backStack, uiViewModel) - } + MeshtasticNavigationSuite( + backStack = backStack, + uiViewModel = uiViewModel, + modifier = Modifier.fillMaxSize(), + ) { + val provider = entryProvider { desktopNavGraph(backStack, uiViewModel) } MeshtasticNavDisplay(backStack = backStack, entryProvider = provider, modifier = Modifier.fillMaxSize()) } } diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index b517434d2..80e9d8c8c 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -51,6 +51,7 @@ kotlin { implementation(libs.jetbrains.compose.material3.adaptive) implementation(libs.jetbrains.compose.material3.adaptive.layout) implementation(libs.jetbrains.compose.material3.adaptive.navigation) + implementation(libs.jetbrains.compose.material3.adaptive.navigation3) } androidMain.dependencies { implementation(libs.androidx.work.runtime.ktx) } diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt index 94794465d..75bd59b8c 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt @@ -16,6 +16,8 @@ */ package org.meshtastic.feature.messaging.navigation +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -33,20 +35,21 @@ import org.meshtastic.feature.messaging.ui.contact.AdaptiveContactsScreen import org.meshtastic.feature.messaging.ui.contact.ContactsViewModel import org.meshtastic.feature.messaging.ui.sharing.ShareScreen +@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Suppress("LongMethod") fun EntryProviderScope.contactsGraph( backStack: NavBackStack, scrollToTopEvents: Flow = MutableSharedFlow(), ) { - entry { + entry(metadata = { ListDetailSceneStrategy.listPane() }) { ContactsEntryContent(backStack = backStack, scrollToTopEvents = scrollToTopEvents) } - entry { + entry(metadata = { ListDetailSceneStrategy.listPane() }) { ContactsEntryContent(backStack = backStack, scrollToTopEvents = scrollToTopEvents) } - entry { args -> + entry(metadata = { ListDetailSceneStrategy.detailPane() }) { args -> ContactsEntryContent( backStack = backStack, scrollToTopEvents = scrollToTopEvents, diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt index 06dd0c69a..cddf92498 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt @@ -16,34 +16,19 @@ */ package org.meshtastic.feature.messaging.ui.contact -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.launch -import org.jetbrains.compose.resources.stringResource 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.resources.Res -import org.meshtastic.core.resources.conversations -import org.meshtastic.core.ui.component.AdaptiveListDetailScaffold -import org.meshtastic.core.ui.component.EmptyDetailPlaceholder import org.meshtastic.core.ui.component.ScrollToTopEvent -import org.meshtastic.core.ui.icon.Conversations -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.feature.messaging.MessageScreen import org.meshtastic.feature.messaging.MessageViewModel import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.SharedContact -@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod") -@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable fun AdaptiveContactsScreen( backStack: NavBackStack, @@ -59,75 +44,18 @@ fun AdaptiveContactsScreen( initialMessage: String = "", detailPaneCustom: @Composable ((contactKey: String) -> Unit)? = null, ) { - val navigator = rememberListDetailPaneScaffoldNavigator() - val scope = rememberCoroutineScope() - - val onBackToGraph: () -> Unit = { - val currentKey = backStack.lastOrNull() - - if ( - currentKey is ContactsRoutes.Messages || - currentKey is ContactsRoutes.Contacts || - currentKey is ContactsRoutes.ContactsGraph - ) { - // Check if we navigated here from another screen (e.g., from Nodes or Map) - val previousKey = if (backStack.size > 1) backStack[backStack.size - 2] else null - val isFromDifferentGraph = - previousKey != null && - previousKey !is ContactsRoutes.ContactsGraph && - previousKey !is ContactsRoutes.Contacts && - previousKey !is ContactsRoutes.Messages - - if (isFromDifferentGraph) { - // Navigate back via NavController to return to the previous screen (e.g. Node Details) - backStack.removeLastOrNull() - } - } - } - - AdaptiveListDetailScaffold( - navigator = navigator, + ContactsScreen( + onNavigateToShare = { backStack.add(ChannelsRoutes.ChannelsGraph) }, + sharedContactRequested = sharedContactRequested, + requestChannelSet = requestChannelSet, + onHandleDeepLink = onHandleDeepLink, + onClearSharedContactRequested = onClearSharedContactRequested, + onClearRequestChannelUrl = onClearRequestChannelUrl, + viewModel = contactsViewModel, + onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) }, + onNavigateToMessages = { contactKey -> backStack.add(ContactsRoutes.Messages(contactKey)) }, + onNavigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetailGraph(it)) }, scrollToTopEvents = scrollToTopEvents, - onBackToGraph = onBackToGraph, - onTabPressedEvent = { it is ScrollToTopEvent.ConversationsTabPressed }, - initialKey = initialContactKey, - listPane = { isActive, activeContactKey -> - ContactsScreen( - onNavigateToShare = { backStack.add(ChannelsRoutes.ChannelsGraph) }, - sharedContactRequested = sharedContactRequested, - requestChannelSet = requestChannelSet, - onHandleDeepLink = onHandleDeepLink, - onClearSharedContactRequested = onClearSharedContactRequested, - onClearRequestChannelUrl = onClearRequestChannelUrl, - viewModel = contactsViewModel, - onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) }, - onNavigateToMessages = { contactKey -> - scope.launch { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, contactKey) } - }, - onNavigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetailGraph(it)) }, - scrollToTopEvents = scrollToTopEvents, - activeContactKey = activeContactKey, - ) - }, - detailPane = { contentKey, handleBack -> - if (detailPaneCustom != null) { - detailPaneCustom(contentKey) - } else { - MessageScreen( - contactKey = contentKey, - message = if (contentKey == initialContactKey) initialMessage else "", - viewModel = messageViewModel, - navigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetailGraph(it)) }, - navigateToQuickChatOptions = { backStack.add(ContactsRoutes.QuickChat) }, - onNavigateBack = handleBack, - ) - } - }, - emptyDetailPane = { - EmptyDetailPlaceholder( - icon = MeshtasticIcons.Conversations, - title = stringResource(Res.string.conversations), - ) - }, + activeContactKey = null, ) } diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts index 56c06606a..7a455abe9 100644 --- a/feature/node/build.gradle.kts +++ b/feature/node/build.gradle.kts @@ -59,6 +59,7 @@ kotlin { implementation(libs.jetbrains.compose.material3.adaptive) implementation(libs.jetbrains.compose.material3.adaptive.layout) implementation(libs.jetbrains.compose.material3.adaptive.navigation) + implementation(libs.jetbrains.compose.material3.adaptive.navigation3) } androidMain.dependencies { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt index 6316ec715..09ec48b63 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt @@ -16,35 +16,18 @@ */ package org.meshtastic.feature.node.navigation -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.launch -import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.navigation.ChannelsRoutes import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.navigation.Route -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.nodes -import org.meshtastic.core.ui.component.AdaptiveListDetailScaffold -import org.meshtastic.core.ui.component.EmptyDetailPlaceholder import org.meshtastic.core.ui.component.ScrollToTopEvent -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.Nodes -import org.meshtastic.feature.node.compass.CompassViewModel -import org.meshtastic.feature.node.detail.NodeDetailScreen -import org.meshtastic.feature.node.detail.NodeDetailViewModel import org.meshtastic.feature.node.list.NodeListScreen import org.meshtastic.feature.node.list.NodeListViewModel -@Suppress("LongMethod") -@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable fun AdaptiveNodeListScreen( backStack: NavBackStack, @@ -55,54 +38,13 @@ fun AdaptiveNodeListScreen( onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> }, ) { val nodeListViewModel: NodeListViewModel = koinViewModel() - val navigator = rememberListDetailPaneScaffoldNavigator() - val scope = rememberCoroutineScope() - val onBackToGraph: () -> Unit = { - val currentKey = backStack.lastOrNull() - val isNodesRoute = currentKey is NodesRoutes.Nodes || currentKey is NodesRoutes.NodesGraph - val previousKey = if (backStack.size > 1) backStack[backStack.size - 2] else null - val isFromDifferentGraph = - previousKey != null && previousKey !is NodesRoutes.NodesGraph && previousKey !is NodesRoutes.Nodes - - if (isFromDifferentGraph && !isNodesRoute) { - // Navigate back via NavController to return to the previous screen - backStack.removeLastOrNull() - } - } - - AdaptiveListDetailScaffold( - navigator = navigator, + NodeListScreen( + viewModel = nodeListViewModel, + navigateToNodeDetails = { nodeId -> backStack.add(NodesRoutes.NodeDetail(nodeId)) }, + onNavigateToChannels = { backStack.add(ChannelsRoutes.ChannelsGraph) }, scrollToTopEvents = scrollToTopEvents, - onBackToGraph = onBackToGraph, - onTabPressedEvent = { it is ScrollToTopEvent.NodesTabPressed }, - initialKey = initialNodeId, - listPane = { isActive, activeNodeId -> - NodeListScreen( - viewModel = nodeListViewModel, - navigateToNodeDetails = { nodeId -> - scope.launch { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, nodeId) } - }, - onNavigateToChannels = { backStack.add(ChannelsRoutes.ChannelsGraph) }, - scrollToTopEvents = scrollToTopEvents, - activeNodeId = activeNodeId, - onHandleDeepLink = onHandleDeepLink, - ) - }, - detailPane = { contentKey, handleBack -> - val nodeDetailViewModel: NodeDetailViewModel = koinViewModel() - val compassViewModel: CompassViewModel = koinViewModel() - NodeDetailScreen( - nodeId = contentKey, - viewModel = nodeDetailViewModel, - compassViewModel = compassViewModel, - navigateToMessages = onNavigateToMessages, - onNavigate = onNavigate, - onNavigateUp = handleBack, - ) - }, - emptyDetailPane = { - EmptyDetailPlaceholder(icon = MeshtasticIcons.Nodes, title = stringResource(Res.string.nodes)) - }, + activeNodeId = null, + onHandleDeepLink = onHandleDeepLink, ) } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt index 48789342f..8cfb72881 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt @@ -26,6 +26,8 @@ import androidx.compose.material.icons.rounded.People import androidx.compose.material.icons.rounded.PermScanWifi import androidx.compose.material.icons.rounded.Power import androidx.compose.material.icons.rounded.Router +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector import androidx.navigation3.runtime.EntryProviderScope @@ -63,13 +65,14 @@ import org.meshtastic.feature.node.metrics.SignalMetricsScreen import org.meshtastic.feature.node.metrics.TracerouteLogScreen import kotlin.reflect.KClass +@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Suppress("LongMethod") fun EntryProviderScope.nodesGraph( backStack: NavBackStack, scrollToTopEvents: Flow = MutableSharedFlow(), onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> }, ) { - entry { + entry(metadata = { ListDetailSceneStrategy.listPane() }) { AdaptiveNodeListScreen( backStack = backStack, scrollToTopEvents = scrollToTopEvents, @@ -79,7 +82,7 @@ fun EntryProviderScope.nodesGraph( ) } - entry { + entry(metadata = { ListDetailSceneStrategy.listPane() }) { AdaptiveNodeListScreen( backStack = backStack, scrollToTopEvents = scrollToTopEvents, @@ -92,13 +95,14 @@ fun EntryProviderScope.nodesGraph( nodeDetailGraph(backStack, scrollToTopEvents, onHandleDeepLink) } +@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Suppress("LongMethod") fun EntryProviderScope.nodeDetailGraph( backStack: NavBackStack, scrollToTopEvents: Flow, onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> }, ) { - entry { args -> + entry(metadata = { ListDetailSceneStrategy.listPane() }) { args -> AdaptiveNodeListScreen( backStack = backStack, scrollToTopEvents = scrollToTopEvents, @@ -109,7 +113,7 @@ fun EntryProviderScope.nodeDetailGraph( ) } - entry { args -> + entry(metadata = { ListDetailSceneStrategy.detailPane() }) { args -> AdaptiveNodeListScreen( backStack = backStack, scrollToTopEvents = scrollToTopEvents, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92bc240d1..7ce547582 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -140,6 +140,7 @@ compose-multiplatform-materialIconsExtended = { module = "org.jetbrains.compose. jetbrains-compose-material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "jetbrains-adaptive" } jetbrains-compose-material3-adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "jetbrains-adaptive" } jetbrains-compose-material3-adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "jetbrains-adaptive" } +jetbrains-compose-material3-adaptive-navigation3 = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation3", version.ref = "jetbrains-adaptive" } jetbrains-compose-material3-adaptive-navigation-suite = { module = "org.jetbrains.compose.material3:material3-adaptive-navigation-suite", version.ref = "compose-multiplatform-material3" } # Google