meta hero layout changes

This commit is contained in:
tapframe 2026-04-01 18:31:41 +05:30
parent 3a54098907
commit b8f7ebdc4b
3 changed files with 417 additions and 359 deletions

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
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.Spacer
import androidx.compose.foundation.layout.WindowInsets
@ -18,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.layout.statusBars
@ -46,6 +48,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.ui.NuvioBackButton
@ -306,303 +309,313 @@ fun MetaDetailsScreen(
label = "detail_floating_header_progress",
)
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
) {
DetailHero(
meta = meta,
scrollOffset = scrollState.value,
onHeightChanged = { heroHeightPx = it },
)
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val isTablet = maxWidth >= 720.dp
val contentHorizontalPadding = if (isTablet) 32.dp else 18.dp
val contentMaxWidth = detailTabletContentMaxWidth(maxWidth, isTablet)
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 18.dp),
verticalArrangement = Arrangement.spacedBy(20.dp),
.fillMaxSize()
.verticalScroll(scrollState),
) {
DetailActionButtons(
playLabel = playButtonLabel,
saveLabel = if (isSaved) "Saved" else "Save",
isSaved = isSaved,
onPlayClick = {
when {
(meta.type == "series" || hasEpisodes) && seriesAction != null -> {
onPlay?.invoke(
meta.type,
seriesAction.videoId,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
seriesAction.seasonNumber,
seriesAction.episodeNumber,
seriesAction.episodeTitle,
seriesAction.episodeThumbnail,
seriesPauseDescription,
seriesAction.resumePositionMs,
)
}
else -> {
onPlay?.invoke(
meta.type,
meta.id,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
null,
null,
null,
null,
meta.description,
movieProgress?.lastPositionMs,
)
}
}
},
onSaveClick = toggleSaved,
)
DetailMetaInfo(meta = meta)
if (hasEpisodes && hasProductionSection) {
DetailProductionSection(meta = meta)
}
DetailCastSection(cast = meta.cast)
if (hasTrailersSection) {
DetailTrailersSection(
trailers = meta.trailers,
onTrailerClick = resolveTrailer,
)
}
if (!hasEpisodes && hasProductionSection) {
DetailProductionSection(meta = meta)
}
DetailSeriesContent(
DetailHero(
meta = meta,
progressByVideoId = watchProgressUiState.byVideoId,
watchedKeys = watchedUiState.watchedKeys,
onEpisodeClick = { video ->
val season = video.season
val episode = video.episode
val playbackVideoId = buildPlaybackVideoId(
parentMetaId = meta.id,
seasonNumber = season,
episodeNumber = episode,
fallbackVideoId = video.id,
)
val savedProgress = watchProgressUiState.byVideoId[playbackVideoId]
?.takeUnless { it.isCompleted }
onPlay?.invoke(
meta.type,
playbackVideoId,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
season,
episode,
video.title,
video.thumbnail,
video.overview,
savedProgress?.lastPositionMs,
)
},
onEpisodeLongPress = { video ->
selectedEpisodeForActions = video
},
isTablet = isTablet,
contentMaxWidth = contentMaxWidth,
scrollOffset = scrollState.value,
onHeightChanged = { heroHeightPx = it },
)
if (hasEpisodes && hasAdditionalInfoSection) {
DetailAdditionalInfoSection(meta = meta)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = contentHorizontalPadding)
.widthIn(max = if (isTablet) contentMaxWidth else Dp.Unspecified),
verticalArrangement = Arrangement.spacedBy(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
DetailActionButtons(
playLabel = playButtonLabel,
saveLabel = if (isSaved) "Saved" else "Save",
isSaved = isSaved,
isTablet = isTablet,
onPlayClick = {
when {
(meta.type == "series" || hasEpisodes) && seriesAction != null -> {
onPlay?.invoke(
meta.type,
seriesAction.videoId,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
seriesAction.seasonNumber,
seriesAction.episodeNumber,
seriesAction.episodeTitle,
seriesAction.episodeThumbnail,
seriesPauseDescription,
seriesAction.resumePositionMs,
)
}
if (!hasEpisodes && hasAdditionalInfoSection) {
DetailAdditionalInfoSection(meta = meta)
}
if (!hasEpisodes && hasCollectionSection) {
DetailPosterRailSection(
title = meta.collectionName.orEmpty(),
items = meta.collectionItems,
watchedKeys = watchedUiState.watchedKeys,
onPosterClick = onOpenMeta,
else -> {
onPlay?.invoke(
meta.type,
meta.id,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
null,
null,
null,
null,
meta.description,
movieProgress?.lastPositionMs,
)
}
}
},
onSaveClick = toggleSaved,
)
}
if (hasMoreLikeThisSection) {
DetailPosterRailSection(
title = "More Like This",
items = meta.moreLikeThis,
watchedKeys = watchedUiState.watchedKeys,
onPosterClick = onOpenMeta,
)
}
DetailMetaInfo(meta = meta)
Spacer(modifier = Modifier.height(32.dp + nuvioPlatformExtraBottomPadding))
}
}
if (headerProgress <= 0.05f) {
NuvioBackButton(
onClick = onBack,
modifier = Modifier.padding(
start = 12.dp,
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 8.dp,
),
containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.5f),
contentColor = MaterialTheme.colorScheme.onBackground,
)
}
DetailFloatingHeader(
meta = meta,
isSaved = isSaved,
progress = headerProgress,
onBack = onBack,
onToggleSaved = toggleSaved,
)
selectedEpisodeForActions?.let { selectedEpisode ->
val isSelectedEpisodeWatched = remember(meta, selectedEpisode, watchedUiState.watchedKeys) {
WatchingState.isEpisodeWatched(
watchedKeys = watchedUiState.watchedKeys,
metaType = meta.type,
metaId = meta.id,
episode = selectedEpisode,
)
}
val previousEpisodes = remember(meta, selectedEpisode, todayIsoDate) {
meta.previousReleasedEpisodesBefore(
target = selectedEpisode,
todayIsoDate = todayIsoDate,
)
}
val seasonEpisodes = remember(meta, selectedEpisode, todayIsoDate) {
meta.releasedEpisodesForSeason(
seasonNumber = selectedEpisode.season,
todayIsoDate = todayIsoDate,
)
}
val arePreviousEpisodesWatched = remember(previousEpisodes, watchedUiState.watchedKeys) {
WatchingState.areEpisodesWatched(
watchedKeys = watchedUiState.watchedKeys,
metaType = meta.type,
metaId = meta.id,
episodes = previousEpisodes,
)
}
val isSeasonWatched = remember(seasonEpisodes, watchedUiState.watchedKeys) {
WatchingState.areEpisodesWatched(
watchedKeys = watchedUiState.watchedKeys,
metaType = meta.type,
metaId = meta.id,
episodes = seasonEpisodes,
)
}
EpisodeWatchedActionSheet(
episode = selectedEpisode,
seasonLabel = selectedEpisode.season?.let { "Season $it" } ?: "Specials",
isEpisodeWatched = isSelectedEpisodeWatched,
canMarkPreviousEpisodes = previousEpisodes.isNotEmpty(),
arePreviousEpisodesWatched = arePreviousEpisodesWatched,
isSeasonWatched = isSeasonWatched,
onDismiss = { selectedEpisodeForActions = null },
onToggleWatched = {
WatchingActions.toggleEpisodeWatched(
meta = meta,
episode = selectedEpisode,
isCurrentlyWatched = isSelectedEpisodeWatched,
)
},
onTogglePreviousWatched = {
WatchingActions.togglePreviousEpisodesWatched(
meta = meta,
episodes = previousEpisodes,
areCurrentlyWatched = arePreviousEpisodesWatched,
)
},
onToggleSeasonWatched = {
WatchingActions.toggleSeasonWatched(
meta = meta,
episodes = seasonEpisodes,
areCurrentlyWatched = isSeasonWatched,
)
},
)
}
TrailerPlayerPopup(
visible = selectedTrailer != null,
trailerTitle = selectedTrailer?.displayName ?: selectedTrailer?.name.orEmpty(),
trailerType = selectedTrailer?.type.orEmpty(),
contentTitle = meta.name,
playbackSource = trailerPlaybackSource,
isLoading = trailerLoading,
errorMessage = trailerErrorMessage,
onDismiss = {
trailerRequestToken += 1
trailerLoading = false
trailerPlaybackSource = null
trailerErrorMessage = null
selectedTrailer = null
},
onRetry = selectedTrailer?.let { trailer ->
{ resolveTrailer(trailer) }
},
)
TraktListPickerDialog(
visible = showLibraryListPicker,
title = meta.name,
tabs = pickerTabs,
membership = pickerMembership,
isPending = pickerPending,
errorMessage = pickerError,
onToggle = { listKey ->
pickerMembership = pickerMembership.toMutableMap().apply {
this[listKey] = !(this[listKey] == true)
}
},
onDismiss = {
if (!pickerPending) {
showLibraryListPicker = false
}
},
onSave = {
detailsScope.launch {
pickerPending = true
pickerError = null
runCatching {
LibraryRepository.applyMembershipChanges(
item = meta.toLibraryItem(savedAtEpochMs = 0L),
desiredMembership = pickerMembership,
)
}.onSuccess {
showLibraryListPicker = false
}.onFailure { error ->
pickerError = error.message ?: "Failed to update Trakt lists"
if (hasEpisodes && hasProductionSection) {
DetailProductionSection(meta = meta)
}
pickerPending = false
DetailCastSection(cast = meta.cast)
if (hasTrailersSection) {
DetailTrailersSection(
trailers = meta.trailers,
onTrailerClick = resolveTrailer,
)
}
if (!hasEpisodes && hasProductionSection) {
DetailProductionSection(meta = meta)
}
DetailSeriesContent(
meta = meta,
progressByVideoId = watchProgressUiState.byVideoId,
watchedKeys = watchedUiState.watchedKeys,
onEpisodeClick = { video ->
val season = video.season
val episode = video.episode
val playbackVideoId = buildPlaybackVideoId(
parentMetaId = meta.id,
seasonNumber = season,
episodeNumber = episode,
fallbackVideoId = video.id,
)
val savedProgress = watchProgressUiState.byVideoId[playbackVideoId]
?.takeUnless { it.isCompleted }
onPlay?.invoke(
meta.type,
playbackVideoId,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
season,
episode,
video.title,
video.thumbnail,
video.overview,
savedProgress?.lastPositionMs,
)
},
onEpisodeLongPress = { video ->
selectedEpisodeForActions = video
},
)
if (hasEpisodes && hasAdditionalInfoSection) {
DetailAdditionalInfoSection(meta = meta)
}
if (!hasEpisodes && hasAdditionalInfoSection) {
DetailAdditionalInfoSection(meta = meta)
}
if (!hasEpisodes && hasCollectionSection) {
DetailPosterRailSection(
title = meta.collectionName.orEmpty(),
items = meta.collectionItems,
watchedKeys = watchedUiState.watchedKeys,
onPosterClick = onOpenMeta,
)
}
if (hasMoreLikeThisSection) {
DetailPosterRailSection(
title = "More Like This",
items = meta.moreLikeThis,
watchedKeys = watchedUiState.watchedKeys,
onPosterClick = onOpenMeta,
)
}
Spacer(modifier = Modifier.height(32.dp + nuvioPlatformExtraBottomPadding))
}
Unit
},
)
}
if (headerProgress <= 0.05f) {
NuvioBackButton(
onClick = onBack,
modifier = Modifier.padding(
start = 12.dp,
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 8.dp,
),
containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.5f),
contentColor = MaterialTheme.colorScheme.onBackground,
)
}
DetailFloatingHeader(
meta = meta,
isSaved = isSaved,
progress = headerProgress,
onBack = onBack,
onToggleSaved = toggleSaved,
)
selectedEpisodeForActions?.let { selectedEpisode ->
val isSelectedEpisodeWatched = remember(meta, selectedEpisode, watchedUiState.watchedKeys) {
WatchingState.isEpisodeWatched(
watchedKeys = watchedUiState.watchedKeys,
metaType = meta.type,
metaId = meta.id,
episode = selectedEpisode,
)
}
val previousEpisodes = remember(meta, selectedEpisode, todayIsoDate) {
meta.previousReleasedEpisodesBefore(
target = selectedEpisode,
todayIsoDate = todayIsoDate,
)
}
val seasonEpisodes = remember(meta, selectedEpisode, todayIsoDate) {
meta.releasedEpisodesForSeason(
seasonNumber = selectedEpisode.season,
todayIsoDate = todayIsoDate,
)
}
val arePreviousEpisodesWatched = remember(previousEpisodes, watchedUiState.watchedKeys) {
WatchingState.areEpisodesWatched(
watchedKeys = watchedUiState.watchedKeys,
metaType = meta.type,
metaId = meta.id,
episodes = previousEpisodes,
)
}
val isSeasonWatched = remember(seasonEpisodes, watchedUiState.watchedKeys) {
WatchingState.areEpisodesWatched(
watchedKeys = watchedUiState.watchedKeys,
metaType = meta.type,
metaId = meta.id,
episodes = seasonEpisodes,
)
}
EpisodeWatchedActionSheet(
episode = selectedEpisode,
seasonLabel = selectedEpisode.season?.let { "Season $it" } ?: "Specials",
isEpisodeWatched = isSelectedEpisodeWatched,
canMarkPreviousEpisodes = previousEpisodes.isNotEmpty(),
arePreviousEpisodesWatched = arePreviousEpisodesWatched,
isSeasonWatched = isSeasonWatched,
onDismiss = { selectedEpisodeForActions = null },
onToggleWatched = {
WatchingActions.toggleEpisodeWatched(
meta = meta,
episode = selectedEpisode,
isCurrentlyWatched = isSelectedEpisodeWatched,
)
},
onTogglePreviousWatched = {
WatchingActions.togglePreviousEpisodesWatched(
meta = meta,
episodes = previousEpisodes,
areCurrentlyWatched = arePreviousEpisodesWatched,
)
},
onToggleSeasonWatched = {
WatchingActions.toggleSeasonWatched(
meta = meta,
episodes = seasonEpisodes,
areCurrentlyWatched = isSeasonWatched,
)
},
)
}
TrailerPlayerPopup(
visible = selectedTrailer != null,
trailerTitle = selectedTrailer?.displayName ?: selectedTrailer?.name.orEmpty(),
trailerType = selectedTrailer?.type.orEmpty(),
contentTitle = meta.name,
playbackSource = trailerPlaybackSource,
isLoading = trailerLoading,
errorMessage = trailerErrorMessage,
onDismiss = {
trailerRequestToken += 1
trailerLoading = false
trailerPlaybackSource = null
trailerErrorMessage = null
selectedTrailer = null
},
onRetry = selectedTrailer?.let { trailer ->
{ resolveTrailer(trailer) }
},
)
TraktListPickerDialog(
visible = showLibraryListPicker,
title = meta.name,
tabs = pickerTabs,
membership = pickerMembership,
isPending = pickerPending,
errorMessage = pickerError,
onToggle = { listKey ->
pickerMembership = pickerMembership.toMutableMap().apply {
this[listKey] = !(this[listKey] == true)
}
},
onDismiss = {
if (!pickerPending) {
showLibraryListPicker = false
}
},
onSave = {
detailsScope.launch {
pickerPending = true
pickerError = null
runCatching {
LibraryRepository.applyMembershipChanges(
item = meta.toLibraryItem(savedAtEpochMs = 0L),
desiredMembership = pickerMembership,
)
}.onSuccess {
showLibraryListPicker = false
}.onFailure { error ->
pickerError = error.message ?: "Failed to update Trakt lists"
}
pickerPending = false
}
},
)
}
}
}
}
@ -621,6 +634,13 @@ fun MetaDetailsScreen(
}
}
private fun detailTabletContentMaxWidth(maxWidth: Dp, isTablet: Boolean): Dp =
if (!isTablet) {
maxWidth
} else {
(maxWidth * 0.6f).coerceIn(520.dp, 680.dp)
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun TraktListPickerDialog(

View file

@ -18,6 +18,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -30,6 +31,7 @@ fun DetailActionButtons(
playLabel: String = "Play",
saveLabel: String = "Save",
isSaved: Boolean = false,
isTablet: Boolean = false,
onPlayClick: () -> Unit = {},
onSaveClick: () -> Unit = {},
) {
@ -38,12 +40,22 @@ fun DetailActionButtons(
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = if (isTablet) {
Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally)
} else {
Arrangement.spacedBy(12.dp)
},
) {
Button(
onClick = onPlayClick,
modifier = Modifier
.weight(1f)
.then(
if (isTablet) {
Modifier.width(220.dp)
} else {
Modifier.weight(1f)
}
)
.height(50.dp),
shape = RoundedCornerShape(40.dp),
colors = ButtonDefaults.buttonColors(
@ -68,7 +80,13 @@ fun DetailActionButtons(
OutlinedButton(
onClick = onSaveClick,
modifier = Modifier
.weight(1f)
.then(
if (isTablet) {
Modifier.width(220.dp)
} else {
Modifier.weight(1f)
}
)
.height(50.dp),
shape = RoundedCornerShape(40.dp),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),

View file

@ -2,13 +2,14 @@ package com.nuvio.app.features.details.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.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.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -19,6 +20,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.graphicsLayer
import coil3.compose.AsyncImage
@ -27,97 +29,115 @@ import com.nuvio.app.features.details.MetaDetails
@Composable
fun DetailHero(
meta: MetaDetails,
isTablet: Boolean = false,
scrollOffset: Int = 0,
contentMaxWidth: Dp = 560.dp,
onHeightChanged: (Int) -> Unit = {},
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(0.75f)
.onSizeChanged { onHeightChanged(it.height) }
.graphicsLayer {
clip = true
},
BoxWithConstraints(
modifier = modifier.fillMaxWidth(),
) {
val heroHeight = detailHeroHeight(maxWidth, isTablet)
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.BottomCenter,
.fillMaxWidth()
.height(heroHeight)
.onSizeChanged { onHeightChanged(it.height) }
.graphicsLayer {
clip = true
},
) {
val imageUrl = meta.background ?: meta.poster
if (imageUrl != null) {
AsyncImage(
model = imageUrl,
contentDescription = meta.name,
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
translationY = scrollOffset * 0.5f
scaleX = 1.08f
scaleY = 1.08f
},
contentScale = ContentScale.Crop,
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(260.dp)
.align(Alignment.BottomCenter)
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
MaterialTheme.colorScheme.background.copy(alpha = 0.7f),
MaterialTheme.colorScheme.background,
),
),
),
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 18.dp)
.padding(bottom = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
.fillMaxSize(),
contentAlignment = Alignment.BottomCenter,
) {
if (meta.logo != null) {
val imageUrl = meta.background ?: meta.poster
if (imageUrl != null) {
AsyncImage(
model = meta.logo,
contentDescription = "${meta.name} logo",
model = imageUrl,
contentDescription = meta.name,
modifier = Modifier
.fillMaxWidth(0.6f)
.height(80.dp),
contentScale = ContentScale.Fit,
.fillMaxSize()
.graphicsLayer {
translationY = scrollOffset * 0.5f
scaleX = 1.08f
scaleY = 1.08f
},
alignment = if (isTablet) Alignment.TopCenter else Alignment.Center,
contentScale = ContentScale.Crop,
)
} else {
Text(
text = meta.name,
style = MaterialTheme.typography.displayLarge,
color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center,
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
)
}
if (meta.genres.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = meta.genres.take(3).joinToString(" \u2022 "),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(260.dp)
.align(Alignment.BottomCenter)
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
MaterialTheme.colorScheme.background.copy(alpha = 0.7f),
MaterialTheme.colorScheme.background,
),
),
),
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = if (isTablet) 32.dp else 18.dp)
.padding(bottom = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (meta.logo != null) {
AsyncImage(
model = meta.logo,
contentDescription = "${meta.name} logo",
modifier = Modifier
.fillMaxWidth(if (isTablet) 0.56f else 0.6f)
.widthIn(max = contentMaxWidth)
.height(if (isTablet) 72.dp else 80.dp),
alignment = Alignment.Center,
contentScale = ContentScale.Fit,
)
} else {
Text(
text = meta.name,
style = if (isTablet) MaterialTheme.typography.displaySmall else MaterialTheme.typography.displayLarge,
color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center,
)
}
if (meta.genres.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = meta.genres.take(3).joinToString(" \u2022 "),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
}
}
}
}
}
private fun detailHeroHeight(maxWidth: Dp, isTablet: Boolean): Dp =
if (!isTablet) {
(maxWidth * 1.33f).coerceIn(420.dp, 760.dp)
} else {
(maxWidth * 0.42f).coerceIn(300.dp, 420.dp)
}