Merge branch 'cmp-rewrite' of https://github.com/NuvioMedia/NuvioMobile into cmp-rewrite

This commit is contained in:
tapframe 2026-05-10 12:36:18 +05:30
commit 46a82dce9a
14 changed files with 1487 additions and 4 deletions

View file

@ -260,6 +260,7 @@ kotlin {
commonMain.dependencies { commonMain.dependencies {
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.coil.network.ktor3) implementation(libs.coil.network.ktor3)
implementation(libs.coil.svg)
implementation("dev.chrisbanes.haze:haze:1.7.2") implementation("dev.chrisbanes.haze:haze:1.7.2")
implementation(libs.compose.runtime) implementation(libs.compose.runtime)
implementation(libs.compose.foundation) implementation(libs.compose.foundation)

View file

@ -9,4 +9,5 @@
<locale android:name="el"/> <locale android:name="el"/>
<locale android:name="pl"/> <locale android:name="pl"/>
<locale android:name="de"/> <locale android:name="de"/>
<locale android:name="cs"/>
</locale-config> </locale-config>

File diff suppressed because it is too large Load diff

View file

@ -541,6 +541,12 @@
<string name="settings_continue_watching_blur_next_up_title">Blur Unwatched in Continue Watching</string> <string name="settings_continue_watching_blur_next_up_title">Blur Unwatched in Continue Watching</string>
<string name="settings_continue_watching_show_unaired_next_up_description">Include upcoming episodes in Continue Watching before they air.</string> <string name="settings_continue_watching_show_unaired_next_up_description">Include upcoming episodes in Continue Watching before they air.</string>
<string name="settings_continue_watching_show_unaired_next_up_title">Show Unaired Next Up Episodes</string> <string name="settings_continue_watching_show_unaired_next_up_title">Show Unaired Next Up Episodes</string>
<string name="settings_continue_watching_section_sort_order">SORT ORDER</string>
<string name="settings_continue_watching_sort_mode_title">Sort Order</string>
<string name="settings_continue_watching_sort_mode_default">Default</string>
<string name="settings_continue_watching_sort_mode_default_desc">Sort all items by recency</string>
<string name="settings_continue_watching_sort_mode_streaming">Streaming Style</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Released items first, upcoming at the end</string>
<string name="settings_continue_watching_section_card_style">Poster Card Style</string> <string name="settings_continue_watching_section_card_style">Poster Card Style</string>
<string name="settings_continue_watching_section_on_launch">ON LAUNCH</string> <string name="settings_continue_watching_section_on_launch">ON LAUNCH</string>
<string name="settings_continue_watching_section_up_next_behavior">UP NEXT BEHAVIOR</string> <string name="settings_continue_watching_section_up_next_behavior">UP NEXT BEHAVIOR</string>

View file

@ -73,6 +73,7 @@ import coil3.ImageLoader
import coil3.compose.setSingletonImageLoaderFactory import coil3.compose.setSingletonImageLoaderFactory
import coil3.request.CachePolicy import coil3.request.CachePolicy
import coil3.request.crossfade import coil3.request.crossfade
import coil3.svg.SvgDecoder
import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.core.auth.AuthRepository import com.nuvio.app.core.auth.AuthRepository
import com.nuvio.app.core.auth.AuthState import com.nuvio.app.core.auth.AuthState
@ -304,6 +305,9 @@ fun App() {
.crossfade(true) .crossfade(true)
.diskCachePolicy(CachePolicy.ENABLED) .diskCachePolicy(CachePolicy.ENABLED)
.memoryCachePolicy(CachePolicy.ENABLED) .memoryCachePolicy(CachePolicy.ENABLED)
.components {
add(SvgDecoder.Factory())
}
.configurePlatformImageLoader() .configurePlatformImageLoader()
.build() .build()
} }

View file

@ -42,6 +42,7 @@ import com.nuvio.app.features.watchprogress.ContinueWatchingEnrichmentCache
import com.nuvio.app.features.watchprogress.CurrentDateProvider import com.nuvio.app.features.watchprogress.CurrentDateProvider
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
import com.nuvio.app.features.watchprogress.ContinueWatchingItem import com.nuvio.app.features.watchprogress.ContinueWatchingItem
import com.nuvio.app.features.watchprogress.ContinueWatchingSortMode
import com.nuvio.app.features.watchprogress.isSeriesTypeForContinueWatching import com.nuvio.app.features.watchprogress.isSeriesTypeForContinueWatching
import com.nuvio.app.features.watchprogress.nextUpDismissKey import com.nuvio.app.features.watchprogress.nextUpDismissKey
import com.nuvio.app.features.watchprogress.WatchProgressClock import com.nuvio.app.features.watchprogress.WatchProgressClock
@ -247,11 +248,14 @@ fun HomeScreen(
visibleContinueWatchingEntries, visibleContinueWatchingEntries,
cachedInProgressItems, cachedInProgressItems,
effectivNextUpItems, effectivNextUpItems,
continueWatchingPreferences.sortMode,
) { ) {
buildHomeContinueWatchingItems( buildHomeContinueWatchingItems(
visibleEntries = visibleContinueWatchingEntries, visibleEntries = visibleContinueWatchingEntries,
cachedInProgressByVideoId = cachedInProgressItems, cachedInProgressByVideoId = cachedInProgressItems,
nextUpItemsBySeries = effectivNextUpItems, nextUpItemsBySeries = effectivNextUpItems,
sortMode = continueWatchingPreferences.sortMode,
todayIsoDate = CurrentDateProvider.todayIsoDate(),
) )
} }
val availableManifests = remember(addonsUiState.addons) { val availableManifests = remember(addonsUiState.addons) {
@ -639,6 +643,8 @@ internal fun buildHomeContinueWatchingItems(
visibleEntries: List<WatchProgressEntry>, visibleEntries: List<WatchProgressEntry>,
cachedInProgressByVideoId: Map<String, ContinueWatchingItem> = emptyMap(), cachedInProgressByVideoId: Map<String, ContinueWatchingItem> = emptyMap(),
nextUpItemsBySeries: Map<String, Pair<Long, ContinueWatchingItem>>, nextUpItemsBySeries: Map<String, Pair<Long, ContinueWatchingItem>>,
sortMode: ContinueWatchingSortMode = ContinueWatchingSortMode.DEFAULT,
todayIsoDate: String = "",
): List<ContinueWatchingItem> { ): List<ContinueWatchingItem> {
val inProgressSeriesIds = visibleEntries val inProgressSeriesIds = visibleEntries
.asSequence() .asSequence()
@ -647,7 +653,7 @@ internal fun buildHomeContinueWatchingItems(
.filter(String::isNotBlank) .filter(String::isNotBlank)
.toSet() .toSet()
return buildList { val candidates = buildList {
addAll( addAll(
visibleEntries.map { entry -> visibleEntries.map { entry ->
val liveItem = entry.toContinueWatchingItem() val liveItem = entry.toContinueWatchingItem()
@ -669,13 +675,62 @@ internal fun buildHomeContinueWatchingItems(
}, },
) )
} }
// Deduplicate by series/content id first (order-stable)
val seen = mutableSetOf<String>()
val deduplicated = candidates
.sortedWith( .sortedWith(
compareByDescending<HomeContinueWatchingCandidate> { it.lastUpdatedEpochMs } compareByDescending<HomeContinueWatchingCandidate> { it.lastUpdatedEpochMs }
.thenByDescending { it.isProgressEntry }, .thenByDescending { it.isProgressEntry },
) )
.filter { candidate -> candidate.item.shouldDisplayInContinueWatching() } .filter { candidate -> candidate.item.shouldDisplayInContinueWatching() }
.distinctBy { candidate -> candidate.item.parentMetaId.ifBlank { candidate.item.videoId } } .filter { candidate ->
val key = candidate.item.parentMetaId.ifBlank { candidate.item.videoId }
seen.add(key)
}
return when (sortMode) {
ContinueWatchingSortMode.DEFAULT -> deduplicated.map(HomeContinueWatchingCandidate::item)
ContinueWatchingSortMode.STREAMING_STYLE -> applyStreamingStyleSort(deduplicated, todayIsoDate)
}
}
private fun applyStreamingStyleSort(
candidates: List<HomeContinueWatchingCandidate>,
todayIsoDate: String,
): List<ContinueWatchingItem> {
val (released, unreleased) = candidates.partition { candidate ->
val item = candidate.item
if (!item.isNextUp) {
true // in-progress items are always "released"
} else {
val itemReleased = item.released
if (itemReleased.isNullOrBlank() || todayIsoDate.isBlank()) {
true // no date info → treat as released
} else {
isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = itemReleased)
}
}
}
// Released: most recently watched first (already sorted by dedup pass)
val sortedReleased = released.map(HomeContinueWatchingCandidate::item)
// Unaired: soonest air date first; unknown dates go to the end
val sortedUnreleased = unreleased
.sortedWith { a, b ->
val dateA = a.item.released?.takeIf { it.isNotBlank() }
val dateB = b.item.released?.takeIf { it.isNotBlank() }
when {
dateA == null && dateB == null -> 0
dateA == null -> 1
dateB == null -> -1
else -> dateA.compareTo(dateB)
}
}
.map(HomeContinueWatchingCandidate::item) .map(HomeContinueWatchingCandidate::item)
return sortedReleased + sortedUnreleased
} }
private data class CompletedSeriesCandidate( private data class CompletedSeriesCandidate(

View file

@ -10,6 +10,7 @@ import nuvio.composeapp.generated.resources.lang_turkish
import nuvio.composeapp.generated.resources.lang_italian import nuvio.composeapp.generated.resources.lang_italian
import nuvio.composeapp.generated.resources.lang_greek import nuvio.composeapp.generated.resources.lang_greek
import nuvio.composeapp.generated.resources.lang_polish import nuvio.composeapp.generated.resources.lang_polish
import nuvio.composeapp.generated.resources.lang_czech
import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.StringResource
enum class AppLanguage( enum class AppLanguage(
@ -25,6 +26,7 @@ enum class AppLanguage(
ITALIAN("it", Res.string.lang_italian), ITALIAN("it", Res.string.lang_italian),
GREEK("el", Res.string.lang_greek), GREEK("el", Res.string.lang_greek),
POLISH("pl", Res.string.lang_polish), POLISH("pl", Res.string.lang_polish),
CZECH("cs", Res.string.lang_czech),
; ;
companion object { companion object {

View file

@ -5,18 +5,27 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.CheckCircle import androidx.compose.material.icons.rounded.CheckCircle
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@ -25,6 +34,7 @@ import androidx.compose.ui.unit.dp
import com.nuvio.app.features.home.components.ContinueWatchingStylePreview import com.nuvio.app.features.home.components.ContinueWatchingStylePreview
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
import com.nuvio.app.features.watchprogress.ContinueWatchingSortMode
import nuvio.composeapp.generated.resources.Res import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.settings_continue_watching_resume_prompt_description import nuvio.composeapp.generated.resources.settings_continue_watching_resume_prompt_description
import nuvio.composeapp.generated.resources.settings_continue_watching_resume_prompt_title import nuvio.composeapp.generated.resources.settings_continue_watching_resume_prompt_title
@ -34,10 +44,16 @@ import nuvio.composeapp.generated.resources.settings_continue_watching_show_unai
import nuvio.composeapp.generated.resources.settings_continue_watching_show_unaired_next_up_title import nuvio.composeapp.generated.resources.settings_continue_watching_show_unaired_next_up_title
import nuvio.composeapp.generated.resources.settings_continue_watching_section_card_style import nuvio.composeapp.generated.resources.settings_continue_watching_section_card_style
import nuvio.composeapp.generated.resources.settings_continue_watching_section_on_launch import nuvio.composeapp.generated.resources.settings_continue_watching_section_on_launch
import nuvio.composeapp.generated.resources.settings_continue_watching_section_sort_order
import nuvio.composeapp.generated.resources.settings_continue_watching_section_up_next_behavior import nuvio.composeapp.generated.resources.settings_continue_watching_section_up_next_behavior
import nuvio.composeapp.generated.resources.settings_continue_watching_section_visibility import nuvio.composeapp.generated.resources.settings_continue_watching_section_visibility
import nuvio.composeapp.generated.resources.settings_continue_watching_show_description import nuvio.composeapp.generated.resources.settings_continue_watching_show_description
import nuvio.composeapp.generated.resources.settings_continue_watching_show_title import nuvio.composeapp.generated.resources.settings_continue_watching_show_title
import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode_default
import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode_default_desc
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_poster 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_poster_description
import nuvio.composeapp.generated.resources.settings_continue_watching_style_wide import nuvio.composeapp.generated.resources.settings_continue_watching_style_wide
@ -58,6 +74,7 @@ internal fun LazyListScope.continueWatchingSettingsContent(
showUnairedNextUp: Boolean, showUnairedNextUp: Boolean,
blurNextUp: Boolean, blurNextUp: Boolean,
showResumePromptOnLaunch: Boolean, showResumePromptOnLaunch: Boolean,
sortMode: ContinueWatchingSortMode,
) { ) {
item { item {
SettingsSection( SettingsSection(
@ -145,6 +162,39 @@ internal fun LazyListScope.continueWatchingSettingsContent(
} }
} }
} }
item {
var showSortModeSheet by remember { mutableStateOf(false) }
SettingsSection(
title = stringResource(Res.string.settings_continue_watching_section_sort_order),
isTablet = isTablet,
) {
SettingsGroup(isTablet = isTablet) {
val currentModeLabel = stringResource(
when (sortMode) {
ContinueWatchingSortMode.DEFAULT -> Res.string.settings_continue_watching_sort_mode_default
ContinueWatchingSortMode.STREAMING_STYLE -> Res.string.settings_continue_watching_sort_mode_streaming
}
)
SettingsNavigationRow(
title = stringResource(Res.string.settings_continue_watching_sort_mode_title),
description = currentModeLabel,
isTablet = isTablet,
onClick = { showSortModeSheet = true },
)
}
}
if (showSortModeSheet) {
ContinueWatchingSortModeDialog(
currentMode = sortMode,
onModeSelected = { mode ->
ContinueWatchingPreferencesRepository.setSortMode(mode)
showSortModeSheet = false
},
onDismiss = { showSortModeSheet = false },
)
}
}
} }
@Composable @Composable
@ -250,3 +300,101 @@ private val ContinueWatchingSectionStyle.descriptionRes: StringResource
ContinueWatchingSectionStyle.Wide -> Res.string.settings_continue_watching_style_wide_description ContinueWatchingSectionStyle.Wide -> Res.string.settings_continue_watching_style_wide_description
ContinueWatchingSectionStyle.Poster -> Res.string.settings_continue_watching_style_poster_description ContinueWatchingSectionStyle.Poster -> Res.string.settings_continue_watching_style_poster_description
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ContinueWatchingSortModeDialog(
currentMode: ContinueWatchingSortMode,
onModeSelected: (ContinueWatchingSortMode) -> Unit,
onDismiss: () -> Unit,
) {
val options = listOf(
Triple(
ContinueWatchingSortMode.DEFAULT,
Res.string.settings_continue_watching_sort_mode_default,
Res.string.settings_continue_watching_sort_mode_default_desc,
),
Triple(
ContinueWatchingSortMode.STREAMING_STYLE,
Res.string.settings_continue_watching_sort_mode_streaming,
Res.string.settings_continue_watching_sort_mode_streaming_desc,
),
)
BasicAlertDialog(
onDismissRequest = onDismiss,
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
color = MaterialTheme.colorScheme.surface,
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = stringResource(Res.string.settings_continue_watching_sort_mode_title),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
options.forEach { (mode, titleRes, descriptionRes) ->
val isSelected = mode == currentMode
val containerColor = if (isSelected) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)
}
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable { onModeSelected(mode) },
shape = RoundedCornerShape(12.dp),
color = containerColor,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(titleRes),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = stringResource(descriptionRes),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Box(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center,
) {
if (isSelected) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
}
}
}
}
}
}
}
}
}
}

