fix: keep home hero clear of continue watching

This commit is contained in:
tapframe 2026-07-06 02:44:06 +05:30
parent aeb5e04321
commit cafb21dbe8
4 changed files with 114 additions and 44 deletions

View file

@ -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<WatchProgressEntry>,
cachedInProgressByVideoId: Map<String, ContinueWatchingItem> = emptyMap(),

View file

@ -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)

View file

@ -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

View file

@ -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)
}
}
@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)
}
}