mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
Merge branch 'parity' into cmp-rewrite
This commit is contained in:
commit
217c1803c7
18 changed files with 301 additions and 33 deletions
|
|
@ -475,6 +475,8 @@
|
||||||
<string name="settings_homescreen_selected_count">%1$d of %2$d selected</string>
|
<string name="settings_homescreen_selected_count">%1$d of %2$d selected</string>
|
||||||
<string name="settings_homescreen_show_hero">Show Hero Section</string>
|
<string name="settings_homescreen_show_hero">Show Hero Section</string>
|
||||||
<string name="settings_homescreen_show_hero_description">Display hero carousel at top of home.</string>
|
<string name="settings_homescreen_show_hero_description">Display hero carousel at top of home.</string>
|
||||||
|
<string name="layout_hide_unreleased">Hide Unreleased Content</string>
|
||||||
|
<string name="layout_hide_unreleased_sub">Hide movies and shows that haven't been released yet.</string>
|
||||||
<string name="settings_homescreen_summary">%1$d of %2$d catalogs visible • %3$d hero sources selected</string>
|
<string name="settings_homescreen_summary">%1$d of %2$d catalogs visible • %3$d hero sources selected</string>
|
||||||
<string name="settings_homescreen_summary_hint">Open a catalog only when you need to rename or reorder it.</string>
|
<string name="settings_homescreen_summary_hint">Open a catalog only when you need to rename or reorder it.</string>
|
||||||
<string name="settings_homescreen_visible">Visible</string>
|
<string name="settings_homescreen_visible">Visible</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ package com.nuvio.app.features.catalog
|
||||||
|
|
||||||
import com.nuvio.app.features.library.LibraryRepository
|
import com.nuvio.app.features.library.LibraryRepository
|
||||||
import com.nuvio.app.features.library.toMetaPreview
|
import com.nuvio.app.features.library.toMetaPreview
|
||||||
|
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||||
|
import com.nuvio.app.features.home.filterReleasedItems
|
||||||
|
import com.nuvio.app.features.watchprogress.CurrentDateProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -124,7 +127,7 @@ object CatalogRepository {
|
||||||
catalogId = request.catalogId,
|
catalogId = request.catalogId,
|
||||||
genre = request.genre,
|
genre = request.genre,
|
||||||
skip = requestedSkip.takeIf { it > 0 },
|
skip = requestedSkip.takeIf { it > 0 },
|
||||||
)
|
).withUnreleasedFilter()
|
||||||
}.fold(
|
}.fold(
|
||||||
onSuccess = { page ->
|
onSuccess = { page ->
|
||||||
if (activeRequest != request) return@fold
|
if (activeRequest != request) return@fold
|
||||||
|
|
@ -158,6 +161,12 @@ object CatalogRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CatalogPage.withUnreleasedFilter(): CatalogPage {
|
||||||
|
if (!HomeCatalogSettingsRepository.snapshot().hideUnreleasedContent) return this
|
||||||
|
val filteredItems = items.filterReleasedItems(CurrentDateProvider.todayIsoDate())
|
||||||
|
return if (filteredItems.size == items.size) this else copy(items = filteredItems)
|
||||||
|
}
|
||||||
|
|
||||||
private data class CatalogRequest(
|
private data class CatalogRequest(
|
||||||
val manifestUrl: String,
|
val manifestUrl: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ import com.nuvio.app.core.ui.posterCardClickable
|
||||||
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
|
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
|
||||||
import com.nuvio.app.core.ui.withDuplicateSafeLazyKeys
|
import com.nuvio.app.core.ui.withDuplicateSafeLazyKeys
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
|
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||||
import com.nuvio.app.features.home.PosterShape
|
import com.nuvio.app.features.home.PosterShape
|
||||||
import com.nuvio.app.features.home.stableKey
|
import com.nuvio.app.features.home.stableKey
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
@ -74,20 +75,21 @@ fun CatalogScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val uiState by CatalogRepository.uiState.collectAsStateWithLifecycle()
|
val uiState by CatalogRepository.uiState.collectAsStateWithLifecycle()
|
||||||
|
val homeCatalogSettingsUiState by HomeCatalogSettingsRepository.uiState.collectAsStateWithLifecycle()
|
||||||
val posterCardStyle = rememberPosterCardStyleUiState()
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
|
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
|
||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyGridState()
|
||||||
var headerHeightPx by remember { mutableIntStateOf(0) }
|
var headerHeightPx by remember { mutableIntStateOf(0) }
|
||||||
var observedOfflineState by remember { mutableStateOf(false) }
|
var observedOfflineState by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(manifestUrl, type, catalogId, genre, supportsPagination) {
|
LaunchedEffect(manifestUrl, type, catalogId, genre, supportsPagination, homeCatalogSettingsUiState.hideUnreleasedContent) {
|
||||||
CatalogRepository.load(
|
CatalogRepository.load(
|
||||||
manifestUrl = manifestUrl,
|
manifestUrl = manifestUrl,
|
||||||
type = type,
|
type = type,
|
||||||
catalogId = catalogId,
|
catalogId = catalogId,
|
||||||
genre = genre,
|
genre = genre,
|
||||||
supportsPagination = supportsPagination,
|
supportsPagination = supportsPagination,
|
||||||
force = false,
|
force = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,18 @@ package com.nuvio.app.features.collection
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
import com.nuvio.app.features.addons.AddonRepository
|
import com.nuvio.app.features.addons.AddonRepository
|
||||||
import com.nuvio.app.features.catalog.CATALOG_PAGE_SIZE
|
import com.nuvio.app.features.catalog.CATALOG_PAGE_SIZE
|
||||||
|
import com.nuvio.app.features.catalog.CatalogPage
|
||||||
import com.nuvio.app.features.catalog.fetchCatalogPage
|
import com.nuvio.app.features.catalog.fetchCatalogPage
|
||||||
import com.nuvio.app.features.catalog.mergeCatalogItems
|
import com.nuvio.app.features.catalog.mergeCatalogItems
|
||||||
import com.nuvio.app.features.catalog.supportsPagination
|
import com.nuvio.app.features.catalog.supportsPagination
|
||||||
import com.nuvio.app.core.i18n.localizedMediaTypeLabel
|
import com.nuvio.app.core.i18n.localizedMediaTypeLabel
|
||||||
|
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||||
import com.nuvio.app.features.home.HomeCatalogSection
|
import com.nuvio.app.features.home.HomeCatalogSection
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
|
import com.nuvio.app.features.home.filterReleasedItems
|
||||||
import com.nuvio.app.features.home.stableKey
|
import com.nuvio.app.features.home.stableKey
|
||||||
import com.nuvio.app.features.trakt.TraktPublicListSourceResolver
|
import com.nuvio.app.features.trakt.TraktPublicListSourceResolver
|
||||||
|
import com.nuvio.app.features.watchprogress.CurrentDateProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -320,7 +324,7 @@ object FolderDetailRepository {
|
||||||
genre = currentTab.genre,
|
genre = currentTab.genre,
|
||||||
skip = requestedSkip.takeIf { it > 0 },
|
skip = requestedSkip.takeIf { it > 0 },
|
||||||
)
|
)
|
||||||
}
|
}.withUnreleasedFilter()
|
||||||
}.onSuccess { page ->
|
}.onSuccess { page ->
|
||||||
updateTab(index) { tab ->
|
updateTab(index) { tab ->
|
||||||
val mergedItems = if (reset) {
|
val mergedItems = if (reset) {
|
||||||
|
|
@ -418,6 +422,12 @@ object FolderDetailRepository {
|
||||||
|
|
||||||
private fun Boolean?.orFalse(): Boolean = this == true
|
private fun Boolean?.orFalse(): Boolean = this == true
|
||||||
|
|
||||||
|
private fun CatalogPage.withUnreleasedFilter(): CatalogPage {
|
||||||
|
if (!HomeCatalogSettingsRepository.snapshot().hideUnreleasedContent) return this
|
||||||
|
val filteredItems = items.filterReleasedItems(CurrentDateProvider.todayIsoDate())
|
||||||
|
return if (filteredItems.size == items.size) this else copy(items = filteredItems)
|
||||||
|
}
|
||||||
|
|
||||||
private fun tmdbCatalogId(source: CollectionSource): String =
|
private fun tmdbCatalogId(source: CollectionSource): String =
|
||||||
buildString {
|
buildString {
|
||||||
append("tmdb_")
|
append("tmdb_")
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,14 @@ import com.nuvio.app.features.addons.AddonManifest
|
||||||
import com.nuvio.app.features.addons.AddonRepository
|
import com.nuvio.app.features.addons.AddonRepository
|
||||||
import com.nuvio.app.features.addons.buildAddonResourceUrl
|
import com.nuvio.app.features.addons.buildAddonResourceUrl
|
||||||
import com.nuvio.app.features.addons.httpGetText
|
import com.nuvio.app.features.addons.httpGetText
|
||||||
|
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||||
|
import com.nuvio.app.features.home.filterReleasedItems
|
||||||
import com.nuvio.app.features.mdblist.MdbListMetadataService
|
import com.nuvio.app.features.mdblist.MdbListMetadataService
|
||||||
import com.nuvio.app.features.mdblist.MdbListSettingsRepository
|
import com.nuvio.app.features.mdblist.MdbListSettingsRepository
|
||||||
import com.nuvio.app.features.tmdb.TmdbMetadataService
|
import com.nuvio.app.features.tmdb.TmdbMetadataService
|
||||||
import com.nuvio.app.features.tmdb.TmdbService
|
import com.nuvio.app.features.tmdb.TmdbService
|
||||||
import com.nuvio.app.features.tmdb.TmdbSettingsRepository
|
import com.nuvio.app.features.tmdb.TmdbSettingsRepository
|
||||||
|
import com.nuvio.app.features.watchprogress.CurrentDateProvider
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -48,14 +51,14 @@ object MetaDetailsRepository {
|
||||||
cachedEntry.metaScreenMeta
|
cachedEntry.metaScreenMeta
|
||||||
?.takeIf { cachedEntry.metaScreenSettingsFingerprint == metaScreenSettingsFingerprint }
|
?.takeIf { cachedEntry.metaScreenSettingsFingerprint == metaScreenSettingsFingerprint }
|
||||||
?.let { cachedMeta ->
|
?.let { cachedMeta ->
|
||||||
_uiState.value = MetaDetailsUiState(meta = cachedMeta)
|
_uiState.value = MetaDetailsUiState(meta = cachedMeta.withUnreleasedFilter())
|
||||||
activeRequestKey = requestKey
|
activeRequestKey = requestKey
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val cachedBaseMeta = cachedEntry.baseMeta
|
val cachedBaseMeta = cachedEntry.baseMeta
|
||||||
if (!shouldFetchMdbListOnMetaScreen(cachedBaseMeta, id, mdbListSettings)) {
|
if (!shouldFetchMdbListOnMetaScreen(cachedBaseMeta, id, mdbListSettings)) {
|
||||||
_uiState.value = MetaDetailsUiState(meta = cachedBaseMeta)
|
_uiState.value = MetaDetailsUiState(meta = cachedBaseMeta.withUnreleasedFilter())
|
||||||
activeRequestKey = requestKey
|
activeRequestKey = requestKey
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -81,7 +84,7 @@ object MetaDetailsRepository {
|
||||||
settingsFingerprint = metaScreenSettingsFingerprint,
|
settingsFingerprint = metaScreenSettingsFingerprint,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_uiState.value = MetaDetailsUiState(meta = enrichedMeta)
|
_uiState.value = MetaDetailsUiState(meta = enrichedMeta.withUnreleasedFilter())
|
||||||
activeRequestKey = requestKey
|
activeRequestKey = requestKey
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
@ -302,7 +305,7 @@ object MetaDetailsRepository {
|
||||||
cachedMetaByRequestKey[requestKey] = cachedEntry
|
cachedMetaByRequestKey[requestKey] = cachedEntry
|
||||||
|
|
||||||
if (!shouldFetchMdbListOnMetaScreen(meta, fallbackItemId, mdbListSettings)) {
|
if (!shouldFetchMdbListOnMetaScreen(meta, fallbackItemId, mdbListSettings)) {
|
||||||
_uiState.value = MetaDetailsUiState(meta = meta)
|
_uiState.value = MetaDetailsUiState(meta = meta.withUnreleasedFilter())
|
||||||
activeRequestKey = requestKey
|
activeRequestKey = requestKey
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -324,7 +327,7 @@ object MetaDetailsRepository {
|
||||||
metaScreenMeta = enrichedMeta,
|
metaScreenMeta = enrichedMeta,
|
||||||
metaScreenSettingsFingerprint = metaScreenSettingsFingerprint,
|
metaScreenSettingsFingerprint = metaScreenSettingsFingerprint,
|
||||||
)
|
)
|
||||||
_uiState.value = MetaDetailsUiState(meta = enrichedMeta)
|
_uiState.value = MetaDetailsUiState(meta = enrichedMeta.withUnreleasedFilter())
|
||||||
activeRequestKey = requestKey
|
activeRequestKey = requestKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -374,6 +377,15 @@ object MetaDetailsRepository {
|
||||||
return "${settings.enabled}:${settings.apiKey.trim()}:$providers"
|
return "${settings.enabled}:${settings.apiKey.trim()}:$providers"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun MetaDetails.withUnreleasedFilter(): MetaDetails {
|
||||||
|
if (!HomeCatalogSettingsRepository.snapshot().hideUnreleasedContent) return this
|
||||||
|
val todayIsoDate = CurrentDateProvider.todayIsoDate()
|
||||||
|
return copy(
|
||||||
|
moreLikeThis = moreLikeThis.filterReleasedItems(todayIsoDate),
|
||||||
|
collectionItems = collectionItems.filterReleasedItems(todayIsoDate),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun findEmbeddedStreams(videoId: String): List<com.nuvio.app.features.streams.StreamItem> {
|
fun findEmbeddedStreams(videoId: String): List<com.nuvio.app.features.streams.StreamItem> {
|
||||||
val meta = _uiState.value.meta ?: return emptyList()
|
val meta = _uiState.value.meta ?: return emptyList()
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ internal object HomeCatalogParser {
|
||||||
posterShape = meta.string("posterShape").toPosterShape(),
|
posterShape = meta.string("posterShape").toPosterShape(),
|
||||||
description = meta.string("description"),
|
description = meta.string("description"),
|
||||||
releaseInfo = meta.string("releaseInfo"),
|
releaseInfo = meta.string("releaseInfo"),
|
||||||
|
rawReleaseDate = meta.string("released"),
|
||||||
imdbRating = meta.string("imdbRating"),
|
imdbRating = meta.string("imdbRating"),
|
||||||
genres = meta.array("genres").mapNotNull { genre ->
|
genres = meta.array("genres").mapNotNull { genre ->
|
||||||
genre.jsonPrimitive.contentOrNull?.takeIf { it.isNotBlank() }
|
genre.jsonPrimitive.contentOrNull?.takeIf { it.isNotBlank() }
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,15 @@ data class HomeCatalogSettingsItem(
|
||||||
|
|
||||||
data class HomeCatalogSettingsUiState(
|
data class HomeCatalogSettingsUiState(
|
||||||
val heroEnabled: Boolean = true,
|
val heroEnabled: Boolean = true,
|
||||||
|
val hideUnreleasedContent: Boolean = false,
|
||||||
val items: List<HomeCatalogSettingsItem> = emptyList(),
|
val items: List<HomeCatalogSettingsItem> = emptyList(),
|
||||||
) {
|
) {
|
||||||
val signature: String
|
val signature: String
|
||||||
get() = buildString {
|
get() = buildString {
|
||||||
append(heroEnabled)
|
append(heroEnabled)
|
||||||
append('|')
|
append('|')
|
||||||
|
append(hideUnreleasedContent)
|
||||||
|
append('|')
|
||||||
append(
|
append(
|
||||||
items.joinToString(separator = "|") { item ->
|
items.joinToString(separator = "|") { item ->
|
||||||
"${item.key}:${item.order}:${item.enabled}:${item.heroSourceEnabled}:${item.customTitle}"
|
"${item.key}:${item.order}:${item.enabled}:${item.heroSourceEnabled}:${item.customTitle}"
|
||||||
|
|
@ -55,6 +58,7 @@ internal data class HomeCatalogPreference(
|
||||||
|
|
||||||
internal data class HomeCatalogSettingsSnapshot(
|
internal data class HomeCatalogSettingsSnapshot(
|
||||||
val heroEnabled: Boolean,
|
val heroEnabled: Boolean,
|
||||||
|
val hideUnreleasedContent: Boolean,
|
||||||
val preferences: Map<String, HomeCatalogPreference>,
|
val preferences: Map<String, HomeCatalogPreference>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -70,6 +74,7 @@ private data class StoredHomeCatalogPreference(
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class StoredHomeCatalogSettingsPayload(
|
private data class StoredHomeCatalogSettingsPayload(
|
||||||
val heroEnabled: Boolean = true,
|
val heroEnabled: Boolean = true,
|
||||||
|
val hideUnreleasedContent: Boolean = false,
|
||||||
val items: List<StoredHomeCatalogPreference> = emptyList(),
|
val items: List<StoredHomeCatalogPreference> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -89,11 +94,13 @@ object HomeCatalogSettingsRepository {
|
||||||
private var collectionDefinitions: List<CollectionCatalogDefinition> = emptyList()
|
private var collectionDefinitions: List<CollectionCatalogDefinition> = emptyList()
|
||||||
private var preferences: MutableMap<String, StoredHomeCatalogPreference> = mutableMapOf()
|
private var preferences: MutableMap<String, StoredHomeCatalogPreference> = mutableMapOf()
|
||||||
private var heroEnabled = true
|
private var heroEnabled = true
|
||||||
|
private var hideUnreleasedContent = false
|
||||||
|
|
||||||
fun onProfileChanged() {
|
fun onProfileChanged() {
|
||||||
hasLoaded = false
|
hasLoaded = false
|
||||||
preferences.clear()
|
preferences.clear()
|
||||||
heroEnabled = true
|
heroEnabled = true
|
||||||
|
hideUnreleasedContent = false
|
||||||
definitions = emptyList()
|
definitions = emptyList()
|
||||||
collectionDefinitions = emptyList()
|
collectionDefinitions = emptyList()
|
||||||
_uiState.value = HomeCatalogSettingsUiState()
|
_uiState.value = HomeCatalogSettingsUiState()
|
||||||
|
|
@ -105,6 +112,7 @@ object HomeCatalogSettingsRepository {
|
||||||
collectionDefinitions = emptyList()
|
collectionDefinitions = emptyList()
|
||||||
preferences.clear()
|
preferences.clear()
|
||||||
heroEnabled = true
|
heroEnabled = true
|
||||||
|
hideUnreleasedContent = false
|
||||||
_uiState.value = HomeCatalogSettingsUiState()
|
_uiState.value = HomeCatalogSettingsUiState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,6 +143,7 @@ object HomeCatalogSettingsRepository {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
return HomeCatalogSettingsSnapshot(
|
return HomeCatalogSettingsSnapshot(
|
||||||
heroEnabled = heroEnabled,
|
heroEnabled = heroEnabled,
|
||||||
|
hideUnreleasedContent = hideUnreleasedContent,
|
||||||
preferences = preferences.mapValues { (_, value) ->
|
preferences = preferences.mapValues { (_, value) ->
|
||||||
HomeCatalogPreference(
|
HomeCatalogPreference(
|
||||||
customTitle = value.customTitle,
|
customTitle = value.customTitle,
|
||||||
|
|
@ -154,6 +163,15 @@ object HomeCatalogSettingsRepository {
|
||||||
HomeRepository.applyCurrentSettings()
|
HomeRepository.applyCurrentSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setHideUnreleasedContent(enabled: Boolean) {
|
||||||
|
ensureLoaded()
|
||||||
|
if (hideUnreleasedContent == enabled) return
|
||||||
|
hideUnreleasedContent = enabled
|
||||||
|
publish()
|
||||||
|
persist()
|
||||||
|
HomeRepository.applyCurrentSettings()
|
||||||
|
}
|
||||||
|
|
||||||
fun setHeroSourceEnabled(key: String, enabled: Boolean) {
|
fun setHeroSourceEnabled(key: String, enabled: Boolean) {
|
||||||
updatePreference(key) { preference ->
|
updatePreference(key) { preference ->
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
|
|
@ -181,6 +199,7 @@ object HomeCatalogSettingsRepository {
|
||||||
fun resetToDefaults() {
|
fun resetToDefaults() {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
heroEnabled = true
|
heroEnabled = true
|
||||||
|
hideUnreleasedContent = false
|
||||||
preferences.clear()
|
preferences.clear()
|
||||||
normalizePreferences()
|
normalizePreferences()
|
||||||
publish()
|
publish()
|
||||||
|
|
@ -226,7 +245,9 @@ object HomeCatalogSettingsRepository {
|
||||||
|
|
||||||
if (parsedPayload != null) {
|
if (parsedPayload != null) {
|
||||||
heroEnabled = parsedPayload.heroEnabled
|
heroEnabled = parsedPayload.heroEnabled
|
||||||
|
hideUnreleasedContent = parsedPayload.hideUnreleasedContent
|
||||||
preferences = parsedPayload.items.associateBy { it.key }.toMutableMap()
|
preferences = parsedPayload.items.associateBy { it.key }.toMutableMap()
|
||||||
|
publish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,6 +256,7 @@ object HomeCatalogSettingsRepository {
|
||||||
}.getOrDefault(emptyList())
|
}.getOrDefault(emptyList())
|
||||||
|
|
||||||
preferences = legacyItems.associateBy { it.key }.toMutableMap()
|
preferences = legacyItems.associateBy { it.key }.toMutableMap()
|
||||||
|
publish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun normalizePreferences() {
|
private fun normalizePreferences() {
|
||||||
|
|
@ -322,6 +344,7 @@ object HomeCatalogSettingsRepository {
|
||||||
|
|
||||||
_uiState.value = HomeCatalogSettingsUiState(
|
_uiState.value = HomeCatalogSettingsUiState(
|
||||||
heroEnabled = heroEnabled,
|
heroEnabled = heroEnabled,
|
||||||
|
hideUnreleasedContent = hideUnreleasedContent,
|
||||||
items = items,
|
items = items,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -331,6 +354,7 @@ object HomeCatalogSettingsRepository {
|
||||||
json.encodeToString(
|
json.encodeToString(
|
||||||
StoredHomeCatalogSettingsPayload(
|
StoredHomeCatalogSettingsPayload(
|
||||||
heroEnabled = heroEnabled,
|
heroEnabled = heroEnabled,
|
||||||
|
hideUnreleasedContent = hideUnreleasedContent,
|
||||||
items = preferences.values.sortedBy { it.order },
|
items = preferences.values.sortedBy { it.order },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -411,26 +435,32 @@ object HomeCatalogSettingsRepository {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SyncHomeCatalogPayload(items = items)
|
return SyncHomeCatalogPayload(
|
||||||
|
hideUnreleasedContent = hideUnreleasedContent,
|
||||||
|
items = items,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyFromRemote(payload: SyncHomeCatalogPayload) {
|
fun applyFromRemote(payload: SyncHomeCatalogPayload) {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
val existingHeroState = preferences.mapValues { it.value.heroSourceEnabled }
|
hideUnreleasedContent = payload.hideUnreleasedContent
|
||||||
preferences = payload.items.associate { item ->
|
if (payload.items.isNotEmpty()) {
|
||||||
val key = if (item.isCollection) {
|
val existingHeroState = preferences.mapValues { it.value.heroSourceEnabled }
|
||||||
"collection_${item.collectionId}"
|
preferences = payload.items.associate { item ->
|
||||||
} else {
|
val key = if (item.isCollection) {
|
||||||
"${item.addonId}:${item.type}:${item.catalogId}"
|
"collection_${item.collectionId}"
|
||||||
}
|
} else {
|
||||||
key to StoredHomeCatalogPreference(
|
"${item.addonId}:${item.type}:${item.catalogId}"
|
||||||
key = key,
|
}
|
||||||
customTitle = item.customTitle,
|
key to StoredHomeCatalogPreference(
|
||||||
enabled = item.enabled,
|
key = key,
|
||||||
heroSourceEnabled = existingHeroState[key] ?: true,
|
customTitle = item.customTitle,
|
||||||
order = item.order,
|
enabled = item.enabled,
|
||||||
)
|
heroSourceEnabled = existingHeroState[key] ?: true,
|
||||||
}.toMutableMap()
|
order = item.order,
|
||||||
|
)
|
||||||
|
}.toMutableMap()
|
||||||
|
}
|
||||||
hasLoaded = true
|
hasLoaded = true
|
||||||
publish()
|
publish()
|
||||||
persist()
|
persist()
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ data class SyncCatalogItem(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SyncHomeCatalogPayload(
|
data class SyncHomeCatalogPayload(
|
||||||
|
@SerialName("hide_unreleased_content") val hideUnreleasedContent: Boolean = false,
|
||||||
val items: List<SyncCatalogItem> = emptyList(),
|
val items: List<SyncCatalogItem> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -101,7 +102,10 @@ object HomeCatalogSettingsSyncService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remotePayload.items.isEmpty()) {
|
if (remotePayload.items.isEmpty()) {
|
||||||
log.i { "pullFromServer — remote has empty items, preserving local" }
|
log.i { "pullFromServer — remote has empty items, preserving local catalog order" }
|
||||||
|
isSyncingFromRemote = true
|
||||||
|
HomeCatalogSettingsRepository.applyFromRemote(remotePayload)
|
||||||
|
isSyncingFromRemote = false
|
||||||
val localPayload = HomeCatalogSettingsRepository.exportToSyncPayload()
|
val localPayload = HomeCatalogSettingsRepository.exportToSyncPayload()
|
||||||
if (localPayload.items.isNotEmpty()) {
|
if (localPayload.items.isNotEmpty()) {
|
||||||
pushToRemote(profileId)
|
pushToRemote(profileId)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.nuvio.app.features.home
|
||||||
|
|
||||||
import com.nuvio.app.features.addons.ManagedAddon
|
import com.nuvio.app.features.addons.ManagedAddon
|
||||||
import com.nuvio.app.features.catalog.fetchCatalogPage
|
import com.nuvio.app.features.catalog.fetchCatalogPage
|
||||||
|
import com.nuvio.app.features.watchprogress.CurrentDateProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -145,13 +146,17 @@ object HomeRepository {
|
||||||
) {
|
) {
|
||||||
val snapshot = HomeCatalogSettingsRepository.snapshot()
|
val snapshot = HomeCatalogSettingsRepository.snapshot()
|
||||||
val preferences = snapshot.preferences
|
val preferences = snapshot.preferences
|
||||||
|
val todayIsoDate = if (snapshot.hideUnreleasedContent) CurrentDateProvider.todayIsoDate() else null
|
||||||
|
fun HomeCatalogSection.withReleaseFilter(): HomeCatalogSection =
|
||||||
|
if (todayIsoDate == null) this else filterReleasedItems(todayIsoDate)
|
||||||
|
|
||||||
val sections = currentDefinitions
|
val sections = currentDefinitions
|
||||||
.sortedBy { definition -> preferences[definition.key]?.order ?: Int.MAX_VALUE }
|
.sortedBy { definition -> preferences[definition.key]?.order ?: Int.MAX_VALUE }
|
||||||
.mapNotNull { definition ->
|
.mapNotNull { definition ->
|
||||||
val preference = preferences[definition.key]
|
val preference = preferences[definition.key]
|
||||||
if (preference?.enabled == false) return@mapNotNull null
|
if (preference?.enabled == false) return@mapNotNull null
|
||||||
|
|
||||||
val section = cachedSections[definition.key] ?: return@mapNotNull null
|
val section = cachedSections[definition.key]?.withReleaseFilter() ?: return@mapNotNull null
|
||||||
if (section.items.isEmpty()) return@mapNotNull null
|
if (section.items.isEmpty()) return@mapNotNull null
|
||||||
val customTitle = preference?.customTitle.orEmpty()
|
val customTitle = preference?.customTitle.orEmpty()
|
||||||
section.copy(
|
section.copy(
|
||||||
|
|
@ -164,6 +169,7 @@ object HomeRepository {
|
||||||
currentDefinitions
|
currentDefinitions
|
||||||
.filter { definition -> preferences[definition.key]?.heroSourceEnabled != false }
|
.filter { definition -> preferences[definition.key]?.heroSourceEnabled != false }
|
||||||
.mapNotNull { definition -> cachedSections[definition.key] }
|
.mapNotNull { definition -> cachedSections[definition.key] }
|
||||||
|
.map { section -> section.withReleaseFilter() }
|
||||||
.flatMap { section -> section.items }
|
.flatMap { section -> section.items }
|
||||||
.distinctBy { item -> "${item.type}:${item.id}" }
|
.distinctBy { item -> "${item.type}:${item.id}" }
|
||||||
.shuffled(heroRandom)
|
.shuffled(heroRandom)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.nuvio.app.features.home
|
||||||
|
|
||||||
|
private val yearRegex = Regex("""\b(19|20)\d{2}\b""")
|
||||||
|
private val isoDateRegex = Regex("""\d{4}-\d{2}-\d{2}""")
|
||||||
|
|
||||||
|
internal fun MetaPreview.isUnreleased(todayIsoDate: String): Boolean {
|
||||||
|
rawReleaseDate
|
||||||
|
?.trim()
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
?.let { rawReleased ->
|
||||||
|
isoCalendarDateOrNull(rawReleased.substringBefore('T'))?.let { releaseDate ->
|
||||||
|
return releaseDate > todayIsoDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val info = releaseInfo ?: return false
|
||||||
|
isoCalendarDateOrNull(info.trim())?.let { releaseDate ->
|
||||||
|
return releaseDate > todayIsoDate
|
||||||
|
}
|
||||||
|
|
||||||
|
val releaseYear = yearRegex.find(info)?.value?.toIntOrNull() ?: return false
|
||||||
|
val currentYear = todayIsoDate.take(4).toIntOrNull() ?: return false
|
||||||
|
return releaseYear > currentYear
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun HomeCatalogSection.filterReleasedItems(todayIsoDate: String): HomeCatalogSection {
|
||||||
|
val filteredItems = items.filterReleasedItems(todayIsoDate)
|
||||||
|
return if (filteredItems.size == items.size) this else copy(items = filteredItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun List<MetaPreview>.filterReleasedItems(todayIsoDate: String): List<MetaPreview> =
|
||||||
|
filterNot { item -> item.isUnreleased(todayIsoDate) }
|
||||||
|
|
||||||
|
private fun isoCalendarDateOrNull(value: String?): String? {
|
||||||
|
val date = value?.trim()?.takeIf { isoDateRegex.matches(it) } ?: return null
|
||||||
|
val year = date.substring(0, 4).toIntOrNull() ?: return null
|
||||||
|
val month = date.substring(5, 7).toIntOrNull()?.takeIf { it in 1..12 } ?: return null
|
||||||
|
val day = date.substring(8, 10).toIntOrNull() ?: return null
|
||||||
|
if (day !in 1..daysInMonth(year, month)) return null
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun daysInMonth(year: Int, month: Int): Int =
|
||||||
|
when (month) {
|
||||||
|
2 -> if (isLeapYear(year)) 29 else 28
|
||||||
|
4, 6, 9, 11 -> 30
|
||||||
|
else -> 31
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isLeapYear(year: Int): Boolean =
|
||||||
|
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
|
||||||
|
|
@ -5,12 +5,16 @@ import com.nuvio.app.core.i18n.localizedMediaTypeLabel
|
||||||
import com.nuvio.app.features.addons.AddonCatalog
|
import com.nuvio.app.features.addons.AddonCatalog
|
||||||
import com.nuvio.app.features.addons.AddonExtraProperty
|
import com.nuvio.app.features.addons.AddonExtraProperty
|
||||||
import com.nuvio.app.features.addons.ManagedAddon
|
import com.nuvio.app.features.addons.ManagedAddon
|
||||||
|
import com.nuvio.app.features.catalog.CatalogPage
|
||||||
import com.nuvio.app.features.catalog.buildCatalogUrl
|
import com.nuvio.app.features.catalog.buildCatalogUrl
|
||||||
import com.nuvio.app.features.catalog.fetchCatalogPage
|
import com.nuvio.app.features.catalog.fetchCatalogPage
|
||||||
import com.nuvio.app.features.catalog.mergeCatalogItems
|
import com.nuvio.app.features.catalog.mergeCatalogItems
|
||||||
import com.nuvio.app.features.catalog.supportsPagination
|
import com.nuvio.app.features.catalog.supportsPagination
|
||||||
|
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||||
import com.nuvio.app.features.home.HomeCatalogSection
|
import com.nuvio.app.features.home.HomeCatalogSection
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
|
import com.nuvio.app.features.home.filterReleasedItems
|
||||||
|
import com.nuvio.app.features.watchprogress.CurrentDateProvider
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -37,6 +41,7 @@ object SearchRepository {
|
||||||
private var activeDiscoverJob: Job? = null
|
private var activeDiscoverJob: Job? = null
|
||||||
private var lastRequestKey: String? = null
|
private var lastRequestKey: String? = null
|
||||||
private var discoverSources: List<DiscoverCatalogOption> = emptyList()
|
private var discoverSources: List<DiscoverCatalogOption> = emptyList()
|
||||||
|
private var lastDiscoverHideUnreleasedContent: Boolean? = null
|
||||||
|
|
||||||
fun search(query: String, addons: List<ManagedAddon>) {
|
fun search(query: String, addons: List<ManagedAddon>) {
|
||||||
val normalizedQuery = query.trim()
|
val normalizedQuery = query.trim()
|
||||||
|
|
@ -71,6 +76,8 @@ object SearchRepository {
|
||||||
val requestKey = buildString {
|
val requestKey = buildString {
|
||||||
append(normalizedQuery.lowercase())
|
append(normalizedQuery.lowercase())
|
||||||
append('|')
|
append('|')
|
||||||
|
append(HomeCatalogSettingsRepository.snapshot().hideUnreleasedContent)
|
||||||
|
append('|')
|
||||||
append(
|
append(
|
||||||
requests.joinToString(separator = "|") { request ->
|
requests.joinToString(separator = "|") { request ->
|
||||||
"${request.addon.manifestUrl}:${request.type}:${request.catalogId}"
|
"${request.addon.manifestUrl}:${request.type}:${request.catalogId}"
|
||||||
|
|
@ -119,6 +126,7 @@ object SearchRepository {
|
||||||
activeDiscoverJob?.cancel()
|
activeDiscoverJob?.cancel()
|
||||||
lastRequestKey = null
|
lastRequestKey = null
|
||||||
discoverSources = emptyList()
|
discoverSources = emptyList()
|
||||||
|
lastDiscoverHideUnreleasedContent = null
|
||||||
_uiState.value = SearchUiState()
|
_uiState.value = SearchUiState()
|
||||||
_discoverUiState.value = DiscoverUiState()
|
_discoverUiState.value = DiscoverUiState()
|
||||||
}
|
}
|
||||||
|
|
@ -128,6 +136,7 @@ object SearchRepository {
|
||||||
if (activeAddons.isEmpty()) {
|
if (activeAddons.isEmpty()) {
|
||||||
activeDiscoverJob?.cancel()
|
activeDiscoverJob?.cancel()
|
||||||
discoverSources = emptyList()
|
discoverSources = emptyList()
|
||||||
|
lastDiscoverHideUnreleasedContent = null
|
||||||
log.d { "Discover refresh aborted: no active addons" }
|
log.d { "Discover refresh aborted: no active addons" }
|
||||||
_discoverUiState.value = DiscoverUiState(
|
_discoverUiState.value = DiscoverUiState(
|
||||||
emptyStateReason = DiscoverEmptyStateReason.NoActiveAddons,
|
emptyStateReason = DiscoverEmptyStateReason.NoActiveAddons,
|
||||||
|
|
@ -137,7 +146,12 @@ object SearchRepository {
|
||||||
|
|
||||||
val sources = buildDiscoverSources(activeAddons)
|
val sources = buildDiscoverSources(activeAddons)
|
||||||
val current = _discoverUiState.value
|
val current = _discoverUiState.value
|
||||||
if (sources == discoverSources && current.canReuseDiscoverState(sources)) {
|
val hideUnreleasedContent = HomeCatalogSettingsRepository.snapshot().hideUnreleasedContent
|
||||||
|
if (
|
||||||
|
sources == discoverSources &&
|
||||||
|
lastDiscoverHideUnreleasedContent == hideUnreleasedContent &&
|
||||||
|
current.canReuseDiscoverState(sources)
|
||||||
|
) {
|
||||||
log.d {
|
log.d {
|
||||||
"Reusing discover state type=${current.selectedType} catalog=${current.selectedCatalogKey} " +
|
"Reusing discover state type=${current.selectedType} catalog=${current.selectedCatalogKey} " +
|
||||||
"genre=${current.selectedGenre ?: "<all>"} items=${current.items.size} nextSkip=${current.nextSkip}"
|
"genre=${current.selectedGenre ?: "<all>"} items=${current.items.size} nextSkip=${current.nextSkip}"
|
||||||
|
|
@ -146,6 +160,7 @@ object SearchRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
discoverSources = sources
|
discoverSources = sources
|
||||||
|
lastDiscoverHideUnreleasedContent = hideUnreleasedContent
|
||||||
if (sources.isEmpty()) {
|
if (sources.isEmpty()) {
|
||||||
activeDiscoverJob?.cancel()
|
activeDiscoverJob?.cancel()
|
||||||
log.d { "Discover refresh found no compatible discover catalogs" }
|
log.d { "Discover refresh found no compatible discover catalogs" }
|
||||||
|
|
@ -310,7 +325,7 @@ object SearchRepository {
|
||||||
type = type,
|
type = type,
|
||||||
catalogId = catalogId,
|
catalogId = catalogId,
|
||||||
search = query,
|
search = query,
|
||||||
)
|
).withUnreleasedFilter()
|
||||||
val items = page.items
|
val items = page.items
|
||||||
require(items.isNotEmpty()) { "No search results returned for $catalogName." }
|
require(items.isNotEmpty()) { "No search results returned for $catalogName." }
|
||||||
|
|
||||||
|
|
@ -364,7 +379,7 @@ object SearchRepository {
|
||||||
catalogId = selectedCatalog.catalogId,
|
catalogId = selectedCatalog.catalogId,
|
||||||
genre = current.selectedGenre,
|
genre = current.selectedGenre,
|
||||||
skip = requestedSkip.takeIf { it > 0 },
|
skip = requestedSkip.takeIf { it > 0 },
|
||||||
)
|
).withUnreleasedFilter()
|
||||||
}.fold(
|
}.fold(
|
||||||
onSuccess = { page ->
|
onSuccess = { page ->
|
||||||
val latest = _discoverUiState.value
|
val latest = _discoverUiState.value
|
||||||
|
|
@ -421,6 +436,12 @@ object SearchRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CatalogPage.withUnreleasedFilter(): CatalogPage {
|
||||||
|
if (!HomeCatalogSettingsRepository.snapshot().hideUnreleasedContent) return this
|
||||||
|
val filteredItems = items.filterReleasedItems(CurrentDateProvider.todayIsoDate())
|
||||||
|
return if (filteredItems.size == items.size) this else copy(items = filteredItems)
|
||||||
|
}
|
||||||
|
|
||||||
private data class SearchCatalogRequest(
|
private data class SearchCatalogRequest(
|
||||||
val addon: ManagedAddon,
|
val addon: ManagedAddon,
|
||||||
val catalogId: String,
|
val catalogId: String,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
|
||||||
import com.nuvio.app.core.ui.NuvioScreenHeader
|
import com.nuvio.app.core.ui.NuvioScreenHeader
|
||||||
import com.nuvio.app.core.ui.withDuplicateSafeLazyKeys
|
import com.nuvio.app.core.ui.withDuplicateSafeLazyKeys
|
||||||
import com.nuvio.app.features.addons.AddonRepository
|
import com.nuvio.app.features.addons.AddonRepository
|
||||||
|
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
import com.nuvio.app.features.home.components.HomeCatalogRowSection
|
import com.nuvio.app.features.home.components.HomeCatalogRowSection
|
||||||
import com.nuvio.app.features.home.components.HomeEmptyStateCard
|
import com.nuvio.app.features.home.components.HomeEmptyStateCard
|
||||||
|
|
@ -88,6 +89,7 @@ fun SearchScreen(
|
||||||
val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle()
|
val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle()
|
||||||
val uiState by SearchRepository.uiState.collectAsStateWithLifecycle()
|
val uiState by SearchRepository.uiState.collectAsStateWithLifecycle()
|
||||||
val discoverUiState by SearchRepository.discoverUiState.collectAsStateWithLifecycle()
|
val discoverUiState by SearchRepository.discoverUiState.collectAsStateWithLifecycle()
|
||||||
|
val homeCatalogSettingsUiState by HomeCatalogSettingsRepository.uiState.collectAsStateWithLifecycle()
|
||||||
val recentSearches by SearchHistoryRepository.uiState.collectAsStateWithLifecycle()
|
val recentSearches by SearchHistoryRepository.uiState.collectAsStateWithLifecycle()
|
||||||
val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle()
|
val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle()
|
||||||
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
|
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
@ -123,11 +125,11 @@ fun SearchScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(addonRefreshKey) {
|
LaunchedEffect(addonRefreshKey, homeCatalogSettingsUiState.hideUnreleasedContent) {
|
||||||
SearchRepository.refreshDiscover(addonsUiState.addons)
|
SearchRepository.refreshDiscover(addonsUiState.addons)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(query, addonRefreshKey) {
|
LaunchedEffect(query, addonRefreshKey, homeCatalogSettingsUiState.hideUnreleasedContent) {
|
||||||
val normalizedQuery = query.trim()
|
val normalizedQuery = query.trim()
|
||||||
if (normalizedQuery.isBlank()) {
|
if (normalizedQuery.isBlank()) {
|
||||||
lastRequestedQuery = null
|
lastRequestedQuery = null
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||||
import com.nuvio.app.features.home.components.HomeEmptyStateCard
|
import com.nuvio.app.features.home.components.HomeEmptyStateCard
|
||||||
import nuvio.composeapp.generated.resources.Res
|
import nuvio.composeapp.generated.resources.Res
|
||||||
import nuvio.composeapp.generated.resources.action_reset
|
import nuvio.composeapp.generated.resources.action_reset
|
||||||
|
import nuvio.composeapp.generated.resources.layout_hide_unreleased
|
||||||
|
import nuvio.composeapp.generated.resources.layout_hide_unreleased_sub
|
||||||
import nuvio.composeapp.generated.resources.settings_homescreen_empty_message
|
import nuvio.composeapp.generated.resources.settings_homescreen_empty_message
|
||||||
import nuvio.composeapp.generated.resources.settings_homescreen_empty_title
|
import nuvio.composeapp.generated.resources.settings_homescreen_empty_title
|
||||||
import nuvio.composeapp.generated.resources.settings_homescreen_keep_home_focused
|
import nuvio.composeapp.generated.resources.settings_homescreen_keep_home_focused
|
||||||
|
|
@ -62,6 +64,7 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
internal fun LazyListScope.homescreenSettingsContent(
|
internal fun LazyListScope.homescreenSettingsContent(
|
||||||
isTablet: Boolean,
|
isTablet: Boolean,
|
||||||
heroEnabled: Boolean,
|
heroEnabled: Boolean,
|
||||||
|
hideUnreleasedContent: Boolean,
|
||||||
items: List<HomeCatalogSettingsItem>,
|
items: List<HomeCatalogSettingsItem>,
|
||||||
) {
|
) {
|
||||||
val selectedHeroSourceCount = items.count { it.heroSourceEnabled }
|
val selectedHeroSourceCount = items.count { it.heroSourceEnabled }
|
||||||
|
|
@ -87,6 +90,14 @@ internal fun LazyListScope.homescreenSettingsContent(
|
||||||
isTablet = isTablet,
|
isTablet = isTablet,
|
||||||
onCheckedChange = HomeCatalogSettingsRepository::setHeroEnabled,
|
onCheckedChange = HomeCatalogSettingsRepository::setHeroEnabled,
|
||||||
)
|
)
|
||||||
|
SettingsGroupDivider(isTablet = isTablet)
|
||||||
|
SettingsSwitchRow(
|
||||||
|
title = stringResource(Res.string.layout_hide_unreleased),
|
||||||
|
description = stringResource(Res.string.layout_hide_unreleased_sub),
|
||||||
|
checked = hideUnreleasedContent,
|
||||||
|
isTablet = isTablet,
|
||||||
|
onCheckedChange = HomeCatalogSettingsRepository::setHideUnreleasedContent,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,10 @@ fun HomescreenSettingsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val homescreenSettingsUiState by HomeCatalogSettingsRepository.uiState.collectAsStateWithLifecycle()
|
val homescreenSettingsUiState by remember {
|
||||||
|
HomeCatalogSettingsRepository.snapshot()
|
||||||
|
HomeCatalogSettingsRepository.uiState
|
||||||
|
}.collectAsStateWithLifecycle()
|
||||||
val collections by CollectionRepository.collections.collectAsStateWithLifecycle()
|
val collections by CollectionRepository.collections.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
@ -74,6 +77,7 @@ fun HomescreenSettingsScreen(
|
||||||
homescreenSettingsContent(
|
homescreenSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
heroEnabled = homescreenSettingsUiState.heroEnabled,
|
heroEnabled = homescreenSettingsUiState.heroEnabled,
|
||||||
|
hideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent,
|
||||||
items = homescreenSettingsUiState.items,
|
items = homescreenSettingsUiState.items,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val homescreenSettingsUiState by remember {
|
val homescreenSettingsUiState by remember {
|
||||||
|
HomeCatalogSettingsRepository.snapshot()
|
||||||
HomeCatalogSettingsRepository.uiState
|
HomeCatalogSettingsRepository.uiState
|
||||||
}.collectAsStateWithLifecycle()
|
}.collectAsStateWithLifecycle()
|
||||||
val metaScreenSettingsUiState by remember {
|
val metaScreenSettingsUiState by remember {
|
||||||
|
|
@ -199,6 +200,7 @@ fun SettingsScreen(
|
||||||
traktCommentsEnabled = traktCommentsEnabled,
|
traktCommentsEnabled = traktCommentsEnabled,
|
||||||
traktSettingsUiState = traktSettingsUiState,
|
traktSettingsUiState = traktSettingsUiState,
|
||||||
homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled,
|
homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled,
|
||||||
|
homescreenHideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent,
|
||||||
homescreenItems = homescreenSettingsUiState.items,
|
homescreenItems = homescreenSettingsUiState.items,
|
||||||
metaScreenSettingsUiState = metaScreenSettingsUiState,
|
metaScreenSettingsUiState = metaScreenSettingsUiState,
|
||||||
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
|
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
|
||||||
|
|
@ -240,6 +242,7 @@ fun SettingsScreen(
|
||||||
traktCommentsEnabled = traktCommentsEnabled,
|
traktCommentsEnabled = traktCommentsEnabled,
|
||||||
traktSettingsUiState = traktSettingsUiState,
|
traktSettingsUiState = traktSettingsUiState,
|
||||||
homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled,
|
homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled,
|
||||||
|
homescreenHideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent,
|
||||||
homescreenItems = homescreenSettingsUiState.items,
|
homescreenItems = homescreenSettingsUiState.items,
|
||||||
metaScreenSettingsUiState = metaScreenSettingsUiState,
|
metaScreenSettingsUiState = metaScreenSettingsUiState,
|
||||||
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
|
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
|
||||||
|
|
@ -291,6 +294,7 @@ private fun MobileSettingsScreen(
|
||||||
traktCommentsEnabled: Boolean,
|
traktCommentsEnabled: Boolean,
|
||||||
traktSettingsUiState: TraktSettingsUiState,
|
traktSettingsUiState: TraktSettingsUiState,
|
||||||
homescreenHeroEnabled: Boolean,
|
homescreenHeroEnabled: Boolean,
|
||||||
|
homescreenHideUnreleasedContent: Boolean,
|
||||||
homescreenItems: List<HomeCatalogSettingsItem>,
|
homescreenItems: List<HomeCatalogSettingsItem>,
|
||||||
metaScreenSettingsUiState: MetaScreenSettingsUiState,
|
metaScreenSettingsUiState: MetaScreenSettingsUiState,
|
||||||
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
|
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
|
||||||
|
|
@ -399,6 +403,7 @@ private fun MobileSettingsScreen(
|
||||||
SettingsPage.Homescreen -> homescreenSettingsContent(
|
SettingsPage.Homescreen -> homescreenSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
heroEnabled = homescreenHeroEnabled,
|
heroEnabled = homescreenHeroEnabled,
|
||||||
|
hideUnreleasedContent = homescreenHideUnreleasedContent,
|
||||||
items = homescreenItems,
|
items = homescreenItems,
|
||||||
)
|
)
|
||||||
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
||||||
|
|
@ -461,6 +466,7 @@ private fun TabletSettingsScreen(
|
||||||
traktCommentsEnabled: Boolean,
|
traktCommentsEnabled: Boolean,
|
||||||
traktSettingsUiState: TraktSettingsUiState,
|
traktSettingsUiState: TraktSettingsUiState,
|
||||||
homescreenHeroEnabled: Boolean,
|
homescreenHeroEnabled: Boolean,
|
||||||
|
homescreenHideUnreleasedContent: Boolean,
|
||||||
homescreenItems: List<HomeCatalogSettingsItem>,
|
homescreenItems: List<HomeCatalogSettingsItem>,
|
||||||
metaScreenSettingsUiState: MetaScreenSettingsUiState,
|
metaScreenSettingsUiState: MetaScreenSettingsUiState,
|
||||||
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
|
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
|
||||||
|
|
@ -640,6 +646,7 @@ private fun TabletSettingsScreen(
|
||||||
SettingsPage.Homescreen -> homescreenSettingsContent(
|
SettingsPage.Homescreen -> homescreenSettingsContent(
|
||||||
isTablet = true,
|
isTablet = true,
|
||||||
heroEnabled = homescreenHeroEnabled,
|
heroEnabled = homescreenHeroEnabled,
|
||||||
|
hideUnreleasedContent = homescreenHideUnreleasedContent,
|
||||||
items = homescreenItems,
|
items = homescreenItems,
|
||||||
)
|
)
|
||||||
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
||||||
|
|
|
||||||
|
|
@ -1052,6 +1052,7 @@ object TmdbMetadataService {
|
||||||
posterShape = PosterShape.Poster,
|
posterShape = PosterShape.Poster,
|
||||||
description = recommendation.overview?.trim()?.takeIf(String::isNotBlank),
|
description = recommendation.overview?.trim()?.takeIf(String::isNotBlank),
|
||||||
releaseInfo = (recommendation.releaseDate ?: recommendation.firstAirDate)?.take(4),
|
releaseInfo = (recommendation.releaseDate ?: recommendation.firstAirDate)?.take(4),
|
||||||
|
rawReleaseDate = recommendation.releaseDate ?: recommendation.firstAirDate,
|
||||||
imdbRating = recommendation.voteAverage?.formatRating(),
|
imdbRating = recommendation.voteAverage?.formatRating(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1087,6 +1088,7 @@ object TmdbMetadataService {
|
||||||
posterShape = PosterShape.Landscape,
|
posterShape = PosterShape.Landscape,
|
||||||
description = part.overview?.trim()?.takeIf(String::isNotBlank),
|
description = part.overview?.trim()?.takeIf(String::isNotBlank),
|
||||||
releaseInfo = part.releaseDate?.take(4),
|
releaseInfo = part.releaseDate?.take(4),
|
||||||
|
rawReleaseDate = part.releaseDate,
|
||||||
imdbRating = part.voteAverage?.formatRating(),
|
imdbRating = part.voteAverage?.formatRating(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,4 +49,26 @@ class HomeCatalogParserTest {
|
||||||
result.items.map { it.stableKey() },
|
result.items.map { it.stableKey() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse catalog response keeps raw released date for unreleased filtering`() {
|
||||||
|
val result = HomeCatalogParser.parseCatalogResponse(
|
||||||
|
payload = """
|
||||||
|
{
|
||||||
|
"metas": [
|
||||||
|
{
|
||||||
|
"id": "tt1",
|
||||||
|
"type": "movie",
|
||||||
|
"name": "Future Movie",
|
||||||
|
"releaseInfo": "2027",
|
||||||
|
"released": "2027-05-12T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("2027", result.items.single().releaseInfo)
|
||||||
|
assertEquals("2027-05-12T00:00:00.000Z", result.items.single().rawReleaseDate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package com.nuvio.app.features.home
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class ReleaseInfoUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `raw released date after today is unreleased`() {
|
||||||
|
val item = preview(rawReleaseDate = "2026-06-15T00:00:00.000Z", releaseInfo = "2026")
|
||||||
|
|
||||||
|
assertTrue(item.isUnreleased(todayIsoDate = "2026-05-06"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `release info full date after today is unreleased`() {
|
||||||
|
val item = preview(rawReleaseDate = null, releaseInfo = "2026-06-15")
|
||||||
|
|
||||||
|
assertTrue(item.isUnreleased(todayIsoDate = "2026-05-06"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `future release info year is unreleased`() {
|
||||||
|
val item = preview(rawReleaseDate = null, releaseInfo = "Coming in 2027")
|
||||||
|
|
||||||
|
assertTrue(item.isUnreleased(todayIsoDate = "2026-05-06"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `released and unknown dates are kept`() {
|
||||||
|
assertFalse(preview(rawReleaseDate = "2026-05-06", releaseInfo = "2026").isUnreleased("2026-05-06"))
|
||||||
|
assertFalse(preview(rawReleaseDate = "2026-05-05", releaseInfo = "2026").isUnreleased("2026-05-06"))
|
||||||
|
assertFalse(preview(rawReleaseDate = null, releaseInfo = null).isUnreleased("2026-05-06"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `catalog section filters unreleased items`() {
|
||||||
|
val section = HomeCatalogSection(
|
||||||
|
key = "addon:movie:popular",
|
||||||
|
title = "Popular",
|
||||||
|
subtitle = "Addon",
|
||||||
|
addonName = "Addon",
|
||||||
|
type = "movie",
|
||||||
|
manifestUrl = "https://example.com/manifest.json",
|
||||||
|
catalogId = "popular",
|
||||||
|
items = listOf(
|
||||||
|
preview(id = "released", rawReleaseDate = "2026-05-01", releaseInfo = "2026"),
|
||||||
|
preview(id = "future", rawReleaseDate = "2026-07-01", releaseInfo = "2026"),
|
||||||
|
),
|
||||||
|
availableItemCount = 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = section.filterReleasedItems(todayIsoDate = "2026-05-06")
|
||||||
|
|
||||||
|
assertEquals(listOf("released"), result.items.map { it.id })
|
||||||
|
assertEquals(2, result.availableItemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun preview(
|
||||||
|
id: String = "tt1",
|
||||||
|
rawReleaseDate: String?,
|
||||||
|
releaseInfo: String?,
|
||||||
|
): MetaPreview = MetaPreview(
|
||||||
|
id = id,
|
||||||
|
type = "movie",
|
||||||
|
name = id,
|
||||||
|
rawReleaseDate = rawReleaseDate,
|
||||||
|
releaseInfo = releaseInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue