mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add ability to filter libraries, collection, etc (#308)
Adds a button to filter contents next to the sort button on grid pages The filters are saved and restored you return to the page. It works with #278, playing the filtered items in the sorted order. The drop down menus for both sorting & filtering are updated to closer match the overall look. Filters for: - Played or unplayed - Favorite or not - Genres - Community (star) rating - Parental rating - Video type (HD vs SD, etc) - Release year and/or decade
This commit is contained in:
parent
50dc4b9a35
commit
78e7eb421f
16 changed files with 1325 additions and 75 deletions
|
|
@ -0,0 +1,315 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "d4874262e325932fe00de49471288d82",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "servers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "users",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "rowId",
|
||||
"columnName": "rowId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "serverId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"rowId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_users_id_serverId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"serverId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_users_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_users_serverId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"serverId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "servers",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serverId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "ItemPlayback",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "rowId",
|
||||
"columnName": "rowId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sourceId",
|
||||
"columnName": "sourceId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "audioIndex",
|
||||
"columnName": "audioIndex",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subtitleIndex",
|
||||
"columnName": "subtitleIndex",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"rowId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ItemPlayback_userId_itemId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "NavDrawerPinnedItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
]
|
||||
},
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "LibraryDisplayInfo",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sort",
|
||||
"columnName": "sort",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "direction",
|
||||
"columnName": "direction",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filter",
|
||||
"columnName": "filter",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'{}'"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_LibraryDisplayInfo_userId_itemId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd4874262e325932fe00de49471288d82')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -7,25 +7,29 @@ import androidx.room.TypeConverter
|
|||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
@Database(
|
||||
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
|
||||
version = 7,
|
||||
version = 8,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(3, 4),
|
||||
AutoMigration(4, 5),
|
||||
AutoMigration(5, 6),
|
||||
AutoMigration(6, 7),
|
||||
AutoMigration(7, 8),
|
||||
],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
@ -57,6 +61,18 @@ class Converters {
|
|||
|
||||
@TypeConverter
|
||||
fun convertSortOrder(sort: String): SortOrder? = SortOrder.fromNameOrNull(sort)
|
||||
|
||||
@TypeConverter
|
||||
fun convertGetItemsFilter(filter: GetItemsFilter): String = Json.encodeToString(filter)
|
||||
|
||||
@TypeConverter
|
||||
fun convertGetItemsFilter(filter: String): GetItemsFilter =
|
||||
try {
|
||||
Json.decodeFromString(filter)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error parsing filter")
|
||||
GetItemsFilter()
|
||||
}
|
||||
}
|
||||
|
||||
object Migrations {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.github.damontecres.wholphin.data.filter
|
||||
|
||||
data class FilterValueOption(
|
||||
val name: String,
|
||||
val value: Any?,
|
||||
)
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
package com.github.damontecres.wholphin.data.filter
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import java.util.UUID
|
||||
|
||||
val DefaultFilterOptions =
|
||||
listOf(
|
||||
PlayedFilter,
|
||||
FavoriteFilter,
|
||||
GenreFilter,
|
||||
CommunityRatingFilter,
|
||||
OfficialRatingFilter,
|
||||
VideoTypeFilter,
|
||||
YearFilter,
|
||||
DecadeFilter,
|
||||
)
|
||||
|
||||
val DefaultForFavoritesFilterOptions =
|
||||
listOf(
|
||||
PlayedFilter,
|
||||
GenreFilter,
|
||||
CommunityRatingFilter,
|
||||
OfficialRatingFilter,
|
||||
VideoTypeFilter,
|
||||
YearFilter,
|
||||
DecadeFilter,
|
||||
)
|
||||
|
||||
val DefaultForGenresFilterOptions =
|
||||
listOf(
|
||||
PlayedFilter,
|
||||
FavoriteFilter,
|
||||
CommunityRatingFilter,
|
||||
OfficialRatingFilter,
|
||||
VideoTypeFilter,
|
||||
YearFilter,
|
||||
DecadeFilter,
|
||||
)
|
||||
|
||||
sealed interface ItemFilterBy<T> {
|
||||
@get:StringRes
|
||||
val stringRes: Int
|
||||
|
||||
val supportMultiple: Boolean
|
||||
|
||||
fun get(filter: GetItemsFilter): T?
|
||||
|
||||
fun set(
|
||||
value: T?,
|
||||
filter: GetItemsFilter,
|
||||
): GetItemsFilter
|
||||
}
|
||||
|
||||
data object GenreFilter : ItemFilterBy<List<UUID>> {
|
||||
override val stringRes: Int = R.string.genres
|
||||
|
||||
override val supportMultiple: Boolean = true
|
||||
|
||||
override fun get(filter: GetItemsFilter): List<UUID>? = filter.genres
|
||||
|
||||
override fun set(
|
||||
value: List<UUID>?,
|
||||
filter: GetItemsFilter,
|
||||
): GetItemsFilter = filter.copy(genres = value)
|
||||
}
|
||||
|
||||
data object PlayedFilter : ItemFilterBy<Boolean> {
|
||||
override val stringRes: Int = R.string.played
|
||||
|
||||
override val supportMultiple: Boolean = false
|
||||
|
||||
override fun get(filter: GetItemsFilter): Boolean? = filter.played
|
||||
|
||||
override fun set(
|
||||
value: Boolean?,
|
||||
filter: GetItemsFilter,
|
||||
): GetItemsFilter = filter.copy(played = value)
|
||||
}
|
||||
|
||||
data object FavoriteFilter : ItemFilterBy<Boolean> {
|
||||
override val stringRes: Int = R.string.favorites
|
||||
|
||||
override val supportMultiple: Boolean = false
|
||||
|
||||
override fun get(filter: GetItemsFilter): Boolean? = filter.favorite
|
||||
|
||||
override fun set(
|
||||
value: Boolean?,
|
||||
filter: GetItemsFilter,
|
||||
): GetItemsFilter = filter.copy(favorite = value)
|
||||
}
|
||||
|
||||
data object OfficialRatingFilter : ItemFilterBy<List<String>> {
|
||||
override val stringRes: Int = R.string.official_rating
|
||||
|
||||
override val supportMultiple: Boolean = true
|
||||
|
||||
override fun get(filter: GetItemsFilter): List<String>? = filter.officialRatings
|
||||
|
||||
override fun set(
|
||||
value: List<String>?,
|
||||
filter: GetItemsFilter,
|
||||
): GetItemsFilter = filter.copy(officialRatings = value)
|
||||
}
|
||||
|
||||
enum class FilterVideoType(
|
||||
val readable: String,
|
||||
) {
|
||||
FOUR_K("4K"),
|
||||
HD("HD"),
|
||||
SD("SD"),
|
||||
THREE_D("3D"),
|
||||
BLU_RAY("Blu-Ray"),
|
||||
DVD("DVD"),
|
||||
}
|
||||
|
||||
data object VideoTypeFilter : ItemFilterBy<List<FilterVideoType>> {
|
||||
override val stringRes: Int = R.string.video
|
||||
|
||||
override val supportMultiple: Boolean = true
|
||||
|
||||
override fun get(filter: GetItemsFilter): List<FilterVideoType>? = filter.videoTypes
|
||||
|
||||
override fun set(
|
||||
value: List<FilterVideoType>?,
|
||||
filter: GetItemsFilter,
|
||||
): GetItemsFilter = filter.copy(videoTypes = value)
|
||||
}
|
||||
|
||||
data object YearFilter : ItemFilterBy<List<Int>> {
|
||||
override val stringRes: Int = R.string.year
|
||||
|
||||
override val supportMultiple: Boolean = true
|
||||
|
||||
override fun get(filter: GetItemsFilter): List<Int>? = filter.years
|
||||
|
||||
override fun set(
|
||||
value: List<Int>?,
|
||||
filter: GetItemsFilter,
|
||||
): GetItemsFilter = filter.copy(years = value)
|
||||
}
|
||||
|
||||
data object DecadeFilter : ItemFilterBy<List<Int>> {
|
||||
override val stringRes: Int = R.string.decade
|
||||
|
||||
override val supportMultiple: Boolean = true
|
||||
|
||||
override fun get(filter: GetItemsFilter): List<Int>? = filter.decades
|
||||
|
||||
override fun set(
|
||||
value: List<Int>?,
|
||||
filter: GetItemsFilter,
|
||||
): GetItemsFilter = filter.copy(decades = value)
|
||||
}
|
||||
|
||||
data object CommunityRatingFilter : ItemFilterBy<Int> {
|
||||
override val stringRes: Int = R.string.community_rating
|
||||
|
||||
override val supportMultiple: Boolean = true
|
||||
|
||||
override fun get(filter: GetItemsFilter): Int? = filter.minCommunityRating?.toInt()
|
||||
|
||||
override fun set(
|
||||
value: Int?,
|
||||
filter: GetItemsFilter,
|
||||
): GetItemsFilter = filter.copy(minCommunityRating = value?.toDouble())
|
||||
}
|
||||
|
|
@ -2,9 +2,13 @@
|
|||
|
||||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
import com.github.damontecres.wholphin.data.filter.FilterVideoType
|
||||
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.VideoType
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetPersonsRequest
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
|
|
@ -14,30 +18,112 @@ import java.util.UUID
|
|||
data class GetItemsFilter(
|
||||
val favorite: Boolean? = null,
|
||||
val genres: List<UUID>? = null,
|
||||
val minCriticRating: Double? = null,
|
||||
val minCommunityRating: Double? = null,
|
||||
val officialRatings: List<String>? = null,
|
||||
val persons: List<UUID>? = null,
|
||||
val played: Boolean? = null,
|
||||
val studios: List<UUID>? = null,
|
||||
val tags: List<String>? = null,
|
||||
val includeItemTypes: List<BaseItemKind>? = null,
|
||||
val videoTypes: List<FilterVideoType>? = null,
|
||||
val years: List<Int>? = null,
|
||||
val decades: List<Int>? = null,
|
||||
val override: GetItemsFilterOverride = GetItemsFilterOverride.NONE,
|
||||
) {
|
||||
/**
|
||||
* Returns how many of filters are actually being used in this [GetItemsFilter]
|
||||
*/
|
||||
fun countFilters(filterOptions: List<ItemFilterBy<*>>): Int {
|
||||
var count = 0
|
||||
filterOptions.forEach {
|
||||
if (it.get(this) != null) count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all of the values for the given filters
|
||||
*/
|
||||
fun delete(filterOptions: List<ItemFilterBy<*>>): GetItemsFilter {
|
||||
var newFilter = this
|
||||
filterOptions.forEach {
|
||||
newFilter = it.set(null, newFilter)
|
||||
}
|
||||
return newFilter
|
||||
}
|
||||
|
||||
fun applyTo(req: GetItemsRequest) =
|
||||
req.copy(
|
||||
includeItemTypes = includeItemTypes,
|
||||
isFavorite = favorite,
|
||||
genreIds = genres,
|
||||
minCriticRating = minCriticRating,
|
||||
minCommunityRating = minCommunityRating,
|
||||
personIds = persons,
|
||||
isPlayed = played,
|
||||
studioIds = studios,
|
||||
tags = tags,
|
||||
officialRatings = officialRatings,
|
||||
years =
|
||||
buildSet {
|
||||
years?.letNotEmpty(::addAll)
|
||||
decades?.forEach { addAll(it..<(it + 10)) }
|
||||
},
|
||||
is4k =
|
||||
videoTypes?.letNotEmpty {
|
||||
videoTypes.contains(FilterVideoType.FOUR_K).takeIf { it }
|
||||
},
|
||||
isHd =
|
||||
videoTypes?.letNotEmpty {
|
||||
if (videoTypes.contains(FilterVideoType.HD)) {
|
||||
true
|
||||
} else if (videoTypes.contains(FilterVideoType.SD)) {
|
||||
false
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
is3d =
|
||||
videoTypes?.letNotEmpty {
|
||||
videoTypes.contains(FilterVideoType.THREE_D).takeIf { it }
|
||||
},
|
||||
videoTypes =
|
||||
videoTypes?.letNotEmpty {
|
||||
it.mapNotNull {
|
||||
when (it) {
|
||||
FilterVideoType.FOUR_K,
|
||||
FilterVideoType.HD,
|
||||
FilterVideoType.SD,
|
||||
FilterVideoType.THREE_D,
|
||||
-> null
|
||||
|
||||
FilterVideoType.BLU_RAY -> VideoType.BLU_RAY
|
||||
FilterVideoType.DVD -> VideoType.DVD
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fun applyTo(req: GetPersonsRequest) =
|
||||
req.copy(
|
||||
isFavorite = favorite,
|
||||
)
|
||||
|
||||
fun merge(filter: GetItemsFilter): GetItemsFilter =
|
||||
this.copy(
|
||||
favorite = favorite ?: filter.favorite,
|
||||
genres = genres ?: filter.genres,
|
||||
minCommunityRating = minCommunityRating ?: filter.minCommunityRating,
|
||||
officialRatings = officialRatings ?: filter.officialRatings,
|
||||
persons = persons ?: filter.persons,
|
||||
played = played ?: filter.played,
|
||||
studios = studios ?: filter.studios,
|
||||
tags = tags ?: filter.tags,
|
||||
includeItemTypes = includeItemTypes ?: filter.includeItemTypes,
|
||||
videoTypes = videoTypes ?: filter.videoTypes,
|
||||
years = years ?: filter.years,
|
||||
decades = decades ?: filter.decades,
|
||||
override = override,
|
||||
)
|
||||
}
|
||||
|
||||
enum class GetItemsFilterOverride {
|
||||
|
|
|
|||
|
|
@ -2,19 +2,18 @@
|
|||
|
||||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import java.util.UUID
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
|
|
@ -35,14 +34,9 @@ data class LibraryDisplayInfo(
|
|||
val itemId: String,
|
||||
val sort: ItemSortBy,
|
||||
val direction: SortOrder,
|
||||
@ColumnInfo(defaultValue = "{}")
|
||||
val filter: GetItemsFilter,
|
||||
) {
|
||||
constructor(
|
||||
userId: Int,
|
||||
itemId: UUID,
|
||||
sort: ItemSortBy,
|
||||
direction: SortOrder,
|
||||
) : this(userId, itemId.toServerString(), sort, direction)
|
||||
|
||||
@Ignore @Transient
|
||||
val sortAndDirection = SortAndDirection(sort, direction)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,16 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -40,6 +44,18 @@ import androidx.tv.material3.Text
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
||||
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.DefaultFilterOptions
|
||||
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.VideoTypeFilter
|
||||
import com.github.damontecres.wholphin.data.filter.YearFilter
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
|
||||
|
|
@ -72,9 +88,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
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.yearsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
|
@ -84,6 +104,8 @@ import org.jellyfin.sdk.model.api.SortOrder
|
|||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetPersonsRequest
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
import java.util.TreeSet
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
|
|
@ -100,6 +122,7 @@ class CollectionFolderViewModel
|
|||
val navigationManager: NavigationManager,
|
||||
) : ItemViewModel(api) {
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
val backgroundLoading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
||||
val sortAndDirection = MutableLiveData<SortAndDirection>()
|
||||
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
|
||||
|
|
@ -121,26 +144,62 @@ class CollectionFolderViewModel
|
|||
) {
|
||||
this@CollectionFolderViewModel.useSeriesForPrimary = useSeriesForPrimary
|
||||
this@CollectionFolderViewModel.itemId = itemId
|
||||
itemId?.toUUIDOrNull()?.let {
|
||||
itemId.toUUIDOrNull()?.let {
|
||||
fetchItem(it)
|
||||
}
|
||||
|
||||
val libraryDisplayInfo =
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
libraryDisplayInfoDao.getItem(user, itemId)
|
||||
}
|
||||
|
||||
val sortAndDirection =
|
||||
initialSortAndDirection
|
||||
?: (
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection
|
||||
} ?: SortAndDirection.DEFAULT
|
||||
)
|
||||
?: libraryDisplayInfo?.sortAndDirection
|
||||
?: SortAndDirection.DEFAULT
|
||||
|
||||
loadResults(sortAndDirection, recursive, filter, useSeriesForPrimary)
|
||||
val filterToUse =
|
||||
if (libraryDisplayInfo?.filter != null) {
|
||||
filter.merge(libraryDisplayInfo.filter)
|
||||
} else {
|
||||
filter
|
||||
}
|
||||
|
||||
loadResults(true, sortAndDirection, recursive, filterToUse, useSeriesForPrimary)
|
||||
}
|
||||
|
||||
fun onFilterChange(
|
||||
newFilter: GetItemsFilter,
|
||||
recursive: Boolean,
|
||||
) {
|
||||
Timber.v("onFilterChange: filter=%s", newFilter)
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val libraryDisplayInfo =
|
||||
LibraryDisplayInfo(
|
||||
userId = user.rowId,
|
||||
itemId = itemId,
|
||||
sort = sortAndDirection.value!!.sort,
|
||||
direction = sortAndDirection.value!!.direction,
|
||||
filter = newFilter,
|
||||
)
|
||||
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
|
||||
}
|
||||
}
|
||||
loadResults(false, sortAndDirection.value!!, recursive, newFilter, useSeriesForPrimary)
|
||||
}
|
||||
|
||||
fun onSortChange(
|
||||
sortAndDirection: SortAndDirection,
|
||||
recursive: Boolean,
|
||||
filter: GetItemsFilter,
|
||||
) {
|
||||
Timber.v(
|
||||
"onSortChange: sort=%s, recursive=%s, filter=%s",
|
||||
sortAndDirection,
|
||||
recursive,
|
||||
filter,
|
||||
)
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val libraryDisplayInfo =
|
||||
|
|
@ -149,14 +208,16 @@ class CollectionFolderViewModel
|
|||
itemId = itemId,
|
||||
sort = sortAndDirection.sort,
|
||||
direction = sortAndDirection.direction,
|
||||
filter = filter,
|
||||
)
|
||||
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
|
||||
}
|
||||
}
|
||||
loadResults(sortAndDirection, recursive, filter, useSeriesForPrimary)
|
||||
loadResults(true, sortAndDirection, recursive, filter, useSeriesForPrimary)
|
||||
}
|
||||
|
||||
private fun loadResults(
|
||||
resetState: Boolean,
|
||||
sortAndDirection: SortAndDirection,
|
||||
recursive: Boolean,
|
||||
filter: GetItemsFilter,
|
||||
|
|
@ -164,17 +225,22 @@ class CollectionFolderViewModel
|
|||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
pager.value = listOf()
|
||||
loading.value = LoadingState.Loading
|
||||
if (resetState) {
|
||||
pager.value = listOf()
|
||||
loading.value = LoadingState.Loading
|
||||
}
|
||||
backgroundLoading.value = LoadingState.Loading
|
||||
this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection
|
||||
this@CollectionFolderViewModel.filter.value = filter
|
||||
}
|
||||
delay(1000)
|
||||
val newPager = createPager(sortAndDirection, recursive, filter, useSeriesForPrimary)
|
||||
newPager.init()
|
||||
if (newPager.isNotEmpty()) newPager.getBlocking(0)
|
||||
withContext(Dispatchers.Main) {
|
||||
pager.value = newPager
|
||||
loading.value = LoadingState.Success
|
||||
backgroundLoading.value = LoadingState.Success
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -256,6 +322,78 @@ class CollectionFolderViewModel
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getFilterOptionValues(filterOption: ItemFilterBy<*>): List<FilterValueOption> =
|
||||
try {
|
||||
when (filterOption) {
|
||||
GenreFilter -> {
|
||||
api.genresApi
|
||||
.getGenres(
|
||||
parentId = itemUuid,
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
).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 = itemUuid,
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
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 = itemUuid,
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
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()
|
||||
}
|
||||
|
||||
suspend fun positionOfLetter(letter: Char): Int? =
|
||||
withContext(Dispatchers.IO) {
|
||||
item.value?.let { item ->
|
||||
|
|
@ -319,6 +457,8 @@ fun CollectionFolderGrid(
|
|||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
params: CollectionFolderGridParameters = CollectionFolderGridParameters(),
|
||||
useSeriesForPrimary: Boolean = true,
|
||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||
) = CollectionFolderGrid(
|
||||
preferences,
|
||||
itemId.toServerString(),
|
||||
|
|
@ -332,6 +472,8 @@ fun CollectionFolderGrid(
|
|||
showTitle = showTitle,
|
||||
positionCallback = positionCallback,
|
||||
params = params,
|
||||
useSeriesForPrimary = useSeriesForPrimary,
|
||||
filterOptions = filterOptions,
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
@ -351,6 +493,7 @@ fun CollectionFolderGrid(
|
|||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
params: CollectionFolderGridParameters = CollectionFolderGridParameters(),
|
||||
useSeriesForPrimary: Boolean = true,
|
||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
OneTimeLaunchedEffect {
|
||||
|
|
@ -362,9 +505,10 @@ fun CollectionFolderGrid(
|
|||
useSeriesForPrimary,
|
||||
)
|
||||
}
|
||||
val sortAndDirection by viewModel.sortAndDirection.observeAsState()
|
||||
val sortAndDirection by viewModel.sortAndDirection.observeAsState(SortAndDirection.DEFAULT)
|
||||
val filter by viewModel.filter.observeAsState(initialFilter)
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
val backgroundLoading by viewModel.backgroundLoading.observeAsState(LoadingState.Loading)
|
||||
val item by viewModel.item.observeAsState()
|
||||
val pager by viewModel.pager.observeAsState()
|
||||
|
||||
|
|
@ -379,40 +523,67 @@ fun CollectionFolderGrid(
|
|||
-> LoadingPage()
|
||||
LoadingState.Success -> {
|
||||
pager?.let { pager ->
|
||||
CollectionFolderGridContent(
|
||||
preferences,
|
||||
item,
|
||||
pager,
|
||||
sortAndDirection = sortAndDirection!!,
|
||||
modifier = modifier,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = { position, item ->
|
||||
moreDialog.makePresent(PositionItem(position, item))
|
||||
},
|
||||
onSortChange = {
|
||||
viewModel.onSortChange(it, recursive, filter)
|
||||
},
|
||||
showTitle = showTitle,
|
||||
sortOptions = sortOptions,
|
||||
positionCallback = positionCallback,
|
||||
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||
params = params,
|
||||
playEnabled = playEnabled,
|
||||
onClickPlay = { shuffle ->
|
||||
itemId.toUUIDOrNull()?.let {
|
||||
viewModel.navigationManager.navigateTo(
|
||||
Destination.PlaybackList(
|
||||
itemId = it,
|
||||
startIndex = 0,
|
||||
shuffle = shuffle,
|
||||
recursive = recursive,
|
||||
sortAndDirection = sortAndDirection,
|
||||
filter = filter,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Box(modifier = modifier) {
|
||||
CollectionFolderGridContent(
|
||||
preferences,
|
||||
item,
|
||||
pager,
|
||||
sortAndDirection = sortAndDirection!!,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = { position, item ->
|
||||
moreDialog.makePresent(PositionItem(position, item))
|
||||
},
|
||||
onSortChange = {
|
||||
viewModel.onSortChange(it, recursive, filter)
|
||||
},
|
||||
filterOptions = filterOptions,
|
||||
currentFilter = filter,
|
||||
onFilterChange = {
|
||||
viewModel.onFilterChange(it, recursive)
|
||||
},
|
||||
getPossibleFilterValues = {
|
||||
viewModel.getFilterOptionValues(it)
|
||||
},
|
||||
showTitle = showTitle,
|
||||
sortOptions = sortOptions,
|
||||
positionCallback = positionCallback,
|
||||
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||
params = params,
|
||||
playEnabled = playEnabled,
|
||||
onClickPlay = { shuffle ->
|
||||
itemId.toUUIDOrNull()?.let {
|
||||
viewModel.navigationManager.navigateTo(
|
||||
Destination.PlaybackList(
|
||||
itemId = it,
|
||||
startIndex = 0,
|
||||
shuffle = shuffle,
|
||||
recursive = recursive,
|
||||
sortAndDirection = sortAndDirection,
|
||||
filter = filter,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
backgroundLoading == LoadingState.Loading,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
CircularProgress(
|
||||
Modifier
|
||||
.background(
|
||||
MaterialTheme.colorScheme.background.copy(alpha = .25f),
|
||||
shape = CircleShape,
|
||||
).size(64.dp)
|
||||
.padding(4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -484,6 +655,10 @@ fun CollectionFolderGridContent(
|
|||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
params: CollectionFolderGridParameters = CollectionFolderGridParameters(),
|
||||
currentFilter: GetItemsFilter = GetItemsFilter(),
|
||||
filterOptions: List<ItemFilterBy<*>> = listOf(),
|
||||
onFilterChange: (GetItemsFilter) -> Unit = {},
|
||||
getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
|
||||
) {
|
||||
val title = item?.name ?: item?.data?.collectionType?.name ?: stringResource(R.string.collection)
|
||||
|
||||
|
|
@ -525,13 +700,30 @@ fun CollectionFolderGridContent(
|
|||
.padding(start = 16.dp, end = endPadding)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
if (sortOptions.isNotEmpty()) {
|
||||
SortByButton(
|
||||
sortOptions = sortOptions,
|
||||
current = sortAndDirection,
|
||||
onSortChange = onSortChange,
|
||||
if (sortOptions.isNotEmpty() || filterOptions.isNotEmpty()) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier,
|
||||
)
|
||||
) {
|
||||
if (sortOptions.isNotEmpty()) {
|
||||
SortByButton(
|
||||
sortOptions = sortOptions,
|
||||
current = sortAndDirection,
|
||||
onSortChange = onSortChange,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
if (filterOptions.isNotEmpty()) {
|
||||
FilterByButton(
|
||||
filterOptions = filterOptions,
|
||||
current = currentFilter,
|
||||
onFilterChange = onFilterChange,
|
||||
getPossibleValues = getPossibleFilterValues,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (playEnabled) {
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,384 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.wholphin.R
|
||||
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.VideoTypeFilter
|
||||
import com.github.damontecres.wholphin.data.filter.YearFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.ui.DefaultButtonPadding
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun FilterByButton(
|
||||
filterOptions: List<ItemFilterBy<*>>,
|
||||
current: GetItemsFilter,
|
||||
onFilterChange: (GetItemsFilter) -> Unit,
|
||||
getPossibleValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var dropDown by remember { mutableStateOf(false) }
|
||||
var nestedDropDown by remember { mutableStateOf<ItemFilterBy<*>?>(null) }
|
||||
val filterCount = remember(current, filterOptions) { current.countFilters(filterOptions) }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
ExpandableFilterButton(
|
||||
filterCount = filterCount,
|
||||
onClick = { dropDown = true },
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = dropDown,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
onDismissRequest = {
|
||||
dropDown = false
|
||||
nestedDropDown = null
|
||||
},
|
||||
) {
|
||||
filterOptions
|
||||
.forEach { filterOption ->
|
||||
val currentValue = remember(current) { filterOption.get(current) }
|
||||
TvDropdownMenuItem(
|
||||
leadingIcon = {
|
||||
if (currentValue != null) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Filter active",
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(filterOption.stringRes),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
nestedDropDown = filterOption
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
if (filterCount > 0) {
|
||||
HorizontalDivider()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
TvDropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = Color.Red.copy(alpha = .8f),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.remove),
|
||||
color = if (focused) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onFilterChange.invoke(current.delete(filterOptions))
|
||||
dropDown = false
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = nestedDropDown != null,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp),
|
||||
offset = DpOffset(80.dp, 16.dp),
|
||||
onDismissRequest = {
|
||||
nestedDropDown = null
|
||||
},
|
||||
) {
|
||||
nestedDropDown?.let { filterOption ->
|
||||
filterOption as ItemFilterBy<Any>
|
||||
val currentValue = remember(current) { filterOption.get(current) }
|
||||
|
||||
var possibleValues by remember { mutableStateOf<List<FilterValueOption>?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
possibleValues = getPossibleValues.invoke(filterOption)
|
||||
}
|
||||
|
||||
if (currentValue != null) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
TvDropdownMenuItem(
|
||||
elevation = 5.dp,
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = Color.Red.copy(alpha = .8f),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.remove),
|
||||
color = if (focused) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onFilterChange.invoke(filterOption.set(null, current))
|
||||
nestedDropDown = null
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = Modifier,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (possibleValues == null) {
|
||||
CircularProgress(Modifier.size(48.dp))
|
||||
} else if (possibleValues?.isEmpty() == true) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results),
|
||||
)
|
||||
} else {
|
||||
possibleValues.orEmpty().forEachIndexed { index, value ->
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
if (index == 0) {
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
}
|
||||
|
||||
val isSelected =
|
||||
remember(currentValue) {
|
||||
when (filterOption) {
|
||||
GenreFilter -> {
|
||||
(currentValue as? List<UUID>)
|
||||
.orEmpty()
|
||||
.contains(value.value)
|
||||
}
|
||||
|
||||
FavoriteFilter,
|
||||
PlayedFilter,
|
||||
-> (currentValue as? Boolean) == value.name.toBoolean()
|
||||
|
||||
OfficialRatingFilter -> {
|
||||
(currentValue as? List<String>)
|
||||
.orEmpty()
|
||||
.contains(value.name)
|
||||
}
|
||||
|
||||
VideoTypeFilter ->
|
||||
(currentValue as? List<FilterVideoType>)
|
||||
.orEmpty()
|
||||
.contains(value.value)
|
||||
|
||||
YearFilter,
|
||||
DecadeFilter,
|
||||
->
|
||||
(currentValue as? List<Int>)
|
||||
.orEmpty()
|
||||
.contains(value.value)
|
||||
|
||||
CommunityRatingFilter -> (currentValue as? Int) == value.value
|
||||
}
|
||||
}
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
TvDropdownMenuItem(
|
||||
elevation = 8.dp,
|
||||
interactionSource = interactionSource,
|
||||
leadingIcon = {
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Filter active",
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
if (filterOption == CommunityRatingFilter) {
|
||||
SimpleStarRating(
|
||||
"${value.name}+",
|
||||
starColor = if (focused) EmptyStarColor else FilledStarColor,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = value.name,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
val newFilter =
|
||||
when (filterOption) {
|
||||
GenreFilter -> {
|
||||
val list = (currentValue as? List<UUID>).orEmpty()
|
||||
val newValue =
|
||||
list
|
||||
.toMutableList()
|
||||
.apply {
|
||||
if (isSelected) {
|
||||
remove(value.value!!)
|
||||
} else {
|
||||
add(value.value!! as UUID)
|
||||
}
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
filterOption.set(newValue, current)
|
||||
}
|
||||
|
||||
FavoriteFilter,
|
||||
PlayedFilter,
|
||||
-> {
|
||||
val played = value.name.toBoolean()
|
||||
filterOption.set(played, current)
|
||||
}
|
||||
|
||||
OfficialRatingFilter -> {
|
||||
val list = (currentValue as? List<String>).orEmpty()
|
||||
val newValue =
|
||||
list
|
||||
.toMutableList()
|
||||
.apply {
|
||||
if (isSelected) {
|
||||
remove(value.name)
|
||||
} else {
|
||||
add(value.name)
|
||||
}
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
filterOption.set(newValue, current)
|
||||
}
|
||||
|
||||
VideoTypeFilter -> {
|
||||
val list =
|
||||
(currentValue as? List<FilterVideoType>).orEmpty()
|
||||
val newValue =
|
||||
list
|
||||
.toMutableList()
|
||||
.apply {
|
||||
if (isSelected) {
|
||||
remove(value.value)
|
||||
} else {
|
||||
add(value.value as FilterVideoType)
|
||||
}
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
filterOption.set(newValue, current)
|
||||
}
|
||||
|
||||
YearFilter,
|
||||
DecadeFilter,
|
||||
-> {
|
||||
val list =
|
||||
(currentValue as? List<Int>).orEmpty()
|
||||
val newValue =
|
||||
list
|
||||
.toMutableList()
|
||||
.apply {
|
||||
if (isSelected) {
|
||||
remove(value.value)
|
||||
} else {
|
||||
add(value.value as Int)
|
||||
}
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
filterOption.set(newValue, current)
|
||||
}
|
||||
|
||||
CommunityRatingFilter ->
|
||||
filterOption.set(
|
||||
value.value as? Int,
|
||||
current,
|
||||
)
|
||||
}
|
||||
|
||||
onFilterChange.invoke(newFilter)
|
||||
if (!filterOption.supportMultiple) {
|
||||
nestedDropDown = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpandableFilterButton(
|
||||
filterCount: Int,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
iconColor: Color = Color.Unspecified,
|
||||
) {
|
||||
val resources = LocalResources.current
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier.heightIn(min = 40.dp),
|
||||
contentPadding = DefaultButtonPadding,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.fa_filter),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = iconColor,
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontAwesome,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier,
|
||||
)
|
||||
AnimatedVisibility(filterCount > 0) {
|
||||
Text(
|
||||
text = filterCount.toString(),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(isFocused) {
|
||||
Text(
|
||||
text = stringResource(R.string.filter),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,10 +38,12 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.LocalContentColor
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.playOnClickSound
|
||||
import com.github.damontecres.wholphin.ui.playSoundOnFocus
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
|
|
@ -67,7 +69,7 @@ enum class StarRatingPrecision {
|
|||
}
|
||||
|
||||
val FilledStarColor = Color(0xFFFFC700)
|
||||
val EmptyStarColor = Color(0x2AFFC700)
|
||||
val EmptyStarColor = Color(0xFF5D4A0A)
|
||||
|
||||
val ratingBarHeight: Dp = 32.dp
|
||||
|
||||
|
|
@ -75,22 +77,34 @@ val ratingBarHeight: Dp = 32.dp
|
|||
fun SimpleStarRating(
|
||||
communityRating: Float?,
|
||||
modifier: Modifier = Modifier,
|
||||
) = SimpleStarRating(
|
||||
text = communityRating?.let { String.format(Locale.getDefault(), "%.1f", it) },
|
||||
modifier = modifier,
|
||||
textColor = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SimpleStarRating(
|
||||
text: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
textColor: Color = LocalContentColor.current,
|
||||
starColor: Color = FilledStarColor,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier,
|
||||
) {
|
||||
communityRating?.let {
|
||||
if (text.isNotNullOrBlank()) {
|
||||
Text(
|
||||
text = String.format(Locale.getDefault(), "%.1f", it),
|
||||
text = text,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
color = textColor,
|
||||
modifier = Modifier,
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Star,
|
||||
tint = FilledStarColor,
|
||||
tint = starColor,
|
||||
contentDescription = null,
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
|
@ -254,7 +268,7 @@ private fun SimpleStarRatingPreview() {
|
|||
Column {
|
||||
SimpleStarRating(7.5f, Modifier.height(32.dp))
|
||||
SimpleStarRating(7.5f, Modifier.height(20.dp))
|
||||
SimpleStarRating(null, Modifier.height(32.dp))
|
||||
SimpleStarRating("", Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.github.damontecres.wholphin.ui.components
|
|||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -15,7 +14,6 @@ import androidx.compose.ui.text.SpanStyle
|
|||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
|
|
@ -77,19 +75,17 @@ fun SortByButton(
|
|||
sortOptions
|
||||
// .sortedBy { it.name }
|
||||
.forEach { sortOption ->
|
||||
DropdownMenuItem(
|
||||
TvDropdownMenuItem(
|
||||
leadingIcon = {
|
||||
if (sortOption == currentSort) {
|
||||
if (currentDirection == SortOrder.ASCENDING) {
|
||||
Text(
|
||||
text = stringResource(R.string.fa_caret_up),
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
fontFamily = FontAwesome,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.fa_caret_down),
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
fontFamily = FontAwesome,
|
||||
)
|
||||
}
|
||||
|
|
@ -98,7 +94,6 @@ fun SortByButton(
|
|||
text = {
|
||||
Text(
|
||||
text = stringResource(getStringRes(sortOption)),
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.MenuDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.LocalContentColor
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
|
||||
@Composable
|
||||
fun TvDropdownMenuItem(
|
||||
text: @Composable () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
enabled: Boolean = true,
|
||||
contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
elevation: Dp = 3.dp,
|
||||
) {
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val backgroundColor =
|
||||
if (focused) {
|
||||
MaterialTheme.colorScheme.inverseSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
|
||||
}
|
||||
val contentColor =
|
||||
if (focused) MaterialTheme.colorScheme.inverseOnSurface else MaterialTheme.colorScheme.onSurface
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
DropdownMenuItem(
|
||||
enabled = enabled,
|
||||
colors =
|
||||
MenuDefaults.itemColors(
|
||||
textColor = contentColor,
|
||||
leadingIconColor = contentColor,
|
||||
trailingIconColor = contentColor,
|
||||
),
|
||||
leadingIcon = leadingIcon,
|
||||
text = text,
|
||||
trailingIcon = trailingIcon,
|
||||
onClick = onClick,
|
||||
interactionSource = interactionSource,
|
||||
contentPadding = contentPadding,
|
||||
modifier =
|
||||
modifier.background(backgroundColor),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.github.damontecres.wholphin.data.filter.DefaultFilterOptions
|
||||
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
|
|
@ -26,6 +28,7 @@ fun CollectionFolderGeneric(
|
|||
playEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
filter: GetItemsFilter = GetItemsFilter(),
|
||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
||||
) {
|
||||
var showHeader by remember { mutableStateOf(true) }
|
||||
|
|
@ -53,5 +56,6 @@ fun CollectionFolderGeneric(
|
|||
},
|
||||
params = params,
|
||||
playEnabled = playEnabled,
|
||||
filterOptions = filterOptions,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.filter.DefaultForFavoritesFilterOptions
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
|
||||
|
|
@ -129,6 +130,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = DefaultForFavoritesFilterOptions,
|
||||
)
|
||||
}
|
||||
// TV
|
||||
|
|
@ -154,6 +156,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = DefaultForFavoritesFilterOptions,
|
||||
)
|
||||
}
|
||||
// Episodes
|
||||
|
|
@ -181,6 +184,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = DefaultForFavoritesFilterOptions,
|
||||
)
|
||||
}
|
||||
// Videos
|
||||
|
|
@ -207,6 +211,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = DefaultForFavoritesFilterOptions,
|
||||
)
|
||||
}
|
||||
// Playlists
|
||||
|
|
@ -233,6 +238,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = DefaultForFavoritesFilterOptions,
|
||||
)
|
||||
}
|
||||
// People
|
||||
|
|
@ -263,6 +269,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = listOf(),
|
||||
)
|
||||
}
|
||||
else -> ErrorMessage("Invalid tab index $selectedTabIndex", null)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.nav
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.filter.DefaultForGenresFilterOptions
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.ItemGrid
|
||||
import com.github.damontecres.wholphin.ui.components.LicenseInfo
|
||||
|
|
@ -166,6 +167,7 @@ fun DestinationContent(
|
|||
recursive = destination.recursive,
|
||||
usePosters = true,
|
||||
playEnabled = true, // TODO only genres use this currently, so might need to change in future
|
||||
filterOptions = DefaultForGenresFilterOptions,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -38,4 +38,5 @@
|
|||
<string name="fa_volume_low" translatable="false"></string>
|
||||
<string name="fa_ellipsis" translatable="false"></string>
|
||||
<string name="fa_ellipsis_vertical" translatable="false"></string>
|
||||
<string name="fa_filter" translatable="false"></string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -319,6 +319,11 @@
|
|||
<string name="skip_segment_recap">Skip Recap</string>
|
||||
<string name="skip_segment_outro">Skip Outro</string>
|
||||
<string name="skip_segment_intro">Skip Intro</string>
|
||||
<string name="played">Played</string>
|
||||
<string name="filter">Filter</string>
|
||||
<string name="year">Year</string>
|
||||
<string name="decade">Decade</string>
|
||||
<string name="remove">Remove</string>
|
||||
|
||||
<string-array name="theme_song_volume">
|
||||
<item>Disabled</item>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue