diff --git a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/episode/EpisodeScreenContentTest.kt b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/episode/EpisodeScreenContentTest.kt index 5df50b94..3c72015c 100644 --- a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/episode/EpisodeScreenContentTest.kt +++ b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/episode/EpisodeScreenContentTest.kt @@ -6,13 +6,9 @@ import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.performKeyInput -import androidx.compose.ui.test.pressKey import androidx.compose.ui.test.onAllNodesWithText -import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.input.key.Key import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.ui.theme.AppTheme @@ -27,13 +23,12 @@ class EpisodeScreenContentTest { val composeRule = createAndroidComposeRule() @Test - fun episodeScreenContent_showsSeriesContext_andMovesFromBackToPlayButton() { + fun episodeScreenContent_showsSeriesContext_andFocusesPlayButton() { composeRule.setContent { AppTheme { EpisodeScreenContent( episode = sampleEpisode(progress = 63.0), seriesTitle = "Severance", - onBack = {}, onPlay = {} ) } @@ -48,12 +43,6 @@ class EpisodeScreenContentTest { composeRule.onAllNodesWithText("Playback").assertCountEquals(1) composeRule.onNodeWithTag(EpisodePlayButtonTag).assertIsDisplayed() composeRule.onNodeWithText("Series").assertIsDisplayed() - composeRule.onNodeWithContentDescription("Back") - .assertIsDisplayed() - .assertIsFocused() - .performKeyInput { - pressKey(Key.DirectionDown) - } composeRule.onNodeWithTag(EpisodePlayButtonTag).assertIsFocused() } @@ -64,7 +53,6 @@ class EpisodeScreenContentTest { EpisodeScreenContent( episode = sampleEpisode(progress = null), seriesTitle = "Severance", - onBack = {}, onPlay = {} ) } diff --git a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/movie/MovieScreenContentTest.kt b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/movie/MovieScreenContentTest.kt index aebbd8fb..1dbcb23d 100644 --- a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/movie/MovieScreenContentTest.kt +++ b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/movie/MovieScreenContentTest.kt @@ -6,13 +6,9 @@ import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.performKeyInput -import androidx.compose.ui.test.pressKey import androidx.compose.ui.test.onAllNodesWithText -import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.input.key.Key import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.ui.theme.AppTheme @@ -27,12 +23,11 @@ class MovieScreenContentTest { val composeRule = createAndroidComposeRule() @Test - fun movieScreenContent_focusesBack_thenMovesToPlayButton() { + fun movieScreenContent_focusesPlayButton() { composeRule.setContent { AppTheme { MovieScreenContent( movie = sampleMovie(progress = 42.0), - onBack = {}, onPlay = {} ) } @@ -43,14 +38,9 @@ class MovieScreenContentTest { composeRule.onNodeWithText("Blade Runner 2049").assertIsDisplayed() composeRule.onNodeWithText("Overview").assertIsDisplayed() composeRule.onAllNodesWithText("Playback").assertCountEquals(1) - composeRule.onNodeWithTag(MoviePlayButtonTag).assertIsDisplayed() - composeRule.onNodeWithContentDescription("Back") + composeRule.onNodeWithTag(MoviePlayButtonTag) .assertIsDisplayed() .assertIsFocused() - .performKeyInput { - pressKey(Key.DirectionDown) - } - composeRule.onNodeWithTag(MoviePlayButtonTag).assertIsFocused() } private fun sampleMovie(progress: Double?): Movie { diff --git a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/series/SeriesScreenContentTest.kt b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/series/SeriesScreenContentTest.kt index 2684d8bc..182844b9 100644 --- a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/series/SeriesScreenContentTest.kt +++ b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/series/SeriesScreenContentTest.kt @@ -5,12 +5,8 @@ import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.performKeyInput -import androidx.compose.ui.test.pressKey -import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.input.key.Key import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Season @@ -27,12 +23,11 @@ class SeriesScreenContentTest { val composeRule = createAndroidComposeRule() @Test - fun seriesScreenContent_movesFromBackToPrimaryAction_whenNextUpExists() { + fun seriesScreenContent_focusesPrimaryAction_whenNextUpExists() { composeRule.setContent { AppTheme { SeriesScreenContent( series = sampleSeriesWithEpisodes(), - onBack = {}, onPlayEpisode = {} ) } @@ -44,25 +39,18 @@ class SeriesScreenContentTest { composeRule.onNodeWithText("Overview").assertIsDisplayed() composeRule.onNodeWithText("Continue Watching").assertIsDisplayed() composeRule.onNodeWithTag(SeriesPlayButtonTag).assertIsDisplayed() + .assertIsFocused() composeRule.onNodeWithText("Season 1").assertIsDisplayed() composeRule.onNodeWithText("Good News About Hell").assertIsDisplayed() composeRule.onNodeWithText("Episode 1 • 57m").assertIsDisplayed() - composeRule.onNodeWithContentDescription("Back") - .assertIsDisplayed() - .assertIsFocused() - .performKeyInput { - pressKey(Key.DirectionDown) - } - composeRule.onNodeWithTag(SeriesPlayButtonTag).assertIsFocused() } @Test - fun seriesScreenContent_movesFromBackToFirstSeason_whenNoPlayableEpisodeExists() { + fun seriesScreenContent_focusesFirstSeason_whenNoPlayableEpisodeExists() { composeRule.setContent { AppTheme { SeriesScreenContent( series = sampleSeriesWithoutEpisodes(), - onBack = {}, onPlayEpisode = {} ) } @@ -72,14 +60,9 @@ class SeriesScreenContentTest { composeRule.onNodeWithText("Overview").assertIsDisplayed() composeRule.onNodeWithText("Choose a season below to start watching.").assertIsDisplayed() - composeRule.onNodeWithTag(SeriesFirstSeasonTabTag).assertIsDisplayed() - composeRule.onNodeWithContentDescription("Back") + composeRule.onNodeWithTag(SeriesFirstSeasonTabTag) .assertIsDisplayed() .assertIsFocused() - .performKeyInput { - pressKey(Key.DirectionDown) - } - composeRule.onNodeWithTag(SeriesFirstSeasonTabTag).assertIsFocused() } private fun sampleSeriesWithEpisodes(): Series { diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeScreen.kt index bc223c4c..33f471fc 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeScreen.kt @@ -18,14 +18,12 @@ import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold -import hu.bbara.purefin.core.data.image.JellyfinImageHelper -import hu.bbara.purefin.core.data.navigation.EpisodeDto -import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack import hu.bbara.purefin.core.data.navigation.LocalNavigationManager import hu.bbara.purefin.core.data.navigation.Route +import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl +import hu.bbara.purefin.core.data.navigation.EpisodeDto import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel -import org.jellyfin.sdk.model.api.ImageType @Composable fun EpisodeScreen( @@ -34,8 +32,6 @@ fun EpisodeScreen( modifier: Modifier = Modifier ) { val navigationManager = LocalNavigationManager.current - val backStack = LocalNavigationBackStack.current - val previousRoute = remember(backStack) { backStack.getOrNull(backStack.lastIndex - 1) } LaunchedEffect(episode) { viewModel.selectEpisode( @@ -57,7 +53,6 @@ fun EpisodeScreen( EpisodeScreenContent( episode = selectedEpisode, seriesTitle = seriesTitle.value, - onBack = viewModel::onBack, onPlay = remember(selectedEpisode.id, navigationManager) { { navigationManager.navigate( @@ -73,7 +68,6 @@ fun EpisodeScreen( internal fun EpisodeScreenContent( episode: Episode, seriesTitle: String?, - onBack: () -> Unit, onPlay: () -> Unit, modifier: Modifier = Modifier, ) { @@ -84,9 +78,7 @@ internal fun EpisodeScreenContent( } TvMediaDetailScaffold( - artworkImageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY), - artworkWidth = 280.dp, - artworkAspectRatio = 16f / 9f, + backgroundImageUrl = tvMediaDetailBackgroundImageUrl(episode.imageUrlPrefix), resetScrollKey = episode.id, modifier = modifier, heroContent = { diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieScreen.kt index 9fdeaad2..eb92ce28 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieScreen.kt @@ -18,11 +18,10 @@ import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold -import hu.bbara.purefin.core.data.image.JellyfinImageHelper +import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl import hu.bbara.purefin.core.data.navigation.MovieDto import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel -import org.jellyfin.sdk.model.api.ImageType @Composable fun MovieScreen( @@ -37,7 +36,6 @@ fun MovieScreen( if (movieItem.value != null) { MovieScreenContent( movie = movieItem.value!!, - onBack = viewModel::onBack, onPlay = viewModel::onPlay, modifier = modifier ) @@ -49,7 +47,6 @@ fun MovieScreen( @Composable internal fun MovieScreenContent( movie: Movie, - onBack: () -> Unit, onPlay: () -> Unit, modifier: Modifier = Modifier, ) { @@ -60,9 +57,7 @@ internal fun MovieScreenContent( } TvMediaDetailScaffold( - artworkImageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY), - artworkWidth = 200.dp, - artworkAspectRatio = 2f / 3f, + backgroundImageUrl = tvMediaDetailBackgroundImageUrl(movie.imageUrlPrefix), resetScrollKey = movie.id, modifier = modifier, heroContent = { diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesScreen.kt index 6f745643..5785a666 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesScreen.kt @@ -7,8 +7,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.unit.dp @@ -17,13 +19,12 @@ import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold -import hu.bbara.purefin.core.data.image.JellyfinImageHelper +import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl import hu.bbara.purefin.core.data.navigation.SeriesDto import hu.bbara.purefin.core.model.Season import hu.bbara.purefin.core.model.Series import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.ImageType @Composable fun SeriesScreen( @@ -41,7 +42,6 @@ fun SeriesScreen( if (seriesData != null && seriesData.seasons.isNotEmpty()) { SeriesScreenContent( series = seriesData, - onBack = viewModel::onBack, onPlayEpisode = viewModel::onPlayEpisode, modifier = modifier ) @@ -53,23 +53,11 @@ fun SeriesScreen( @Composable internal fun SeriesScreenContent( series: Series, - onBack: () -> Unit, onPlayEpisode: (UUID) -> Unit, modifier: Modifier = Modifier, ) { - fun getDefaultSeason(): Season { - for (season in series.seasons) { - val firstUnwatchedEpisode = season.episodes.firstOrNull { it.watched.not() } - if (firstUnwatchedEpisode != null) return season - } - return series.seasons.first() - } - val selectedSeason = remember(series.id) { mutableStateOf(getDefaultSeason()) } - val nextUpEpisode = remember(series.id) { - series.seasons.firstNotNullOfOrNull { season -> - season.episodes.firstOrNull { !it.watched } - } ?: series.seasons.firstOrNull()?.episodes?.firstOrNull() - } + var selectedSeason by remember(series.id) { mutableStateOf(series.defaultSeason()) } + val nextUpEpisode = remember(series.id) { series.nextUpEpisode() } val playFocusRequester = remember { FocusRequester() } val firstContentFocusRequester = remember { FocusRequester() } @@ -82,9 +70,7 @@ internal fun SeriesScreenContent( } TvMediaDetailScaffold( - artworkImageUrl = JellyfinImageHelper.finishImageUrl(series.imageUrlPrefix, ImageType.PRIMARY), - artworkWidth = 200.dp, - artworkAspectRatio = 2f / 3f, + backgroundImageUrl = tvMediaDetailBackgroundImageUrl(series.imageUrlPrefix), resetScrollKey = series.id, modifier = modifier, heroContent = { @@ -112,17 +98,17 @@ internal fun SeriesScreenContent( item { SeasonTabs( seasons = series.seasons, - selectedSeason = selectedSeason.value, + selectedSeason = selectedSeason, firstItemFocusRequester = firstContentFocusRequester, firstItemTestTag = SeriesFirstSeasonTabTag, - onSelect = { selectedSeason.value = it }, + onSelect = { selectedSeason = it }, modifier = it ) } item { Spacer(modifier = Modifier.height(20.dp)) EpisodeCarousel( - episodes = selectedSeason.value.episodes, + episodes = selectedSeason.episodes, modifier = it ) } @@ -139,3 +125,16 @@ internal fun SeriesScreenContent( } } } + +private fun Series.defaultSeason(): Season { + for (season in seasons) { + if (season.episodes.any { !it.watched }) { + return season + } + } + return seasons.first() +} + +private fun Series.nextUpEpisode() = seasons.firstNotNullOfOrNull { season -> + season.episodes.firstOrNull { !it.watched } +} ?: seasons.firstOrNull()?.episodes?.firstOrNull() diff --git a/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt b/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt index 915b16cb..3dfe6a9a 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt @@ -2,18 +2,14 @@ package hu.bbara.purefin.common.ui.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.rememberLazyListState @@ -21,33 +17,24 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.foundation.shape.RoundedCornerShape import hu.bbara.purefin.common.ui.MediaSynopsis +import hu.bbara.purefin.core.data.image.JellyfinImageHelper +import org.jellyfin.sdk.model.api.ImageType internal val MediaDetailHorizontalPadding = 48.dp private val MediaDetailHeaderTopPadding = 104.dp private val MediaDetailHeaderBottomPadding = 36.dp -private val MediaDetailCornerArtworkTopPadding = 40.dp -private val MediaDetailCornerArtworkShape = RoundedCornerShape(24.dp) -private val MediaDetailMinimumContentWidth = 280.dp -private val MediaDetailContentArtworkGap = 32.dp @Composable internal fun TvMediaDetailScaffold( - artworkImageUrl: String, - artworkWidth: Dp, - artworkAspectRatio: Float, + backgroundImageUrl: String, resetScrollKey: Any, modifier: Modifier = Modifier, headerHeightFraction: Float = 0.48f, @@ -62,103 +49,42 @@ internal fun TvMediaDetailScaffold( listState.scrollToItem(0) } - Box( + LazyColumn( + state = listState, modifier = modifier .fillMaxSize() .background(scheme.background) ) { - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize() - ) { - item { - TvMediaDetailHeader( - artworkImageUrl = artworkImageUrl, - artworkWidth = artworkWidth, - artworkAspectRatio = artworkAspectRatio, - headerHeightFraction = headerHeightFraction, - heroContent = heroContent - ) - } - bodyContent(contentPadding) + item { + TvMediaDetailHeader( + backgroundImageUrl = backgroundImageUrl, + headerHeightFraction = headerHeightFraction, + heroContent = heroContent + ) } + bodyContent(contentPadding) } } @Composable private fun TvMediaDetailHeader( - artworkImageUrl: String, - artworkWidth: Dp, - artworkAspectRatio: Float, + backgroundImageUrl: String, headerHeightFraction: Float, heroContent: @Composable ColumnScope.() -> Unit ) { + val scheme = MaterialTheme.colorScheme val screenHeight = LocalConfiguration.current.screenHeightDp.dp val headerHeight = screenHeight * headerHeightFraction - BoxWithConstraints( + Box( modifier = Modifier .fillMaxWidth() .heightIn(min = headerHeight) - ) { - val contentMaxWidth = ( - maxWidth - - (MediaDetailHorizontalPadding * 2) - - artworkWidth - - MediaDetailContentArtworkGap - ).coerceAtLeast(MediaDetailMinimumContentWidth) - - Box(modifier = Modifier.fillMaxWidth()) { - TvMediaDetailCornerArtwork( - artworkImageUrl = artworkImageUrl, - artworkWidth = artworkWidth, - artworkAspectRatio = artworkAspectRatio, - modifier = Modifier - .align(Alignment.TopEnd) - .padding( - top = MediaDetailCornerArtworkTopPadding, - end = MediaDetailHorizontalPadding - ) - ) - Column( - modifier = Modifier - .align(Alignment.TopStart) - .padding( - start = MediaDetailHorizontalPadding, - top = MediaDetailHeaderTopPadding, - end = MediaDetailHorizontalPadding, - bottom = MediaDetailHeaderBottomPadding - ) - .widthIn(max = contentMaxWidth) - ) { - heroContent() - } - } - } -} - -@Composable -private fun TvMediaDetailCornerArtwork( - artworkImageUrl: String, - artworkWidth: Dp, - artworkAspectRatio: Float, - modifier: Modifier = Modifier -) { - val scheme = MaterialTheme.colorScheme - - Box( - modifier = modifier - .width(artworkWidth) - .aspectRatio(artworkAspectRatio) - .clip(MediaDetailCornerArtworkShape) - .background(scheme.surfaceVariant.copy(alpha = 0.28f)) ) { PurefinAsyncImage( - model = artworkImageUrl, + model = backgroundImageUrl, contentDescription = null, - modifier = Modifier - .fillMaxSize() - .graphicsLayer { alpha = 0.52f }, + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) Box( @@ -167,9 +93,10 @@ private fun TvMediaDetailCornerArtwork( .background( Brush.horizontalGradient( colorStops = arrayOf( - 0.0f to scheme.background.copy(alpha = 0.08f), - 0.55f to scheme.background.copy(alpha = 0.2f), - 1.0f to scheme.background.copy(alpha = 0.56f) + 0.0f to scheme.background, + 0.28f to scheme.background.copy(alpha = 0.88f), + 0.62f to scheme.background.copy(alpha = 0.42f), + 1.0f to scheme.background.copy(alpha = 0.12f) ) ) ) @@ -180,16 +107,34 @@ private fun TvMediaDetailCornerArtwork( .background( Brush.verticalGradient( colorStops = arrayOf( - 0.0f to scheme.background.copy(alpha = 0.06f), - 0.65f to scheme.background.copy(alpha = 0.22f), - 1.0f to scheme.background.copy(alpha = 0.74f) + 0.0f to scheme.background.copy(alpha = 0.08f), + 0.55f to scheme.background.copy(alpha = 0.16f), + 1.0f to scheme.background ) ) ) ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = MediaDetailHorizontalPadding, + top = MediaDetailHeaderTopPadding, + end = MediaDetailHorizontalPadding, + bottom = MediaDetailHeaderBottomPadding + ) + ) { + heroContent() + } } } +internal fun tvMediaDetailBackgroundImageUrl(imageUrlPrefix: String?): String { + val primaryImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY) + val backdropImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.BACKDROP) + return backdropImageUrl.ifBlank { primaryImageUrl } +} + @Composable internal fun MediaDetailSectionTitle( text: String,