summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKalle Lindström <karl.lindstrom@mullvad.net>2026-03-30 13:51:21 +0200
committerKalle Lindström <karl.lindstrom@mullvad.net>2026-03-30 13:51:21 +0200
commit8522ba1a285978c3de65a4d3fbfa35f977f532e9 (patch)
treee416b630debc2a387ee668af71cdbab28ef41409
parent35f20650a17aa9139d72b36305f660078e8a3882 (diff)
parent7101452514577b31fd4a1bbc91eff524dff3df24 (diff)
downloadmullvadvpn-8522ba1a285978c3de65a4d3fbfa35f977f532e9.tar.xz
mullvadvpn-8522ba1a285978c3de65a4d3fbfa35f977f532e9.zip
Merge branch 'add-nav3-master-detail-scene-droid-2419'
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/app/MullvadApp.kt19
-rw-r--r--android/gradle/libs.versions.toml5
-rw-r--r--android/gradle/verification-metadata.keys.xml1
-rw-r--r--android/gradle/verification-metadata.xml94
-rw-r--r--android/lib/common-compose/build.gradle.kts1
-rw-r--r--android/lib/common-compose/src/main/kotlin/net/mullvad/mullvadvpn/common/compose/Screen.kt37
-rw-r--r--android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/AntiCensorshipSettingsScreen.kt3
-rw-r--r--android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/navigation/AnticensorshipEntryProvider.kt5
-rw-r--r--android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/selectport/SelectPortScreen.kt3
-rw-r--r--android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/navigation/ApiAccessEntryProvider.kt5
-rw-r--r--android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/list/ApiAccessListScreen.kt3
-rw-r--r--android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/AppearanceScreen.kt3
-rw-r--r--android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/navigation/AppearanceEntryProvider.kt5
-rw-r--r--android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoScreen.kt5
-rw-r--r--android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/navigation/AppInfoEntryProvider.kt5
-rw-r--r--android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/AutoConnectAndLockdownModeScreen.kt6
-rw-r--r--android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/navigation/AutoConnectEntryProvider.kt5
-rw-r--r--android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/DaitaScreen.kt3
-rw-r--r--android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/navigation/DaitaEntryProvider.kt5
-rw-r--r--android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectScreen.kt9
-rw-r--r--android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/MultihopScreen.kt3
-rw-r--r--android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/navigation/MultihopEntryProvider.kt5
-rw-r--r--android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/NotificationSettingsScreen.kt5
-rw-r--r--android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/navigation/NotificationEntryProvider.kt5
-rw-r--r--android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/ReportProblemScreen.kt3
-rw-r--r--android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/navigation/ProblemReportEntryProvider.kt5
-rw-r--r--android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/ServerIpOverridesScreen.kt3
-rw-r--r--android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/navigation/ServerIpOverrideEntryProvider.kt5
-rw-r--r--android/lib/feature/settings/impl/build.gradle.kts2
-rw-r--r--android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt47
-rw-r--r--android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/SettingsEntryProvider.kt5
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt3
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SplitTunnelingEntryProvider.kt5
-rw-r--r--android/lib/feature/vpnsettings/impl/build.gradle.kts1
-rw-r--r--android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/VpnSettingsScreen.kt22
-rw-r--r--android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/navigation/VpnSettingsEntryProvider.kt5
-rw-r--r--android/lib/navigation/build.gradle.kts2
-rw-r--r--android/lib/navigation/src/main/kotlin/net/mullvad/mullvadvpn/core/Navigator.kt17
-rw-r--r--android/lib/navigation/src/main/kotlin/net/mullvad/mullvadvpn/core/scene/ListDetailScene.kt147
-rw-r--r--android/lib/navigation/src/test/kotlin/net/mullvad/mullvadvpn/lib/navigation/NavigatorTest.kt1
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,
)
}
}