feat; horizontal episde card

This commit is contained in:
tapframe 2026-04-12 01:39:51 +05:30
parent 5288278789
commit 39c93aaeb5
4 changed files with 629 additions and 26 deletions

View file

@ -999,6 +999,7 @@ private fun ConfiguredMetaSections(
meta = meta,
showHeader = showHeader,
preferredSeasonNumber = preferredEpisodeSeasonNumber,
episodeCardStyle = settings.episodeCardStyle,
progressByVideoId = progressByVideoId,
watchedKeys = watchedKeys,
onEpisodeClick = onEpisodeClick,

View file

@ -40,8 +40,28 @@ data class MetaScreenSettingsUiState(
val items: List<MetaScreenSectionItem> = emptyList(),
val cinematicBackground: Boolean = false,
val tabLayout: Boolean = false,
val episodeCardStyle: MetaEpisodeCardStyle = MetaEpisodeCardStyle.Horizontal,
)
enum class MetaEpisodeCardStyle {
Horizontal,
List,
;
companion object {
fun parse(raw: String?): MetaEpisodeCardStyle? = when (raw?.lowercase()) {
"horizontal" -> Horizontal
"list" -> List
else -> null
}
fun persist(style: MetaEpisodeCardStyle): String = when (style) {
Horizontal -> "horizontal"
List -> "list"
}
}
}
@Serializable
private data class StoredMetaScreenSectionPreference(
val key: String,
@ -56,6 +76,7 @@ private data class StoredMetaScreenSettingsPayload(
val cinematicBackground: Boolean = false,
@SerialName("tvStyleLayout")
val tabLayout: Boolean = false,
val episodeCardStyle: String = "horizontal",
)
private data class MetaScreenSectionDefinition(
@ -130,6 +151,7 @@ object MetaScreenSettingsRepository {
private var preferences: MutableMap<MetaScreenSectionKey, StoredMetaScreenSectionPreference> = mutableMapOf()
private var cinematicBackground: Boolean = false
private var tabLayout: Boolean = false
private var episodeCardStyle: MetaEpisodeCardStyle = MetaEpisodeCardStyle.Horizontal
fun ensureLoaded() {
if (hasLoaded) return
@ -143,6 +165,8 @@ object MetaScreenSettingsRepository {
if (parsed != null) {
cinematicBackground = parsed.cinematicBackground
tabLayout = parsed.tabLayout
episodeCardStyle = MetaEpisodeCardStyle.parse(parsed.episodeCardStyle)
?: MetaEpisodeCardStyle.Horizontal
preferences = parsed.items.mapNotNull { item ->
val key = runCatching { MetaScreenSectionKey.valueOf(item.key) }.getOrNull() ?: return@mapNotNull null
key to item
@ -160,6 +184,7 @@ object MetaScreenSettingsRepository {
preferences.clear()
cinematicBackground = false
tabLayout = false
episodeCardStyle = MetaEpisodeCardStyle.Horizontal
_uiState.value = MetaScreenSettingsUiState()
ensureLoaded()
}
@ -178,6 +203,13 @@ object MetaScreenSettingsRepository {
persist()
}
fun setEpisodeCardStyle(style: MetaEpisodeCardStyle) {
ensureLoaded()
episodeCardStyle = style
publish()
persist()
}
fun setTabGroup(key: MetaScreenSectionKey, groupId: Int?) {
ensureLoaded()
if (!key.canBeTabbed) return
@ -203,10 +235,12 @@ object MetaScreenSettingsRepository {
items: List<MetaScreenSectionItem>,
cinematicBackground: Boolean,
tabLayout: Boolean,
episodeCardStyle: MetaEpisodeCardStyle = MetaEpisodeCardStyle.Horizontal,
) {
ensureLoaded()
this.cinematicBackground = cinematicBackground
this.tabLayout = tabLayout
this.episodeCardStyle = episodeCardStyle
preferences = items.associate { item ->
item.key to StoredMetaScreenSectionPreference(
key = item.key.name,
@ -231,6 +265,7 @@ object MetaScreenSettingsRepository {
preferences.clear()
cinematicBackground = false
tabLayout = false
episodeCardStyle = MetaEpisodeCardStyle.Horizontal
normalizePreferences()
publish()
persist()
@ -296,6 +331,7 @@ object MetaScreenSettingsRepository {
},
cinematicBackground = cinematicBackground,
tabLayout = tabLayout,
episodeCardStyle = episodeCardStyle,
)
}
@ -306,6 +342,7 @@ object MetaScreenSettingsRepository {
items = preferences.values.sortedBy { it.order },
cinematicBackground = cinematicBackground,
tabLayout = tabLayout,
episodeCardStyle = MetaEpisodeCardStyle.persist(episodeCardStyle),
),
),
)

View file

@ -19,6 +19,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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
@ -44,6 +45,7 @@ import androidx.compose.runtime.setValue
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.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
@ -58,6 +60,7 @@ import com.nuvio.app.core.format.formatReleaseDateForDisplay
import com.nuvio.app.core.ui.NuvioAnimatedWatchedBadge
import com.nuvio.app.core.ui.NuvioProgressBar
import com.nuvio.app.features.details.MetaDetails
import com.nuvio.app.features.details.MetaEpisodeCardStyle
import com.nuvio.app.features.details.MetaVideo
import com.nuvio.app.features.details.SeasonViewMode
import com.nuvio.app.features.details.SeasonViewModeStorage
@ -76,6 +79,7 @@ fun DetailSeriesContent(
modifier: Modifier = Modifier,
showHeader: Boolean = true,
preferredSeasonNumber: Int? = null,
episodeCardStyle: MetaEpisodeCardStyle = MetaEpisodeCardStyle.Horizontal,
progressByVideoId: Map<String, WatchProgressEntry> = emptyMap(),
watchedKeys: Set<String> = emptySet(),
onEpisodeClick: ((MetaVideo) -> Unit)? = null,
@ -155,6 +159,7 @@ fun DetailSeriesContent(
BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
val sizing = seriesContentSizing(maxWidth.value)
val containerWidthDp = maxWidth.value
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
@ -253,31 +258,46 @@ fun DetailSeriesContent(
DetailSectionTitle(
title = sectionTitle,
)
Column(
verticalArrangement = Arrangement.spacedBy(sizing.cardGap),
) {
groupedEpisodes.getValue(seasonForContent).forEach { episode ->
val episodeVideoId = buildPlaybackVideoId(
parentMetaId = meta.id,
seasonNumber = episode.season,
episodeNumber = episode.episode,
fallbackVideoId = episode.id,
)
EpisodeCard(
video = episode,
fallbackImage = meta.background ?: meta.poster,
progressEntry = progressByVideoId[episodeVideoId],
isWatched = progressByVideoId[episodeVideoId]?.isCompleted == true ||
WatchingState.isEpisodeWatched(
watchedKeys = watchedKeys,
metaType = meta.type,
metaId = meta.id,
episode = episode,
),
sizing = sizing,
onClick = { onEpisodeClick?.invoke(episode) },
onLongPress = { onEpisodeLongPress?.invoke(episode) },
)
val seasonEpisodes = groupedEpisodes.getValue(seasonForContent)
if (episodeCardStyle == MetaEpisodeCardStyle.Horizontal) {
EpisodeHorizontalRow(
episodes = seasonEpisodes,
maxWidthDp = containerWidthDp,
parentMetaId = meta.id,
metaType = meta.type,
watchedKeys = watchedKeys,
fallbackImage = meta.background ?: meta.poster,
progressByVideoId = progressByVideoId,
onEpisodeClick = onEpisodeClick,
onEpisodeLongPress = onEpisodeLongPress,
)
} else {
Column(
verticalArrangement = Arrangement.spacedBy(sizing.cardGap),
) {
seasonEpisodes.forEach { episode ->
val episodeVideoId = buildPlaybackVideoId(
parentMetaId = meta.id,
seasonNumber = episode.season,
episodeNumber = episode.episode,
fallbackVideoId = episode.id,
)
EpisodeListCard(
video = episode,
fallbackImage = meta.background ?: meta.poster,
progressEntry = progressByVideoId[episodeVideoId],
isWatched = progressByVideoId[episodeVideoId]?.isCompleted == true ||
WatchingState.isEpisodeWatched(
watchedKeys = watchedKeys,
metaType = meta.type,
metaId = meta.id,
episode = episode,
),
sizing = sizing,
onClick = { onEpisodeClick?.invoke(episode) },
onLongPress = { onEpisodeLongPress?.invoke(episode) },
)
}
}
}
}
@ -512,7 +532,330 @@ private fun SeasonPosterButton(
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun EpisodeCard(
private fun EpisodeHorizontalRow(
episodes: List<MetaVideo>,
maxWidthDp: Float,
parentMetaId: String,
metaType: String,
watchedKeys: Set<String>,
fallbackImage: String?,
progressByVideoId: Map<String, WatchProgressEntry>,
onEpisodeClick: ((MetaVideo) -> Unit)?,
onEpisodeLongPress: ((MetaVideo) -> Unit)?,
) {
val rowMetrics = rememberEpisodeHorizontalCardMetrics(maxWidthDp)
LazyRow(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = rowMetrics.rowHorizontalPadding, vertical = rowMetrics.rowVerticalPadding),
horizontalArrangement = Arrangement.spacedBy(rowMetrics.itemSpacing),
) {
items(episodes, key = { it.id }) { episode ->
val episodeVideoId = buildPlaybackVideoId(
parentMetaId = parentMetaId,
seasonNumber = episode.season,
episodeNumber = episode.episode,
fallbackVideoId = episode.id,
)
EpisodeHorizontalCard(
video = episode,
fallbackImage = fallbackImage,
progressEntry = progressByVideoId[episodeVideoId],
isWatched = progressByVideoId[episodeVideoId]?.isCompleted == true ||
WatchingState.isEpisodeWatched(
watchedKeys = watchedKeys,
metaType = metaType,
metaId = parentMetaId,
episode = episode,
),
metrics = rowMetrics,
onClick = { onEpisodeClick?.invoke(episode) },
onLongPress = { onEpisodeLongPress?.invoke(episode) },
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun EpisodeHorizontalCard(
video: MetaVideo,
fallbackImage: String?,
progressEntry: WatchProgressEntry?,
isWatched: Boolean,
metrics: EpisodeHorizontalCardMetrics,
onClick: (() -> Unit)? = null,
onLongPress: (() -> Unit)? = null,
) {
val cardShape = RoundedCornerShape(metrics.cornerRadius)
Box(
modifier = Modifier
.width(metrics.cardWidth)
.height(metrics.cardHeight)
.clip(cardShape)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f))
.border(
width = 1.dp,
color = Color.White.copy(alpha = 0.12f),
shape = cardShape,
)
.combinedClickable(
enabled = onClick != null || onLongPress != null,
onClick = { onClick?.invoke() },
onLongClick = onLongPress,
),
) {
val imageUrl = video.thumbnail ?: fallbackImage
if (imageUrl != null) {
AsyncImage(
model = imageUrl,
contentDescription = video.title,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
)
}
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.10f),
Color.Black.copy(alpha = 0.42f),
Color.Black.copy(alpha = 0.78f),
),
),
),
)
Box(
modifier = Modifier
.align(Alignment.TopStart)
.padding(start = metrics.contentPadding, top = metrics.contentPadding)
.clip(RoundedCornerShape(metrics.badgeRadius))
.background(Color.Black.copy(alpha = 0.75f))
.border(
width = 1.dp,
color = Color.White.copy(alpha = 0.18f),
shape = RoundedCornerShape(metrics.badgeRadius),
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text(
text = video.episodeBadge(),
style = MaterialTheme.typography.labelMedium.copy(
fontSize = metrics.badgeTextSize,
fontWeight = FontWeight.SemiBold,
letterSpacing = 0.5.sp,
),
color = Color.White,
)
}
NuvioAnimatedWatchedBadge(
isVisible = isWatched,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(metrics.contentPadding),
)
Column(
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth()
.padding(
start = metrics.contentPadding,
end = metrics.contentPadding,
top = metrics.contentPadding,
bottom = metrics.contentBottomPadding,
),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(
text = video.title,
style = MaterialTheme.typography.titleMedium.copy(
fontSize = metrics.titleTextSize,
fontWeight = FontWeight.ExtraBold,
lineHeight = metrics.titleLineHeight,
),
color = Color.White,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
if (!video.overview.isNullOrBlank()) {
Text(
text = video.overview,
style = MaterialTheme.typography.bodySmall.copy(
fontSize = metrics.bodyTextSize,
lineHeight = metrics.bodyLineHeight,
),
color = Color.White.copy(alpha = 0.86f),
maxLines = metrics.overviewMaxLines,
overflow = TextOverflow.Ellipsis,
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
video.runtime?.takeIf { it > 0 }?.let { runtimeMinutes ->
Text(
text = formatEpisodeRuntime(runtimeMinutes),
style = MaterialTheme.typography.labelSmall.copy(fontSize = metrics.metaTextSize),
color = Color.White.copy(alpha = 0.78f),
maxLines = 1,
)
}
video.released?.let { formatReleaseDateForDisplay(it) }?.let { formattedDate ->
Text(
text = formattedDate,
style = MaterialTheme.typography.labelSmall.copy(fontSize = metrics.metaTextSize),
color = Color.White.copy(alpha = 0.78f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
progressEntry
?.takeIf { it.durationMs > 0L && !it.isCompleted }
?.let { entry ->
NuvioProgressBar(
progress = entry.progressFraction,
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth()
.padding(horizontal = metrics.contentPadding, vertical = 8.dp),
height = 4.dp,
trackColor = Color.White.copy(alpha = 0.22f),
fillColor = MaterialTheme.colorScheme.primary,
)
}
}
}
private data class EpisodeHorizontalCardMetrics(
val rowHorizontalPadding: Dp,
val rowVerticalPadding: Dp,
val itemSpacing: Dp,
val cardWidth: Dp,
val cardHeight: Dp,
val cornerRadius: Dp,
val contentPadding: Dp,
val contentBottomPadding: Dp,
val titleTextSize: androidx.compose.ui.unit.TextUnit,
val titleLineHeight: androidx.compose.ui.unit.TextUnit,
val bodyTextSize: androidx.compose.ui.unit.TextUnit,
val bodyLineHeight: androidx.compose.ui.unit.TextUnit,
val overviewMaxLines: Int,
val metaTextSize: androidx.compose.ui.unit.TextUnit,
val badgeTextSize: androidx.compose.ui.unit.TextUnit,
val badgeRadius: Dp,
)
@Composable
private fun rememberEpisodeHorizontalCardMetrics(maxWidthDp: Float): EpisodeHorizontalCardMetrics {
return remember(maxWidthDp) {
when {
maxWidthDp >= 1300f -> EpisodeHorizontalCardMetrics(
rowHorizontalPadding = 0.dp,
rowVerticalPadding = 0.dp,
itemSpacing = 18.dp,
cardWidth = 420.dp,
cardHeight = 256.dp,
cornerRadius = 18.dp,
contentPadding = 16.dp,
contentBottomPadding = 18.dp,
titleTextSize = 18.sp,
titleLineHeight = 24.sp,
bodyTextSize = 14.sp,
bodyLineHeight = 20.sp,
overviewMaxLines = 3,
metaTextSize = 12.sp,
badgeTextSize = 11.sp,
badgeRadius = 6.dp,
)
maxWidthDp >= 1000f -> EpisodeHorizontalCardMetrics(
rowHorizontalPadding = 0.dp,
rowVerticalPadding = 0.dp,
itemSpacing = 16.dp,
cardWidth = 384.dp,
cardHeight = 236.dp,
cornerRadius = 16.dp,
contentPadding = 14.dp,
contentBottomPadding = 16.dp,
titleTextSize = 17.sp,
titleLineHeight = 22.sp,
bodyTextSize = 13.sp,
bodyLineHeight = 18.sp,
overviewMaxLines = 3,
metaTextSize = 12.sp,
badgeTextSize = 10.sp,
badgeRadius = 6.dp,
)
maxWidthDp >= 760f -> EpisodeHorizontalCardMetrics(
rowHorizontalPadding = 0.dp,
rowVerticalPadding = 0.dp,
itemSpacing = 14.dp,
cardWidth = 340.dp,
cardHeight = 212.dp,
cornerRadius = 14.dp,
contentPadding = 12.dp,
contentBottomPadding = 14.dp,
titleTextSize = 16.sp,
titleLineHeight = 21.sp,
bodyTextSize = 12.sp,
bodyLineHeight = 17.sp,
overviewMaxLines = 2,
metaTextSize = 11.sp,
badgeTextSize = 10.sp,
badgeRadius = 5.dp,
)
else -> EpisodeHorizontalCardMetrics(
rowHorizontalPadding = 0.dp,
rowVerticalPadding = 0.dp,
itemSpacing = 12.dp,
cardWidth = 296.dp,
cardHeight = 184.dp,
cornerRadius = 14.dp,
contentPadding = 10.dp,
contentBottomPadding = 12.dp,
titleTextSize = 14.sp,
titleLineHeight = 19.sp,
bodyTextSize = 11.sp,
bodyLineHeight = 15.sp,
overviewMaxLines = 2,
metaTextSize = 10.sp,
badgeTextSize = 9.sp,
badgeRadius = 5.dp,
)
}
}
}
private fun formatEpisodeRuntime(runtimeMinutes: Int): String {
if (runtimeMinutes <= 0) return ""
val hours = runtimeMinutes / 60
val minutes = runtimeMinutes % 60
return when {
hours > 0 && minutes > 0 -> "${hours}h ${minutes}m"
hours > 0 -> "${hours}h"
else -> "${minutes}m"
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun EpisodeListCard(
video: MetaVideo,
fallbackImage: String?,
progressEntry: WatchProgressEntry?,

View file

@ -4,7 +4,9 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -13,9 +15,11 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
@ -44,6 +48,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.nuvio.app.core.ui.NuvioActionLabel
import com.nuvio.app.features.details.MetaEpisodeCardStyle
import com.nuvio.app.features.details.MetaScreenSectionItem
import com.nuvio.app.features.details.MetaScreenSettingsRepository
import com.nuvio.app.features.details.MetaScreenSettingsUiState
@ -76,6 +81,12 @@ internal fun LazyListScope.metaScreenSettingsContent(
isTablet = isTablet,
onCheckedChange = { MetaScreenSettingsRepository.setTabLayout(it) },
)
SettingsGroupDivider(isTablet = isTablet)
MetaEpisodeCardStyleSelector(
isTablet = isTablet,
selectedStyle = uiState.episodeCardStyle,
onStyleSelected = MetaScreenSettingsRepository::setEpisodeCardStyle,
)
}
}
}
@ -308,4 +319,215 @@ private fun TabGroupChip(
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
)
}
@Composable
private fun MetaEpisodeCardStyleSelector(
isTablet: Boolean,
selectedStyle: MetaEpisodeCardStyle,
onStyleSelected: (MetaEpisodeCardStyle) -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = if (isTablet) 20.dp else 16.dp, vertical = if (isTablet) 18.dp else 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = "Episode Cards",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
)
Text(
text = "Choose how episodes are rendered on the metadata screen.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
MetaEpisodeCardStyle.entries.forEach { style ->
Box(modifier = Modifier.weight(1f)) {
MetaEpisodeCardStyleOption(
style = style,
selected = selectedStyle == style,
isTablet = isTablet,
onClick = { onStyleSelected(style) },
)
}
}
}
}
}
@Composable
private fun MetaEpisodeCardStyleOption(
style: MetaEpisodeCardStyle,
selected: Boolean,
isTablet: Boolean,
onClick: () -> Unit,
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
color = if (selected) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.08f)
} else {
MaterialTheme.colorScheme.surface
},
shape = RoundedCornerShape(12.dp),
border = BorderStroke(
1.dp,
if (selected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.outlineVariant,
),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(148.dp),
contentAlignment = Alignment.Center,
) {
MetaEpisodeCardStylePreview(
style = style,
isSelected = selected,
)
}
Text(
text = if (style == MetaEpisodeCardStyle.Horizontal) "Horizontal" else "List",
style = MaterialTheme.typography.bodyMedium,
color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
)
Text(
text = if (style == MetaEpisodeCardStyle.Horizontal) {
"Backdrop-style row cards"
} else {
"Detail-first stacked cards"
},
style = if (isTablet) MaterialTheme.typography.bodySmall else MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
@Composable
private fun MetaEpisodeCardStylePreview(
style: MetaEpisodeCardStyle,
isSelected: Boolean,
) {
val borderColor = if (isSelected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f)
}
val backgroundColor = if (isSelected) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.10f)
} else {
MaterialTheme.colorScheme.surface
}
Column(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(backgroundColor)
.border(1.dp, borderColor, RoundedCornerShape(12.dp))
.padding(horizontal = 12.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
when (style) {
MetaEpisodeCardStyle.Horizontal -> {
Box(
modifier = Modifier
.width(128.dp)
.height(80.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.85f)),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(26.dp)
.align(Alignment.BottomCenter)
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.36f)),
)
Box(
modifier = Modifier
.width(42.dp)
.height(7.dp)
.align(Alignment.TopStart)
.padding(start = 6.dp, top = 6.dp)
.clip(RoundedCornerShape(3.dp))
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.26f)),
)
Box(
modifier = Modifier
.width(72.dp)
.height(6.dp)
.align(Alignment.BottomStart)
.padding(start = 8.dp, bottom = 8.dp)
.clip(RoundedCornerShape(3.dp))
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.18f)),
)
}
}
MetaEpisodeCardStyle.List -> {
Row(
modifier = Modifier
.width(132.dp)
.height(78.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.78f)),
) {
Box(
modifier = Modifier
.width(48.dp)
.height(78.dp)
.background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.65f)),
)
Column(
modifier = Modifier
.weight(1f)
.padding(6.dp),
verticalArrangement = Arrangement.spacedBy(5.dp),
) {
Box(
modifier = Modifier
.fillMaxWidth(0.82f)
.height(8.dp)
.clip(RoundedCornerShape(3.dp))
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.20f)),
)
Box(
modifier = Modifier
.fillMaxWidth(0.52f)
.height(6.dp)
.clip(RoundedCornerShape(3.dp))
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.16f)),
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.clip(RoundedCornerShape(2.dp))
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.55f)),
)
}
}
}
}
}
}