Fix metadata rail scrolling

This commit is contained in:
tapframe 2026-07-15 23:59:26 +05:30
parent 500eb24fea
commit f9b5ef8aef
9 changed files with 131 additions and 11 deletions

View file

@ -0,0 +1,46 @@
package com.nuvio.app.core.ui
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Keeps the initial horizontal inset while allowing a horizontal scroller's
* content to move through that inset once scrolling starts.
*/
fun Modifier.nuvioHorizontalScrollBleed(horizontalPadding: Dp): Modifier {
if (horizontalPadding <= 0.dp) return this
return layout { measurable, constraints ->
if (!constraints.hasBoundedWidth) {
val placeable = measurable.measure(constraints)
return@layout layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
val paddingPx = horizontalPadding.roundToPx()
if (paddingPx == 0) {
val placeable = measurable.measure(constraints)
return@layout layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
val visibleWidth = constraints.maxWidth
val expandedWidth = visibleWidth + paddingPx * 2
val expandedConstraints = Constraints(
minWidth = expandedWidth,
maxWidth = expandedWidth,
minHeight = constraints.minHeight,
maxHeight = constraints.maxHeight,
)
val placeable = measurable.measure(expandedConstraints)
layout(visibleWidth, placeable.height) {
placeable.placeRelative(-paddingPx, 0)
}
}
}

View file

@ -66,6 +66,7 @@ fun <T> NuvioShelfSection(
modifier: Modifier = Modifier,
headerHorizontalPadding: Dp = 0.dp,
rowContentPadding: PaddingValues = PaddingValues(0.dp),
rowModifier: Modifier = Modifier,
itemSpacing: Dp = 10.dp,
showHeaderAccent: Boolean = true,
onViewAllClick: (() -> Unit)? = null,
@ -90,6 +91,7 @@ fun <T> NuvioShelfSection(
)
}
LazyRow(
modifier = rowModifier,
state = state,
contentPadding = rowContentPadding,
horizontalArrangement = Arrangement.spacedBy(itemSpacing),

View file

@ -1608,6 +1608,7 @@ private fun LazyListScope.configuredMetaSectionItems(
),
meta = meta,
isTablet = isTablet,
horizontalScrollPadding = contentHorizontalPadding,
playButtonLabel = playButtonLabel,
isSaved = isSaved,
isWatched = isWatched,
@ -1756,6 +1757,7 @@ private fun ConfiguredMetaSections(
settings: MetaScreenSettingsUiState,
meta: MetaDetails,
isTablet: Boolean,
horizontalScrollPadding: Dp,
playButtonLabel: String,
isSaved: Boolean,
isWatched: Boolean,
@ -1858,7 +1860,10 @@ private fun ConfiguredMetaSections(
)
}
MetaScreenSectionKey.OVERVIEW -> {
DetailMetaInfo(meta = meta)
DetailMetaInfo(
meta = meta,
horizontalScrollPadding = horizontalScrollPadding,
)
}
MetaScreenSectionKey.PRODUCTION -> {
if (hasProductionSection) {
@ -1869,6 +1874,7 @@ private fun ConfiguredMetaSections(
DetailCastSection(
cast = meta.cast,
showHeader = showHeader,
horizontalScrollPadding = horizontalScrollPadding,
onCastClick = onCastClick,
sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope,
@ -1886,12 +1892,18 @@ private fun ConfiguredMetaSections(
onLoadMore = onLoadMoreComments,
onCommentClick = onCommentClick,
showHeader = showHeader,
horizontalScrollPadding = horizontalScrollPadding,
)
}
}
MetaScreenSectionKey.TRAILERS -> {
if (hasTrailersSection) {
DetailTrailersSection(trailers = meta.trailers, onTrailerClick = onTrailerClick, showHeader = showHeader)
DetailTrailersSection(
trailers = meta.trailers,
onTrailerClick = onTrailerClick,
showHeader = showHeader,
horizontalScrollPadding = horizontalScrollPadding,
)
}
}
MetaScreenSectionKey.EPISODES -> {
@ -1899,6 +1911,7 @@ private fun ConfiguredMetaSections(
DetailSeriesContent(
meta = meta,
showHeader = showHeader,
horizontalScrollPadding = horizontalScrollPadding,
preferredSeasonNumber = preferredEpisodeSeasonNumber,
preferredEpisodeNumber = preferredEpisodeNumber,
episodeCardStyle = settings.episodeCardStyle,
@ -1924,6 +1937,7 @@ private fun ConfiguredMetaSections(
items = meta.collectionItems,
watchedKeys = watchedKeys,
showHeader = showHeader,
horizontalScrollPadding = horizontalScrollPadding,
onPosterClick = onOpenMeta,
)
}
@ -1940,6 +1954,7 @@ private fun ConfiguredMetaSections(
items = meta.moreLikeThis,
watchedKeys = watchedKeys,
showHeader = showHeader,
horizontalScrollPadding = horizontalScrollPadding,
sourceLabel = sourceLabel,
onPosterClick = onOpenMeta,
)

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@ -26,6 +27,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -33,6 +35,7 @@ import coil3.compose.AsyncImage
import coil3.compose.LocalPlatformContext
import coil3.request.ImageRequest
import com.nuvio.app.core.ui.NuvioCardDepthSurface
import com.nuvio.app.core.ui.nuvioHorizontalScrollBleed
import com.nuvio.app.core.ui.nuvioCardDepth
import com.nuvio.app.features.details.MetaPerson
import com.nuvio.app.features.details.castAvatarSharedTransitionKey
@ -45,6 +48,7 @@ fun DetailCastSection(
cast: List<MetaPerson>,
modifier: Modifier = Modifier,
showHeader: Boolean = true,
horizontalScrollPadding: Dp = 0.dp,
onCastClick: ((MetaPerson, String?) -> Unit)? = null,
sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null,
@ -60,6 +64,10 @@ fun DetailCastSection(
val sizing = castSectionSizing(maxWidth.value)
LazyRow(
modifier = Modifier
.nuvioHorizontalScrollBleed(horizontalScrollPadding)
.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = horizontalScrollPadding),
horizontalArrangement = Arrangement.spacedBy(sizing.avatarGap),
) {
itemsIndexed(

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -36,8 +37,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nuvio.app.core.ui.nuvioHorizontalScrollBleed
import com.nuvio.app.core.ui.withDuplicateSafeLazyKeys
import com.nuvio.app.features.trakt.TraktCommentReview
import kotlinx.coroutines.flow.distinctUntilChanged
@ -56,6 +59,7 @@ fun DetailCommentsSection(
onCommentClick: (TraktCommentReview) -> Unit,
modifier: Modifier = Modifier,
showHeader: Boolean = true,
horizontalScrollPadding: Dp = 0.dp,
) {
val listState = rememberLazyListState()
@ -81,7 +85,10 @@ fun DetailCommentsSection(
when {
isLoading -> {
LazyRow(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.nuvioHorizontalScrollBleed(horizontalScrollPadding)
.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = horizontalScrollPadding),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(3) {
@ -119,8 +126,11 @@ fun DetailCommentsSection(
else -> {
LazyRow(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.nuvioHorizontalScrollBleed(horizontalScrollPadding)
.fillMaxWidth(),
state = listState,
contentPadding = PaddingValues(horizontal = horizontalScrollPadding),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(

View file

@ -37,9 +37,11 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.core.ui.nuvioHorizontalScrollBleed
import com.nuvio.app.features.details.MetaDetails
import com.nuvio.app.features.details.MetaExternalRating
import com.nuvio.app.features.details.formatRuntimeForDisplay
@ -71,6 +73,7 @@ import kotlin.math.roundToInt
fun DetailMetaInfo(
meta: MetaDetails,
modifier: Modifier = Modifier,
horizontalScrollPadding: Dp = 0.dp,
) {
Column(
modifier = modifier
@ -142,6 +145,7 @@ fun DetailMetaInfo(
) {
DetailRatingsRow(
ratings = meta.externalRatings,
horizontalScrollPadding = horizontalScrollPadding,
)
}
@ -199,6 +203,7 @@ fun DetailMetaInfo(
@Composable
private fun DetailRatingsRow(
ratings: List<MetaExternalRating>,
horizontalScrollPadding: Dp,
) {
val orderedRatings = remember(ratings) {
val bySource = ratings.associateBy { it.source }
@ -211,8 +216,10 @@ private fun DetailRatingsRow(
Row(
modifier = Modifier
.nuvioHorizontalScrollBleed(horizontalScrollPadding)
.fillMaxWidth()
.horizontalScroll(rememberScrollState()),
.horizontalScroll(rememberScrollState())
.padding(horizontal = horizontalScrollPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp),
) {

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.nuvio.app.core.ui.nuvioHorizontalScrollBleed
import com.nuvio.app.core.ui.NuvioShelfSection
import com.nuvio.app.features.home.MetaPreview
import com.nuvio.app.features.home.components.HomePosterCard
@ -26,6 +27,7 @@ fun DetailPosterRailSection(
modifier: Modifier = Modifier,
showHeader: Boolean = true,
headerHorizontalPadding: Dp = 0.dp,
horizontalScrollPadding: Dp = 0.dp,
sourceLabel: String? = null,
onPosterClick: ((MetaPreview) -> Unit)? = null,
onPosterLongClick: ((MetaPreview) -> Unit)? = null,
@ -37,7 +39,10 @@ fun DetailPosterRailSection(
title = if (showHeader) title else "",
entries = items,
headerHorizontalPadding = headerHorizontalPadding,
rowContentPadding = PaddingValues(horizontal = headerHorizontalPadding),
rowContentPadding = PaddingValues(
horizontal = headerHorizontalPadding + horizontalScrollPadding,
),
rowModifier = Modifier.nuvioHorizontalScrollBleed(horizontalScrollPadding),
showHeaderAccent = false,
key = { item -> item.stableKey() },
) { item ->

View file

@ -67,6 +67,7 @@ import com.nuvio.app.core.ui.NuvioAnimatedWatchedBadge
import com.nuvio.app.core.ui.NuvioCardDepthSurface
import com.nuvio.app.core.ui.NuvioProgressBar
import com.nuvio.app.core.ui.nuvioCardDepth
import com.nuvio.app.core.ui.nuvioHorizontalScrollBleed
import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.features.details.MetaDetails
import com.nuvio.app.features.details.MetaEpisodeCardStyle
@ -95,6 +96,7 @@ fun DetailSeriesContent(
meta: MetaDetails,
modifier: Modifier = Modifier,
showHeader: Boolean = true,
horizontalScrollPadding: Dp = 0.dp,
preferredSeasonNumber: Int? = null,
preferredEpisodeNumber: Int? = null,
episodeCardStyle: MetaEpisodeCardStyle = MetaEpisodeCardStyle.Horizontal,
@ -234,6 +236,7 @@ fun DetailSeriesContent(
meta = meta,
currentSeason = currentSeason,
sizing = sizing,
horizontalScrollPadding = horizontalScrollPadding,
onSelect = { selectedSeasonOverride = it },
onLongPress = onSeasonLongPress,
)
@ -241,6 +244,7 @@ fun DetailSeriesContent(
seasons = seasons,
currentSeason = currentSeason,
sizing = sizing,
horizontalScrollPadding = horizontalScrollPadding,
onSelect = { selectedSeasonOverride = it },
onLongPress = onSeasonLongPress,
)
@ -251,6 +255,7 @@ fun DetailSeriesContent(
seasons = seasons,
currentSeason = currentSeason,
sizing = sizing,
horizontalScrollPadding = horizontalScrollPadding,
onSelect = { selectedSeasonOverride = it },
onLongPress = onSeasonLongPress,
)
@ -287,6 +292,7 @@ fun DetailSeriesContent(
EpisodeHorizontalRow(
episodes = seasonEpisodes,
maxWidthDp = containerWidthDp,
horizontalScrollPadding = horizontalScrollPadding,
parentMetaId = meta.id,
metaType = meta.type,
watchedKeys = watchedKeys,
@ -386,6 +392,7 @@ private fun SeasonTextChipScrollRow(
seasons: List<Int>,
currentSeason: Int,
sizing: SeriesContentSizing,
horizontalScrollPadding: Dp,
onSelect: (Int) -> Unit,
onLongPress: ((Int) -> Unit)?,
) {
@ -406,7 +413,10 @@ private fun SeasonTextChipScrollRow(
LazyRow(
state = seasonListState,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.nuvioHorizontalScrollBleed(horizontalScrollPadding)
.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = horizontalScrollPadding),
horizontalArrangement = Arrangement.spacedBy(sizing.seasonChipGap),
) {
items(seasons, key = { season -> season }) { season ->
@ -455,6 +465,7 @@ private fun SeasonPosterScrollRow(
meta: MetaDetails,
currentSeason: Int,
sizing: SeriesContentSizing,
horizontalScrollPadding: Dp,
onSelect: (Int) -> Unit,
onLongPress: ((Int) -> Unit)?,
) {
@ -475,7 +486,10 @@ private fun SeasonPosterScrollRow(
LazyRow(
state = seasonListState,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.nuvioHorizontalScrollBleed(horizontalScrollPadding)
.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = horizontalScrollPadding),
horizontalArrangement = Arrangement.spacedBy(sizing.seasonChipGap),
) {
items(seasons, key = { season -> season }) { season ->
@ -580,6 +594,7 @@ private fun SeasonPosterButton(
private fun EpisodeHorizontalRow(
episodes: List<MetaVideo>,
maxWidthDp: Float,
horizontalScrollPadding: Dp,
parentMetaId: String,
metaType: String,
watchedKeys: Set<String>,
@ -613,8 +628,13 @@ private fun EpisodeHorizontalRow(
LazyRow(
state = listState,
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = rowMetrics.rowHorizontalPadding, vertical = rowMetrics.rowVerticalPadding),
modifier = Modifier
.nuvioHorizontalScrollBleed(horizontalScrollPadding)
.fillMaxWidth(),
contentPadding = PaddingValues(
horizontal = horizontalScrollPadding + rowMetrics.rowHorizontalPadding,
vertical = rowMetrics.rowVerticalPadding,
),
horizontalArrangement = Arrangement.spacedBy(rowMetrics.itemSpacing),
) {
itemsIndexed(

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -34,11 +35,13 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.nuvio.app.core.ui.NuvioCardDepthSurface
import com.nuvio.app.core.ui.nuvioCardDepth
import com.nuvio.app.core.ui.nuvioHorizontalScrollBleed
import com.nuvio.app.features.details.MetaTrailer
import nuvio.composeapp.generated.resources.*
import nuvio.composeapp.generated.resources.detail_tab_trailer
@ -52,6 +55,7 @@ fun DetailTrailersSection(
onTrailerClick: (MetaTrailer) -> Unit,
modifier: Modifier = Modifier,
showHeader: Boolean = true,
horizontalScrollPadding: Dp = 0.dp,
) {
if (trailers.isEmpty()) return
@ -157,7 +161,10 @@ fun DetailTrailersSection(
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
val sizing = trailerSectionSizing(maxWidth.value)
LazyRow(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.nuvioHorizontalScrollBleed(horizontalScrollPadding)
.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = horizontalScrollPadding),
horizontalArrangement = Arrangement.spacedBy(sizing.cardSpacing),
) {
itemsIndexed(