diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.android.kt index a1820c50c..d9e4e7b44 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.android.kt @@ -1,7 +1,9 @@ package com.nuvio.app.core.ui import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.statusBars import androidx.compose.runtime.Composable import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -11,3 +13,7 @@ internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp internal actual val nuvioBottomNavigationExtraVerticalPadding: Dp = 6.dp @Composable internal actual fun nuvioBottomNavigationBarInsets(): WindowInsets = WindowInsets.navigationBars + +@Composable +internal actual fun platformPhysicalTopInset(): Dp = + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.kt index 2fc73a4f4..bd4c58f07 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.kt @@ -13,6 +13,10 @@ internal expect val nuvioBottomNavigationExtraVerticalPadding: Dp @Composable internal expect fun nuvioBottomNavigationBarInsets(): WindowInsets +/** Physical display-safe top inset, excluding any enclosing native toolbar. */ +@Composable +internal expect fun platformPhysicalTopInset(): Dp + internal val LocalNuvioBottomNavigationOverlayPadding = staticCompositionLocalOf { 0.dp } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailFloatingHeader.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailFloatingHeader.kt index 90d7c2980..b5cec37a7 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailFloatingHeader.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailFloatingHeader.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import coil3.compose.AsyncImage import com.nuvio.app.core.ui.NuvioBackButton +import com.nuvio.app.core.ui.platformPhysicalTopInset import com.nuvio.app.features.details.MetaDetails import com.nuvio.app.isIos import com.nuvio.app.navigation.LocalUseNativeNavigation @@ -55,10 +56,14 @@ fun DetailFloatingHeader( onToggleSaved: () -> Unit, modifier: Modifier = Modifier, ) { - val safeAreaTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + val useNativeNavigation = LocalUseNativeNavigation.current + val safeAreaTop = if (useNativeNavigation) { + platformPhysicalTopInset() + } else { + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + } val headerTopPadding = (safeAreaTop - 6.dp).coerceAtLeast(safeAreaTop * 0.8f) val interactive = progress > 0.05f - val useNativeNavigation = LocalUseNativeNavigation.current val surfaceColor = backgroundColor ?: if (isIos) { MaterialTheme.colorScheme.surface.copy(alpha = 1.0f) } else { diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.ios.kt index 5042b4a43..590e17cd8 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.ios.kt @@ -2,11 +2,16 @@ package com.nuvio.app.core.ui import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.statusBars import androidx.compose.runtime.Composable +import androidx.compose.ui.uikit.LocalUIViewController import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.useContents internal actual val nuvioPlatformExtraTopPadding: Dp = 0.dp internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp @@ -15,3 +20,14 @@ internal actual val nuvioBottomNavigationExtraVerticalPadding: Dp = 0.dp @Composable internal actual fun nuvioBottomNavigationBarInsets(): WindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom) + +@OptIn(ExperimentalForeignApi::class) +@Composable +internal actual fun platformPhysicalTopInset(): Dp { + val physicalTop = LocalUIViewController.current.view.window + ?.safeAreaInsets + ?.useContents { top.toFloat() } + + return physicalTop?.dp + ?: WindowInsets.statusBars.asPaddingValues().calculateTopPadding() +} diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 542e7a251..12337c38d 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -596,6 +596,11 @@ struct TabContentView: View { coordinator: coordinator, appCoordinator: appCoordinator ) + // A native replace keeps the same NavigationStack depth. + // Keying by the wrapper forces SwiftUI to replace the + // embedded Compose controller instead of reusing the old + // screen with the new route's toolbar preferences. + .id(wrapper.id) } else { Color.clear } @@ -642,12 +647,12 @@ private struct DetailDestinationView: View { @ObservedObject var coordinator: TabNavigationCoordinator @ObservedObject var appCoordinator: AppNavigationCoordinator - private var isMetaDetail: Bool { - wrapper.route is DetailRoute + private var usesComposeNavigationHeader: Bool { + wrapper.route is DetailRoute || wrapper.route is StreamRoute } private var showsReadabilityFade: Bool { - !wrapper.route.hidesNavigationBar && !isMetaDetail + !wrapper.route.hidesNavigationBar && !usesComposeNavigationHeader } private var content: some View { @@ -663,9 +668,9 @@ private struct DetailDestinationView: View { NativeToolbarReadabilityFade() } } - .navigationTitle(isMetaDetail ? "" : wrapper.route.title ?? "") + .navigationTitle(usesComposeNavigationHeader ? "" : wrapper.route.title ?? "") .navigationBarTitleDisplayMode(.inline) - .toolbarRole(isMetaDetail ? .editor : .automatic) + .toolbarRole(usesComposeNavigationHeader ? .editor : .automatic) .toolbar(.hidden, for: .tabBar) .toolbar( wrapper.route.hidesNavigationBar ? Visibility.hidden : Visibility.visible, @@ -675,7 +680,7 @@ private struct DetailDestinationView: View { @ViewBuilder var body: some View { - if #available(iOS 26.0, *), !isMetaDetail { + if #available(iOS 26.0, *), !usesComposeNavigationHeader { content.navigationSubtitle(wrapper.route.subtitle ?? "") } else { content