From e89d7c5406bb62f6c8a2c7b38a0ac4352cf03ea2 Mon Sep 17 00:00:00 2001
From: tapframe <85391825+tapframe@users.noreply.github.com>
Date: Fri, 29 May 2026 02:25:35 +0530
Subject: [PATCH] feat: adding tv style cw card
---
.../composeResources/values/strings.xml | 4 +
.../com/nuvio/app/features/home/HomeScreen.kt | 10 +
.../components/HomeContinueWatchingSection.kt | 429 +++++++++++++++++-
.../settings/ContinueWatchingSettingsPage.kt | 43 +-
.../ContinueWatchingPreferencesRepository.kt | 2 +-
.../watchprogress/WatchProgressModels.kt | 3 +-
6 files changed, 465 insertions(+), 26 deletions(-)
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 319408a0..cc434a1c 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -586,6 +586,8 @@
VISIBILITY
Display the Continue Watching shelf on the Home screen.
Show Continue Watching
+ Card
+ TV-style landscape card
Poster
Artwork-first poster card
Wide
@@ -1107,6 +1109,8 @@
Mark as watched
Up next
%1$s watched
+ %1$dh %2$dm left
+ %1$dm left
Install and validate at least one addon before loading catalog rows on Home.
Installed addons do not currently expose board-compatible catalogs without required extras.
No home rows available
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt
index e7d59bf6..d06980b9 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt
@@ -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
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt
index 8f85e8ba..de8bf71e 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt
@@ -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)
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt
index f0483992..8004fa77 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt
@@ -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
}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt
index 93704067..dd227de8 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt
@@ -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,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt
index 013d1c44..29c41fa3 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt
@@ -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,