diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt index aa8f6e37..1a185ae9 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nuvio.app.core.network.NetworkCondition @@ -39,6 +38,7 @@ import com.nuvio.app.features.home.components.HomeHeroReservedSpace import com.nuvio.app.features.home.components.HomeHeroSection import com.nuvio.app.features.home.components.HomeSkeletonHero import com.nuvio.app.features.home.components.HomeSkeletonRow +import com.nuvio.app.features.home.components.HomeContinueWatchingSectionBottomPadding import com.nuvio.app.features.trakt.TraktAuthRepository import com.nuvio.app.features.trakt.TRAKT_CONTINUE_WATCHING_DAYS_CAP_ALL import com.nuvio.app.features.trakt.TraktSettingsRepository @@ -74,7 +74,6 @@ import com.nuvio.app.features.watching.domain.isReleasedBy import com.nuvio.app.features.collection.CollectionRepository import com.nuvio.app.features.profiles.ProfileRepository import com.nuvio.app.features.home.components.HomeCollectionRowSection -import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.coroutines.async @@ -85,8 +84,7 @@ import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.yield import com.nuvio.app.features.trakt.TraktEpisodeMappingService -import com.nuvio.app.features.home.components.ContinueWatchingLayout -import com.nuvio.app.features.home.components.continueWatchingLandscapeCardHeight +import com.nuvio.app.features.home.components.continueWatchingHeroViewportReserveHeight import com.nuvio.app.features.home.components.homeSectionHorizontalPaddingForWidth import com.nuvio.app.features.home.components.rememberContinueWatchingLayout import kotlinx.coroutines.CancellationException @@ -693,9 +691,6 @@ fun HomeScreen( val homeSectionPadding = homeSectionHorizontalPaddingForWidth(maxWidth.value) val continueWatchingLayout = rememberContinueWatchingLayout(maxWidth.value) val posterCardStyle = rememberPosterCardStyleUiState() - val continueWatchingCardHeight = remember(posterCardStyle.widthDp) { - continueWatchingLandscapeCardHeight(posterCardStyle.widthDp) - } val nativeBottomNavigationOverlayHeight = if (LocalNuvioBottomNavigationOverlayPadding.current > 0.dp) { nuvioSafeBottomPadding() @@ -708,18 +703,24 @@ fun HomeScreen( continueWatchingPreferences.style, continueWatchingItems.isNotEmpty(), continueWatchingLayout, - continueWatchingCardHeight, + posterCardStyle.widthDp, + homeSettingsUiState.hideCatalogUnderline, nativeBottomNavigationOverlayHeight, ) { - heroMobileBelowSectionHeightHint( - maxWidthDp = maxWidth.value, - continueWatchingVisible = continueWatchingPreferences.isVisible, - hasContinueWatchingItems = continueWatchingItems.isNotEmpty(), - continueWatchingStyle = continueWatchingPreferences.style, - continueWatchingLayout = continueWatchingLayout, - continueWatchingCardHeight = continueWatchingCardHeight, - bottomNavigationOverlayHeight = nativeBottomNavigationOverlayHeight, - ) + if ( + maxWidth.value < 600f && + continueWatchingPreferences.isVisible && + continueWatchingItems.isNotEmpty() + ) { + continueWatchingHeroViewportReserveHeight( + style = continueWatchingPreferences.style, + layout = continueWatchingLayout, + basePosterWidthDp = posterCardStyle.widthDp, + showHeaderAccent = !homeSettingsUiState.hideCatalogUnderline, + ) + nativeBottomNavigationOverlayHeight + } else { + null + } } NuvioScreen( @@ -764,7 +765,7 @@ fun HomeScreen( style = continueWatchingPreferences.style, useEpisodeThumbnails = continueWatchingPreferences.useEpisodeThumbnails, blurNextUp = continueWatchingPreferences.blurNextUp, - modifier = Modifier.padding(bottom = 12.dp), + modifier = Modifier.padding(bottom = HomeContinueWatchingSectionBottomPadding), sectionPadding = homeSectionPadding, layout = continueWatchingLayout, onItemClick = onContinueWatchingClick, @@ -789,7 +790,7 @@ fun HomeScreen( style = continueWatchingPreferences.style, useEpisodeThumbnails = continueWatchingPreferences.useEpisodeThumbnails, blurNextUp = continueWatchingPreferences.blurNextUp, - modifier = Modifier.padding(bottom = 12.dp), + modifier = Modifier.padding(bottom = HomeContinueWatchingSectionBottomPadding), sectionPadding = homeSectionPadding, layout = continueWatchingLayout, onItemClick = onContinueWatchingClick, @@ -837,7 +838,7 @@ fun HomeScreen( style = continueWatchingPreferences.style, useEpisodeThumbnails = continueWatchingPreferences.useEpisodeThumbnails, blurNextUp = continueWatchingPreferences.blurNextUp, - modifier = Modifier.padding(bottom = 12.dp), + modifier = Modifier.padding(bottom = HomeContinueWatchingSectionBottomPadding), sectionPadding = homeSectionPadding, layout = continueWatchingLayout, onItemClick = onContinueWatchingClick, @@ -1168,26 +1169,6 @@ private fun shouldTreatAsActiveInProgressForNextUpSuppression( return progress.lastUpdatedEpochMs >= latestCompletedAt } -private fun heroMobileBelowSectionHeightHint( - maxWidthDp: Float, - continueWatchingVisible: Boolean, - hasContinueWatchingItems: Boolean, - continueWatchingStyle: ContinueWatchingSectionStyle, - continueWatchingLayout: ContinueWatchingLayout, - continueWatchingCardHeight: Dp, - bottomNavigationOverlayHeight: Dp, -): Dp? { - if (maxWidthDp >= 600f || !continueWatchingVisible || !hasContinueWatchingItems) return null - - val sectionHeight = when (continueWatchingStyle) { - ContinueWatchingSectionStyle.Card -> continueWatchingCardHeight + 56.dp - ContinueWatchingSectionStyle.Wide -> continueWatchingLayout.wideCardHeight + 56.dp - ContinueWatchingSectionStyle.Poster -> - continueWatchingLayout.posterCardHeight + continueWatchingLayout.posterTitleBlockHeight + 70.dp - } - return sectionHeight + bottomNavigationOverlayHeight -} - internal fun buildHomeContinueWatchingItems( visibleEntries: List, cachedInProgressByVideoId: Map = emptyMap(), diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt index e35f2c50..80d3b4e0 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt @@ -50,6 +50,7 @@ import coil3.compose.AsyncImage import com.nuvio.app.core.ui.DisintegratingContainer import com.nuvio.app.core.ui.NuvioProgressBar import com.nuvio.app.core.ui.NuvioShelfSection +import com.nuvio.app.core.ui.NuvioTokens import com.nuvio.app.core.ui.PosterLandscapeAspectRatio import com.nuvio.app.core.ui.landscapePosterHeightForWidth import com.nuvio.app.core.ui.landscapePosterWidth @@ -70,6 +71,7 @@ private val ContinueWatchingStatusBadgeShape = RoundedCornerShape(4.dp) private val ContinueWatchingNewEpisodeBadgeColor = Color(0xFF1D4ED8) private val ContinueWatchingNewSeasonBadgeColor = Color(0xFFB45309) private const val ContinueWatchingLandscapeCardScale = 1.2f +internal val HomeContinueWatchingSectionBottomPadding = 12.dp internal fun continueWatchingLandscapeCardWidth(basePosterWidthDp: Int): Dp = (landscapePosterWidth(basePosterWidthDp).value * ContinueWatchingLandscapeCardScale).dp @@ -77,6 +79,45 @@ internal fun continueWatchingLandscapeCardWidth(basePosterWidthDp: Int): Dp = internal fun continueWatchingLandscapeCardHeight(basePosterWidthDp: Int): Dp = landscapePosterHeightForWidth(continueWatchingLandscapeCardWidth(basePosterWidthDp)) +internal fun continueWatchingSectionHeightEstimate( + style: ContinueWatchingSectionStyle, + layout: ContinueWatchingLayout, + basePosterWidthDp: Int, + showHeaderAccent: Boolean, +): Dp { + val headerHeight = NuvioTokens.Space.s40 + if (showHeaderAccent) { + NuvioTokens.Space.s6 + NuvioTokens.Space.s4 + } else { + 0.dp + } + val headerToRowGap = NuvioTokens.Space.s8 + NuvioTokens.Space.s2 + val rowHeight = when (style) { + ContinueWatchingSectionStyle.Card -> continueWatchingLandscapeCardHeight(basePosterWidthDp) + ContinueWatchingSectionStyle.Wide -> layout.wideCardHeight + ContinueWatchingSectionStyle.Poster -> layout.posterCardHeight + layout.posterTitleBlockHeight + } + return headerHeight + headerToRowGap + rowHeight + HomeContinueWatchingSectionBottomPadding +} + +internal fun continueWatchingHeroViewportReserveHeight( + style: ContinueWatchingSectionStyle, + layout: ContinueWatchingLayout, + basePosterWidthDp: Int, + showHeaderAccent: Boolean, +): Dp { + val bottomNavigationClearance = when (style) { + ContinueWatchingSectionStyle.Card, + ContinueWatchingSectionStyle.Wide -> NuvioTokens.Space.s24 + ContinueWatchingSectionStyle.Poster -> 0.dp + } + return continueWatchingSectionHeightEstimate( + style = style, + layout = layout, + basePosterWidthDp = basePosterWidthDp, + showHeaderAccent = showHeaderAccent, + ) + bottomNavigationClearance +} + private fun continueWatchingProgressPercent(progressFraction: Float): Int = (progressFraction * 100f).roundToInt().coerceIn(1, 99) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeHeroSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeHeroSection.kt index 2cfebe19..30d73d55 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeHeroSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeHeroSection.kt @@ -489,14 +489,24 @@ private fun mobileHeroHeight( val widthFallbackHeight = (maxWidthDp * 1.16f).dp val baseHeight = viewportDrivenHeight ?: widthFallbackHeight - val cappedHeight = if (viewportHeightDp != null && mobileBelowSectionHeightHintDp != null) { - val maxAllowedFromViewport = (viewportHeightDp - mobileBelowSectionHeightHintDp).dp + val maxAllowedFromViewportDp = if (viewportHeightDp != null && mobileBelowSectionHeightHintDp != null) { + viewportHeightDp - mobileBelowSectionHeightHintDp + } else { + null + } + val cappedHeight = if (maxAllowedFromViewportDp != null) { + val maxAllowedFromViewport = maxAllowedFromViewportDp.dp baseHeight.coerceAtMost(maxAllowedFromViewport) } else { baseHeight } + val minHeight = if (maxAllowedFromViewportDp != null) { + minOf(MOBILE_HERO_MIN_HEIGHT_DP, maxAllowedFromViewportDp.coerceAtLeast(0f)).dp + } else { + MOBILE_HERO_MIN_HEIGHT_DP.dp + } - return cappedHeight.coerceIn(MOBILE_HERO_MIN_HEIGHT_DP.dp, MOBILE_HERO_MAX_HEIGHT_DP.dp) + return cappedHeight.coerceIn(minHeight, MOBILE_HERO_MAX_HEIGHT_DP.dp) } @Composable diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/home/components/HomeHeroSectionTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/home/components/HomeHeroSectionTest.kt index 95b42b16..fe3c5028 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/home/components/HomeHeroSectionTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/home/components/HomeHeroSectionTest.kt @@ -1,5 +1,6 @@ package com.nuvio.app.features.home.components +import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle import kotlin.test.Test import kotlin.test.assertEquals @@ -26,4 +27,41 @@ class HomeHeroSectionTest { assertEquals(true, layout.isTablet) assertEquals(386.4f, layout.heroHeight.value, 0.001f) } -} \ No newline at end of file + + @Test + fun `mobile hero height leaves room for continue watching card section`() { + val viewportHeight = 844f + val continueWatchingLayout = rememberContinueWatchingLayout(maxWidthDp = 390f) + val continueWatchingHeight = continueWatchingSectionHeightEstimate( + style = ContinueWatchingSectionStyle.Card, + layout = continueWatchingLayout, + basePosterWidthDp = 110, + showHeaderAccent = true, + ) + val reserveHeight = continueWatchingHeroViewportReserveHeight( + style = ContinueWatchingSectionStyle.Card, + layout = continueWatchingLayout, + basePosterWidthDp = 110, + showHeaderAccent = true, + ) + val layout = homeHeroLayout( + maxWidthDp = 390f, + viewportHeightDp = viewportHeight, + mobileBelowSectionHeightHintDp = reserveHeight.value, + ) + + assertEquals(24f, viewportHeight - layout.heroHeight.value - continueWatchingHeight.value, 0.001f) + } + + @Test + fun `mobile hero can shrink below default minimum to fit short viewport`() { + val layout = homeHeroLayout( + maxWidthDp = 390f, + viewportHeightDp = 568f, + mobileBelowSectionHeightHintDp = 300f, + ) + + assertEquals(false, layout.isTablet) + assertEquals(268f, layout.heroHeight.value, 0.001f) + } +}