Fixes #79, added setting to hide none released movies and show

This commit is contained in:
CrissZollo 2026-02-27 20:53:15 +01:00
parent f1d57a5865
commit cd5952709c
No known key found for this signature in database
10 changed files with 151 additions and 13 deletions

View file

@ -0,0 +1,30 @@
package com.nuvio.tv.core.util
import com.nuvio.tv.domain.model.CatalogRow
import com.nuvio.tv.domain.model.MetaPreview
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
private val YEAR_REGEX = Regex("""\b(19|20)\d{2}\b""")
private val ISO_DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE
fun MetaPreview.isUnreleased(today: LocalDate): Boolean {
val info = releaseInfo ?: return false
// Try full date parse first (e.g. "2026-06-15")
try {
val date = LocalDate.parse(info.trim(), ISO_DATE_FORMATTER)
return date.isAfter(today)
} catch (_: DateTimeParseException) {
// fall through to year-only
}
// Fall back to year extraction
val yearStr = YEAR_REGEX.find(info)?.value ?: return false
val year = yearStr.toIntOrNull() ?: return false
return year > today.year
}
fun CatalogRow.filterReleasedItems(today: LocalDate): CatalogRow {
val filtered = items.filterNot { it.isUnreleased(today) }
return if (filtered.size == items.size) this else copy(items = filtered)
}

View file

