Fixes #79, added setting to hide none released movies and show
This commit is contained in:
parent
f1d57a5865
commit
cd5952709c
10 changed files with 151 additions and 13 deletions
30
app/src/main/java/com/nuvio/tv/core/util/ReleaseInfoUtils.kt
Normal file
30
app/src/main/java/com/nuvio/tv/core/util/ReleaseInfoUtils.kt
Normal 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)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue