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,