diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/HorizontalScrollModifiers.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/HorizontalScrollModifiers.kt new file mode 100644 index 000000000..0b6a113b5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/HorizontalScrollModifiers.kt @@ -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) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt index d9554540a..deec50250 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt @@ -66,6 +66,7 @@ fun 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 NuvioShelfSection( ) } LazyRow( + modifier = rowModifier, state = state, contentPadding = rowContentPadding, horizontalArrangement = Arrangement.spacedBy(itemSpacing), diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt index 5fc776495..80c3265c2 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt @@ -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, ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt index 34de39b12..e430bdeca 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt @@ -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, 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( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCommentsSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCommentsSection.kt index 5bc7112ac..ea5331b4d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCommentsSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCommentsSection.kt @@ -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( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt index 0d8b5185d..bbf2e7233 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt @@ -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, + 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), ) { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailPosterRailSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailPosterRailSection.kt index 58c2269f8..e1b0a6f0f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailPosterRailSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailPosterRailSection.kt @@ -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 -> diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt index 039e10bd6..71ba2eded 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt @@ -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, 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, maxWidthDp: Float, + horizontalScrollPadding: Dp, parentMetaId: String, metaType: String, watchedKeys: Set, @@ -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( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt index 5ca4fa96f..a3fe8a51f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt @@ -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(