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 2e81224c..ebe5c1c3 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 @@ -999,6 +999,7 @@ private fun ConfiguredMetaSections( meta = meta, showHeader = showHeader, preferredSeasonNumber = preferredEpisodeSeasonNumber, + episodeCardStyle = settings.episodeCardStyle, progressByVideoId = progressByVideoId, watchedKeys = watchedKeys, onEpisodeClick = onEpisodeClick, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaScreenSettingsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaScreenSettingsRepository.kt index ec313873..93660110 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaScreenSettingsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaScreenSettingsRepository.kt @@ -40,8 +40,28 @@ data class MetaScreenSettingsUiState( val items: List = 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 = 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, 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), ), ), ) 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 9d3ca3bf..a74a7ac6 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 @@ -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 = emptyMap(), watchedKeys: Set = 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, + maxWidthDp: Float, + parentMetaId: String, + metaType: String, + watchedKeys: Set, + fallbackImage: String?, + progressByVideoId: Map, + 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?, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/MetaScreenSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/MetaScreenSettingsPage.kt index 68884a0b..8885fa89 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/MetaScreenSettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/MetaScreenSettingsPage.kt @@ -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)), + ) + } + } + } + } + } } \ No newline at end of file