feat: adding tv style cw card

This commit is contained in:
tapframe 2026-05-29 02:25:35 +05:30
parent 32f99fc252
commit e89d7c5406
6 changed files with 465 additions and 26 deletions

View file

@ -586,6 +586,8 @@
<string name="settings_continue_watching_section_visibility">VISIBILITY</string>
<string name="settings_continue_watching_show_description">Display the Continue Watching shelf on the Home screen.</string>
<string name="settings_continue_watching_show_title">Show Continue Watching</string>
<string name="settings_continue_watching_style_card">Card</string>
<string name="settings_continue_watching_style_card_description">TV-style landscape card</string>
<string name="settings_continue_watching_style_poster">Poster</string>
<string name="settings_continue_watching_style_poster_description">Artwork-first poster card</string>
<string name="settings_continue_watching_style_wide">Wide</string>
@ -1107,6 +1109,8 @@
<string name="episode_mark_watched">Mark as watched</string>
<string name="home_continue_watching_up_next">Up next</string>
<string name="home_continue_watching_watched">%1$s watched</string>
<string name="home_continue_watching_hours_minutes_left">%1$dh %2$dm left</string>
<string name="home_continue_watching_minutes_left">%1$dm left</string>
<string name="home_empty_no_active_addons_message">Install and validate at least one addon before loading catalog rows on Home.</string>
<string name="home_empty_no_rows_message">Installed addons do not currently expose board-compatible catalogs without required extras.</string>
<string name="home_empty_no_rows_title">No home rows available</string>

View file

