Add filtering & sorting for discover grid pages (#1614)

## Description
This PR adds sort & filter options for the discover grid pages.

Sort options:
- Popularity
- Release date
- "Community rating" (TMDB user vote)
- Name

Filter options:
- Genre
- Studio/Network
- "Community rating" (TMDB user vote)
- Parental rating
- Production status (TV only)

Also, two more tabs are added to the discover page for browsing movies
and TV. These are essentially the same as clicking "View more" on the
respective rows on the Discover tab.

### Limitations

This PR hardcodes the region used for studio listing and filtering to
"United States". I'll work on a follow up PR to enable users to specify
this.

The sort & filters are not persisted. I think they should be on the two
new tabs, but not elsewhere, but this will be a follow up PR.

### Related issues
Related to #702 
Implements some of #1511
Rewrites the filter logic in #1610 

### Testing
Emulator, but needs further testing

## Screenshots
<img width="1280" height="720" alt="discover_filters Large"
src="https://github.com/user-attachments/assets/cdefb95c-c4b7-4e06-9bac-b535d372ba03"
/>


## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-06-23 17:24:37 -04:00 • committed by GitHub
parent 552c3b5500
commit d705a3ea01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1200 additions and 157 deletions

View file

@ -1,14 +1,24 @@
package com.github.damontecres.wholphin.services
import android.content.Context
import com.github.damontecres.wholphin.api.seerr.model.TvDetails
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.filter.CommunityRatingFilter
import com.github.damontecres.wholphin.data.filter.DecadeFilter
import com.github.damontecres.wholphin.data.filter.DiscoverFilterBy
import com.github.damontecres.wholphin.data.filter.DiscoverMovieContentRatingFilter
import com.github.damontecres.wholphin.data.filter.DiscoverMovieGenreFilter
import com.github.damontecres.wholphin.data.filter.DiscoverMovieStudiosFilter
import com.github.damontecres.wholphin.data.filter.DiscoverRatingFilter
import com.github.damontecres.wholphin.data.filter.DiscoverTvContentRatingFilter
import com.github.damontecres.wholphin.data.filter.DiscoverTvGenreFilter
import com.github.damontecres.wholphin.data.filter.DiscoverTvStatusFilter
import com.github.damontecres.wholphin.data.filter.DiscoverTvStudiosFilter
import com.github.damontecres.wholphin.data.filter.FavoriteFilter
import com.github.damontecres.wholphin.data.filter.FilterBy
import com.github.damontecres.wholphin.data.filter.FilterValueOption
import com.github.damontecres.wholphin.data.filter.FilterVideoType
import com.github.damontecres.wholphin.data.filter.GenreFilter
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
import com.github.damontecres.wholphin.data.filter.OfficialRatingFilter
import com.github.damontecres.wholphin.data.filter.PlayedFilter
import com.github.damontecres.wholphin.data.filter.StudioFilter
@ -20,6 +30,7 @@ import com.mayakapps.kache.InMemoryKache
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.firstOrNull
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.filterApi
import org.jellyfin.sdk.api.client.extensions.genresApi
@ -47,6 +58,7 @@ class FilterOptionCache
@param:IoCoroutineScope private val ioCoroutineScope: CoroutineScope,
private val api: ApiClient,
private val serverRepository: ServerRepository,
private val seerrService: SeerrService,
) {
private val cache =
InMemoryKache<FilterOptionCacheKey, List<FilterValueOption>>(16) {
@ -61,7 +73,7 @@ class FilterOptionCache
*/
suspend fun getFilterOptionValues(
parentId: UUID?,
filterOption: ItemFilterBy<*>,
filterOption: FilterBy<*, *>,
): List<FilterValueOption> {
val cacheKey = FilterOptionCacheKey(serverRepository.currentUser?.id, parentId, filterOption)
return try {
@ -81,7 +93,7 @@ class FilterOptionCache
private suspend fun getFilterOptionValues(
userId: UUID?,
parentId: UUID?,
filterOption: ItemFilterBy<*>,
filterOption: FilterBy<*, *>,
) = when (filterOption) {
GenreFilter -> {
api.genresApi
@ -173,11 +185,86 @@ class FilterOptionCache
FilterValueOption("$it", it)
}
}
is DiscoverFilterBy -> {
getDiscoverFilterOptionValues(filterOption)
}
}
private suspend fun getDiscoverFilterOptionValues(filterOption: DiscoverFilterBy<*>): List<FilterValueOption> {
if (seerrService.active.firstOrNull() != true) {
return emptyList()
}
return when (filterOption) {
DiscoverMovieGenreFilter -> {
seerrService.api.searchApi
.discoverGenresliderMovieGet()
.filter { it.name != null && it.id != null }
.map {
FilterValueOption(it.name!!, it.id!!)
}
}
DiscoverTvGenreFilter -> {
seerrService.api.searchApi
.discoverGenresliderTvGet()
.filter { it.name != null && it.id != null }
.map {
FilterValueOption(it.name!!, it.id!!)
}
}
DiscoverMovieStudiosFilter -> {
// TODO region
seerrService.api.otherApi
.watchprovidersMoviesGet("US")
.filter { it.name != null && it.id != null }
// TODO not what this is for
// .sortedBy { it.displayPriority }
.map {
// TODO logo?
FilterValueOption(it.name!!, it.id!!)
}
}
DiscoverTvStudiosFilter -> {
// TODO region
seerrService.api.otherApi
.watchprovidersTvGet("US")
.filter { it.name != null && it.id != null }
// .sortedBy { it.displayPriority }
.map {
FilterValueOption(it.name!!, it.id!!)
}
}
DiscoverTvStatusFilter -> {
TvDetails.Status.entries.map {
FilterValueOption(it.value, it)
}
}
DiscoverMovieContentRatingFilter -> {
listOf("NR", "G", "PG", "PG-13", "R", "NC-17")
.map { FilterValueOption(it, it) }
}
DiscoverTvContentRatingFilter -> {
listOf("NR", "TV-Y", "TV-Y7", "TV-G", "TV-PG", "TV-14", "TV-MA")
.map { FilterValueOption(it, it) }
}
DiscoverRatingFilter -> {
(1..10).map {
FilterValueOption("$it", it)
}
}
}
}
private data class FilterOptionCacheKey(
val userId: UUID?,
val parentId: UUID?,
val filterOption: ItemFilterBy<*>,
val filterOption: FilterBy<*, *>,
)
}