View file

@ -135,6 +135,7 @@ fun ContinueWatchingSettingsScreen(
showUnairedNextUp = continueWatchingPreferencesUiState.showUnairedNextUp, showUnairedNextUp = continueWatchingPreferencesUiState.showUnairedNextUp,
blurNextUp = continueWatchingPreferencesUiState.blurNextUp, blurNextUp = continueWatchingPreferencesUiState.blurNextUp,
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch, showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
sortMode = continueWatchingPreferencesUiState.sortMode,
) )
} }
} }

View file

@ -509,6 +509,7 @@ private fun MobileSettingsScreen(
showUnairedNextUp = continueWatchingPreferencesUiState.showUnairedNextUp, showUnairedNextUp = continueWatchingPreferencesUiState.showUnairedNextUp,
blurNextUp = continueWatchingPreferencesUiState.blurNextUp, blurNextUp = continueWatchingPreferencesUiState.blurNextUp,
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch, showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
sortMode = continueWatchingPreferencesUiState.sortMode,
) )
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent( SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
isTablet = false, isTablet = false,
@ -866,6 +867,7 @@ private fun TabletSettingsScreen(
showUnairedNextUp = continueWatchingPreferencesUiState.showUnairedNextUp, showUnairedNextUp = continueWatchingPreferencesUiState.showUnairedNextUp,
blurNextUp = continueWatchingPreferencesUiState.blurNextUp, blurNextUp = continueWatchingPreferencesUiState.blurNextUp,
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch, showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
sortMode = continueWatchingPreferencesUiState.sortMode,
) )
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent( SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
isTablet = true, isTablet = true,

View file

@ -15,8 +15,7 @@ private const val COMMENTS_SORT = "likes"
private const val COMMENTS_LIMIT = 100 private const val COMMENTS_LIMIT = 100
private const val COMMENTS_CACHE_TTL_MS = 10 * 60_000L private const val COMMENTS_CACHE_TTL_MS = 10 * 60_000L
private val INLINE_SPOILER_REGEX = Regex( private val INLINE_SPOILER_REGEX = Regex(
"\\[spoiler\\].*?\\[/spoiler\\]", "(?is)\\[spoiler\\].*?\\[/spoiler\\]"
setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),
) )
private val INLINE_SPOILER_TAG_REGEX = Regex("\\[/?spoiler\\]", RegexOption.IGNORE_CASE) private val INLINE_SPOILER_TAG_REGEX = Regex("\\[/?spoiler\\]", RegexOption.IGNORE_CASE)