@ -20,6 +20,7 @@ import com.nuvio.app.core.ui.LocalNuvioBottomNavigationOverlayPadding
import com.nuvio.app.core.ui.NuvioScreen
import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.addons.enabledAddons
import com.nuvio.app.features.cloud.CloudLibraryContentType
@ -79,6 +80,7 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import com.nuvio.app.features.home.components.ContinueWatchingLayout
import com.nuvio.app.features.home.components.continueWatchingLandscapeCardHeight
import com.nuvio.app.features.home.components.homeSectionHorizontalPaddingForWidth
import com.nuvio.app.features.home.components.rememberContinueWatchingLayout
import kotlinx.coroutines.CancellationException
@ -585,6 +587,10 @@ fun HomeScreen(
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
val homeSectionPadding = homeSectionHorizontalPaddingForWidth(maxWidth.value)
val continueWatchingLayout = rememberContinueWatchingLayout(maxWidth.value)
val posterCardStyle = rememberPosterCardStyleUiState()
val continueWatchingCardHeight = remember(posterCardStyle.widthDp) {
continueWatchingLandscapeCardHeight(posterCardStyle.widthDp)
}
val nativeBottomNavigationOverlayHeight =
if (LocalNuvioBottomNavigationOverlayPadding.current > 0.dp) {
nuvioSafeBottomPadding()
@ -597,6 +603,7 @@ fun HomeScreen(
continueWatchingPreferences.style,
continueWatchingItems.isNotEmpty(),
continueWatchingLayout,
continueWatchingCardHeight,
nativeBottomNavigationOverlayHeight,
) {
heroMobileBelowSectionHeightHint(
@ -605,6 +612,7 @@ fun HomeScreen(
hasContinueWatchingItems = continueWatchingItems.isNotEmpty(),
continueWatchingStyle = continueWatchingPreferences.style,
continueWatchingLayout = continueWatchingLayout,
continueWatchingCardHeight = continueWatchingCardHeight,
bottomNavigationOverlayHeight = nativeBottomNavigationOverlayHeight,
)
}
@ -967,11 +975,13 @@ private fun heroMobileBelowSectionHeightHint(
hasContinueWatchingItems: Boolean,
continueWatchingStyle: ContinueWatchingSectionStyle,
continueWatchingLayout: ContinueWatchingLayout,
continueWatchingCardHeight: Dp,
bottomNavigationOverlayHeight: Dp,
): Dp? {
if (maxWidthDp >= 600f || !continueWatchingVisible || !hasContinueWatchingItems) return null
val sectionHeight = when (continueWatchingStyle) {
ContinueWatchingSectionStyle.Card -> continueWatchingCardHeight + 56.dp
ContinueWatchingSectionStyle.Wide -> continueWatchingLayout.wideCardHeight + 56.dp
ContinueWatchingSectionStyle.Poster ->
continueWatchingLayout.posterCardHeight + continueWatchingLayout.posterTitleBlockHeight + 70.dp

View file

@ -1,5 +1,6 @@
package com.nuvio.app.features.home.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -32,19 +33,27 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
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
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.nuvio.app.core.ui.NuvioProgressBar
import com.nuvio.app.core.ui.NuvioShelfSection
import com.nuvio.app.core.ui.PosterLandscapeAspectRatio
import com.nuvio.app.core.ui.landscapePosterHeightForWidth
import com.nuvio.app.core.ui.landscapePosterWidth
import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.features.cloud.CloudLibraryContentType
import com.nuvio.app.features.cloud.cloudLibraryDisplayArtworkUrl
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
@ -56,6 +65,17 @@ import kotlin.math.roundToInt
import nuvio.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
private val ContinueWatchingStatusBadgeShape = RoundedCornerShape(4.dp)
private val ContinueWatchingNewEpisodeBadgeColor = Color(0xFF1D4ED8)
private val ContinueWatchingNewSeasonBadgeColor = Color(0xFFB45309)
private const val ContinueWatchingLandscapeCardScale = 1.2f
internal fun continueWatchingLandscapeCardWidth(basePosterWidthDp: Int): Dp =
(landscapePosterWidth(basePosterWidthDp).value * ContinueWatchingLandscapeCardScale).dp
internal fun continueWatchingLandscapeCardHeight(basePosterWidthDp: Int): Dp =
landscapePosterHeightForWidth(continueWatchingLandscapeCardWidth(basePosterWidthDp))
private fun continueWatchingProgressPercent(progressFraction: Float): Int =
(progressFraction * 100f).roundToInt().coerceIn(1, 99)
@ -123,6 +143,42 @@ private fun ContinueWatchingItem.continueWatchingPosterArtworkUrl(
)
}
private fun ContinueWatchingItem.continueWatchingCardArtworkUrl(
useEpisodeThumbnails: Boolean,
preferBackdropForNextUp: Boolean,
): String? = when {
isNextUp && preferBackdropForNextUp -> firstNonBlank(
background,
poster,
episodeThumbnail,
imageUrl,
)
isNextUp && useEpisodeThumbnails -> firstNonBlank(
episodeThumbnail,
background,
poster,
imageUrl,
)
isNextUp -> firstNonBlank(
background,
poster,
episodeThumbnail,
imageUrl,
)
useEpisodeThumbnails -> firstNonBlank(
episodeThumbnail,
background,
poster,
imageUrl,
)
else -> firstNonBlank(
background,
poster,
episodeThumbnail,
imageUrl,
)
}
private fun firstNonBlank(vararg values: String?): String? =
values.firstOrNull { value -> !value.isNullOrBlank() }?.trim()
@ -202,6 +258,13 @@ private fun HomeContinueWatchingSectionContent(
key = { item -> item.videoId },
) { item ->
when (style) {
ContinueWatchingSectionStyle.Card -> ContinueWatchingCard(
item = item,
useEpisodeThumbnails = useEpisodeThumbnails,
blurNextUp = blurNextUp,
onClick = onItemClick?.let { { it(item) } },
onLongClick = onItemLongPress?.let { { it(item) } },
)
ContinueWatchingSectionStyle.Wide -> ContinueWatchingWideCard(
item = item,
layout = layout,
@ -246,33 +309,108 @@ fun ContinueWatchingStylePreview(
modifier: Modifier = Modifier,
isSelected: Boolean = false,
) {
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
}
val previewBorder = if (style == ContinueWatchingSectionStyle.Wide) {
BorderStroke(
1.dp,
if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f),
)
} else {
null
}
Column(
modifier = modifier
.clip(RoundedCornerShape(12.dp))
.background(backgroundColor)
.border(1.dp, borderColor, RoundedCornerShape(12.dp))
.then(
if (previewBorder != null) {
Modifier.border(previewBorder, RoundedCornerShape(12.dp))
} else {
Modifier
},
)
.padding(horizontal = 12.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
when (style) {
ContinueWatchingSectionStyle.Card -> CardStylePreview()
ContinueWatchingSectionStyle.Wide -> WideCardPreview()
ContinueWatchingSectionStyle.Poster -> PosterCardPreview()
}
}
}
@Composable
private fun CardStylePreview() {
Box(
modifier = Modifier
.width(100.dp)
.height(56.dp)
.clip(RoundedCornerShape(6.dp))
.background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.65f)),
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colorStops = arrayOf(
0.0f to Color.Transparent,
0.60f to MaterialTheme.colorScheme.background.copy(alpha = 0.45f),
1.0f to MaterialTheme.colorScheme.background.copy(alpha = 0.90f),
),
),
),
)
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(5.dp)
.width(26.dp)
.height(9.dp)
.clip(RoundedCornerShape(2.dp))
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.80f)),
)
Column(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(7.dp),
verticalArrangement = Arrangement.spacedBy(3.dp),
) {
Box(
modifier = Modifier
.width(28.dp)
.height(5.dp)
.clip(RoundedCornerShape(2.dp))
.background(Color.White.copy(alpha = 0.55f)),
)
Box(
modifier = Modifier
.width(58.dp)
.height(7.dp)
.clip(RoundedCornerShape(2.dp))
.background(Color.White.copy(alpha = 0.75f)),
)
}
NuvioProgressBar(
progress = 0.55f,
modifier = Modifier
.align(Alignment.BottomStart)
.padding(horizontal = 5.dp, vertical = 3.dp)
.fillMaxWidth(),
height = 3.dp,
trackColor = Color.Black.copy(alpha = 0.30f),
)
}
}
@Composable
private fun WideCardPreview() {
Row(
@ -385,6 +523,280 @@ private fun PosterCardPreview() {
}
}
private data class ContinueWatchingLandscapeCardMetrics(
val width: Dp,
val cornerRadius: Dp,
val contentPadding: Dp,
val textGap: Dp,
val badgeInset: Dp,
val badgeHorizontalPadding: Dp,
val badgeVerticalPadding: Dp,
val progressHorizontalPadding: Dp,
val progressBottomPadding: Dp,
val progressHeight: Dp,
val titleTextSize: TextUnit,
val metaTextSize: TextUnit,
val badgeTextSize: TextUnit,
)
private fun continueWatchingLandscapeCardMetrics(
basePosterWidthDp: Int,
cornerRadiusDp: Int,
): ContinueWatchingLandscapeCardMetrics {
val width = continueWatchingLandscapeCardWidth(basePosterWidthDp)
return when {
basePosterWidthDp <= 108 -> ContinueWatchingLandscapeCardMetrics(
width = width,
cornerRadius = cornerRadiusDp.dp,
contentPadding = 8.dp,
textGap = 1.dp,
badgeInset = 6.dp,
badgeHorizontalPadding = 6.dp,
badgeVerticalPadding = 2.dp,
progressHorizontalPadding = 8.dp,
progressBottomPadding = 3.dp,
progressHeight = 3.dp,
titleTextSize = 12.sp,
metaTextSize = 9.sp,
badgeTextSize = 8.sp,
)
basePosterWidthDp <= 120 -> ContinueWatchingLandscapeCardMetrics(
width = width,
cornerRadius = cornerRadiusDp.dp,
contentPadding = 9.dp,
textGap = 1.dp,
badgeInset = 6.dp,
badgeHorizontalPadding = 7.dp,
badgeVerticalPadding = 3.dp,
progressHorizontalPadding = 8.dp,
progressBottomPadding = 3.dp,
progressHeight = 3.dp,
titleTextSize = 13.sp,
metaTextSize = 10.sp,
badgeTextSize = 9.sp,
)
else -> ContinueWatchingLandscapeCardMetrics(
width = width,
cornerRadius = cornerRadiusDp.dp,
contentPadding = 10.dp,
textGap = 2.dp,
badgeInset = 7.dp,
badgeHorizontalPadding = 7.dp,
badgeVerticalPadding = 3.dp,
progressHorizontalPadding = 9.dp,
progressBottomPadding = 4.dp,
progressHeight = 3.dp,
titleTextSize = 14.sp,
metaTextSize = 10.sp,
badgeTextSize = 10.sp,
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ContinueWatchingCard(
item: ContinueWatchingItem,
useEpisodeThumbnails: Boolean,
blurNextUp: Boolean,
onClick: (() -> Unit)?,
onLongClick: (() -> Unit)?,
) {
val posterCardStyle = rememberPosterCardStyleUiState()
val cardMetrics = remember(posterCardStyle.widthDp, posterCardStyle.cornerRadiusDp) {
continueWatchingLandscapeCardMetrics(
basePosterWidthDp = posterCardStyle.widthDp,
cornerRadiusDp = posterCardStyle.cornerRadiusDp,
)
}
val todayIsoDate = CurrentDateProvider.todayIsoDate()
val compactAirDateText = if (item.progressFraction <= 0f && item.seasonNumber != null && item.episodeNumber != null) {
computeAirDateBadgeText(item.released, todayIsoDate, compact = true)
} else {
null
}
val preferBackdropForNextUp = item.isNextUp && compactAirDateText != null && !item.isReleaseAlert
val imageUrl = item.continueWatchingCardArtworkUrl(
useEpisodeThumbnails = useEpisodeThumbnails,
preferBackdropForNextUp = preferBackdropForNextUp,
)
val shouldBlurArtwork = blurNextUp && useEpisodeThumbnails && item.isNextUp
val episodeCode = if (item.seasonNumber != null && item.episodeNumber != null) {
stringResource(Res.string.streams_episode_badge, item.seasonNumber, item.episodeNumber)
} else {
null
}
val episodeTitle = item.episodeTitle?.trim()?.takeIf { it.isNotBlank() } ?: compactAirDateText
val badgeText = continueWatchingCardBadgeText(item = item, compactAirDateText = compactAirDateText)
val backgroundColor = MaterialTheme.colorScheme.background
val badgeBackground = when {
item.isNewSeasonRelease -> ContinueWatchingNewSeasonBadgeColor
item.isReleaseAlert -> ContinueWatchingNewEpisodeBadgeColor
else -> backgroundColor.copy(alpha = 0.80f)
}
Box(
modifier = Modifier
.width(cardMetrics.width)
.aspectRatio(PosterLandscapeAspectRatio)
.clip(RoundedCornerShape(cardMetrics.cornerRadius))
.background(MaterialTheme.colorScheme.surfaceVariant)
.posterCardClickable(onClick = onClick, onLongClick = onLongClick),
) {
if (imageUrl != null) {
AsyncImage(
model = cloudLibraryDisplayArtworkUrl(imageUrl),
contentDescription = item.title,
modifier = Modifier
.fillMaxSize()
.then(if (shouldBlurArtwork) Modifier.blur(18.dp) else Modifier)
.drawWithContent {
drawContent()
val startY = size.height * 0.45f
val gradient = Brush.verticalGradient(
colorStops = arrayOf(
0.0f to Color.Transparent,
0.60f to backgroundColor.copy(alpha = 0.70f),
1.0f to backgroundColor.copy(alpha = 0.95f),
),
startY = startY,
endY = size.height,
)
drawRect(
brush = gradient,
topLeft = Offset(-2f, startY),
size = Size(size.width + 4f, (size.height - startY) + 4f),
)
},
contentScale = ContentScale.Crop,
)
}
if (!posterCardStyle.hideLabelsEnabled) {
Column(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(cardMetrics.contentPadding),
verticalArrangement = Arrangement.spacedBy(cardMetrics.textGap),
) {
if (episodeCode != null) {
Text(
text = episodeCode,
style = MaterialTheme.typography.labelMedium.copy(
fontSize = cardMetrics.metaTextSize,
fontWeight = FontWeight.SemiBold,
),
color = MaterialTheme.colorScheme.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Text(
text = item.title,
style = MaterialTheme.typography.titleSmall.copy(
fontSize = cardMetrics.titleTextSize,
fontWeight = FontWeight.SemiBold,
),
color = MaterialTheme.colorScheme.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (episodeTitle != null) {
Text(
text = episodeTitle,
style = MaterialTheme.typography.bodySmall.copy(
fontSize = cardMetrics.metaTextSize,
fontWeight = FontWeight.Medium,
),
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.72f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(cardMetrics.badgeInset)
.clip(ContinueWatchingStatusBadgeShape)
.background(badgeBackground)
.padding(
horizontal = cardMetrics.badgeHorizontalPadding,
vertical = cardMetrics.badgeVerticalPadding,
),
) {
Text(
text = badgeText,
style = MaterialTheme.typography.labelSmall.copy(
fontSize = cardMetrics.badgeTextSize,
fontWeight = FontWeight.SemiBold,
),
color = MaterialTheme.colorScheme.onBackground,
maxLines = 1,
)
}
if (item.progressFraction > 0f) {
Box(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(
horizontal = cardMetrics.progressHorizontalPadding,
vertical = cardMetrics.progressBottomPadding,
)
.fillMaxWidth()
.clip(RoundedCornerShape(999.dp))
.height(cardMetrics.progressHeight)
.background(Color.Black.copy(alpha = 0.30f)),
) {
Box(
modifier = Modifier
.fillMaxWidth(item.progressFraction.coerceIn(0f, 1f))
.fillMaxHeight()
.clip(RoundedCornerShape(999.dp))
.background(MaterialTheme.colorScheme.primary),
)
}
}
}
}
@Composable
private fun continueWatchingCardBadgeText(
item: ContinueWatchingItem,
compactAirDateText: String?,
): String {
if (item.progressFraction > 0f) {
if (item.durationMs <= 0L) {
return stringResource(
Res.string.home_continue_watching_watched,
"${continueWatchingProgressPercent(item.progressFraction)}%",
)
}
val effectivePositionMs = when {
item.resumePositionMs > 0L -> item.resumePositionMs
else -> (item.durationMs * item.progressFraction.coerceIn(0f, 1f)).toLong()
}
val remainingMinutes = ((item.durationMs - effectivePositionMs).coerceAtLeast(0L) / 60_000L)
.coerceAtLeast(1L)
val hours = remainingMinutes / 60L
val minutes = remainingMinutes % 60L
return if (hours > 0L) {
stringResource(Res.string.home_continue_watching_hours_minutes_left, hours, minutes)
} else {
stringResource(Res.string.home_continue_watching_minutes_left, remainingMinutes)
}
}
return when {
item.isReleaseAlert && item.isNewSeasonRelease -> stringResource(Res.string.cw_new_season)
item.isReleaseAlert -> stringResource(Res.string.cw_new_episode)
compactAirDateText != null -> compactAirDateText
else -> stringResource(Res.string.home_continue_watching_up_next)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ContinueWatchingWideCard(
@ -532,11 +944,6 @@ private fun ContinueWatchingPosterCard(
.height(layout.posterCardHeight)
.clip(RoundedCornerShape(layout.cardRadius))
.background(MaterialTheme.colorScheme.surfaceVariant)
.border(
width = 1.5.dp,
color = Color.White.copy(alpha = 0.15f),
shape = RoundedCornerShape(layout.cardRadius),
)
.posterCardClickable(onClick = onClick, onLongClick = onLongClick),
) {
val imageUrl = item.continueWatchingPosterArtworkUrl(useEpisodeThumbnails)

View file

@ -30,6 +30,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.nuvio.app.features.home.components.ContinueWatchingStylePreview
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
@ -54,6 +55,8 @@ import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode
import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode_streaming
import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode_streaming_desc
import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode_title
import nuvio.composeapp.generated.resources.settings_continue_watching_style_card
import nuvio.composeapp.generated.resources.settings_continue_watching_style_card_description
import nuvio.composeapp.generated.resources.settings_continue_watching_style_poster
import nuvio.composeapp.generated.resources.settings_continue_watching_style_poster_description
import nuvio.composeapp.generated.resources.settings_continue_watching_style_wide
@ -205,7 +208,7 @@ private fun ContinueWatchingStyleSelector(
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(if (isTablet) 12.dp else 8.dp),
) {
ContinueWatchingSectionStyle.entries.forEach { style ->
Box(modifier = Modifier.weight(1f)) {
@ -227,6 +230,16 @@ private fun ContinueWatchingStyleOption(
isTablet: Boolean,
onClick: () -> Unit,
) {
val optionBorder = if (style == ContinueWatchingSectionStyle.Wide) {
androidx.compose.foundation.BorderStroke(
1.dp,
if (selected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.outlineVariant,
)
} else {
null
}
Surface(
modifier = Modifier
.fillMaxWidth()
@ -237,36 +250,34 @@ private fun ContinueWatchingStyleOption(
MaterialTheme.colorScheme.surface
},
shape = RoundedCornerShape(12.dp),
border = androidx.compose.foundation.BorderStroke(
1.dp,
if (selected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.outlineVariant,
),
border = optionBorder,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 16.dp),
.padding(horizontal = if (isTablet) 12.dp else 8.dp, vertical = if (isTablet) 14.dp else 10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(if (isTablet) 8.dp else 6.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp),
.padding(bottom = if (isTablet) 4.dp else 0.dp),
horizontalArrangement = Arrangement.End,
) {
Icon(
imageVector = Icons.Rounded.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.alpha(if (selected) 1f else 0f),
modifier = Modifier
.size(if (isTablet) 24.dp else 18.dp)
.alpha(if (selected) 1f else 0f),
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(148.dp),
.height(if (isTablet) 96.dp else 66.dp),
contentAlignment = Alignment.Center,
) {
ContinueWatchingStylePreview(
@ -276,14 +287,18 @@ private fun ContinueWatchingStyleOption(
}
Text(
text = stringResource(style.labelRes),
style = MaterialTheme.typography.bodyMedium,
style = if (isTablet) MaterialTheme.typography.bodyMedium else MaterialTheme.typography.labelMedium,
color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = stringResource(style.descriptionRes),
style = if (isTablet) MaterialTheme.typography.bodySmall else MaterialTheme.typography.labelMedium,
style = if (isTablet) MaterialTheme.typography.bodySmall else MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
@ -291,12 +306,14 @@ private fun ContinueWatchingStyleOption(
private val ContinueWatchingSectionStyle.labelRes: StringResource
get() = when (this) {
ContinueWatchingSectionStyle.Card -> Res.string.settings_continue_watching_style_card
ContinueWatchingSectionStyle.Wide -> Res.string.settings_continue_watching_style_wide
ContinueWatchingSectionStyle.Poster -> Res.string.settings_continue_watching_style_poster
}
private val ContinueWatchingSectionStyle.descriptionRes: StringResource
get() = when (this) {
ContinueWatchingSectionStyle.Card -> Res.string.settings_continue_watching_style_card_description
ContinueWatchingSectionStyle.Wide -> Res.string.settings_continue_watching_style_wide_description
ContinueWatchingSectionStyle.Poster -> Res.string.settings_continue_watching_style_poster_description
}

View file

@ -12,7 +12,7 @@ import kotlinx.serialization.json.Json
@Serializable
private data class StoredContinueWatchingPreferences(
val isVisible: Boolean = true,
val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Wide,
val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Card,
val upNextFromFurthestEpisode: Boolean = true,
@SerialName("use_episode_thumbnails_in_cw")
val useEpisodeThumbnails: Boolean = true,

View file

@ -15,6 +15,7 @@ internal const val WatchProgressSourceTraktShowProgress = "trakt_show_progress"
@Serializable
enum class ContinueWatchingSectionStyle {
Card,
Wide,
Poster,
}
@ -179,7 +180,7 @@ data class ContinueWatchingItem(
data class ContinueWatchingPreferencesUiState(
val isVisible: Boolean = true,
val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Wide,
val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Card,
val upNextFromFurthestEpisode: Boolean = true,
val useEpisodeThumbnails: Boolean = true,
val showUnairedNextUp: Boolean = true,