@ -62,6 +62,7 @@ class LayoutPreferenceDataStore @Inject constructor(
private val blurUnwatchedEpisodesKey = booleanPreferencesKey("blur_unwatched_episodes")
private val detailPageTrailerButtonEnabledKey = booleanPreferencesKey("detail_page_trailer_button_enabled")
private val preferExternalMetaAddonDetailKey = booleanPreferencesKey("prefer_external_meta_addon_detail")
private val hideUnreleasedContentKey = booleanPreferencesKey("hide_unreleased_content")
private fun <T> profileFlow(extract: (prefs: androidx.datastore.preferences.core.Preferences) -> T): Flow<T> =
profileManager.activeProfileId.flatMapLatest { pid ->
@ -198,6 +199,10 @@ class LayoutPreferenceDataStore @Inject constructor(
prefs[preferExternalMetaAddonDetailKey] ?: false
}
val hideUnreleasedContent: Flow<Boolean> = profileFlow { prefs ->
prefs[hideUnreleasedContentKey] ?: false
}
suspend fun setLayout(layout: HomeLayout) {
store().edit { prefs ->
val hadChosenLayout = prefs[hasChosenKey] ?: false
@ -389,6 +394,12 @@ class LayoutPreferenceDataStore @Inject constructor(
}
}
suspend fun setHideUnreleasedContent(enabled: Boolean) {
store().edit { prefs ->
prefs[hideUnreleasedContentKey] = enabled
}
}
private fun parseCatalogKeys(json: String?): List<String> {
if (json.isNullOrBlank()) return emptyList()
return try {

View file

@ -27,6 +27,8 @@ import com.nuvio.tv.domain.repository.WatchProgressRepository
import com.nuvio.tv.data.local.WatchedItemsPreferences
import com.nuvio.tv.data.local.TrailerSettingsDataStore
import com.nuvio.tv.data.trailer.TrailerService
import com.nuvio.tv.core.util.isUnreleased
import java.time.LocalDate
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
@ -80,6 +82,7 @@ class MetaDetailsViewModel @Inject constructor(
private var trailerAutoplayEnabled = false
private var isPlayButtonFocused = false
private var hideUnreleasedContent = false
init {
observeMetaViewSettings()
@ -89,9 +92,20 @@ class MetaDetailsViewModel @Inject constructor(
observeWatchedEpisodes()
observeMovieWatched()
observeBlurUnwatchedEpisodes()
observeHideUnreleasedContent()
loadMeta()
}
private fun observeHideUnreleasedContent() {
viewModelScope.launch {
layoutPreferenceDataStore.hideUnreleasedContent
.distinctUntilChanged()
.collectLatest { enabled ->
hideUnreleasedContent = enabled
}
}
}
private fun observeMetaViewSettings() {
viewModelScope.launch {
layoutPreferenceDataStore.detailPageTrailerButtonEnabled
@ -444,7 +458,7 @@ class MetaDetailsViewModel @Inject constructor(
return@launch
}
val recommendations = runCatching {
val rawRecommendations = runCatching {
tmdbMetadataService.fetchMoreLikeThis(
tmdbId = tmdbId,
contentType = tmdbContentType,
@ -455,6 +469,13 @@ class MetaDetailsViewModel @Inject constructor(
emptyList()
}
val recommendations = if (hideUnreleasedContent) {
val today = LocalDate.now()
rawRecommendations.filterNot { it.isUnreleased(today) }
} else {
rawRecommendations
}
_uiState.update { state ->
if (state.meta == null || state.meta.id == meta.id) {
state.copy(moreLikeThis = recommendations)

View file

@ -45,7 +45,8 @@ data class HomeUiState(
val posterListPickerMembership: Map<String, Boolean> = emptyMap(),
val posterListPickerPending: Boolean = false,
val posterListPickerError: String? = null,
val gridItems: List<GridItem> = emptyList()
val gridItems: List<GridItem> = emptyList(),
val hideUnreleasedContent: Boolean = false
)
@Immutable

View file

@ -14,7 +14,9 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withPermit
import com.nuvio.tv.core.util.filterReleasedItems
import kotlinx.coroutines.withContext
import java.time.LocalDate
internal fun HomeViewModel.loadHomeCatalogOrderPreferencePipeline() {
viewModelScope.launch {
@ -259,9 +261,16 @@ internal suspend fun HomeViewModel.updateCatalogRowsPipeline() {
val currentLayout = _uiState.value.homeLayout
val currentGridItems = _uiState.value.gridItems
val heroSectionEnabled = _uiState.value.heroSectionEnabled
val hideUnreleased = _uiState.value.hideUnreleasedContent
val (displayRows, baseHeroItems, baseGridItems) = withContext(Dispatchers.Default) {
val orderedRows = orderedKeys.mapNotNull { key -> catalogSnapshot[key] }
val rawRows = orderedKeys.mapNotNull { key -> catalogSnapshot[key] }
val orderedRows = if (hideUnreleased) {
val today = LocalDate.now()
rawRows.map { it.filterReleasedItems(today) }
} else {
rawRows
}
val selectedHeroCatalogSet = heroCatalogKeys.toSet()
val selectedHeroRows = if (selectedHeroCatalogSet.isNotEmpty()) {
orderedRows.filter { row ->
@ -364,7 +373,13 @@ internal suspend fun HomeViewModel.updateCatalogRowsPipeline() {
Triple(computedDisplayRows, computedHeroItems, computedGridItems)
}
val fullRows = orderedKeys.mapNotNull { key -> catalogSnapshot[key] }
val rawFullRows = orderedKeys.mapNotNull { key -> catalogSnapshot[key] }
val fullRows = if (hideUnreleased) {
val today = LocalDate.now()
rawFullRows.map { it.filterReleasedItems(today) }
} else {
rawFullRows
}
_fullCatalogRows.update { rows ->
if (rows == fullRows) rows else fullRows
}

View file

@ -28,7 +28,8 @@ private data class CoreLayoutPrefs(
val heroSectionEnabled: Boolean,
val posterLabelsEnabled: Boolean,
val catalogAddonNameEnabled: Boolean,
val catalogTypeSuffixEnabled: Boolean
val catalogTypeSuffixEnabled: Boolean,
val hideUnreleasedContent: Boolean
)
private data class FocusedBackdropPrefs(
@ -46,6 +47,7 @@ private data class LayoutUiPrefs(
val posterLabelsEnabled: Boolean,
val catalogAddonNameEnabled: Boolean,
val catalogTypeSuffixEnabled: Boolean,
val hideUnreleasedContent: Boolean,
val modernLandscapePostersEnabled: Boolean,
val focusedBackdropExpandEnabled: Boolean,
val focusedBackdropExpandDelaySeconds: Int,
@ -73,12 +75,17 @@ internal fun HomeViewModel.observeLayoutPreferencesPipeline() {
heroSectionEnabled = heroSectionEnabled,
posterLabelsEnabled = posterLabelsEnabled,
catalogAddonNameEnabled = catalogAddonNameEnabled,
catalogTypeSuffixEnabled = true
catalogTypeSuffixEnabled = true,
hideUnreleasedContent = false
)
},
layoutPreferenceDataStore.catalogTypeSuffixEnabled
) { corePrefs, catalogTypeSuffixEnabled ->
corePrefs.copy(catalogTypeSuffixEnabled = catalogTypeSuffixEnabled)
layoutPreferenceDataStore.catalogTypeSuffixEnabled,
layoutPreferenceDataStore.hideUnreleasedContent
) { corePrefs, catalogTypeSuffixEnabled, hideUnreleasedContent ->
corePrefs.copy(
catalogTypeSuffixEnabled = catalogTypeSuffixEnabled,
hideUnreleasedContent = hideUnreleasedContent
)
}
val focusedBackdropPrefsFlow = combine(
@ -113,6 +120,7 @@ internal fun HomeViewModel.observeLayoutPreferencesPipeline() {
posterLabelsEnabled = corePrefs.posterLabelsEnabled,
catalogAddonNameEnabled = corePrefs.catalogAddonNameEnabled,
catalogTypeSuffixEnabled = corePrefs.catalogTypeSuffixEnabled,
hideUnreleasedContent = corePrefs.hideUnreleasedContent,
modernLandscapePostersEnabled = false,
focusedBackdropExpandEnabled = focusedBackdropPrefs.expandEnabled,
focusedBackdropExpandDelaySeconds = focusedBackdropPrefs.expandDelaySeconds,
@ -146,7 +154,8 @@ internal fun HomeViewModel.observeLayoutPreferencesPipeline() {
val shouldRefreshCatalogPresentation =
currentHeroCatalogKeys != prefs.heroCatalogKeys ||
previousState.heroSectionEnabled != prefs.heroSectionEnabled ||
previousState.homeLayout != prefs.layout
previousState.homeLayout != prefs.layout ||
previousState.hideUnreleasedContent != prefs.hideUnreleasedContent
currentHeroCatalogKeys = prefs.heroCatalogKeys
_uiState.update {
it.copy(
@ -156,6 +165,7 @@ internal fun HomeViewModel.observeLayoutPreferencesPipeline() {
posterLabelsEnabled = effectivePosterLabelsEnabled,
catalogAddonNameEnabled = prefs.catalogAddonNameEnabled,
catalogTypeSuffixEnabled = prefs.catalogTypeSuffixEnabled,
hideUnreleasedContent = prefs.hideUnreleasedContent,
modernLandscapePostersEnabled = prefs.modernLandscapePostersEnabled,
focusedPosterBackdropExpandEnabled = prefs.focusedBackdropExpandEnabled,
focusedPosterBackdropExpandDelaySeconds = prefs.focusedBackdropExpandDelaySeconds,

View file

@ -7,7 +7,10 @@ import com.nuvio.tv.data.local.LayoutPreferenceDataStore
import com.nuvio.tv.domain.model.Addon
import com.nuvio.tv.domain.model.CatalogDescriptor
import com.nuvio.tv.domain.model.CatalogRow
import com.nuvio.tv.core.util.filterReleasedItems
import com.nuvio.tv.core.util.isUnreleased
import com.nuvio.tv.domain.repository.AddonRepository
import java.time.LocalDate
import com.nuvio.tv.domain.repository.CatalogRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
@ -41,6 +44,7 @@ class SearchViewModel @Inject constructor(
private var hasRenderedFirstCatalog = false
private var pendingCatalogResponses = 0
private var revealBatchAfterNextDiscoverFetch = false
private var hideUnreleasedContent = false
private companion object {
const val DISCOVER_INITIAL_LIMIT = 100
@ -93,6 +97,12 @@ class SearchViewModel @Inject constructor(
_uiState.update { it.copy(catalogTypeSuffixEnabled = enabled) }
}
}
viewModelScope.launch {
layoutPreferenceDataStore.hideUnreleasedContent.collectLatest { enabled ->
hideUnreleasedContent = enabled
scheduleCatalogRowsUpdate()
}
}
}
private data class LayoutPrefs(
@ -342,8 +352,14 @@ class SearchViewModel @Inject constructor(
private fun updateCatalogRowsNow() {
_uiState.update { state ->
val orderedRows = catalogOrder.mapNotNull { key -> catalogsMap[key] }
val filteredRows = if (hideUnreleasedContent) {
val today = LocalDate.now()
orderedRows.map { it.filterReleasedItems(today) }
} else {
orderedRows
}
state.copy(
catalogRows = orderedRows
catalogRows = filteredRows
)
}
}
@ -546,7 +562,13 @@ class SearchViewModel @Inject constructor(
"${item.apiType}:${item.id}" !in existingKeys
}
val merged = if (reset) incoming else (existing + incoming)
val deduped = merged.distinctBy { "${it.apiType}:${it.id}" }
val rawDeduped = merged.distinctBy { "${it.apiType}:${it.id}" }
val deduped = if (hideUnreleasedContent) {
val today = LocalDate.now()
rawDeduped.filterNot { it.isUnreleased(today) }
} else {
rawDeduped
}
val shouldRevealBatch = !reset && revealBatchAfterNextDiscoverFetch
val visibleLimit = if (reset) {
DISCOVER_INITIAL_LIMIT

View file

@ -361,6 +361,17 @@ fun LayoutSettingsContent(
},
onFocused = { focusedSection = LayoutSettingsSection.HOME_CONTENT }
)
CompactToggleRow(
title = stringResource(R.string.layout_hide_unreleased),
subtitle = stringResource(R.string.layout_hide_unreleased_sub),
checked = uiState.hideUnreleasedContent,
onToggle = {
viewModel.onEvent(
LayoutSettingsEvent.SetHideUnreleasedContent(!uiState.hideUnreleasedContent)
)
},
onFocused = { focusedSection = LayoutSettingsSection.HOME_CONTENT }
)
}
}

View file

@ -41,7 +41,8 @@ data class LayoutSettingsUiState(
val posterCardCornerRadiusDp: Int = 12,
val blurUnwatchedEpisodes: Boolean = false,
val detailPageTrailerButtonEnabled: Boolean = false,
val preferExternalMetaAddonDetail: Boolean = false
val preferExternalMetaAddonDetail: Boolean = false,
val hideUnreleasedContent: Boolean = false
)
data class CatalogInfo(
@ -74,6 +75,7 @@ sealed class LayoutSettingsEvent {
data class SetBlurUnwatchedEpisodes(val enabled: Boolean) : LayoutSettingsEvent()
data class SetDetailPageTrailerButtonEnabled(val enabled: Boolean) : LayoutSettingsEvent()
data class SetPreferExternalMetaAddonDetail(val enabled: Boolean) : LayoutSettingsEvent()
data class SetHideUnreleasedContent(val enabled: Boolean) : LayoutSettingsEvent()
data object ResetPosterCardStyle : LayoutSettingsEvent()
}
@ -212,6 +214,11 @@ class LayoutSettingsViewModel @Inject constructor(
updateUiStateIfChanged { it.copy(preferExternalMetaAddonDetail = enabled) }
}
}
viewModelScope.launch {
layoutPreferenceDataStore.hideUnreleasedContent.distinctUntilChanged().collectLatest { enabled ->
updateUiStateIfChanged { it.copy(hideUnreleasedContent = enabled) }
}
}
loadAvailableCatalogs()
}
@ -239,6 +246,7 @@ class LayoutSettingsViewModel @Inject constructor(
is LayoutSettingsEvent.SetBlurUnwatchedEpisodes -> setBlurUnwatchedEpisodes(event.enabled)
is LayoutSettingsEvent.SetDetailPageTrailerButtonEnabled -> setDetailPageTrailerButtonEnabled(event.enabled)
is LayoutSettingsEvent.SetPreferExternalMetaAddonDetail -> setPreferExternalMetaAddonDetail(event.enabled)
is LayoutSettingsEvent.SetHideUnreleasedContent -> setHideUnreleasedContent(event.enabled)
LayoutSettingsEvent.ResetPosterCardStyle -> resetPosterCardStyle()
}
}
@ -397,6 +405,13 @@ class LayoutSettingsViewModel @Inject constructor(
}
}
private fun setHideUnreleasedContent(enabled: Boolean) {
if (_uiState.value.hideUnreleasedContent == enabled) return
viewModelScope.launch {
layoutPreferenceDataStore.setHideUnreleasedContent(enabled)
}
}
private fun resetPosterCardStyle() {
if (
_uiState.value.posterCardWidthDp == 126 &&

View file

@ -320,6 +320,8 @@
<string name="layout_addon_name_sub">Show source name under catalog titles.</string>
<string name="layout_catalog_type">Show Catalog Type</string>
<string name="layout_catalog_type_sub">Show type suffix next to catalog name (Movie/Series).</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="layout_section_detail">Detail Page</string>
<string name="layout_section_detail_desc">Settings for the detail and episode screens.</string>
<string name="layout_blur_unwatched">Blur Unwatched Episodes</string>