View file

@ -22,6 +22,8 @@ private data class StoredContinueWatchingPreferences(
val blurNextUp: Boolean = false, val blurNextUp: Boolean = false,
val dismissedNextUpKeys: Set<String> = emptySet(), val dismissedNextUpKeys: Set<String> = emptySet(),
val showResumePromptOnLaunch: Boolean = true, val showResumePromptOnLaunch: Boolean = true,
@SerialName("sort_mode")
val sortMode: ContinueWatchingSortMode = ContinueWatchingSortMode.DEFAULT,
) )
object ContinueWatchingPreferencesRepository { object ContinueWatchingPreferencesRepository {
@ -97,6 +99,7 @@ object ContinueWatchingPreferencesRepository {
blurNextUp = stored.blurNextUp, blurNextUp = stored.blurNextUp,
dismissedNextUpKeys = stored.dismissedNextUpKeys, dismissedNextUpKeys = stored.dismissedNextUpKeys,
showResumePromptOnLaunch = stored.showResumePromptOnLaunch, showResumePromptOnLaunch = stored.showResumePromptOnLaunch,
sortMode = stored.sortMode,
) )
} else { } else {
ContinueWatchingPreferencesUiState() ContinueWatchingPreferencesUiState()
@ -155,6 +158,13 @@ object ContinueWatchingPreferencesRepository {
persist() persist()
} }
fun setSortMode(mode: ContinueWatchingSortMode) {
ensureLoaded()
if (_uiState.value.sortMode == mode) return
_uiState.value = _uiState.value.copy(sortMode = mode)
persist()
}
fun removeDismissedNextUpKeysForContent(contentId: String) { fun removeDismissedNextUpKeysForContent(contentId: String) {
ensureLoaded() ensureLoaded()
val normalizedContentId = contentId.trim() val normalizedContentId = contentId.trim()
@ -178,6 +188,7 @@ object ContinueWatchingPreferencesRepository {
blurNextUp = _uiState.value.blurNextUp, blurNextUp = _uiState.value.blurNextUp,
dismissedNextUpKeys = _uiState.value.dismissedNextUpKeys, dismissedNextUpKeys = _uiState.value.dismissedNextUpKeys,
showResumePromptOnLaunch = _uiState.value.showResumePromptOnLaunch, showResumePromptOnLaunch = _uiState.value.showResumePromptOnLaunch,
sortMode = _uiState.value.sortMode,
), ),
), ),
) )

View file

@ -17,6 +17,12 @@ enum class ContinueWatchingSectionStyle {
Poster, Poster,
} }
@Serializable
enum class ContinueWatchingSortMode {
DEFAULT,
STREAMING_STYLE,
}
@Serializable @Serializable
data class WatchProgressEntry( data class WatchProgressEntry(
val contentType: String, val contentType: String,
@ -175,6 +181,7 @@ data class ContinueWatchingPreferencesUiState(
val blurNextUp: Boolean = false, val blurNextUp: Boolean = false,
val dismissedNextUpKeys: Set<String> = emptySet(), val dismissedNextUpKeys: Set<String> = emptySet(),
val showResumePromptOnLaunch: Boolean = true, val showResumePromptOnLaunch: Boolean = true,
val sortMode: ContinueWatchingSortMode = ContinueWatchingSortMode.DEFAULT,
) )
internal fun nextUpDismissKey( internal fun nextUpDismissKey(

View file

@ -51,6 +51,7 @@ compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-previ
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" } coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }