mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-19 13:35:57 +00:00
Fix missing Discover catalogs and add dynamic content types (movie/series/anime)
Diagnosed against NuvioMobile's open-source discover implementation (same addons, more catalogs visible there). Two real gaps: 1. buildDiscoverCatalogOptions() excluded any catalog requiring an extra parameter beyond genre. Nuvio's equivalent (supportsDiscover()) explicitly whitelists "skip" (pagination) as never blocking a catalog even when required — Fluxa's filter didn't, silently dropping every catalog that declares skip as required, which is a common Stremio addon SDK convention. Added "skip" to the allowed set. 2. The Type filter (movie/series) was hardcoded in the shared Discover UI instead of derived from what addons actually expose. Nuvio builds its type list from distinct catalog types across installed addons, which is why "anime" shows up there. Added a new buildDiscoverContentTypes() helper (same extras filter, minus the "all"/type restriction), threaded contentTypes through the readDiscoverCatalogFilters effect, Rust's DiscoverState (new field, needed since the effect completion flows back through the engine's own state, not directly to Kotlin), HomeViewModel, and the shared DiscoverUiState/DiscoverScreen, with a movie/series fallback so the dropdown isn't empty before the first fetch completes.
This commit is contained in:
parent
01385e7197
commit
7b8a5c61bf
9 changed files with 54 additions and 12 deletions
|
|
@ -46,6 +46,7 @@ import com.fluxa.app.ui.catalog.EpisodeNotificationHelper
|
|||
import com.fluxa.app.ui.catalog.HomeCategory
|
||||
import com.fluxa.app.ui.catalog.HomeCatalogSource
|
||||
import com.fluxa.app.domain.discovery.buildDiscoverCatalogOptions
|
||||
import com.fluxa.app.domain.discovery.buildDiscoverContentTypes
|
||||
import com.fluxa.app.domain.discovery.buildMetadataFeedOptions
|
||||
import com.fluxa.app.domain.discovery.effectiveHomeMetadataFeedSelection
|
||||
import com.fluxa.app.domain.discovery.isMetadataFeedEnabled
|
||||
|
|
@ -853,6 +854,7 @@ class FluxaAndroidHeadlessEnvironment @Inject constructor(
|
|||
val profile = payload.profile()
|
||||
val addons = addonRepository.getUserAddons(profile?.authKey.orEmpty(), profile?.safeLocalAddons.orEmpty())
|
||||
val catalogOptions = buildDiscoverCatalogOptions(addons, payload.string("contentType"))
|
||||
val contentTypes = buildDiscoverContentTypes(addons)
|
||||
val selectedCatalog = catalogOptions.firstOrNull { it.key == payload.stringOrNull("selectedCatalogKey") }
|
||||
val selectedGenres = selectedCatalog?.genres.orEmpty()
|
||||
.distinct()
|
||||
|
|
@ -865,7 +867,7 @@ class FluxaAndroidHeadlessEnvironment @Inject constructor(
|
|||
} else {
|
||||
selectedGenres
|
||||
}
|
||||
return ok(effect, mapOf("catalogs" to catalogOptions, "genres" to genres))
|
||||
return ok(effect, mapOf("catalogs" to catalogOptions, "genres" to genres, "contentTypes" to contentTypes))
|
||||
}
|
||||
|
||||
private suspend fun fetchCatalogPage(effect: NativeHeadlessEffect): HeadlessEffectCompletion {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.fluxa.app.ui.catalog
|
||||
|
||||
import com.fluxa.app.common.AppStrings
|
||||
import com.fluxa.app.data.local.UserProfile
|
||||
import com.fluxa.app.shared.feature.catalog.CatalogItemUiModel
|
||||
import com.fluxa.app.shared.feature.catalog.CatalogSourceUiModel
|
||||
|
|
@ -22,8 +23,12 @@ class AndroidDiscoverDataSource(
|
|||
homeViewModel.discoverUiState
|
||||
) { selectedFilters, state ->
|
||||
val profile = activeProfile()
|
||||
val language = profile?.language
|
||||
DiscoverUiState(
|
||||
filters = selectedFilters,
|
||||
typeOptions = state.contentTypes.map { type ->
|
||||
DiscoverFilterOptionUiModel(type, discoverContentTypeLabel(type, language))
|
||||
},
|
||||
catalogOptions = state.catalogs.map { DiscoverFilterOptionUiModel(it.key, it.label) },
|
||||
genreOptions = state.genres.map { DiscoverFilterOptionUiModel(it.id, it.label) },
|
||||
results = state.results.map { meta ->
|
||||
|
|
@ -63,3 +68,13 @@ class AndroidDiscoverDataSource(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun discoverContentTypeLabel(type: String, language: String?): String {
|
||||
val key = when (type) {
|
||||
"movie" -> "auto.movie"
|
||||
"series" -> "auto.series"
|
||||
"anime" -> "auto.anime"
|
||||
else -> null
|
||||
}
|
||||
return key?.let { AppStrings.t(language, it) } ?: type.replaceFirstChar { it.uppercase() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ data class DiscoverUiState(
|
|||
val isLoading: Boolean = false,
|
||||
val genres: List<DiscoverGenreOption> = emptyList(),
|
||||
val catalogs: List<DiscoverCatalogOption> = emptyList(),
|
||||
val contentTypes: List<String> = emptyList(),
|
||||
val resultSources: Map<String, HomeCatalogSource> = emptyMap()
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class HomeViewModel @Inject constructor(
|
|||
private val introTimestampsListType = object : TypeToken<List<IntroTimestamps>>() {}.type
|
||||
private val discoverCatalogListType = object : TypeToken<List<DiscoverCatalogOption>>() {}.type
|
||||
private val discoverGenreListType = object : TypeToken<List<DiscoverGenreOption>>() {}.type
|
||||
private val discoverContentTypeListType = object : TypeToken<List<String>>() {}.type
|
||||
private val addonListType = object : TypeToken<List<AddonDescriptor>>() {}.type
|
||||
private val headlessRuntime = FluxaHeadlessRuntimeFactory.createUniFfi(headlessEnvironment)
|
||||
private val initialSearchHistory = searchHistoryStore.load(null)
|
||||
|
|
@ -140,16 +141,20 @@ class HomeViewModel @Inject constructor(
|
|||
private val _headlessDiscoverLoading = MutableStateFlow(false)
|
||||
private val _headlessDiscoverGenres = MutableStateFlow<List<DiscoverGenreOption>>(emptyList())
|
||||
private val _headlessDiscoverCatalogs = MutableStateFlow<List<DiscoverCatalogOption>>(emptyList())
|
||||
private val _headlessDiscoverContentTypes = MutableStateFlow<List<String>>(emptyList())
|
||||
private val discoverResultSourceMapType = object : TypeToken<Map<String, HomeCatalogSource>>() {}.type
|
||||
|
||||
val discoverUiState: StateFlow<DiscoverUiState> = combine(
|
||||
_headlessDiscoverResults,
|
||||
_headlessDiscoverResultSources,
|
||||
_headlessDiscoverLoading,
|
||||
combine(
|
||||
_headlessDiscoverResults,
|
||||
_headlessDiscoverResultSources,
|
||||
_headlessDiscoverLoading
|
||||
) { results, resultSources, loading -> Triple(results, resultSources, loading) },
|
||||
_headlessDiscoverGenres,
|
||||
_headlessDiscoverCatalogs
|
||||
) { results, resultSources, loading, genres, catalogs ->
|
||||
DiscoverUiState(results, loading, genres, catalogs, resultSources)
|
||||
_headlessDiscoverCatalogs,
|
||||
_headlessDiscoverContentTypes
|
||||
) { (results, resultSources, loading), genres, catalogs, contentTypes ->
|
||||
DiscoverUiState(results, loading, genres, catalogs, contentTypes, resultSources)
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), DiscoverUiState())
|
||||
|
||||
val discoverGenres: StateFlow<List<DiscoverGenreOption>> get() = _headlessDiscoverGenres.asStateFlow()
|
||||
|
|
@ -1065,6 +1070,7 @@ class HomeViewModel @Inject constructor(
|
|||
val discover = result.state["discover"] as? Map<*, *> ?: return@launch
|
||||
_headlessDiscoverCatalogs.value = fromStateList(discover["catalogs"], discoverCatalogListType)
|
||||
_headlessDiscoverGenres.value = fromStateList(discover["genres"], discoverGenreListType)
|
||||
_headlessDiscoverContentTypes.value = fromStateList(discover["contentTypes"], discoverContentTypeListType)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@
|
|||
"auto.series_02b3bdba": "Series",
|
||||
"auto.genre": "Genre",
|
||||
"auto.catalog": "Catalog",
|
||||
"auto.anime": "Anime",
|
||||
"auto.platform": "Platform",
|
||||
"auto.recent_searches": "Recent Searches",
|
||||
"auto.no_results_yet": "No results yet",
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@
|
|||
"auto.series_02b3bdba": "Diziler",
|
||||
"auto.genre": "Kategori",
|
||||
"auto.catalog": "Katalog",
|
||||
"auto.anime": "Anime",
|
||||
"auto.platform": "Platform",
|
||||
"auto.recent_searches": "Son Aramalar",
|
||||
"auto.no_results_yet": "Henüz sonuç yok",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@ fun buildMetadataFeedOptions(addons: List<AddonDescriptor>, language: String? =
|
|||
return addonFeeds
|
||||
}
|
||||
|
||||
fun buildDiscoverContentTypes(addons: List<AddonDescriptor>): List<String> {
|
||||
return addons.filter { it.manifest.hasStremioResource("catalog") }.flatMap { addon ->
|
||||
addon.manifest.catalogs.orEmpty().mapNotNull { catalog ->
|
||||
val type = catalog.type?.normalizeContentType() ?: return@mapNotNull null
|
||||
if (catalog.hasRequiredCatalogExtraExcept(setOf("genre", "skip"))) return@mapNotNull null
|
||||
type
|
||||
}
|
||||
}.distinct()
|
||||
}
|
||||
|
||||
fun buildDiscoverCatalogOptions(addons: List<AddonDescriptor>, selectedType: String): List<DiscoverCatalogOption> {
|
||||
val normalizedType = selectedType.lowercase()
|
||||
val rawOptions = addons.filter { it.manifest.hasStremioResource("catalog") }.flatMap { addon ->
|
||||
|
|
@ -45,7 +55,7 @@ fun buildDiscoverCatalogOptions(addons: List<AddonDescriptor>, selectedType: Str
|
|||
val type = catalog.type?.normalizeContentType()?.takeIf { normalizedType == "all" || it == normalizedType }
|
||||
?: return@mapNotNull null
|
||||
val id = catalog.id?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
|
||||
if (catalog.hasRequiredCatalogExtraExcept(setOf("genre"))) return@mapNotNull null
|
||||
if (catalog.hasRequiredCatalogExtraExcept(setOf("genre", "skip"))) return@mapNotNull null
|
||||
val name = discoverCatalogLabel(catalog.name, id)
|
||||
val genres = (
|
||||
catalog.genres.orEmpty() +
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ data class DiscoverFiltersUiModel(
|
|||
|
||||
data class DiscoverUiState(
|
||||
val filters: DiscoverFiltersUiModel = DiscoverFiltersUiModel(),
|
||||
val typeOptions: List<DiscoverFilterOptionUiModel> = emptyList(),
|
||||
val catalogOptions: List<DiscoverFilterOptionUiModel> = emptyList(),
|
||||
val genreOptions: List<DiscoverFilterOptionUiModel> = emptyList(),
|
||||
val results: List<CatalogItemUiModel> = emptyList(),
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ fun DiscoverScreen(
|
|||
} else {
|
||||
DiscoverFilters(
|
||||
filters = state.filters,
|
||||
typeOptions = state.typeOptions,
|
||||
catalogOptions = state.catalogOptions,
|
||||
genreOptions = state.genreOptions,
|
||||
language = language,
|
||||
|
|
@ -194,18 +195,22 @@ private fun DiscoverSkeletonGrid(modifier: Modifier = Modifier) {
|
|||
@Composable
|
||||
private fun DiscoverFilters(
|
||||
filters: DiscoverFiltersUiModel,
|
||||
typeOptions: List<DiscoverFilterOptionUiModel>,
|
||||
catalogOptions: List<DiscoverFilterOptionUiModel>,
|
||||
genreOptions: List<DiscoverFilterOptionUiModel>,
|
||||
language: String?,
|
||||
onFiltersChanged: (DiscoverFiltersUiModel) -> Unit
|
||||
) {
|
||||
val effectiveTypeOptions = typeOptions.ifEmpty {
|
||||
listOf(
|
||||
DiscoverFilterOptionUiModel("movie", AppStrings.t(language, "auto.movie")),
|
||||
DiscoverFilterOptionUiModel("series", AppStrings.t(language, "auto.series"))
|
||||
)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
DiscoverDropdownFilter(
|
||||
label = AppStrings.t(language, "auto.type"),
|
||||
options = listOf(
|
||||
DiscoverFilterOptionUiModel("movie", AppStrings.t(language, "auto.movie")),
|
||||
DiscoverFilterOptionUiModel("series", AppStrings.t(language, "auto.series"))
|
||||
),
|
||||
options = effectiveTypeOptions,
|
||||
selectedId = filters.contentType,
|
||||
onSelected = { value ->
|
||||
onFiltersChanged(DiscoverFiltersUiModel(contentType = value.orEmpty()))
|
||||
|
|
|
|||
Loading…
Reference in a new issue