mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Refactor TV media detail backdrop layout
This commit is contained in:
@@ -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<ComponentActivity>()
|
||||
|
||||
@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 = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<ComponentActivity>()
|
||||
|
||||
@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 {
|
||||
|
||||
@@ -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<ComponentActivity>()
|
||||
|
||||
@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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,20 +49,15 @@ 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,
|
||||
backgroundImageUrl = backgroundImageUrl,
|
||||
headerHeightFraction = headerHeightFraction,
|
||||
heroContent = heroContent
|
||||
)
|
||||
@@ -83,82 +65,26 @@ internal fun TvMediaDetailScaffold(
|
||||
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,15 +107,33 @@ 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(
|
||||
|
||||
Reference in New Issue
Block a user