Only show relevant parental ratings when filtering (#1529)

## Description
Updates the parental rating filter so it only shows the relevant options
instead of all of them. This is now the same behavior as the web UI.

Additionally, the filter options are cached for a short time so the UI
doesn't need to load them every time the menu is opened.

Dev note: Refactors `FilterUtils` into a service which allows for the
caching

### Related issues
Closes #410

### Testing
Emulator with movie & tv show libraries

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-06-07 07:15:40 -04:00 • committed by GitHub
parent d1b8c09615
commit a4ee1e5175
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 192 additions and 138 deletions

View file

@ -44,6 +44,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.FilterOptionCache
import com.github.damontecres.wholphin.services.MediaManagementService
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.MusicService
@ -63,7 +64,6 @@ import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.FilterUtils
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.DataLoadingState
import com.github.damontecres.wholphin.util.ExceptionHandler
@ -117,6 +117,7 @@ class CollectionFolderViewModel
private val musicService: MusicService,
val streamChoiceService: StreamChoiceService,
val mediaReportService: MediaReportService,
private val filterOptionCache: FilterOptionCache,
@Assisted val itemId: String,
@Assisted initialSortAndDirection: SortAndDirection?,
@Assisted("recursive") private val recursive: Boolean,
@ -430,9 +431,7 @@ class CollectionFolderViewModel
}
suspend fun getFilterOptionValues(filterOption: ItemFilterBy<*>): List<FilterValueOption> =
FilterUtils.getFilterOptionValues(
api,
serverRepository.currentUser?.id,
filterOptionCache.getFilterOptionValues(
itemId.toUUID(),
filterOption,
)

View file

@ -64,6 +64,7 @@ import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.FilterOptionCache
import com.github.damontecres.wholphin.services.MediaManagementService
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.MusicService
@ -101,7 +102,6 @@ import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.FilterUtils
import com.github.damontecres.wholphin.ui.util.LocalClock
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler
@ -142,6 +142,7 @@ class PlaylistViewModel
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
private val favoriteWatchManager: FavoriteWatchManager,
private val mediaReportService: MediaReportService,
private val filterOptionCache: FilterOptionCache,
@Assisted itemId: UUID,
) : MusicViewModel(itemId, context, api, musicService, navigationManager, mediaManagementService) {
@AssistedFactory
@ -301,9 +302,7 @@ class PlaylistViewModel
}
suspend fun getFilterOptionValues(filterOption: ItemFilterBy<*>): List<FilterValueOption> =
FilterUtils.getFilterOptionValues(
api,
serverRepository.currentUser?.id,
filterOptionCache.getFilterOptionValues(
itemId,
filterOption,
)

View file

@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.FilterOptionCache
import com.github.damontecres.wholphin.services.ImageUrlService
import com.github.damontecres.wholphin.services.KeyValueService
import com.github.damontecres.wholphin.services.MediaManagementService
@ -33,7 +34,6 @@ import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.ui.util.FilterUtils
import com.github.damontecres.wholphin.ui.util.ResStringProvider
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler
@ -90,6 +90,7 @@ class CollectionViewModel
private val imageUrlService: ImageUrlService,
private val musicService: MusicService,
val mediaReportService: MediaReportService,
private val filterOptionCache: FilterOptionCache,
@Assisted private val itemId: UUID,
) : ViewModel() {
@AssistedFactory
@ -352,9 +353,7 @@ class CollectionViewModel
}
suspend fun getPossibleFilterValues(filterOption: ItemFilterBy<*>): List<FilterValueOption> =
FilterUtils.getFilterOptionValues(
api,
serverRepository.currentUser?.id,
filterOptionCache.getFilterOptionValues(
itemId,
filterOption,
)

View file

@ -1,126 +0,0 @@
package com.github.damontecres.wholphin.ui.util
import com.github.damontecres.wholphin.data.filter.CommunityRatingFilter
import com.github.damontecres.wholphin.data.filter.DecadeFilter
import com.github.damontecres.wholphin.data.filter.FavoriteFilter
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
import com.github.damontecres.wholphin.data.filter.VideoTypeFilter
import com.github.damontecres.wholphin.data.filter.YearFilter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.genresApi
import org.jellyfin.sdk.api.client.extensions.localizationApi
import org.jellyfin.sdk.api.client.extensions.studiosApi
import org.jellyfin.sdk.api.client.extensions.yearsApi
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import timber.log.Timber
import java.util.TreeSet
import java.util.UUID
object FilterUtils {
/**
* Gets the possible values for a filter
*
* For example, the possible genres in the parent ID
*/
suspend fun getFilterOptionValues(
api: ApiClient,
userId: UUID?,
parentId: UUID?,
filterOption: ItemFilterBy<*>,
): List<FilterValueOption> =
withContext(Dispatchers.IO) {
try {
when (filterOption) {
GenreFilter -> {
api.genresApi
.getGenres(
parentId = parentId,
userId = userId,
).content.items
.map { FilterValueOption(it.name ?: "", it.id) }
}
StudioFilter -> {
api.studiosApi
.getStudios(
parentId = parentId,
userId = userId,
includeItemTypes = listOf(BaseItemKind.SERIES),
).content.items
.map { FilterValueOption(it.name ?: "", it.id) }
}
FavoriteFilter,
PlayedFilter,
-> {
listOf(
FilterValueOption("True", null),
FilterValueOption("False", null),
)
}
OfficialRatingFilter -> {
api.localizationApi.getParentalRatings().content.map {
FilterValueOption(it.name ?: "", it.value)
}
}
VideoTypeFilter -> {
FilterVideoType.entries.map {
FilterValueOption(it.readable, it)
}
}
YearFilter -> {
api.yearsApi
.getYears(
parentId = parentId,
userId = userId,
sortBy = listOf(ItemSortBy.SORT_NAME),
sortOrder = listOf(SortOrder.ASCENDING),
).content.items
.mapNotNull {
it.name?.toIntOrNull()?.let { FilterValueOption(it.toString(), it) }
}
}
DecadeFilter -> {
val items = TreeSet<Int>()
api.yearsApi
.getYears(
parentId = parentId,
userId = userId,
sortBy = listOf(ItemSortBy.SORT_NAME),
sortOrder = listOf(SortOrder.ASCENDING),
).content.items
.mapNotNullTo(items) {
it.name
?.toIntOrNull()
?.div(10)
?.times(10)
}
items.toList().sorted().map { FilterValueOption("$it's", it) }
}
CommunityRatingFilter -> {
(1..10).map {
FilterValueOption("$it", it)
}
}
}
} catch (ex: Exception) {
Timber.e(ex, "Exception get filter value options for $filterOption")
listOf()
}
}
}