diff options
| author | Kalle Lindström <karl.lindstrom@mullvad.net> | 2026-03-30 13:51:21 +0200 |
|---|---|---|
| committer | Kalle Lindström <karl.lindstrom@mullvad.net> | 2026-03-30 13:51:21 +0200 |
| commit | 8522ba1a285978c3de65a4d3fbfa35f977f532e9 (patch) | |
| tree | e416b630debc2a387ee668af71cdbab28ef41409 | |
| parent | 35f20650a17aa9139d72b36305f660078e8a3882 (diff) | |
| parent | 7101452514577b31fd4a1bbc91eff524dff3df24 (diff) | |
| download | mullvadvpn-8522ba1a285978c3de65a4d3fbfa35f977f532e9.tar.xz mullvadvpn-8522ba1a285978c3de65a4d3fbfa35f977f532e9.zip | |
Merge branch 'add-nav3-master-detail-scene-droid-2419'
40 files changed, 462 insertions, 51 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/app/MullvadApp.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/app/MullvadApp.kt index 81a6d6a938..2052a5de11 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/app/MullvadApp.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/app/MullvadApp.kt @@ -36,10 +36,12 @@ import kotlinx.coroutines.cancel import net.mullvad.mullvadvpn.common.compose.LocalSharedTransitionScope import net.mullvad.mullvadvpn.common.compose.accessibilityDataSensitive import net.mullvad.mullvadvpn.core.LocalResultStore +import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.TRANSITION_DEFAULT_DURATION_MS import net.mullvad.mullvadvpn.core.rememberNavigationState import net.mullvad.mullvadvpn.core.rememberResultStore +import net.mullvad.mullvadvpn.core.scene.rememberListDetailSceneStrategy import net.mullvad.mullvadvpn.core.toEntries import net.mullvad.mullvadvpn.feature.account.impl.navigation.accountEntry import net.mullvad.mullvadvpn.feature.addtime.impl.navigation.addTimeVerificationPendingEntry @@ -85,7 +87,15 @@ import org.koin.androidx.compose.koinViewModel fun MullvadApp(serviceConnectionManager: ServiceConnectionManager) { val resultStore = rememberResultStore() val navigationState = rememberNavigationState(SplashNavKey) - val nav3 = remember { Navigator(navigationState, resultStore) } + val listDetailStrategy = rememberListDetailSceneStrategy<NavKey2>() + + val nav3 = remember { + Navigator( + state = navigationState, + resultStore = resultStore, + screenIsListDetailTargetWidth = listDetailStrategy.isListDetailTargetWidth(), + ) + } val mullvadAppViewModel = koinViewModel<MullvadAppViewModel>() @@ -141,7 +151,12 @@ fun MullvadApp(serviceConnectionManager: ServiceConnectionManager) { Modifier.semantics { testTagsAsResourceId = true } .fillMaxSize() .accessibilityDataSensitive(), - sceneStrategies = listOf(DialogSceneStrategy(), SinglePaneSceneStrategy()), + sceneStrategies = + listOf( + listDetailStrategy, + DialogSceneStrategy(), + SinglePaneSceneStrategy(), + ), entries = navigationState.toEntries(entryProvider), onBack = { nav3.goBack() }, sharedTransitionScope = this@SharedTransitionLayout, diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 8d1f6af9fa..1c92d2708f 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -26,7 +26,7 @@ androidx-espresso = "3.7.0" androidx-ktx = "1.18.0" androidx-lifecycle = "2.10.0" androidx-nav3 = "1.1.0-rc01" -androidx-material3-adaptive = "1.2.0" +androidx-adaptive = "1.2.0" androidx-test = "1.7.0" androidx-testmonitor = "1.8.0" androidx-testorchestrator = "1.6.1" @@ -85,6 +85,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "compose" } androidx-annotation-jvm = { group = "androidx.annotation", name = "annotation-jvm", version.ref = "annotation-jvm" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "androidx-adaptive" } androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmark-macro-junit4" } androidx-coresplashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-coresplashscreen" } androidx-credentials = { module = "androidx.credentials:credentials", version.ref = "androidx-credentials" } @@ -100,7 +101,6 @@ androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-ru androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-lifecycle" } -androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "androidx-material3-adaptive" } androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-nav3" } androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-nav3" } androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" } @@ -122,6 +122,7 @@ compose-constrainlayout = { module = "androidx.constraintlayout:constraintlayout compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } compose-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose-material-icons-extended" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } +compose-material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "compose-material3" } compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } diff --git a/android/gradle/verification-metadata.keys.xml b/android/gradle/verification-metadata.keys.xml index f973de52eb..6b296c62c5 100644 --- a/android/gradle/verification-metadata.keys.xml +++ b/android/gradle/verification-metadata.keys.xml @@ -48,6 +48,7 @@ </trusted-key> <trusted-key id="0F06FF86BEEAF4E71866EE5232EE5355A6BC6E42"> <trusting group="androidx.activity"/> + <trusting group="androidx.annotation"/> <trusting group="androidx.appcompat"/> <trusting group="androidx.benchmark"/> <trusting group="androidx.collection"/> diff --git a/android/gradle/verification-metadata.xml b/android/gradle/verification-metadata.xml index c775d4a7c0..60aa8af64f 100644 --- a/android/gradle/verification-metadata.xml +++ b/android/gradle/verification-metadata.xml @@ -258,6 +258,14 @@ <sha256 value="2ac2f7106e12f263425b4a4dfc80989447fb895675fe902d86759aa74fd12b7d" origin="Generated by Gradle"/> </artifact> </component> + <component group="androidx.annotation" name="annotation-experimental" version="1.5.1"> + <artifact name="annotation-experimental-1.5.1.aar"> + <sha256 value="323a119b32e91d44e932fb97ed28eb568ca2a9d3ba57ec09392074b5c5e24ad4" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-experimental-1.5.1.module"> + <sha256 value="ae1ff29a7f3965e6589acfe7f07ef39d36405d7ab278472327afd92fc6d72a3f" origin="Generated by Gradle"/> + </artifact> + </component> <component group="androidx.annotation" name="annotation-jvm" version="1.7.0"> <artifact name="annotation-jvm-1.7.0.module"> <sha256 value="07ce60c377ab94e47c8c902589b9776030064fd1a7e4d5a01a38d700e35e5db4" origin="Generated by Gradle"/> @@ -468,6 +476,11 @@ <sha256 value="209bfe0d6bc8a700597762c6944eba401f1ae920dd7f6f959738bbce3f4dd1d2" origin="Generated by Gradle"/> </artifact> </component> + <component group="androidx.compose.animation" name="animation-core" version="1.9.0"> + <artifact name="animation-core-1.9.0.module"> + <sha256 value="4c6c8ea8e5773499eeab487336c083f6bd966997520b208f05691fd700b052ac" origin="Generated by Gradle"/> + </artifact> + </component> <component group="androidx.compose.animation" name="animation-core-android" version="1.10.6"> <artifact name="animation-core-android-1.10.6.module"> <sha256 value="05878f7ca41fcd878c018985de4375bc5f0abefc6bb40e8623e187d59a92b461" origin="Generated by Gradle"/> @@ -484,6 +497,11 @@ <sha256 value="db3f2a9df5b78286bd01f75107e6146e23095b9bfdc1186773e7efe344c9ab79" origin="Generated by Gradle"/> </artifact> </component> + <component group="androidx.compose.animation" name="animation-core-android" version="1.9.0"> + <artifact name="animation-core-android-1.9.0.module"> + <sha256 value="8f4963bbed56beb8df895e71e889c23c25883518b6ce2192a895c30e9d2fa032" origin="Generated by Gradle"/> + </artifact> + </component> <component group="androidx.compose.animation" name="animation-core-jvmstubs" version="1.10.6"> <artifact name="animation-core-jvmstubs-1.10.6.jar"> <sha256 value="0d69b7dca5c4c38db46ca50701b13248ea62cfaee4fc0459aba36294ae3bd22b" origin="Generated by Gradle"/> @@ -746,6 +764,69 @@ <sha256 value="23f6c9753a99718baa2be29538f62d1518ac53d6dc19992fc2c510473310cba0" origin="Generated by Gradle"/> </artifact> </component> + <component group="androidx.compose.material3" name="material3-window-size-class" version="1.4.0"> + <artifact name="material3-window-size-class-1.4.0.module"> + <sha256 value="73af2582aefa91c0b2c9c10556f3c8163e1c7a23bf2c64334adf7676a534bfc0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.compose.material3" name="material3-window-size-class-android" version="1.4.0"> + <artifact name="material3-window-size-class-android-1.4.0.module"> + <sha256 value="33342faf0a95cb7cd2700b95ce7c022434a476ce55d4534a0a7f8cf04b39a649" origin="Generated by Gradle"/> + </artifact> + <artifact name="material3-window-size-class.aar"> + <sha256 value="97b32a3a7d09c68ccb729ec5dc5b780e4c0f4f5e81d82c64557c6daec2aceb35" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.compose.material3" name="material3-window-size-class-jvmstubs" version="1.4.0"> + <artifact name="material3-window-size-class-jvmstubs-1.4.0.jar"> + <sha256 value="429715b96ddba8d00117b019fd5656eb347637cc846d44fbe1ac2beebd04d67b" origin="Generated by Gradle"/> + </artifact> + <artifact name="material3-window-size-class-jvmstubs-1.4.0.module"> + <sha256 value="8986885885ff9b2f905fb99ec27a8eb750aae2d8e563884870bafd78f464030b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.compose.material3.adaptive" name="adaptive" version="1.2.0"> + <artifact name="adaptive-1.2.0.module"> + <sha256 value="460af7b7afb8c56f841323c594586bc59cbe1c9ec50a038826d982f05201ef67" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.compose.material3.adaptive" name="adaptive-android" version="1.2.0"> + <artifact name="adaptive-android-1.2.0.module"> + <sha256 value="95f45eb07dd805e0f7dbf6f1426cac3ff2107594fa873cabc6111a4736e6654c" origin="Generated by Gradle"/> + </artifact> + <artifact name="adaptive.aar"> + <sha256 value="4b3edeb29928e6dcb6ef764fbb85c91e1150b72c393a1e39102ad76e5dfb2385" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.compose.material3.adaptive" name="adaptive-jvmstubs" version="1.2.0"> + <artifact name="adaptive-jvmstubs-1.2.0.jar"> + <sha256 value="7859b375375b11a25602b4e4e44c625c613da10810c9352cf6093322039055b4" origin="Generated by Gradle"/> + </artifact> + <artifact name="adaptive-jvmstubs-1.2.0.module"> + <sha256 value="3b19cef60d3305bfc90e32ca72f829508eb001f7c1bb87011208cbd6eaf61b67" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.compose.material3.adaptive" name="adaptive-layout" version="1.2.0"> + <artifact name="adaptive-layout-1.2.0.module"> + <sha256 value="4a97cf8220bc4b915eee635618642b083668e9dfded4d2961c6a1b32198f89e2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.compose.material3.adaptive" name="adaptive-layout-android" version="1.2.0"> + <artifact name="adaptive-layout-android-1.2.0.module"> + <sha256 value="50b3066e94f647b01f55bf639fff0c2daf14c74281f06eb85547ff461bb97e1d" origin="Generated by Gradle"/> + </artifact> + <artifact name="adaptive-layout.aar"> + <sha256 value="353b9fa242d298bc73b16fce44b74169c9dea72eeba145c45dbf0ff0b1f49e56" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.compose.material3.adaptive" name="adaptive-layout-jvmstubs" version="1.2.0"> + <artifact name="adaptive-layout-jvmstubs-1.2.0.jar"> + <sha256 value="0d524e09cbf0bd83719d625dd0a76940c7662700cde6786b84a39ec6441159b0" origin="Generated by Gradle"/> + </artifact> + <artifact name="adaptive-layout-jvmstubs-1.2.0.module"> + <sha256 value="b9591d57233c2b778c997d0e39e4db9fcb77cbe8f57a0ca79c46835bbc7b2fe5" origin="Generated by Gradle"/> + </artifact> + </component> <component group="androidx.compose.runtime" name="runtime" version="1.10.0"> <artifact name="runtime-1.10.0.module"> <sha256 value="c1be9e41b03bc50e3850e86e5827fdf3eca11707989d93affb38e1bc8b53d675" origin="Generated by Gradle"/> @@ -2654,11 +2735,24 @@ <sha256 value="8ac5d2772e97c957dddcbcb1c1198229d4afe57cdf5ecd5eb3cd436a05190743" origin="Generated by Gradle"/> </artifact> </component> + <component group="androidx.window" name="window-core" version="1.4.0"> + <artifact name="window-core-1.4.0.module"> + <sha256 value="2bfe555d7fea5456c5cb6d4d140ac88d8dcb6dbbd284443a8d2376eaa1de09ba" origin="Generated by Gradle"/> + </artifact> + </component> <component group="androidx.window" name="window-core" version="1.5.0"> <artifact name="window-core-1.5.0.module"> <sha256 value="595604f055a12508ec3c774f2d90c5e56414ba6246c754172d9c95c8a00ec04f" origin="Generated by Gradle"/> </artifact> </component> + <component group="androidx.window" name="window-core-android" version="1.4.0"> + <artifact name="window-core-android-1.4.0.module"> + <sha256 value="ff8230297fa7779d90c4e016571139dc06cac39429267d4c630dad36f486e631" origin="Generated by Gradle"/> + </artifact> + <artifact name="window-core.aar"> + <sha256 value="c6818f612fa0adee25de8583db0def3f609c3a6339204cd538a037fe38439fd3" origin="Generated by Gradle"/> + </artifact> + </component> <component group="androidx.window" name="window-core-android" version="1.5.0"> <artifact name="window-core-android-1.5.0.module"> <sha256 value="5bd818686803bf3c037f61a0e96af4441e884aa8a4d91062d2ad57876f917b50" origin="Generated by Gradle"/> diff --git a/android/lib/common-compose/build.gradle.kts b/android/lib/common-compose/build.gradle.kts index 2ed1631b4c..9416136a7c 100644 --- a/android/lib/common-compose/build.gradle.kts +++ b/android/lib/common-compose/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(projects.lib.ui.resource) implementation(projects.lib.model) implementation(projects.lib.common) + implementation(projects.lib.navigation) implementation(libs.arrow) implementation(libs.kermit) } diff --git a/android/lib/common-compose/src/main/kotlin/net/mullvad/mullvadvpn/common/compose/Screen.kt b/android/lib/common-compose/src/main/kotlin/net/mullvad/mullvadvpn/common/compose/Screen.kt new file mode 100644 index 0000000000..7ddc2bf71a --- /dev/null +++ b/android/lib/common-compose/src/main/kotlin/net/mullvad/mullvadvpn/common/compose/Screen.kt @@ -0,0 +1,37 @@ +package net.mullvad.mullvadvpn.common.compose + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import net.mullvad.mullvadvpn.core.NavKey2 +import net.mullvad.mullvadvpn.core.Navigator +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy +import net.mullvad.mullvadvpn.core.scene.LocalSceneRole + +@SuppressLint("ComposableNaming") +@Composable +fun unlessIsDetail(block: @Composable () -> Unit) { + if (LocalSceneRole.current != ListDetailSceneStrategy.Role.Detail) { + block() + } +} + +// If we are in portrait and then rotate to landscape which triggers the list-detail scene +// the list pane is already on the back stack, but the detail pane isn't so we need to push it. +@SuppressLint("ComposableNaming") +@Composable +inline fun <reified T : NavKey2> Navigator.assureHasDetailPane(detailKey: NavKey2) { + LaunchedEffect(detailKey) { + if (screenIsListDetailTargetWidth && backStack.last() is T) { + navigate(detailKey) + } + } +} + +fun Navigator.navigateReplaceIfDetailPane(key: NavKey2) { + if (screenIsListDetailTargetWidth) { + navigateReplaceTop(key) + } else { + navigate(key) + } +} diff --git a/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/AntiCensorshipSettingsScreen.kt b/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/AntiCensorshipSettingsScreen.kt index 38d009b28e..cb853f5bc5 100644 --- a/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/AntiCensorshipSettingsScreen.kt +++ b/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/AntiCensorshipSettingsScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed import net.mullvad.mullvadvpn.common.compose.itemWithDivider +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.anticensorship.api.AntiCensorshipNavKey import net.mullvad.mullvadvpn.feature.anticensorship.api.SelectPortNavKey @@ -117,7 +118,7 @@ fun AntiCensorshipSettingsScreen( if (state.contentOrNull()?.isModal == true) { NavigateCloseIconButton(onBackClick) } else { - NavigateBackIconButton(onNavigateBack = onBackClick) + unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } } }, ) { modifier, lazyListState: LazyListState -> diff --git a/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/navigation/AnticensorshipEntryProvider.kt b/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/navigation/AnticensorshipEntryProvider.kt index e2a154b97b..aa57953637 100644 --- a/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/navigation/AnticensorshipEntryProvider.kt +++ b/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/navigation/AnticensorshipEntryProvider.kt @@ -6,11 +6,14 @@ import net.mullvad.mullvadvpn.common.compose.LocalSharedTransitionScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.anticensorship.api.AntiCensorshipNavKey import net.mullvad.mullvadvpn.feature.anticensorship.impl.AntiCensorshipSettings fun EntryProviderScope<NavKey2>.anticensorshipEntry(navigator: Navigator) { - entry<AntiCensorshipNavKey>(metadata = slideInHorizontalTransition()) { navKey -> + entry<AntiCensorshipNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { navKey -> LocalSharedTransitionScope.current?.AntiCensorshipSettings( navigator = navigator, navArgs = navKey, diff --git a/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/selectport/SelectPortScreen.kt b/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/selectport/SelectPortScreen.kt index a87b5ac790..6b58c579dd 100644 --- a/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/selectport/SelectPortScreen.kt +++ b/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/selectport/SelectPortScreen.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed import net.mullvad.mullvadvpn.common.compose.dropUnlessResumed import net.mullvad.mullvadvpn.common.compose.itemWithDivider +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.LocalResultStore import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.anticensorship.api.CustomPortNavKey @@ -99,7 +100,7 @@ fun SelectPortScreen( ScaffoldWithMediumTopBar( appBarTitle = state.contentOrNull()?.title ?: "", - navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, + navigationIcon = { unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } }, ) { modifier, lazyListState: LazyListState -> LazyColumn( horizontalAlignment = Alignment.CenterHorizontally, diff --git a/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/navigation/ApiAccessEntryProvider.kt b/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/navigation/ApiAccessEntryProvider.kt index 3e3194d284..191f5b09aa 100644 --- a/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/navigation/ApiAccessEntryProvider.kt +++ b/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/navigation/ApiAccessEntryProvider.kt @@ -4,11 +4,14 @@ import androidx.navigation3.runtime.EntryProviderScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.apiaccess.api.ApiAccessNavKey import net.mullvad.mullvadvpn.feature.apiaccess.impl.screen.list.ApiAccessList fun EntryProviderScope<NavKey2>.apiAccessEntry(navigator: Navigator) { - entry<ApiAccessNavKey>(metadata = slideInHorizontalTransition()) { + entry<ApiAccessNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { ApiAccessList(navigator = navigator) } diff --git a/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/list/ApiAccessListScreen.kt b/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/list/ApiAccessListScreen.kt index c81217663d..d06ca73d7d 100644 --- a/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/list/ApiAccessListScreen.kt +++ b/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/list/ApiAccessListScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import net.mullvad.mullvadvpn.common.compose.itemsIndexedWithDivider +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.apiaccess.api.ApiAccessMethodDetailsNavKey import net.mullvad.mullvadvpn.feature.apiaccess.api.ApiAccessMethodInfoNavKey @@ -82,7 +83,7 @@ fun ApiAccessListScreen( ) { ScaffoldWithMediumTopBar( appBarTitle = stringResource(id = R.string.settings_api_access), - navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, + navigationIcon = { unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } }, ) { modifier, lazyListState: LazyListState -> LazyColumn( modifier = modifier.padding(horizontal = Dimens.sideMarginNew), diff --git a/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/AppearanceScreen.kt b/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/AppearanceScreen.kt index 01ace49453..c65939e55f 100644 --- a/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/AppearanceScreen.kt +++ b/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/AppearanceScreen.kt @@ -29,6 +29,7 @@ import androidx.lifecycle.compose.dropUnlessResumed import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.common.compose.isTv import net.mullvad.mullvadvpn.common.compose.showSnackbarImmediately +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.appearance.impl.obfuscation.AppObfuscation import net.mullvad.mullvadvpn.lib.common.Lc @@ -101,7 +102,7 @@ fun AppearanceScreen( ScaffoldWithMediumTopBar( snackbarHostState = snackbarHostState, appBarTitle = stringResource(id = R.string.appearance), - navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, + navigationIcon = { unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } }, ) { modifier, lazyGridState: LazyGridState -> LazyVerticalGrid( state = lazyGridState, diff --git a/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/navigation/AppearanceEntryProvider.kt b/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/navigation/AppearanceEntryProvider.kt index b13b784789..c9d7e44916 100644 --- a/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/navigation/AppearanceEntryProvider.kt +++ b/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/navigation/AppearanceEntryProvider.kt @@ -4,11 +4,14 @@ import androidx.navigation3.runtime.EntryProviderScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.appearance.api.AppearanceNavKey import net.mullvad.mullvadvpn.feature.appearance.impl.Appearance fun EntryProviderScope<NavKey2>.appearanceEntry(navigator: Navigator) { - entry<AppearanceNavKey>(metadata = slideInHorizontalTransition()) { + entry<AppearanceNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { Appearance(navigator = navigator) } } diff --git a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoScreen.kt b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoScreen.kt index 49122a9360..5414d35ebd 100644 --- a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoScreen.kt +++ b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoScreen.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.compose.dropUnlessResumed import net.mullvad.mullvadvpn.common.compose.CollectSideEffectWithLifecycle import net.mullvad.mullvadvpn.common.compose.safeOpenUri import net.mullvad.mullvadvpn.common.compose.showSnackbarImmediately +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.appinfo.api.ChangelogNavKey import net.mullvad.mullvadvpn.lib.common.Lc @@ -94,7 +95,9 @@ fun AppInfo( ) { ScaffoldWithMediumTopBar( appBarTitle = stringResource(id = R.string.app_info), - navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, + navigationIcon = { + unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } + }, snackbarHostState = snackbarHostState, ) { modifier -> Column( diff --git a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/navigation/AppInfoEntryProvider.kt b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/navigation/AppInfoEntryProvider.kt index 40f244fb6e..d3fe9473a0 100644 --- a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/navigation/AppInfoEntryProvider.kt +++ b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/navigation/AppInfoEntryProvider.kt @@ -4,11 +4,14 @@ import androidx.navigation3.runtime.EntryProviderScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.appinfo.api.AppInfoNavKey import net.mullvad.mullvadvpn.feature.appinfo.impl.AppInfo internal fun EntryProviderScope<NavKey2>.appInfoEntry(navigator: Navigator) { - entry<AppInfoNavKey>(metadata = slideInHorizontalTransition()) { + entry<AppInfoNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { AppInfo(navigator = navigator) } } diff --git a/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/AutoConnectAndLockdownModeScreen.kt b/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/AutoConnectAndLockdownModeScreen.kt index 1890ed035e..568e41ef2d 100644 --- a/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/AutoConnectAndLockdownModeScreen.kt +++ b/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/AutoConnectAndLockdownModeScreen.kt @@ -57,6 +57,7 @@ import androidx.core.text.HtmlCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.autoconnect.impl.PAGES.Companion.annotatedTopText import net.mullvad.mullvadvpn.lib.common.util.appendHideNavOnPlayBuild @@ -92,6 +93,7 @@ fun AutoConnectAndLockdownMode(navigator: Navigator) { ) } +@Suppress("LongMethod") @Composable fun AutoConnectAndLockdownModeScreen( state: AutoConnectAndLockdownModeUiState, @@ -100,7 +102,9 @@ fun AutoConnectAndLockdownModeScreen( val context = LocalContext.current ScaffoldWithMediumTopBar( appBarTitle = stringResource(id = R.string.auto_connect_and_lockdown_mode), - navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, + navigationIcon = { + unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } + }, bottomBar = { PrimaryButton( text = stringResource(id = R.string.go_to_vpn_settings), diff --git a/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/navigation/AutoConnectEntryProvider.kt b/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/navigation/AutoConnectEntryProvider.kt index 1a1b3e2241..56f0880573 100644 --- a/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/navigation/AutoConnectEntryProvider.kt +++ b/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/navigation/AutoConnectEntryProvider.kt @@ -4,11 +4,14 @@ import androidx.navigation3.runtime.EntryProviderScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.autoconnect.api.AutoConnectNavKey import net.mullvad.mullvadvpn.feature.autoconnect.impl.AutoConnectAndLockdownMode fun EntryProviderScope<NavKey2>.autoConnectEntry(navigator: Navigator) { - entry<AutoConnectNavKey>(metadata = slideInHorizontalTransition()) { + entry<AutoConnectNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { AutoConnectAndLockdownMode(navigator = navigator) } } diff --git a/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/DaitaScreen.kt b/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/DaitaScreen.kt index 865a88d84e..585a9f1194 100644 --- a/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/DaitaScreen.kt +++ b/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/DaitaScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.LocalResultStore import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.daita.api.DaitaDirectOnlyConfirmationNavKey @@ -118,7 +119,7 @@ fun DaitaScreen( if (state.isModal()) { NavigateCloseIconButton { onBackClick() } } else { - NavigateBackIconButton { onBackClick() } + unlessIsDetail { NavigateBackIconButton { onBackClick() } } } }, ) { modifier -> diff --git a/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/navigation/DaitaEntryProvider.kt b/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/navigation/DaitaEntryProvider.kt index 50da103765..745558f521 100644 --- a/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/navigation/DaitaEntryProvider.kt +++ b/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/navigation/DaitaEntryProvider.kt @@ -6,11 +6,14 @@ import net.mullvad.mullvadvpn.common.compose.LocalSharedTransitionScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.daita.api.DaitaNavKey import net.mullvad.mullvadvpn.feature.daita.impl.Daita fun EntryProviderScope<NavKey2>.daitaEntry(navigator: Navigator) { - entry<DaitaNavKey>(metadata = slideInHorizontalTransition()) { navKey -> + entry<DaitaNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { navKey -> LocalSharedTransitionScope.current?.Daita( navigator = navigator, isModal = navKey.isModal, diff --git a/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectScreen.kt b/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectScreen.kt index eae39a44a6..04625a7a4c 100644 --- a/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectScreen.kt +++ b/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectScreen.kt @@ -285,7 +285,14 @@ fun Connect(navigator: Navigator, animatedVisibilityScope: AnimatedVisibilitySco onChangelogClick = dropUnlessResumed { navigator.navigate(ChangelogNavKey(isModal = true)) }, onDismissChangelogClick = connectViewModel::dismissNewChangelogNotification, - onSettingsClick = dropUnlessResumed { navigator.navigate(SettingsNavKey) }, + onSettingsClick = + dropUnlessResumed { + if (navigator.screenIsListDetailTargetWidth) { + navigator.navigate(SettingsNavKey, DaitaNavKey()) + } else { + navigator.navigate(SettingsNavKey) + } + }, onAccountClick = dropUnlessResumed { navigator.navigate(AccountNavKey) }, onDismissNewDeviceClick = connectViewModel::dismissNewDeviceNotification, onNavigateToFeature = diff --git a/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/MultihopScreen.kt b/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/MultihopScreen.kt index 79feb71b76..478eb23c11 100644 --- a/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/MultihopScreen.kt +++ b/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/MultihopScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.model.FeatureIndicator @@ -84,7 +85,7 @@ fun MultihopScreen( if (state.isModal()) { NavigateCloseIconButton(onBackClick) } else { - NavigateBackIconButton(onNavigateBack = onBackClick) + unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } } }, ) { modifier -> diff --git a/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/navigation/MultihopEntryProvider.kt b/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/navigation/MultihopEntryProvider.kt index 2dd4fa8925..ba771f6f03 100644 --- a/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/navigation/MultihopEntryProvider.kt +++ b/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/navigation/MultihopEntryProvider.kt @@ -6,11 +6,14 @@ import net.mullvad.mullvadvpn.common.compose.LocalSharedTransitionScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.multihop.api.MultihopNavKey import net.mullvad.mullvadvpn.feature.multihop.impl.Multihop fun EntryProviderScope<NavKey2>.multihopEntry(navigator: Navigator) { - entry<MultihopNavKey>(metadata = slideInHorizontalTransition()) { navKey -> + entry<MultihopNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { navKey -> LocalSharedTransitionScope.current?.Multihop( isModal = navKey.isModal, navigator = navigator, diff --git a/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/NotificationSettingsScreen.kt b/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/NotificationSettingsScreen.kt index 4646670988..999512b802 100644 --- a/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/NotificationSettingsScreen.kt +++ b/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/NotificationSettingsScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import net.mullvad.mullvadvpn.common.compose.CollectSideEffectWithLifecycle import net.mullvad.mullvadvpn.common.compose.isTv +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.common.util.openAppInfoNotificationSettings @@ -83,7 +84,9 @@ fun NotificationSettingsScreen( ) { ScaffoldWithMediumTopBar( appBarTitle = stringResource(id = R.string.settings_notifications), - navigationIcon = { NavigateBackIconButton { onBackClick() } }, + navigationIcon = { + unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } + }, bottomBar = { if (!isTv()) { PrimaryButton( diff --git a/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/navigation/NotificationEntryProvider.kt b/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/navigation/NotificationEntryProvider.kt index bef71019ca..1b6b03fa7e 100644 --- a/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/navigation/NotificationEntryProvider.kt +++ b/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/navigation/NotificationEntryProvider.kt @@ -4,11 +4,14 @@ import androidx.navigation3.runtime.EntryProviderScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.notification.api.NotificationSettingsNavKey import net.mullvad.mullvadvpn.feature.notification.impl.NotificationSettings fun EntryProviderScope<NavKey2>.notificationEntry(navigator: Navigator) { - entry<NotificationSettingsNavKey>(metadata = slideInHorizontalTransition()) { + entry<NotificationSettingsNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { NotificationSettings(navigator = navigator) } } diff --git a/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/ReportProblemScreen.kt b/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/ReportProblemScreen.kt index 82ed03a5d5..0a7b6a52c9 100644 --- a/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/ReportProblemScreen.kt +++ b/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/ReportProblemScreen.kt @@ -48,6 +48,7 @@ import net.mullvad.mullvadvpn.common.compose.SecureScreenWhileInView import net.mullvad.mullvadvpn.common.compose.clickableAnnotatedString import net.mullvad.mullvadvpn.common.compose.createUriHook import net.mullvad.mullvadvpn.common.compose.isTv +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.LocalResultStore import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.problemreport.api.ProblemReportNoEmailConfirmedNavResult @@ -136,7 +137,7 @@ private fun ReportProblemScreen( ScaffoldWithMediumTopBar( appBarTitle = stringResource(id = R.string.report_a_problem), - navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, + navigationIcon = { unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } }, ) { modifier -> // Show sending states if (state.sendingState != null) { diff --git a/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/navigation/ProblemReportEntryProvider.kt b/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/navigation/ProblemReportEntryProvider.kt index b79435427f..927810e287 100644 --- a/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/navigation/ProblemReportEntryProvider.kt +++ b/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/navigation/ProblemReportEntryProvider.kt @@ -4,11 +4,14 @@ import androidx.navigation3.runtime.EntryProviderScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.problemreport.api.ProblemReportNavKey import net.mullvad.mullvadvpn.feature.problemreport.impl.ReportProblem fun EntryProviderScope<NavKey2>.problemReportEntry(navigator: Navigator) { - entry<ProblemReportNavKey>(metadata = slideInHorizontalTransition()) { + entry<ProblemReportNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { ReportProblem(navigator = navigator) } diff --git a/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/ServerIpOverridesScreen.kt b/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/ServerIpOverridesScreen.kt index 041c630c46..150603bcbe 100644 --- a/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/ServerIpOverridesScreen.kt +++ b/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/ServerIpOverridesScreen.kt @@ -51,6 +51,7 @@ import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.common.compose.CollectSideEffectWithLifecycle import net.mullvad.mullvadvpn.common.compose.showSnackbarImmediately +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.LocalResultStore import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.serveripoverride.api.ImportOverrideByTextNavKey @@ -195,7 +196,7 @@ fun ServerIpOverridesScreen( if (state.isModal()) { NavigateCloseIconButton(onBackClick) } else { - NavigateBackIconButton(onNavigateBack = onBackClick) + unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } } }, actions = { diff --git a/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/navigation/ServerIpOverrideEntryProvider.kt b/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/navigation/ServerIpOverrideEntryProvider.kt index d059b270e8..e73eda3663 100644 --- a/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/navigation/ServerIpOverrideEntryProvider.kt +++ b/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/navigation/ServerIpOverrideEntryProvider.kt @@ -6,11 +6,14 @@ import net.mullvad.mullvadvpn.common.compose.LocalSharedTransitionScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.serveripoverride.api.ServerIpOverrideNavKey import net.mullvad.mullvadvpn.feature.serveripoverride.impl.ServerIpOverrides fun EntryProviderScope<NavKey2>.serverIpOverrideEntry(navigator: Navigator) { - entry<ServerIpOverrideNavKey>(metadata = slideInHorizontalTransition()) { navKey -> + entry<ServerIpOverrideNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { navKey -> LocalSharedTransitionScope.current?.ServerIpOverrides( navArgs = navKey, navigator = navigator, diff --git a/android/lib/feature/settings/impl/build.gradle.kts b/android/lib/feature/settings/impl/build.gradle.kts index a7d325b986..147b2cff86 100644 --- a/android/lib/feature/settings/impl/build.gradle.kts +++ b/android/lib/feature/settings/impl/build.gradle.kts @@ -10,9 +10,11 @@ plugins { android { namespace = "net.mullvad.mullvadvpn.feature.settings.impl" } dependencies { + implementation(projects.lib.feature.anticensorship.api) implementation(projects.lib.feature.apiaccess.api) implementation(projects.lib.feature.appearance.api) implementation(projects.lib.feature.appinfo.api) + implementation(projects.lib.feature.autoconnect.api) implementation(projects.lib.feature.daita.api) implementation(projects.lib.feature.multihop.api) implementation(projects.lib.feature.notification.api) diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt index 7d72d706fe..dca81319f8 100644 --- a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt @@ -1,5 +1,6 @@ package net.mullvad.mullvadvpn.feature.settings.impl +import androidx.activity.compose.BackHandler import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -20,16 +21,22 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed +import net.mullvad.mullvadvpn.common.compose.assureHasDetailPane import net.mullvad.mullvadvpn.common.compose.createUriHook +import net.mullvad.mullvadvpn.common.compose.isTv import net.mullvad.mullvadvpn.common.compose.itemWithDivider +import net.mullvad.mullvadvpn.common.compose.navigateReplaceIfDetailPane import net.mullvad.mullvadvpn.core.Navigator +import net.mullvad.mullvadvpn.feature.anticensorship.api.AntiCensorshipNavKey import net.mullvad.mullvadvpn.feature.apiaccess.api.ApiAccessNavKey import net.mullvad.mullvadvpn.feature.appearance.api.AppearanceNavKey import net.mullvad.mullvadvpn.feature.appinfo.api.AppInfoNavKey +import net.mullvad.mullvadvpn.feature.autoconnect.api.AutoConnectNavKey import net.mullvad.mullvadvpn.feature.daita.api.DaitaNavKey import net.mullvad.mullvadvpn.feature.multihop.api.MultihopNavKey import net.mullvad.mullvadvpn.feature.notification.api.NotificationSettingsNavKey import net.mullvad.mullvadvpn.feature.problemreport.api.ProblemReportNavKey +import net.mullvad.mullvadvpn.feature.settings.api.SettingsNavKey import net.mullvad.mullvadvpn.feature.splittunneling.api.SplitTunnelingNavKey import net.mullvad.mullvadvpn.feature.vpnsettings.api.VpnSettingsNavKey import net.mullvad.mullvadvpn.lib.common.Lc @@ -77,20 +84,40 @@ private fun PreviewSettingsScreen( fun Settings(navigator: Navigator) { val vm = koinViewModel<SettingsViewModel>() val state by vm.uiState.collectAsStateWithLifecycle() + val isTv = isTv() + + BackHandler(enabled = navigator.screenIsListDetailTargetWidth) { + navigator.goBackUntil(SettingsNavKey, inclusive = true) + } + + navigator.assureHasDetailPane<SettingsNavKey>(DaitaNavKey()) + SettingsScreen( state = state, - onVpnSettingCellClick = dropUnlessResumed { navigator.navigate(VpnSettingsNavKey()) }, + onVpnSettingCellClick = + dropUnlessResumed { + if (navigator.screenIsListDetailTargetWidth) { + val detailKey = if (isTv) AntiCensorshipNavKey() else AutoConnectNavKey + navigator.navigate(VpnSettingsNavKey(), detailKey) + } else { + navigator.navigate(VpnSettingsNavKey()) + } + }, onSplitTunnelingCellClick = - dropUnlessResumed { navigator.navigate(SplitTunnelingNavKey()) }, - onAppInfoClick = dropUnlessResumed { navigator.navigate(AppInfoNavKey) }, - onApiAccessClick = dropUnlessResumed { navigator.navigate(ApiAccessNavKey) }, - onReportProblemCellClick = dropUnlessResumed { navigator.navigate(ProblemReportNavKey) }, - onMultihopClick = dropUnlessResumed { navigator.navigate(MultihopNavKey()) }, - onDaitaClick = dropUnlessResumed { navigator.navigate(DaitaNavKey()) }, - onBackClick = dropUnlessResumed { navigator.goBack() }, + dropUnlessResumed { navigator.navigateReplaceIfDetailPane(SplitTunnelingNavKey()) }, + onAppInfoClick = dropUnlessResumed { navigator.navigateReplaceIfDetailPane(AppInfoNavKey) }, + onApiAccessClick = + dropUnlessResumed { navigator.navigateReplaceIfDetailPane(ApiAccessNavKey) }, + onReportProblemCellClick = + dropUnlessResumed { navigator.navigateReplaceIfDetailPane(ProblemReportNavKey) }, + onMultihopClick = + dropUnlessResumed { navigator.navigateReplaceIfDetailPane(MultihopNavKey()) }, + onDaitaClick = dropUnlessResumed { navigator.navigateReplaceIfDetailPane(DaitaNavKey()) }, onNotificationSettingsCellClick = - dropUnlessResumed { navigator.navigate(NotificationSettingsNavKey) }, - onAppObfuscationClick = dropUnlessResumed { navigator.navigate(AppearanceNavKey) }, + dropUnlessResumed { navigator.navigateReplaceIfDetailPane(NotificationSettingsNavKey) }, + onAppObfuscationClick = + dropUnlessResumed { navigator.navigateReplaceIfDetailPane(AppearanceNavKey) }, + onBackClick = dropUnlessResumed { navigator.goBackUntil(SettingsNavKey, inclusive = true) }, ) } diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/SettingsEntryProvider.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/SettingsEntryProvider.kt index fe9f5660d5..77714f15cb 100644 --- a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/SettingsEntryProvider.kt +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/SettingsEntryProvider.kt @@ -4,9 +4,12 @@ import androidx.navigation3.runtime.EntryProviderScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.topLevelTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.settings.api.SettingsNavKey import net.mullvad.mullvadvpn.feature.settings.impl.Settings fun EntryProviderScope<NavKey2>.settingsEntry(navigator: Navigator) { - entry<SettingsNavKey>(metadata = topLevelTransition()) { Settings(navigator = navigator) } + entry<SettingsNavKey>(metadata = ListDetailSceneStrategy.listPane() + topLevelTransition()) { + Settings(navigator = navigator) + } } diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt index c09c8456d0..f647531047 100644 --- a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt @@ -38,6 +38,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData import net.mullvad.mullvadvpn.feature.splittunneling.impl.extensions.hasValidSize @@ -127,7 +128,7 @@ fun SplitTunnelingScreen( if (state.isModal()) { NavigateCloseIconButton(onNavigateClose = onBackClick) } else { - NavigateBackIconButton(onNavigateBack = onBackClick) + unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } } }, ) { modifier, lazyListState: LazyListState -> diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SplitTunnelingEntryProvider.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SplitTunnelingEntryProvider.kt index f11c34bcaf..f6594bc21e 100644 --- a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SplitTunnelingEntryProvider.kt +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SplitTunnelingEntryProvider.kt @@ -6,11 +6,14 @@ import net.mullvad.mullvadvpn.common.compose.LocalSharedTransitionScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.splittunneling.api.SplitTunnelingNavKey import net.mullvad.mullvadvpn.feature.splittunneling.impl.SplitTunneling fun EntryProviderScope<NavKey2>.splitTunnelingEntry(navigator: Navigator) { - entry<SplitTunnelingNavKey>(metadata = slideInHorizontalTransition()) { navArgs -> + entry<SplitTunnelingNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { navArgs -> LocalSharedTransitionScope.current?.SplitTunneling( isModal = navArgs.isModal, navigator = navigator, diff --git a/android/lib/feature/vpnsettings/impl/build.gradle.kts b/android/lib/feature/vpnsettings/impl/build.gradle.kts index 7d6287a50d..0184514aa6 100644 --- a/android/lib/feature/vpnsettings/impl/build.gradle.kts +++ b/android/lib/feature/vpnsettings/impl/build.gradle.kts @@ -18,7 +18,6 @@ dependencies { implementation(projects.lib.repository) implementation(projects.lib.usecase) - implementation(libs.androidx.navigation3.ui) implementation(libs.koin.compose) implementation(libs.arrow) // This dependency can be replaced when minimum SDK is 29 or higher. diff --git a/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/VpnSettingsScreen.kt b/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/VpnSettingsScreen.kt index 0b6e6b654d..c0b0276a56 100644 --- a/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/VpnSettingsScreen.kt +++ b/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/VpnSettingsScreen.kt @@ -3,6 +3,7 @@ package net.mullvad.mullvadvpn.feature.vpnsettings.impl import android.content.res.Resources +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope @@ -50,7 +51,9 @@ import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.common.compose.CollectSideEffectWithLifecycle import net.mullvad.mullvadvpn.common.compose.RunOnKeyChange import net.mullvad.mullvadvpn.common.compose.SETTINGS_HIGHLIGHT_REPEAT_COUNT +import net.mullvad.mullvadvpn.common.compose.assureHasDetailPane import net.mullvad.mullvadvpn.common.compose.dropUnlessResumed +import net.mullvad.mullvadvpn.common.compose.navigateReplaceIfDetailPane import net.mullvad.mullvadvpn.common.compose.showSnackbarImmediately import net.mullvad.mullvadvpn.core.LocalResultStore import net.mullvad.mullvadvpn.core.Navigator @@ -162,10 +165,16 @@ fun SharedTransitionScope.VpnSettings( navigator: Navigator, animatedVisibilityScope: AnimatedVisibilityScope, ) { - val vm = koinViewModel<VpnSettingsViewModel>() { parametersOf(navArgs) } + val vm = koinViewModel<VpnSettingsViewModel> { parametersOf(navArgs) } val state by vm.uiState.collectAsStateWithLifecycle() val resultStore = LocalResultStore.current + BackHandler(enabled = navigator.screenIsListDetailTargetWidth) { + navigator.goBackUntil(VpnSettingsNavKey(), inclusive = true) + } + + navigator.assureHasDetailPane<VpnSettingsNavKey>(AutoConnectNavKey) + resultStore.consumeResult<DnsNavResult>()?.let { result -> when (result) { is DnsNavResult.Success -> { @@ -210,7 +219,8 @@ fun SharedTransitionScope.VpnSettings( snackbarHostState = snackbarHostState, navigateToContentBlockersInfo = dropUnlessResumed { navigator.navigate(ContentBlockersInfoNavKey) }, - navigateToAutoConnectScreen = dropUnlessResumed { navigator.navigate(AutoConnectNavKey) }, + navigateToAutoConnectScreen = + dropUnlessResumed { navigator.navigateReplaceIfDetailPane(AutoConnectNavKey) }, navigateToCustomDnsInfo = dropUnlessResumed { navigator.navigate(CustomDnsInfoNavKey) }, navigateToMalwareInfo = dropUnlessResumed { navigator.navigate(MalwareInfoNavKey) }, navigateToQuantumResistanceInfo = @@ -218,7 +228,7 @@ fun SharedTransitionScope.VpnSettings( navigateToLocalNetworkSharingInfo = dropUnlessResumed { navigator.navigate(LocalNetworkSharingInfoNavKey) }, navigateToServerIpOverrides = - dropUnlessResumed { navigator.navigate(ServerIpOverrideNavKey()) }, + dropUnlessResumed { navigator.navigateReplaceIfDetailPane(ServerIpOverrideNavKey()) }, onToggleContentBlockersExpanded = vm::onToggleContentBlockersExpand, onToggleAllBlockers = vm::onToggleAllBlockers, onToggleBlockTrackers = vm::onToggleBlockTrackers, @@ -234,7 +244,6 @@ fun SharedTransitionScope.VpnSettings( navigator.navigate(DnsNavKey(index, address)) }, onToggleDnsClick = vm::onToggleCustomDns, - onBackClick = dropUnlessResumed { navigator.goBack() }, onSelectQuantumResistanceSetting = vm::onSelectQuantumResistanceSetting, onToggleAutoStartAndConnectOnBoot = vm::onToggleAutoStartAndConnectOnBoot, onSelectDeviceIpVersion = vm::onDeviceIpVersionSelected, @@ -243,7 +252,10 @@ fun SharedTransitionScope.VpnSettings( navigateToDeviceIpInfo = dropUnlessResumed { navigator.navigate(DeviceIpInfoNavKey) }, navigateToConnectOnDeviceOnStartUpInfo = dropUnlessResumed { navigator.navigate(ConnectOnStartupInfoNavKey) }, - navigateToAntiCensorship = dropUnlessResumed { navigator.navigate(AntiCensorshipNavKey()) }, + navigateToAntiCensorship = + dropUnlessResumed { navigator.navigateReplaceIfDetailPane(AntiCensorshipNavKey()) }, + onBackClick = + dropUnlessResumed { navigator.goBackUntil(VpnSettingsNavKey(), inclusive = true) }, ) } diff --git a/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/navigation/VpnSettingsEntryProvider.kt b/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/navigation/VpnSettingsEntryProvider.kt index baf3963de8..3966f7724d 100644 --- a/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/navigation/VpnSettingsEntryProvider.kt +++ b/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/navigation/VpnSettingsEntryProvider.kt @@ -6,11 +6,14 @@ import net.mullvad.mullvadvpn.common.compose.LocalSharedTransitionScope import net.mullvad.mullvadvpn.core.NavKey2 import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy import net.mullvad.mullvadvpn.feature.vpnsettings.api.VpnSettingsNavKey import net.mullvad.mullvadvpn.feature.vpnsettings.impl.VpnSettings fun EntryProviderScope<NavKey2>.vpnSettingsEntry(navigator: Navigator) { - entry<VpnSettingsNavKey>(metadata = slideInHorizontalTransition()) { navArgs -> + entry<VpnSettingsNavKey>( + metadata = ListDetailSceneStrategy.listPane() + slideInHorizontalTransition() + ) { navArgs -> LocalSharedTransitionScope.current?.VpnSettings( navArgs = navArgs, navigator = navigator, diff --git a/android/lib/navigation/build.gradle.kts b/android/lib/navigation/build.gradle.kts index 0d6112172c..6d72fab6c3 100644 --- a/android/lib/navigation/build.gradle.kts +++ b/android/lib/navigation/build.gradle.kts @@ -13,4 +13,6 @@ dependencies { api(libs.androidx.navigation3.runtime) api(libs.androidx.navigation3.ui) implementation(libs.androidx.lifecycle.viewmodel.navigation3) + implementation(libs.compose.material3.windowsizeclass) + implementation(libs.androidx.adaptive.layout) } diff --git a/android/lib/navigation/src/main/kotlin/net/mullvad/mullvadvpn/core/Navigator.kt b/android/lib/navigation/src/main/kotlin/net/mullvad/mullvadvpn/core/Navigator.kt index f96017b0bb..c6541c3ebb 100644 --- a/android/lib/navigation/src/main/kotlin/net/mullvad/mullvadvpn/core/Navigator.kt +++ b/android/lib/navigation/src/main/kotlin/net/mullvad/mullvadvpn/core/Navigator.kt @@ -3,7 +3,11 @@ package net.mullvad.mullvadvpn.core import androidx.compose.runtime.toMutableStateList /** Handles navigation events (forward and back) by updating the navigation state. */ -class Navigator(private val state: NavigationState, val resultStore: ResultStore) { +class Navigator( + private val state: NavigationState, + val resultStore: ResultStore, + val screenIsListDetailTargetWidth: Boolean, +) { /** A view of the previous back stack as it was before the last navigation/pop event. */ var previousBackStack: List<NavKey2> = state.backStack.toList() @@ -14,10 +18,10 @@ class Navigator(private val state: NavigationState, val resultStore: ResultStore /** * Navigate to a navigation key. * - * @param key the navigation key to navigate to. + * @param keys the navigation keys to navigate to. * @param clearBackStack if true clears the back stack before pushing the new key */ - fun navigate(key: NavKey2, clearBackStack: Boolean = false) { + fun navigate(vararg keys: NavKey2, clearBackStack: Boolean = false) { previousBackStack = state.backStack.toList() state.backStack.apply { @@ -25,8 +29,10 @@ class Navigator(private val state: NavigationState, val resultStore: ResultStore clear() } - if (key != state.backStack.lastOrNull()) { - add(key) + keys.forEach { key -> + if (key != state.backStack.lastOrNull()) { + add(key) + } } } } @@ -103,4 +109,5 @@ val EmptyNavigator = Navigator( state = NavigationState(emptyList<NavKey2>().toMutableStateList()), resultStore = ResultStore(), + screenIsListDetailTargetWidth = false, ) diff --git a/android/lib/navigation/src/main/kotlin/net/mullvad/mullvadvpn/core/scene/ListDetailScene.kt b/android/lib/navigation/src/main/kotlin/net/mullvad/mullvadvpn/core/scene/ListDetailScene.kt new file mode 100644 index 0000000000..87f2b9a7c5 --- /dev/null +++ b/android/lib/navigation/src/main/kotlin/net/mullvad/mullvadvpn/core/scene/ListDetailScene.kt @@ -0,0 +1,147 @@ +package net.mullvad.mullvadvpn.core.scene + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.scene.Scene +import androidx.navigation3.scene.SceneStrategy +import androidx.navigation3.scene.SceneStrategyScope +import androidx.window.core.layout.WindowSizeClass +import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_EXPANDED_LOWER_BOUND +import net.mullvad.mullvadvpn.core.animation.ENTER_TRANSITION_SLIDE_FACTOR +import net.mullvad.mullvadvpn.core.animation.TRANSITION_DEFAULT_DURATION_MS + +/** A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split. */ +class ListDetailScene<T : Any>( + override val key: Any, + override val previousEntries: List<NavEntry<T>>, + val listEntry: NavEntry<T>, + val detailEntry: NavEntry<T>, +) : Scene<T> { + override val entries: List<NavEntry<T>> = listOf(listEntry, detailEntry) + override val content: @Composable (() -> Unit) = { + Row(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.weight(0.4f)) { + listEntry.ContentForRole(ListDetailSceneStrategy.Role.List) + } + + Column(modifier = Modifier.weight(0.6f)) { + AnimatedContent( + targetState = detailEntry, + contentKey = { entry -> entry.contentKey }, + transitionSpec = { + fadeIn(tween(TRANSITION_DEFAULT_DURATION_MS)) + + slideIntoContainer( + animationSpec = tween(TRANSITION_DEFAULT_DURATION_MS), + towards = AnimatedContentTransitionScope.SlideDirection.Start, + initialOffset = { (it * ENTER_TRANSITION_SLIDE_FACTOR).toInt() }, + ) togetherWith + slideOutOfContainer( + animationSpec = tween(TRANSITION_DEFAULT_DURATION_MS), + towards = AnimatedContentTransitionScope.SlideDirection.End, + targetOffset = { (it * ENTER_TRANSITION_SLIDE_FACTOR).toInt() }, + ) + fadeOut(tween(TRANSITION_DEFAULT_DURATION_MS)) + }, + ) { entry -> + entry.ContentForRole(ListDetailSceneStrategy.Role.Detail) + } + } + } + } +} + +@Composable +private fun <T : Any> NavEntry<T>.ContentForRole(role: SceneRole) { + CompositionLocalProvider(LocalSceneRole provides role) { Content() } +} + +/** + * This `CompositionLocal` can be used by a `NavEntry` to determine what role it is playing in the + * current scene. + */ +val LocalSceneRole = compositionLocalOf<SceneRole> { SceneRole.Unknown } + +interface SceneRole { + data object Unknown : SceneRole +} + +@Composable +fun <T : Any> rememberListDetailSceneStrategy(): ListDetailSceneStrategy<T> { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + + return remember(windowSizeClass) { ListDetailSceneStrategy(windowSizeClass) } +} + +/** + * A [SceneStrategy] that returns a [ListDetailScene] if: + * - the window width is over 600dp + * - A `Detail` entry is the last item in the back stack + * - A `List` entry is in the back stack + * + * Notably, when the detail entry changes the scene's key does not change. This allows the scene, + * rather than the NavDisplay, to handle animations when the detail entry changes. + */ +class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> { + + sealed interface Role : SceneRole { + data object List : Role + + data object Detail : Role + } + + @Suppress("ReturnCount") + override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? { + + if (!isListDetailTargetWidth()) { + return null + } + + val detailEntry = + entries.lastOrNull()?.takeIf { it.metadata[ROLE_KEY] is Role.Detail } ?: return null + val listEntry = entries.findLast { it.metadata[ROLE_KEY] is Role.List } ?: return null + + // We use the list's contentKey to uniquely identify the scene. + // This allows the detail panes to be animated in and out by the scene, rather than + // having NavDisplay animate the whole scene out when the selected detail item changes. + val sceneKey = listEntry.contentKey + + return ListDetailScene( + key = sceneKey, + previousEntries = entries.dropLast(1), + listEntry = listEntry, + detailEntry = detailEntry, + ) + } + + fun isListDetailTargetWidth(): Boolean = + windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND) + + companion object { + internal const val ROLE_KEY = "ListDetailScene-Role" + + /** + * Helper function to add metadata to a [NavEntry] indicating it can be displayed in the + * list pane of a [ListDetailScene]. + */ + fun listPane() = mapOf(ROLE_KEY to Role.List) + + /** + * Helper function to add metadata to a [NavEntry] indicating it can be displayed in the + * detail pane of a the [ListDetailScene]. + */ + fun detailPane() = mapOf(ROLE_KEY to Role.Detail) + } +} diff --git a/android/lib/navigation/src/test/kotlin/net/mullvad/mullvadvpn/lib/navigation/NavigatorTest.kt b/android/lib/navigation/src/test/kotlin/net/mullvad/mullvadvpn/lib/navigation/NavigatorTest.kt index 7e4b5f238a..b1083bd2f8 100644 --- a/android/lib/navigation/src/test/kotlin/net/mullvad/mullvadvpn/lib/navigation/NavigatorTest.kt +++ b/android/lib/navigation/src/test/kotlin/net/mullvad/mullvadvpn/lib/navigation/NavigatorTest.kt @@ -110,6 +110,7 @@ class NavigatorTestTest { return Navigator( state = NavigationState(backStack.toMutableStateList()), resultStore = ResultStore(), + screenIsListDetailTargetWidth = false, ) } } |
