mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat: Add Continue Watching feature with customizable settings
- Introduced ContinueWatchingPreferencesStorage for managing user preferences. - Added ContinueWatchingPreferencesRepository to handle loading and saving preferences. - Created UI components for Continue Watching settings, including visibility toggle and style selection. - Implemented new settings pages for Appearance and Continue Watching. - Updated MainActivity to initialize ContinueWatchingPreferencesStorage. - Enhanced HomeScreen to display Continue Watching section based on user preferences. - Refactored NuvioShelfSection to allow customizable item spacing. - Added new styles for Continue Watching cards: Wide and Poster.
This commit is contained in:
parent
2e7da22c25
commit
bb00cde10a
14 changed files with 820 additions and 60 deletions
|
|
@ -7,6 +7,7 @@ import androidx.activity.enableEdgeToEdge
|
|||
import com.nuvio.app.features.addons.AddonStorage
|
||||
import com.nuvio.app.features.home.HomeCatalogSettingsStorage
|
||||
import com.nuvio.app.features.player.PlayerSettingsStorage
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage
|
||||
import com.nuvio.app.features.watchprogress.WatchProgressStorage
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
|
@ -16,6 +17,7 @@ class MainActivity : ComponentActivity() {
|
|||
AddonStorage.initialize(applicationContext)
|
||||
HomeCatalogSettingsStorage.initialize(applicationContext)
|
||||
PlayerSettingsStorage.initialize(applicationContext)
|
||||
ContinueWatchingPreferencesStorage.initialize(applicationContext)
|
||||
WatchProgressStorage.initialize(applicationContext)
|
||||
|
||||
setContent {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
actual object ContinueWatchingPreferencesStorage {
|
||||
private const val preferencesName = "nuvio_continue_watching_preferences"
|
||||
private const val payloadKey = "continue_watching_preferences_payload"
|
||||
|
||||
private var preferences: SharedPreferences? = null
|
||||
|
||||
fun initialize(context: Context) {
|
||||
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
preferences?.getString(payloadKey, null)
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.putString(payloadKey, payload)
|
||||
?.apply()
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ fun <T> NuvioShelfSection(
|
|||
modifier: Modifier = Modifier,
|
||||
headerHorizontalPadding: Dp = 0.dp,
|
||||
rowContentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
itemSpacing: Dp = 10.dp,
|
||||
onViewAllClick: (() -> Unit)? = null,
|
||||
viewAllPillSize: NuvioViewAllPillSize = NuvioViewAllPillSize.Default,
|
||||
key: ((T) -> Any)? = null,
|
||||
|
|
@ -66,7 +67,7 @@ fun <T> NuvioShelfSection(
|
|||
)
|
||||
LazyRow(
|
||||
contentPadding = rowContentPadding,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||
) {
|
||||
if (key != null) {
|
||||
items(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.nuvio.app.features.home.components.HomeContinueWatchingSection
|
|||
import com.nuvio.app.features.home.components.HomeEmptyStateCard
|
||||
import com.nuvio.app.features.home.components.HomeHeroSection
|
||||
import com.nuvio.app.features.home.components.HomeSkeletonRow
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingItem
|
||||
import com.nuvio.app.features.watchprogress.WatchProgressRepository
|
||||
import com.nuvio.app.features.watchprogress.toContinueWatchingItem
|
||||
|
|
@ -29,11 +30,13 @@ fun HomeScreen(
|
|||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
AddonRepository.initialize()
|
||||
ContinueWatchingPreferencesRepository.ensureLoaded()
|
||||
WatchProgressRepository.ensureLoaded()
|
||||
}
|
||||
|
||||
val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle()
|
||||
val homeUiState by HomeRepository.uiState.collectAsStateWithLifecycle()
|
||||
val continueWatchingPreferences by ContinueWatchingPreferencesRepository.uiState.collectAsStateWithLifecycle()
|
||||
val watchProgressUiState by WatchProgressRepository.uiState.collectAsStateWithLifecycle()
|
||||
val continueWatchingItems = remember(watchProgressUiState.entries) {
|
||||
watchProgressUiState.entries.take(20).map { it.toContinueWatchingItem() }
|
||||
|
|
@ -64,10 +67,11 @@ fun HomeScreen(
|
|||
) {
|
||||
when {
|
||||
addonsUiState.addons.none { it.manifest != null } -> {
|
||||
if (continueWatchingItems.isNotEmpty()) {
|
||||
if (continueWatchingPreferences.isVisible && continueWatchingItems.isNotEmpty()) {
|
||||
item {
|
||||
HomeContinueWatchingSection(
|
||||
items = continueWatchingItems,
|
||||
style = continueWatchingPreferences.style,
|
||||
modifier = Modifier.padding(bottom = 12.dp),
|
||||
onItemClick = onContinueWatchingClick,
|
||||
onItemLongPress = onContinueWatchingLongPress,
|
||||
|
|
@ -84,10 +88,11 @@ fun HomeScreen(
|
|||
}
|
||||
|
||||
homeUiState.isLoading && homeUiState.sections.isEmpty() -> {
|
||||
if (continueWatchingItems.isNotEmpty()) {
|
||||
if (continueWatchingPreferences.isVisible && continueWatchingItems.isNotEmpty()) {
|
||||
item {
|
||||
HomeContinueWatchingSection(
|
||||
items = continueWatchingItems,
|
||||
style = continueWatchingPreferences.style,
|
||||
modifier = Modifier.padding(bottom = 12.dp),
|
||||
onItemClick = onContinueWatchingClick,
|
||||
onItemLongPress = onContinueWatchingLongPress,
|
||||
|
|
@ -99,7 +104,8 @@ fun HomeScreen(
|
|||
}
|
||||
}
|
||||
|
||||
homeUiState.sections.isEmpty() && homeUiState.heroItems.isEmpty() && continueWatchingItems.isEmpty() -> {
|
||||
homeUiState.sections.isEmpty() && homeUiState.heroItems.isEmpty() &&
|
||||
(!continueWatchingPreferences.isVisible || continueWatchingItems.isEmpty()) -> {
|
||||
item {
|
||||
HomeEmptyStateCard(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
|
|
@ -120,10 +126,11 @@ fun HomeScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
if (continueWatchingItems.isNotEmpty()) {
|
||||
if (continueWatchingPreferences.isVisible && continueWatchingItems.isNotEmpty()) {
|
||||
item {
|
||||
HomeContinueWatchingSection(
|
||||
items = continueWatchingItems,
|
||||
style = continueWatchingPreferences.style,
|
||||
modifier = Modifier.padding(bottom = 12.dp),
|
||||
onItemClick = onContinueWatchingClick,
|
||||
onItemLongPress = onContinueWatchingLongPress,
|
||||
|
|
|
|||
|
|
@ -2,85 +2,303 @@ package com.nuvio.app.features.home.components
|
|||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioProgressBar
|
||||
import com.nuvio.app.core.ui.NuvioShelfSection
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingItem
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun HomeContinueWatchingSection(
|
||||
items: List<ContinueWatchingItem>,
|
||||
style: ContinueWatchingSectionStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
onItemClick: ((ContinueWatchingItem) -> Unit)? = null,
|
||||
onItemLongPress: ((ContinueWatchingItem) -> Unit)? = null,
|
||||
) {
|
||||
if (items.isEmpty()) return
|
||||
|
||||
NuvioShelfSection(
|
||||
title = "Continue Watching",
|
||||
entries = items,
|
||||
modifier = modifier,
|
||||
headerHorizontalPadding = 16.dp,
|
||||
rowContentPadding = PaddingValues(horizontal = 16.dp),
|
||||
key = { item -> item.videoId },
|
||||
) { item ->
|
||||
ContinueWatchingCard(
|
||||
item = item,
|
||||
onClick = onItemClick?.let { { it(item) } },
|
||||
onLongClick = onItemLongPress?.let { { it(item) } },
|
||||
BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
|
||||
val layout = rememberContinueWatchingLayout(maxWidth.value)
|
||||
NuvioShelfSection(
|
||||
title = "Continue Watching",
|
||||
entries = items,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
headerHorizontalPadding = layout.sectionPadding,
|
||||
rowContentPadding = PaddingValues(horizontal = layout.sectionPadding),
|
||||
itemSpacing = layout.itemGap,
|
||||
key = { item -> item.videoId },
|
||||
) { item ->
|
||||
when (style) {
|
||||
ContinueWatchingSectionStyle.Wide -> ContinueWatchingWideCard(
|
||||
item = item,
|
||||
layout = layout,
|
||||
onClick = onItemClick?.let { { it(item) } },
|
||||
onLongClick = onItemLongPress?.let { { it(item) } },
|
||||
)
|
||||
ContinueWatchingSectionStyle.Poster -> ContinueWatchingPosterCard(
|
||||
item = item,
|
||||
layout = layout,
|
||||
onClick = onItemClick?.let { { it(item) } },
|
||||
onLongClick = onItemLongPress?.let { { it(item) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContinueWatchingStylePreview(
|
||||
style: ContinueWatchingSectionStyle,
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
ContinueWatchingSectionStyle.Wide -> WideCardPreview()
|
||||
ContinueWatchingSectionStyle.Poster -> PosterCardPreview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WideCardPreview() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.height(60.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f)),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(40.dp)
|
||||
.fillMaxHeight()
|
||||
.background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.65f)),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(1f)
|
||||
.padding(4.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.7f)
|
||||
.height(8.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.20f)),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.5f)
|
||||
.height(6.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.14f)),
|
||||
)
|
||||
NuvioProgressBar(
|
||||
progress = 0.6f,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
height = 4.dp,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceTint.copy(alpha = 0.16f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PosterCardPreview() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(44.dp)
|
||||
.height(60.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.65f)),
|
||||
) {
|
||||
NuvioProgressBar(
|
||||
progress = 0.45f,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
height = 4.dp,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceTint.copy(alpha = 0.16f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun ContinueWatchingCard(
|
||||
private fun ContinueWatchingWideCard(
|
||||
item: ContinueWatchingItem,
|
||||
layout: ContinueWatchingLayout,
|
||||
onClick: (() -> Unit)?,
|
||||
onLongClick: (() -> Unit)?,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.width(layout.wideCardWidth)
|
||||
.height(layout.wideCardHeight)
|
||||
.clip(RoundedCornerShape(layout.cardRadius))
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.92f))
|
||||
.border(
|
||||
width = 1.5.dp,
|
||||
color = Color.White.copy(alpha = 0.15f),
|
||||
shape = RoundedCornerShape(layout.cardRadius),
|
||||
)
|
||||
.combinedClickable(
|
||||
enabled = onClick != null || onLongClick != null,
|
||||
onClick = { onClick?.invoke() },
|
||||
onLongClick = onLongClick,
|
||||
),
|
||||
) {
|
||||
ArtworkPanel(
|
||||
imageUrl = item.imageUrl,
|
||||
width = layout.widePosterStripWidth,
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(1f)
|
||||
.padding(layout.wideContentPadding),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Text(
|
||||
text = item.title,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontSize = layout.wideTitleSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (item.progressFraction <= 0f && item.seasonNumber != null && item.episodeNumber != null) {
|
||||
UpNextBadge(compact = false, textSize = layout.wideBadgeTextSize)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = item.subtitle,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
fontSize = layout.wideMetaSize,
|
||||
fontWeight = FontWeight.Medium,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
if (item.progressFraction > 0f) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
NuvioProgressBar(
|
||||
progress = item.progressFraction,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
height = layout.progressHeight,
|
||||
trackColor = Color.White.copy(alpha = 0.10f),
|
||||
)
|
||||
Text(
|
||||
text = "${(item.progressFraction * 100).roundToInt()}% watched",
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontSize = layout.progressLabelSize,
|
||||
fontWeight = FontWeight.Medium,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun ContinueWatchingPosterCard(
|
||||
item: ContinueWatchingItem,
|
||||
layout: ContinueWatchingLayout,
|
||||
onClick: (() -> Unit)?,
|
||||
onLongClick: (() -> Unit)?,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(260.dp)
|
||||
.clip(RoundedCornerShape(22.dp))
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.88f))
|
||||
.width(layout.posterCardWidth)
|
||||
.combinedClickable(
|
||||
enabled = onClick != null || onLongClick != null,
|
||||
onClick = { onClick?.invoke() },
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
.padding(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1.72f)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
.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),
|
||||
),
|
||||
) {
|
||||
val imageUrl = item.imageUrl
|
||||
if (imageUrl != null) {
|
||||
|
|
@ -91,44 +309,233 @@ private fun ContinueWatchingCard(
|
|||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(10.dp)
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.78f))
|
||||
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Resume",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
.fillMaxWidth()
|
||||
.height(layout.posterCardHeight * 0.5f)
|
||||
.align(Alignment.BottomCenter)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.8f)),
|
||||
),
|
||||
),
|
||||
)
|
||||
if (item.seasonNumber != null && item.episodeNumber != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(8.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(Color.Black.copy(alpha = 0.7f))
|
||||
.padding(horizontal = 6.dp, vertical = 3.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "S${item.seasonNumber} E${item.episodeNumber}",
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontSize = layout.posterMetaSize,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
),
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (item.progressFraction <= 0f && item.seasonNumber != null && item.episodeNumber != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
UpNextBadge(compact = true, textSize = layout.posterBadgeTextSize)
|
||||
}
|
||||
}
|
||||
if (item.progressFraction > 0f) {
|
||||
NuvioProgressBar(
|
||||
progress = item.progressFraction,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
height = 4.dp,
|
||||
trackColor = Color.White.copy(alpha = 0.3f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = item.subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
NuvioProgressBar(
|
||||
progress = item.progressFraction,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
height = 5.dp,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Text(
|
||||
text = item.title,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontSize = layout.posterTitleSize,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
lineHeight = 18.sp,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (item.progressFraction > 0f) {
|
||||
Text(
|
||||
text = "${(item.progressFraction * 100).roundToInt()}%",
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontSize = layout.progressLabelSize,
|
||||
fontWeight = FontWeight.Medium,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArtworkPanel(
|
||||
imageUrl: String?,
|
||||
width: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(width)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpNextBadge(
|
||||
compact: Boolean,
|
||||
textSize: androidx.compose.ui.unit.TextUnit,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(if (compact) 4.dp else 12.dp))
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.padding(
|
||||
horizontal = if (compact) 6.dp else 8.dp,
|
||||
vertical = if (compact) 3.dp else 4.dp,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = "Up next",
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontSize = textSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
color = Color.White,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class ContinueWatchingLayout(
|
||||
val sectionPadding: Dp,
|
||||
val itemGap: Dp,
|
||||
val wideCardWidth: Dp,
|
||||
val wideCardHeight: Dp,
|
||||
val widePosterStripWidth: Dp,
|
||||
val wideContentPadding: Dp,
|
||||
val posterCardWidth: Dp,
|
||||
val posterCardHeight: Dp,
|
||||
val cardRadius: Dp,
|
||||
val progressHeight: Dp,
|
||||
val wideTitleSize: androidx.compose.ui.unit.TextUnit,
|
||||
val wideMetaSize: androidx.compose.ui.unit.TextUnit,
|
||||
val posterTitleSize: androidx.compose.ui.unit.TextUnit,
|
||||
val posterMetaSize: androidx.compose.ui.unit.TextUnit,
|
||||
val progressLabelSize: androidx.compose.ui.unit.TextUnit,
|
||||
val wideBadgeTextSize: androidx.compose.ui.unit.TextUnit,
|
||||
val posterBadgeTextSize: androidx.compose.ui.unit.TextUnit,
|
||||
)
|
||||
|
||||
private fun rememberContinueWatchingLayout(maxWidthDp: Float): ContinueWatchingLayout =
|
||||
when {
|
||||
maxWidthDp >= 1440f -> ContinueWatchingLayout(
|
||||
sectionPadding = 32.dp,
|
||||
itemGap = 20.dp,
|
||||
wideCardWidth = 400.dp,
|
||||
wideCardHeight = 160.dp,
|
||||
widePosterStripWidth = 100.dp,
|
||||
wideContentPadding = 16.dp,
|
||||
posterCardWidth = 180.dp,
|
||||
posterCardHeight = 270.dp,
|
||||
cardRadius = 18.dp,
|
||||
progressHeight = 6.dp,
|
||||
wideTitleSize = 20.sp,
|
||||
wideMetaSize = 16.sp,
|
||||
posterTitleSize = 16.sp,
|
||||
posterMetaSize = 14.sp,
|
||||
progressLabelSize = 14.sp,
|
||||
wideBadgeTextSize = 14.sp,
|
||||
posterBadgeTextSize = 12.sp,
|
||||
)
|
||||
maxWidthDp >= 1024f -> ContinueWatchingLayout(
|
||||
sectionPadding = 28.dp,
|
||||
itemGap = 18.dp,
|
||||
wideCardWidth = 350.dp,
|
||||
wideCardHeight = 140.dp,
|
||||
widePosterStripWidth = 90.dp,
|
||||
wideContentPadding = 14.dp,
|
||||
posterCardWidth = 160.dp,
|
||||
posterCardHeight = 240.dp,
|
||||
cardRadius = 16.dp,
|
||||
progressHeight = 5.dp,
|
||||
wideTitleSize = 18.sp,
|
||||
wideMetaSize = 15.sp,
|
||||
posterTitleSize = 15.sp,
|
||||
posterMetaSize = 13.sp,
|
||||
progressLabelSize = 13.sp,
|
||||
wideBadgeTextSize = 13.sp,
|
||||
posterBadgeTextSize = 10.sp,
|
||||
)
|
||||
maxWidthDp >= 768f -> ContinueWatchingLayout(
|
||||
sectionPadding = 24.dp,
|
||||
itemGap = 16.dp,
|
||||
wideCardWidth = 320.dp,
|
||||
wideCardHeight = 130.dp,
|
||||
widePosterStripWidth = 85.dp,
|
||||
wideContentPadding = 12.dp,
|
||||
posterCardWidth = 140.dp,
|
||||
posterCardHeight = 210.dp,
|
||||
cardRadius = 16.dp,
|
||||
progressHeight = 4.dp,
|
||||
wideTitleSize = 17.sp,
|
||||
wideMetaSize = 14.sp,
|
||||
posterTitleSize = 14.sp,
|
||||
posterMetaSize = 12.sp,
|
||||
progressLabelSize = 12.sp,
|
||||
wideBadgeTextSize = 12.sp,
|
||||
posterBadgeTextSize = 10.sp,
|
||||
)
|
||||
else -> ContinueWatchingLayout(
|
||||
sectionPadding = 16.dp,
|
||||
itemGap = 16.dp,
|
||||
wideCardWidth = 280.dp,
|
||||
wideCardHeight = 120.dp,
|
||||
widePosterStripWidth = 80.dp,
|
||||
wideContentPadding = 12.dp,
|
||||
posterCardWidth = 120.dp,
|
||||
posterCardHeight = 180.dp,
|
||||
cardRadius = 16.dp,
|
||||
progressHeight = 4.dp,
|
||||
wideTitleSize = 16.sp,
|
||||
wideMetaSize = 13.sp,
|
||||
posterTitleSize = 14.sp,
|
||||
posterMetaSize = 12.sp,
|
||||
progressLabelSize = 11.sp,
|
||||
wideBadgeTextSize = 12.sp,
|
||||
posterBadgeTextSize = 10.sp,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Style
|
||||
|
||||
internal fun LazyListScope.appearanceSettingsContent(
|
||||
isTablet: Boolean,
|
||||
onContinueWatchingClick: () -> Unit,
|
||||
) {
|
||||
item {
|
||||
SettingsSection(
|
||||
title = "HOME",
|
||||
isTablet = isTablet,
|
||||
) {
|
||||
SettingsNavigationRow(
|
||||
title = "Continue Watching",
|
||||
description = "Show, hide, and style the Continue Watching shelf.",
|
||||
icon = Icons.Rounded.Style,
|
||||
isTablet = isTablet,
|
||||
onClick = onContinueWatchingClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.CheckCircle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.nuvio.app.features.home.components.ContinueWatchingStylePreview
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
|
||||
|
||||
internal fun LazyListScope.continueWatchingSettingsContent(
|
||||
isTablet: Boolean,
|
||||
isVisible: Boolean,
|
||||
style: ContinueWatchingSectionStyle,
|
||||
) {
|
||||
item {
|
||||
SettingsSection(
|
||||
title = "VISIBILITY",
|
||||
isTablet = isTablet,
|
||||
) {
|
||||
SettingsSwitchRow(
|
||||
title = "Show Continue Watching",
|
||||
description = "Display the Continue Watching shelf on the Home screen.",
|
||||
checked = isVisible,
|
||||
isTablet = isTablet,
|
||||
onCheckedChange = ContinueWatchingPreferencesRepository::setVisible,
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
SettingsSection(
|
||||
title = "CARD STYLE",
|
||||
isTablet = isTablet,
|
||||
) {
|
||||
ContinueWatchingStyleSelector(
|
||||
isTablet = isTablet,
|
||||
selectedStyle = style,
|
||||
onStyleSelected = ContinueWatchingPreferencesRepository::setStyle,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueWatchingStyleSelector(
|
||||
isTablet: Boolean,
|
||||
selectedStyle: ContinueWatchingSectionStyle,
|
||||
onStyleSelected: (ContinueWatchingSectionStyle) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
ContinueWatchingSectionStyle.entries.forEach { style ->
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
ContinueWatchingStyleOption(
|
||||
style = style,
|
||||
selected = selectedStyle == style,
|
||||
isTablet = isTablet,
|
||||
onClick = { onStyleSelected(style) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueWatchingStyleOption(
|
||||
style: ContinueWatchingSectionStyle,
|
||||
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 = androidx.compose.foundation.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),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
if (selected) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
ContinueWatchingStylePreview(
|
||||
style = style,
|
||||
isSelected = selected,
|
||||
)
|
||||
Text(
|
||||
text = style.name.lowercase().replaceFirstChar(Char::uppercase),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = if (style == ContinueWatchingSectionStyle.Wide) {
|
||||
"Info-dense horizontal card"
|
||||
} else {
|
||||
"Artwork-first poster card"
|
||||
},
|
||||
style = if (isTablet) MaterialTheme.typography.bodySmall else MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Palette
|
||||
import androidx.compose.material.icons.rounded.Style
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
|
|
@ -16,6 +18,8 @@ internal enum class SettingsPage(
|
|||
) {
|
||||
Root("Settings"),
|
||||
Playback("Playback"),
|
||||
Appearance("Appearance"),
|
||||
ContinueWatching("Continue Watching"),
|
||||
ContentDiscovery("Content & Discovery"),
|
||||
Addons("Addons"),
|
||||
Homescreen("Homescreen"),
|
||||
|
|
@ -25,6 +29,8 @@ internal fun SettingsPage.previousPage(): SettingsPage? =
|
|||
when (this) {
|
||||
SettingsPage.Root -> null
|
||||
SettingsPage.Playback -> SettingsPage.Root
|
||||
SettingsPage.Appearance -> SettingsPage.Root
|
||||
SettingsPage.ContinueWatching -> SettingsPage.Appearance
|
||||
SettingsPage.ContentDiscovery -> SettingsPage.Root
|
||||
SettingsPage.Addons -> SettingsPage.ContentDiscovery
|
||||
SettingsPage.Homescreen -> SettingsPage.ContentDiscovery
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.nuvio.app.features.settings
|
|||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Extension
|
||||
import androidx.compose.material.icons.rounded.Palette
|
||||
import androidx.compose.material.icons.rounded.PlayArrow
|
||||
|
||||
internal fun LazyListScope.settingsRootContent(
|
||||
isTablet: Boolean,
|
||||
onPlaybackClick: () -> Unit,
|
||||
onAppearanceClick: () -> Unit,
|
||||
onContentDiscoveryClick: () -> Unit,
|
||||
) {
|
||||
item {
|
||||
|
|
@ -22,6 +24,13 @@ internal fun LazyListScope.settingsRootContent(
|
|||
isTablet = isTablet,
|
||||
onClick = onPlaybackClick,
|
||||
)
|
||||
SettingsNavigationRow(
|
||||
title = "Appearance",
|
||||
description = "Tune home presentation and visual preferences.",
|
||||
icon = Icons.Rounded.Palette,
|
||||
isTablet = isTablet,
|
||||
onClick = onAppearanceClick,
|
||||
)
|
||||
SettingsNavigationRow(
|
||||
title = "Content & Discovery",
|
||||
description = "Manage addons and discovery sources.",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ import com.nuvio.app.features.addons.AddonRepository
|
|||
import com.nuvio.app.features.home.HomeCatalogSettingsItem
|
||||
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
|
|
@ -60,6 +62,10 @@ fun SettingsScreen(
|
|||
PlayerSettingsRepository.ensureLoaded()
|
||||
PlayerSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val continueWatchingPreferencesUiState by remember {
|
||||
ContinueWatchingPreferencesRepository.ensureLoaded()
|
||||
ContinueWatchingPreferencesRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(addonsUiState.addons) {
|
||||
HomeCatalogSettingsRepository.syncCatalogs(addonsUiState.addons)
|
||||
|
|
@ -81,6 +87,8 @@ fun SettingsScreen(
|
|||
heroEnabled = homescreenSettingsUiState.heroEnabled,
|
||||
homescreenSettings = homescreenSettingsUiState.items,
|
||||
showLoadingOverlay = playerSettingsUiState.showLoadingOverlay,
|
||||
continueWatchingVisible = continueWatchingPreferencesUiState.isVisible,
|
||||
continueWatchingStyle = continueWatchingPreferencesUiState.style,
|
||||
)
|
||||
} else {
|
||||
MobileSettingsScreen(
|
||||
|
|
@ -89,6 +97,8 @@ fun SettingsScreen(
|
|||
heroEnabled = homescreenSettingsUiState.heroEnabled,
|
||||
homescreenSettings = homescreenSettingsUiState.items,
|
||||
showLoadingOverlay = playerSettingsUiState.showLoadingOverlay,
|
||||
continueWatchingVisible = continueWatchingPreferencesUiState.isVisible,
|
||||
continueWatchingStyle = continueWatchingPreferencesUiState.style,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -101,6 +111,8 @@ private fun MobileSettingsScreen(
|
|||
heroEnabled: Boolean,
|
||||
homescreenSettings: List<HomeCatalogSettingsItem>,
|
||||
showLoadingOverlay: Boolean,
|
||||
continueWatchingVisible: Boolean,
|
||||
continueWatchingStyle: ContinueWatchingSectionStyle,
|
||||
) {
|
||||
NuvioScreen {
|
||||
stickyHeader {
|
||||
|
|
@ -115,12 +127,22 @@ private fun MobileSettingsScreen(
|
|||
SettingsPage.Root -> settingsRootContent(
|
||||
isTablet = false,
|
||||
onPlaybackClick = { onPageChange(SettingsPage.Playback) },
|
||||
onAppearanceClick = { onPageChange(SettingsPage.Appearance) },
|
||||
onContentDiscoveryClick = { onPageChange(SettingsPage.ContentDiscovery) },
|
||||
)
|
||||
SettingsPage.Playback -> playbackSettingsContent(
|
||||
isTablet = false,
|
||||
showLoadingOverlay = showLoadingOverlay,
|
||||
)
|
||||
SettingsPage.Appearance -> appearanceSettingsContent(
|
||||
isTablet = false,
|
||||
onContinueWatchingClick = { onPageChange(SettingsPage.ContinueWatching) },
|
||||
)
|
||||
SettingsPage.ContinueWatching -> continueWatchingSettingsContent(
|
||||
isTablet = false,
|
||||
isVisible = continueWatchingVisible,
|
||||
style = continueWatchingStyle,
|
||||
)
|
||||
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
||||
isTablet = false,
|
||||
onAddonsClick = { onPageChange(SettingsPage.Addons) },
|
||||
|
|
@ -143,6 +165,8 @@ private fun TabletSettingsScreen(
|
|||
heroEnabled: Boolean,
|
||||
homescreenSettings: List<HomeCatalogSettingsItem>,
|
||||
showLoadingOverlay: Boolean,
|
||||
continueWatchingVisible: Boolean,
|
||||
continueWatchingStyle: ContinueWatchingSectionStyle,
|
||||
) {
|
||||
var selectedCategory by rememberSaveable { mutableStateOf(SettingsCategory.General.name) }
|
||||
val activeCategory = SettingsCategory.valueOf(selectedCategory)
|
||||
|
|
@ -206,12 +230,22 @@ private fun TabletSettingsScreen(
|
|||
SettingsPage.Root -> settingsRootContent(
|
||||
isTablet = true,
|
||||
onPlaybackClick = { onPageChange(SettingsPage.Playback) },
|
||||
onAppearanceClick = { onPageChange(SettingsPage.Appearance) },
|
||||
onContentDiscoveryClick = { onPageChange(SettingsPage.ContentDiscovery) },
|
||||
)
|
||||
SettingsPage.Playback -> playbackSettingsContent(
|
||||
isTablet = true,
|
||||
showLoadingOverlay = showLoadingOverlay,
|
||||
)
|
||||
SettingsPage.Appearance -> appearanceSettingsContent(
|
||||
isTablet = true,
|
||||
onContinueWatchingClick = { onPageChange(SettingsPage.ContinueWatching) },
|
||||
)
|
||||
SettingsPage.ContinueWatching -> continueWatchingSettingsContent(
|
||||
isTablet = true,
|
||||
isVisible = continueWatchingVisible,
|
||||
style = continueWatchingStyle,
|
||||
)
|
||||
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
||||
isTablet = true,
|
||||
onAddonsClick = { onPageChange(SettingsPage.Addons) },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
private data class StoredContinueWatchingPreferences(
|
||||
val isVisible: Boolean = true,
|
||||
val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Wide,
|
||||
)
|
||||
|
||||
object ContinueWatchingPreferencesRepository {
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow(ContinueWatchingPreferencesUiState())
|
||||
val uiState: StateFlow<ContinueWatchingPreferencesUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var hasLoaded = false
|
||||
|
||||
fun ensureLoaded() {
|
||||
if (hasLoaded) return
|
||||
hasLoaded = true
|
||||
|
||||
val payload = ContinueWatchingPreferencesStorage.loadPayload().orEmpty().trim()
|
||||
if (payload.isEmpty()) return
|
||||
|
||||
val stored = runCatching {
|
||||
json.decodeFromString<StoredContinueWatchingPreferences>(payload)
|
||||
}.getOrNull() ?: return
|
||||
|
||||
_uiState.value = ContinueWatchingPreferencesUiState(
|
||||
isVisible = stored.isVisible,
|
||||
style = stored.style,
|
||||
)
|
||||
}
|
||||
|
||||
fun setVisible(isVisible: Boolean) {
|
||||
ensureLoaded()
|
||||
_uiState.value = _uiState.value.copy(isVisible = isVisible)
|
||||
persist()
|
||||
}
|
||||
|
||||
fun setStyle(style: ContinueWatchingSectionStyle) {
|
||||
ensureLoaded()
|
||||
_uiState.value = _uiState.value.copy(style = style)
|
||||
persist()
|
||||
}
|
||||
|
||||
private fun persist() {
|
||||
ContinueWatchingPreferencesStorage.savePayload(
|
||||
json.encodeToString(
|
||||
StoredContinueWatchingPreferences(
|
||||
isVisible = _uiState.value.isVisible,
|
||||
style = _uiState.value.style,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
internal expect object ContinueWatchingPreferencesStorage {
|
||||
fun loadPayload(): String?
|
||||
fun savePayload(payload: String)
|
||||
}
|
||||
|
|
@ -2,6 +2,12 @@ package com.nuvio.app.features.watchprogress
|
|||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class ContinueWatchingSectionStyle {
|
||||
Wide,
|
||||
Poster,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class WatchProgressEntry(
|
||||
val contentType: String,
|
||||
|
|
@ -82,6 +88,11 @@ data class ContinueWatchingItem(
|
|||
val progressFraction: Float,
|
||||
)
|
||||
|
||||
data class ContinueWatchingPreferencesUiState(
|
||||
val isVisible: Boolean = true,
|
||||
val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Wide,
|
||||
)
|
||||
|
||||
internal fun WatchProgressEntry.toContinueWatchingItem(): ContinueWatchingItem {
|
||||
val subtitle = if (seasonNumber != null && episodeNumber != null) {
|
||||
buildString {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import platform.Foundation.NSUserDefaults
|
||||
|
||||
actual object ContinueWatchingPreferencesStorage {
|
||||
private const val payloadKey = "continue_watching_preferences_payload"
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
NSUserDefaults.standardUserDefaults.stringForKey(payloadKey)
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(payload, forKey = payloadKey)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue