mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Add ability to sort and filter items in a playlist (#818)
## Description Add ability to sort and filter items in a playlist. There's a new button for each added to the playlist details page. Starting playback will run in the order selected. The sort and filter are persisted locally per playlist, so returning to the playlist will remember the previous settings. Note: playlist playback is still limited to 100 items (#42) ### Related issues Closes #285 ### Testing Emulator & nvidia shield ## Screenshots  ## AI or LLM usage None
This commit is contained in:
parent
ae5035703f
commit
045b689994
5 changed files with 497 additions and 246 deletions
|
|
@ -39,6 +39,17 @@ val DefaultForGenresFilterOptions =
|
||||||
DecadeFilter,
|
DecadeFilter,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val DefaultPlaylistItemsOptions =
|
||||||
|
listOf(
|
||||||
|
PlayedFilter,
|
||||||
|
FavoriteFilter,
|
||||||
|
CommunityRatingFilter,
|
||||||
|
OfficialRatingFilter,
|
||||||
|
VideoTypeFilter,
|
||||||
|
YearFilter,
|
||||||
|
DecadeFilter,
|
||||||
|
)
|
||||||
|
|
||||||
sealed interface ItemFilterBy<T> {
|
sealed interface ItemFilterBy<T> {
|
||||||
@get:StringRes
|
@get:StringRes
|
||||||
val stringRes: Int
|
val stringRes: Int
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import com.github.damontecres.wholphin.ui.toServerString
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
|
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetPlaylistItemsRequestHandler
|
|
||||||
import com.github.damontecres.wholphin.util.TransformList
|
import com.github.damontecres.wholphin.util.TransformList
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -34,7 +33,6 @@ import org.jellyfin.sdk.model.api.PlaylistUserPermissions
|
||||||
import org.jellyfin.sdk.model.api.SortOrder
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest
|
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -79,19 +77,22 @@ class PlaylistCreator
|
||||||
suspend fun createFromPlaylistId(
|
suspend fun createFromPlaylistId(
|
||||||
playlistId: UUID,
|
playlistId: UUID,
|
||||||
startIndex: Int?,
|
startIndex: Int?,
|
||||||
shuffled: Boolean,
|
sortAndDirection: SortAndDirection,
|
||||||
|
filter: GetItemsFilter,
|
||||||
): Playlist {
|
): Playlist {
|
||||||
val request =
|
val request =
|
||||||
GetPlaylistItemsRequest(
|
filter.applyTo(
|
||||||
playlistId = playlistId,
|
GetItemsRequest(
|
||||||
fields = DefaultItemFields,
|
userId = serverRepository.currentUser.value?.id,
|
||||||
startIndex = startIndex,
|
parentId = playlistId,
|
||||||
limit = Playlist.MAX_SIZE,
|
fields = DefaultItemFields,
|
||||||
|
startIndex = startIndex,
|
||||||
|
limit = Playlist.MAX_SIZE,
|
||||||
|
sortBy = listOf(sortAndDirection.sort),
|
||||||
|
sortOrder = listOf(sortAndDirection.direction),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
var items = GetPlaylistItemsRequestHandler.execute(api, request).content.items
|
val items = GetItemsRequestHandler.execute(api, request).content.items
|
||||||
if (shuffled) {
|
|
||||||
items = items.shuffled()
|
|
||||||
}
|
|
||||||
return Playlist(items.convertAndAddParts(), 0)
|
return Playlist(items.convertAndAddParts(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,9 +207,18 @@ class PlaylistCreator
|
||||||
BaseItemKind.PLAYLIST -> {
|
BaseItemKind.PLAYLIST -> {
|
||||||
PlaylistCreationResult.Success(
|
PlaylistCreationResult.Success(
|
||||||
createFromPlaylistId(
|
createFromPlaylistId(
|
||||||
item.id,
|
playlistId = item.id,
|
||||||
startIndex,
|
startIndex = startIndex,
|
||||||
shuffled,
|
sortAndDirection =
|
||||||
|
if (shuffled) {
|
||||||
|
SortAndDirection(ItemSortBy.RANDOM, SortOrder.ASCENDING)
|
||||||
|
} else {
|
||||||
|
sortAndDirection ?: SortAndDirection(
|
||||||
|
ItemSortBy.DEFAULT,
|
||||||
|
SortOrder.ASCENDING,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
filter = filter,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,18 +47,9 @@ import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
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.DefaultFilterOptions
|
||||||
import com.github.damontecres.wholphin.data.filter.FavoriteFilter
|
|
||||||
import com.github.damontecres.wholphin.data.filter.FilterValueOption
|
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.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.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
|
|
@ -88,6 +79,7 @@ import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
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.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
|
@ -103,9 +95,6 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
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.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
|
@ -116,7 +105,6 @@ import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetPersonsRequest
|
import org.jellyfin.sdk.model.api.request.GetPersonsRequest
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.TreeSet
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
|
@ -391,79 +379,12 @@ class CollectionFolderViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getFilterOptionValues(filterOption: ItemFilterBy<*>): List<FilterValueOption> =
|
suspend fun getFilterOptionValues(filterOption: ItemFilterBy<*>): List<FilterValueOption> =
|
||||||
try {
|
FilterUtils.getFilterOptionValues(
|
||||||
when (filterOption) {
|
api,
|
||||||
GenreFilter -> {
|
serverRepository.currentUser.value?.id,
|
||||||
api.genresApi
|
itemUuid,
|
||||||
.getGenres(
|
filterOption,
|
||||||
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? =
|
suspend fun positionOfLetter(letter: Char): Int? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.ui.detail
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -22,7 +23,9 @@ import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowForward
|
import androidx.compose.material.icons.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
|
@ -49,7 +52,14 @@ import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.wholphin.R
|
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.DefaultPlaylistItemsOptions
|
||||||
|
import com.github.damontecres.wholphin.data.filter.FilterValueOption
|
||||||
|
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
|
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||||
|
|
@ -61,26 +71,39 @@ import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
|
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
|
||||||
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
|
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
|
||||||
|
import com.github.damontecres.wholphin.ui.components.FilterByButton
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
|
import com.github.damontecres.wholphin.ui.components.SortByButton
|
||||||
|
import com.github.damontecres.wholphin.ui.data.BoxSetSortOptions
|
||||||
|
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||||
|
import com.github.damontecres.wholphin.ui.formatDateTime
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
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.ui.util.LocalClock
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.GetPlaylistItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
@ -92,9 +115,22 @@ class PlaylistViewModel
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
private val backdropService: BackdropService,
|
private val backdropService: BackdropService,
|
||||||
|
private val serverRepository: ServerRepository,
|
||||||
|
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
|
||||||
) : ItemViewModel(api) {
|
) : ItemViewModel(api) {
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
val items = MutableLiveData<List<BaseItem?>>(listOf())
|
val items = MutableLiveData<List<BaseItem?>>(listOf())
|
||||||
|
val filterAndSort =
|
||||||
|
MutableStateFlow<FilterAndSort>(
|
||||||
|
FilterAndSort(
|
||||||
|
filter = GetItemsFilter(),
|
||||||
|
sortAndDirection =
|
||||||
|
SortAndDirection(
|
||||||
|
ItemSortBy.DEFAULT,
|
||||||
|
SortOrder.ASCENDING,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
fun init(playlistId: UUID) {
|
fun init(playlistId: UUID) {
|
||||||
loading.value = LoadingState.Loading
|
loading.value = LoadingState.Loading
|
||||||
|
|
@ -103,19 +139,93 @@ class PlaylistViewModel
|
||||||
LoadingExceptionHandler(loading, "Failed to fetch playlist $playlistId"),
|
LoadingExceptionHandler(loading, "Failed to fetch playlist $playlistId"),
|
||||||
) {
|
) {
|
||||||
val playlist = fetchItem(playlistId)
|
val playlist = fetchItem(playlistId)
|
||||||
val request =
|
val libraryDisplayInfo =
|
||||||
GetPlaylistItemsRequest(
|
serverRepository.currentUser.value?.let { user ->
|
||||||
playlistId = playlist.id,
|
libraryDisplayInfoDao.getItem(user, itemId)
|
||||||
fields = DefaultItemFields,
|
}
|
||||||
|
val filter = libraryDisplayInfo?.filter ?: GetItemsFilter()
|
||||||
|
val sortAndDirection =
|
||||||
|
libraryDisplayInfo?.sortAndDirection ?: SortAndDirection(
|
||||||
|
ItemSortBy.DEFAULT,
|
||||||
|
SortOrder.ASCENDING,
|
||||||
)
|
)
|
||||||
val pager = ApiRequestPager(api, request, GetPlaylistItemsRequestHandler, viewModelScope).init()
|
loadItems(filter, sortAndDirection)
|
||||||
withContext(Dispatchers.Main) {
|
}
|
||||||
items.value = pager
|
}
|
||||||
loading.value = LoadingState.Success
|
|
||||||
|
fun loadItems(
|
||||||
|
filter: GetItemsFilter,
|
||||||
|
sortAndDirection: SortAndDirection,
|
||||||
|
) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
backdropService.clearBackdrop()
|
||||||
|
loading.setValueOnMain(LoadingState.Loading)
|
||||||
|
this@PlaylistViewModel.filterAndSort.update {
|
||||||
|
FilterAndSort(filter, sortAndDirection)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverRepository.currentUser.value?.let { user ->
|
||||||
|
val playlistId = item.value!!.id
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val libraryDisplayInfo =
|
||||||
|
libraryDisplayInfoDao.getItem(user, itemId)?.copy(
|
||||||
|
filter = filter,
|
||||||
|
sort = sortAndDirection.sort,
|
||||||
|
direction = sortAndDirection.direction,
|
||||||
|
)
|
||||||
|
?: LibraryDisplayInfo(
|
||||||
|
userId = user.rowId,
|
||||||
|
itemId = itemId,
|
||||||
|
sort = sortAndDirection.sort,
|
||||||
|
direction = sortAndDirection.direction,
|
||||||
|
filter = filter,
|
||||||
|
viewOptions = null,
|
||||||
|
)
|
||||||
|
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
val request =
|
||||||
|
filter.applyTo(
|
||||||
|
GetItemsRequest(
|
||||||
|
parentId = playlistId,
|
||||||
|
userId = user.id,
|
||||||
|
fields = DefaultItemFields,
|
||||||
|
sortBy = listOf(sortAndDirection.sort),
|
||||||
|
sortOrder = listOf(sortAndDirection.direction),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
val pager =
|
||||||
|
ApiRequestPager(
|
||||||
|
api,
|
||||||
|
request,
|
||||||
|
GetItemsRequestHandler,
|
||||||
|
viewModelScope,
|
||||||
|
).init()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
items.value = pager
|
||||||
|
loading.value = LoadingState.Success
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error fetching playlist %s", itemId)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
items.value = listOf()
|
||||||
|
loading.value = LoadingState.Error(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getFilterOptionValues(filterOption: ItemFilterBy<*>): List<FilterValueOption> =
|
||||||
|
FilterUtils.getFilterOptionValues(
|
||||||
|
api,
|
||||||
|
serverRepository.currentUser.value?.id,
|
||||||
|
itemUuid,
|
||||||
|
filterOption,
|
||||||
|
)
|
||||||
|
|
||||||
fun updateBackdrop(item: BaseItem) {
|
fun updateBackdrop(item: BaseItem) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
backdropService.submit(item)
|
backdropService.submit(item)
|
||||||
|
|
@ -123,6 +233,12 @@ class PlaylistViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class FilterAndSort(
|
||||||
|
val filter: GetItemsFilter,
|
||||||
|
val sortAndDirection: SortAndDirection,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaylistDetails(
|
fun PlaylistDetails(
|
||||||
destination: Destination.MediaItem,
|
destination: Destination.MediaItem,
|
||||||
|
|
@ -136,78 +252,75 @@ fun PlaylistDetails(
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Pending)
|
val loading by viewModel.loading.observeAsState(LoadingState.Pending)
|
||||||
val playlist by viewModel.item.observeAsState(null)
|
val playlist by viewModel.item.observeAsState(null)
|
||||||
val items by viewModel.items.observeAsState(listOf())
|
val items by viewModel.items.observeAsState(listOf())
|
||||||
|
val filterAndSort by viewModel.filterAndSort.collectAsState()
|
||||||
|
|
||||||
var longClickDialog by remember { mutableStateOf<DialogParams?>(null) }
|
var longClickDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
|
|
||||||
when (val st = loading) {
|
val goToString = stringResource(R.string.go_to)
|
||||||
is LoadingState.Error -> {
|
val playFromHereString = stringResource(R.string.play_from_here)
|
||||||
ErrorMessage(st, modifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadingState.Pending, LoadingState.Loading -> {
|
PlaylistDetailsContent(
|
||||||
LoadingPage(modifier)
|
loadingState = loading,
|
||||||
}
|
playlist = playlist,
|
||||||
|
items = items,
|
||||||
LoadingState.Success -> {
|
onChangeBackdrop = viewModel::updateBackdrop,
|
||||||
playlist?.let {
|
onClickIndex = { index, _ ->
|
||||||
val focusRequester = remember { FocusRequester() }
|
viewModel.navigationManager.navigateTo(
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
Destination.PlaybackList(
|
||||||
PlaylistDetailsContent(
|
itemId = destination.itemId,
|
||||||
playlist = it,
|
startIndex = index,
|
||||||
items = items,
|
shuffle = false,
|
||||||
focusRequester = focusRequester,
|
filter = filterAndSort.filter,
|
||||||
onChangeBackdrop = viewModel::updateBackdrop,
|
sortAndDirection = filterAndSort.sortAndDirection,
|
||||||
onClickIndex = { index, _ ->
|
),
|
||||||
viewModel.navigationManager.navigateTo(
|
)
|
||||||
Destination.PlaybackList(
|
},
|
||||||
itemId = it.id,
|
onClickPlay = { shuffle ->
|
||||||
startIndex = index,
|
viewModel.navigationManager.navigateTo(
|
||||||
shuffle = false,
|
Destination.PlaybackList(
|
||||||
),
|
itemId = destination.itemId,
|
||||||
)
|
startIndex = 0,
|
||||||
},
|
shuffle = shuffle,
|
||||||
onClickPlay = { shuffle ->
|
filter = filterAndSort.filter,
|
||||||
viewModel.navigationManager.navigateTo(
|
sortAndDirection = filterAndSort.sortAndDirection,
|
||||||
Destination.PlaybackList(
|
),
|
||||||
itemId = it.id,
|
)
|
||||||
startIndex = 0,
|
},
|
||||||
shuffle = shuffle,
|
onLongClickIndex = { index, item ->
|
||||||
),
|
longClickDialog =
|
||||||
)
|
DialogParams(
|
||||||
},
|
fromLongClick = true,
|
||||||
onLongClickIndex = { index, item ->
|
title = item.name ?: "",
|
||||||
longClickDialog =
|
items =
|
||||||
DialogParams(
|
listOf(
|
||||||
fromLongClick = true,
|
DialogItem(
|
||||||
title = item.name ?: "",
|
goToString,
|
||||||
items =
|
Icons.Default.ArrowForward,
|
||||||
listOf(
|
) {
|
||||||
DialogItem(
|
viewModel.navigationManager.navigateTo(item.destination())
|
||||||
context.getString(R.string.go_to),
|
},
|
||||||
Icons.Default.ArrowForward,
|
DialogItem(
|
||||||
) {
|
playFromHereString,
|
||||||
viewModel.navigationManager.navigateTo(item.destination())
|
Icons.Default.PlayArrow,
|
||||||
},
|
) {
|
||||||
DialogItem(
|
viewModel.navigationManager.navigateTo(
|
||||||
context.getString(R.string.play_from_here),
|
Destination.PlaybackList(
|
||||||
Icons.Default.PlayArrow,
|
itemId = destination.itemId,
|
||||||
) {
|
startIndex = index,
|
||||||
viewModel.navigationManager.navigateTo(
|
shuffle = false,
|
||||||
Destination.PlaybackList(
|
filter = filterAndSort.filter,
|
||||||
itemId = it.id,
|
sortAndDirection = filterAndSort.sortAndDirection,
|
||||||
startIndex = index,
|
|
||||||
shuffle = false,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = modifier,
|
),
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
}
|
filterAndSort = filterAndSort,
|
||||||
}
|
onFilterAndSortChange = viewModel::loadItems,
|
||||||
|
getPossibleFilterValues = viewModel::getFilterOptionValues,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
longClickDialog?.let { params ->
|
longClickDialog?.let { params ->
|
||||||
DialogPopup(
|
DialogPopup(
|
||||||
params = params,
|
params = params,
|
||||||
|
|
@ -218,14 +331,17 @@ fun PlaylistDetails(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaylistDetailsContent(
|
fun PlaylistDetailsContent(
|
||||||
playlist: BaseItem,
|
playlist: BaseItem?,
|
||||||
items: List<BaseItem?>,
|
items: List<BaseItem?>,
|
||||||
onClickIndex: (Int, BaseItem) -> Unit,
|
onClickIndex: (Int, BaseItem) -> Unit,
|
||||||
onLongClickIndex: (Int, BaseItem) -> Unit,
|
onLongClickIndex: (Int, BaseItem) -> Unit,
|
||||||
onClickPlay: (shuffle: Boolean) -> Unit,
|
onClickPlay: (shuffle: Boolean) -> Unit,
|
||||||
onChangeBackdrop: (BaseItem) -> Unit,
|
onChangeBackdrop: (BaseItem) -> Unit,
|
||||||
|
filterAndSort: FilterAndSort,
|
||||||
|
onFilterAndSortChange: (GetItemsFilter, SortAndDirection) -> Unit,
|
||||||
|
getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
|
||||||
|
loadingState: LoadingState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
|
||||||
) {
|
) {
|
||||||
var savedIndex by rememberSaveable { mutableIntStateOf(0) }
|
var savedIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||||
var focusedIndex by remember { mutableIntStateOf(savedIndex) }
|
var focusedIndex by remember { mutableIntStateOf(savedIndex) }
|
||||||
|
|
@ -234,6 +350,12 @@ fun PlaylistDetailsContent(
|
||||||
LaunchedEffect(focusedItem) {
|
LaunchedEffect(focusedItem) {
|
||||||
focusedItem?.let(onChangeBackdrop)
|
focusedItem?.let(onChangeBackdrop)
|
||||||
}
|
}
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(loadingState) {
|
||||||
|
if (loadingState is LoadingState.Success || loadingState is LoadingState.Error) {
|
||||||
|
focusRequester.tryRequestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val playButtonFocusRequester = remember { FocusRequester() }
|
val playButtonFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
|
@ -248,83 +370,123 @@ fun PlaylistDetailsContent(
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 8.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
PlaylistDetailsHeader(
|
PlaylistDetailsHeader(
|
||||||
focusedItem = focusedItem,
|
focusedItem = focusedItem,
|
||||||
onClickPlay = onClickPlay,
|
onClickPlay = onClickPlay,
|
||||||
playButtonFocusRequester = playButtonFocusRequester,
|
playButtonFocusRequester = playButtonFocusRequester,
|
||||||
|
focusRequester = if (items.isEmpty()) focusRequester else remember { FocusRequester() },
|
||||||
|
filterAndSort = filterAndSort,
|
||||||
|
onFilterAndSortChange = onFilterAndSortChange,
|
||||||
|
getPossibleFilterValues = getPossibleFilterValues,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 16.dp)
|
.padding(start = 16.dp, top = 80.dp)
|
||||||
.fillMaxWidth(.25f),
|
.fillMaxWidth(.25f),
|
||||||
)
|
)
|
||||||
Column(
|
when (loadingState) {
|
||||||
modifier =
|
is LoadingState.Error -> {
|
||||||
Modifier
|
ErrorMessage(loadingState, modifier)
|
||||||
.fillMaxWidth()
|
}
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
) {
|
LoadingState.Pending, LoadingState.Loading -> {
|
||||||
Text(
|
LoadingPage(modifier)
|
||||||
text = playlist.name ?: stringResource(R.string.playlist),
|
}
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
style = MaterialTheme.typography.displayMedium,
|
LoadingState.Success -> {
|
||||||
textAlign = TextAlign.Center,
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier =
|
||||||
)
|
Modifier
|
||||||
LazyColumn(
|
.fillMaxWidth()
|
||||||
contentPadding = PaddingValues(8.dp),
|
.padding(horizontal = 16.dp),
|
||||||
modifier =
|
) {
|
||||||
Modifier
|
Text(
|
||||||
.padding(bottom = 32.dp)
|
text = playlist?.name ?: stringResource(R.string.playlist),
|
||||||
.fillMaxHeight()
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
// .fillMaxWidth(.8f)
|
style = MaterialTheme.typography.displayMedium,
|
||||||
.weight(1f)
|
textAlign = TextAlign.Center,
|
||||||
.background(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
MaterialTheme.colorScheme
|
|
||||||
.surfaceColorAtElevation(1.dp)
|
|
||||||
.copy(alpha = .75f),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
).focusRequester(focusRequester)
|
|
||||||
.focusGroup()
|
|
||||||
.focusRestorer(focus),
|
|
||||||
) {
|
|
||||||
itemsIndexed(items) { index, item ->
|
|
||||||
PlaylistItem(
|
|
||||||
item = item,
|
|
||||||
index = index,
|
|
||||||
onClick = {
|
|
||||||
savedIndex = index
|
|
||||||
item?.let {
|
|
||||||
onClickIndex.invoke(index, item)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
savedIndex = index
|
|
||||||
item?.let {
|
|
||||||
onLongClickIndex.invoke(index, item)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.height(80.dp)
|
|
||||||
.ifElse(
|
|
||||||
index == savedIndex,
|
|
||||||
Modifier.focusRequester(focus),
|
|
||||||
).onFocusChanged {
|
|
||||||
if (it.isFocused) {
|
|
||||||
focusedIndex = index
|
|
||||||
}
|
|
||||||
}.focusProperties {
|
|
||||||
left = playButtonFocusRequester
|
|
||||||
previous = playButtonFocusRequester
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
if (items.isNotEmpty()) {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(bottom = 32.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
// .fillMaxWidth(.8f)
|
||||||
|
.weight(1f)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme
|
||||||
|
.surfaceColorAtElevation(1.dp)
|
||||||
|
.copy(alpha = .75f),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
).focusProperties {
|
||||||
|
onExit = {
|
||||||
|
playButtonFocusRequester.tryRequestFocus()
|
||||||
|
}
|
||||||
|
}.focusRequester(focusRequester)
|
||||||
|
.focusGroup()
|
||||||
|
.focusRestorer(focus),
|
||||||
|
) {
|
||||||
|
itemsIndexed(items) { index, item ->
|
||||||
|
PlaylistItem(
|
||||||
|
item = item,
|
||||||
|
index = index,
|
||||||
|
onClick = {
|
||||||
|
savedIndex = index
|
||||||
|
item?.let {
|
||||||
|
onClickIndex.invoke(index, item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
savedIndex = index
|
||||||
|
item?.let {
|
||||||
|
onLongClickIndex.invoke(index, item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.height(80.dp)
|
||||||
|
.ifElse(
|
||||||
|
index == savedIndex,
|
||||||
|
Modifier.focusRequester(focus),
|
||||||
|
).onFocusChanged {
|
||||||
|
if (it.isFocused) {
|
||||||
|
focusedIndex = index
|
||||||
|
}
|
||||||
|
}.focusProperties {
|
||||||
|
left = playButtonFocusRequester
|
||||||
|
previous = playButtonFocusRequester
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.no_results),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.focusProperties {
|
||||||
|
onExit = {
|
||||||
|
playButtonFocusRequester.tryRequestFocus()
|
||||||
|
}
|
||||||
|
}.focusRequester(focusRequester)
|
||||||
|
.focusable(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -338,6 +500,10 @@ fun PlaylistDetailsHeader(
|
||||||
focusedItem: BaseItem?,
|
focusedItem: BaseItem?,
|
||||||
onClickPlay: (shuffle: Boolean) -> Unit,
|
onClickPlay: (shuffle: Boolean) -> Unit,
|
||||||
playButtonFocusRequester: FocusRequester,
|
playButtonFocusRequester: FocusRequester,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
filterAndSort: FilterAndSort,
|
||||||
|
onFilterAndSortChange: (GetItemsFilter, SortAndDirection) -> Unit,
|
||||||
|
getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -346,13 +512,14 @@ fun PlaylistDetailsHeader(
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier.focusRequester(playButtonFocusRequester),
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
ExpandablePlayButton(
|
ExpandablePlayButton(
|
||||||
title = R.string.play,
|
title = R.string.play,
|
||||||
resume = Duration.ZERO,
|
resume = Duration.ZERO,
|
||||||
icon = Icons.Default.PlayArrow,
|
icon = Icons.Default.PlayArrow,
|
||||||
onClick = { onClickPlay.invoke(false) },
|
onClick = { onClickPlay.invoke(false) },
|
||||||
|
modifier = Modifier.focusRequester(playButtonFocusRequester),
|
||||||
)
|
)
|
||||||
ExpandableFaButton(
|
ExpandableFaButton(
|
||||||
title = R.string.shuffle,
|
title = R.string.shuffle,
|
||||||
|
|
@ -360,16 +527,45 @@ fun PlaylistDetailsHeader(
|
||||||
onClick = { onClickPlay.invoke(true) },
|
onClick = { onClickPlay.invoke(true) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier,
|
||||||
|
) {
|
||||||
|
FilterByButton(
|
||||||
|
filterOptions = DefaultPlaylistItemsOptions,
|
||||||
|
current = filterAndSort.filter,
|
||||||
|
onFilterChange = {
|
||||||
|
onFilterAndSortChange.invoke(
|
||||||
|
it,
|
||||||
|
filterAndSort.sortAndDirection,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
getPossibleValues = getPossibleFilterValues,
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
SortByButton(
|
||||||
|
sortOptions = BoxSetSortOptions,
|
||||||
|
current = filterAndSort.sortAndDirection,
|
||||||
|
onSortChange = { onFilterAndSortChange.invoke(filterAndSort.filter, it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = focusedItem?.title ?: "",
|
text = focusedItem?.title ?: "",
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.headlineLarge,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = focusedItem?.subtitle ?: "",
|
text = focusedItem?.subtitle ?: "",
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
|
if (focusedItem?.type == BaseItemKind.EPISODE && focusedItem.data.premiereDate != null) {
|
||||||
|
Text(
|
||||||
|
text = formatDateTime(focusedItem.data.premiereDate!!),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
OverviewText(
|
OverviewText(
|
||||||
overview = focusedItem?.data?.overview ?: "",
|
overview = focusedItem?.data?.overview ?: "",
|
||||||
maxLines = 10,
|
maxLines = 10,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
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.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.yearsApi
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue