Sort names, static home rows, & only show supported content

This commit is contained in:
Damontecres 2025-10-06 17:15:24 -04:00
parent ead2431bc4
commit 1a4ddabff9
No known key found for this signature in database
10 changed files with 203 additions and 74 deletions

View file

@ -50,6 +50,7 @@ import com.github.damontecres.dolphin.ui.ifElse
import com.github.damontecres.dolphin.util.seasonEpisode
import kotlinx.coroutines.delay
import org.jellyfin.sdk.model.api.BaseItemKind
import timber.log.Timber
@Composable
fun ItemCard(
@ -167,6 +168,12 @@ fun ItemCardImage(
contentDescription = name,
contentScale = ContentScale.Fit,
alignment = Alignment.Center,
// TODO error/fallback images
error = null,
fallback = null,
onError = {
Timber.e(it.result.throwable, "Error loading image: $imageUrl")
},
modifier =
Modifier
.fillMaxSize()

View file

@ -21,6 +21,7 @@ import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.ui.data.SortAndDirection
import com.github.damontecres.dolphin.ui.data.flip
import com.github.damontecres.dolphin.ui.data.getStringRes
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
@ -32,6 +33,7 @@ fun SortByButton(
modifier: Modifier = Modifier,
) {
val currentSort = current.sort
val name = stringResource(getStringRes(currentSort))
val currentDirection = current.direction
var sortByDropDown by remember { mutableStateOf(false) }
val context = LocalContext.current
@ -58,7 +60,7 @@ fun SortByButton(
)
}
append(" ")
append(currentSort.name) // TODO names
append(name)
},
)
}
@ -69,7 +71,7 @@ fun SortByButton(
onDismissRequest = { sortByDropDown = false },
) {
sortOptions
.sortedBy { it.name }
// .sortedBy { it.name }
.forEach { sortOption ->
DropdownMenuItem(
leadingIcon = {
@ -91,7 +93,7 @@ fun SortByButton(
},
text = {
Text(
text = sortOption.name,
text = stringResource(getStringRes(sortOption)),
color = MaterialTheme.colorScheme.onSecondaryContainer,
)
},

View file

@ -1,5 +1,7 @@
package com.github.damontecres.dolphin.ui.data
import androidx.annotation.StringRes
import com.github.damontecres.dolphin.R
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
@ -15,13 +17,9 @@ fun SortOrder.flip() = if (this == SortOrder.ASCENDING) SortOrder.DESCENDING els
val MovieSortOptions =
listOf(
ItemSortBy.SORT_NAME,
ItemSortBy.PRODUCTION_YEAR,
ItemSortBy.COMMUNITY_RATING,
ItemSortBy.PREMIERE_DATE,
ItemSortBy.DATE_CREATED,
ItemSortBy.DATE_PLAYED,
ItemSortBy.PLAY_COUNT,
ItemSortBy.STUDIO,
ItemSortBy.OFFICIAL_RATING,
ItemSortBy.RANDOM,
)
@ -29,12 +27,9 @@ val SeriesSortOptions =
listOf(
ItemSortBy.SORT_NAME,
ItemSortBy.PREMIERE_DATE,
ItemSortBy.COMMUNITY_RATING,
ItemSortBy.DATE_CREATED,
ItemSortBy.DATE_LAST_CONTENT_ADDED,
ItemSortBy.DATE_PLAYED,
ItemSortBy.STUDIO,
ItemSortBy.OFFICIAL_RATING,
ItemSortBy.RANDOM,
)
@ -45,3 +40,15 @@ val VideoSortOptions =
ItemSortBy.DATE_PLAYED,
ItemSortBy.RANDOM,
)
@StringRes
fun getStringRes(sort: ItemSortBy): Int =
when (sort) {
ItemSortBy.SORT_NAME -> R.string.sort_by_name
ItemSortBy.PREMIERE_DATE -> R.string.sort_by_date_released
ItemSortBy.DATE_CREATED -> R.string.sort_by_date_added
ItemSortBy.DATE_LAST_CONTENT_ADDED -> R.string.sort_by_date_episode_added
ItemSortBy.DATE_PLAYED -> R.string.sort_by_date_played
ItemSortBy.RANDOM -> R.string.sort_by_random
else -> throw IllegalArgumentException("Unsupported sort option: $sort")
}

View file

@ -34,7 +34,6 @@ import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Button
@ -46,7 +45,6 @@ import com.github.damontecres.dolphin.ui.AppColors
import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.ui.cards.ItemCard
import com.github.damontecres.dolphin.ui.ifElse
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.ui.playback.isBackwardButton
import com.github.damontecres.dolphin.ui.playback.isForwardButton
import com.github.damontecres.dolphin.ui.playback.isPlayKeyUp
@ -66,7 +64,7 @@ fun CardGrid(
letterPosition: suspend (Char) -> Int,
requestFocus: Boolean,
gridFocusRequester: FocusRequester,
navigationManager: NavigationManager,
showJumpButtons: Boolean,
modifier: Modifier = Modifier,
initialPosition: Int = 0,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
@ -120,9 +118,6 @@ fun CardGrid(
// hasRun = true
}
val context = LocalContext.current
val showJumpButtons = true
var alphabetFocus by remember { mutableStateOf(false) }
val focusOn = { index: Int ->
if (DEBUG) Timber.v("focusOn: focusedIndex=$focusedIndex, index=$index")
@ -237,7 +232,7 @@ fun CardGrid(
}
},
) {
if (showJumpButtons && pager.size > 0) {
if (showJumpButtons && pager.isNotEmpty()) {
JumpButtons(
jump1 = jump1,
jump2 = jump2,
@ -286,8 +281,7 @@ fun CardGrid(
} else {
Modifier
}
// TODO
val item = pager[index] // ?.let { convertModel(it, api) }
val item = pager[index]
if (!hasRequestFocusRun && requestFocus && initialPosition >= 0) {
// On very first composition, if parent wants to focus on the grid, do so
LaunchedEffect(Unit) {
@ -328,7 +322,7 @@ fun CardGrid(
)
}
}
if (pager.size == 0) {
if (pager.isEmpty()) {
// focusedIndex = -1
Box(modifier = Modifier.fillMaxSize()) {
Text(

View file

@ -87,20 +87,11 @@ class CollectionFolderViewModel
}
val includeItemTypes =
when (item.data.collectionType) {
CollectionType.UNKNOWN -> TODO()
CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE)
CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES)
CollectionType.MUSIC -> TODO()
CollectionType.MUSICVIDEOS -> TODO()
CollectionType.TRAILERS -> TODO()
CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO)
CollectionType.BOXSETS -> TODO()
CollectionType.BOOKS -> TODO()
CollectionType.PHOTOS -> TODO()
CollectionType.LIVETV -> TODO()
CollectionType.PLAYLISTS -> TODO()
CollectionType.FOLDERS -> TODO()
null -> listOf()
else -> listOf()
}
val request =
GetItemsRequest(
@ -110,7 +101,12 @@ class CollectionFolderViewModel
// recursive = true,
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB),
includeItemTypes = includeItemTypes,
sortBy = listOf(sortAndDirection.sort),
sortBy =
listOf(
sortAndDirection.sort,
ItemSortBy.SORT_NAME,
ItemSortBy.PRODUCTION_YEAR,
),
sortOrder = listOf(sortAndDirection.direction),
fields = DefaultItemFields,
)
@ -225,7 +221,7 @@ fun CollectionDetails(
letterPosition = { 0 },
requestFocus = true,
gridFocusRequester = gridFocusRequester,
navigationManager = navigationManager,
showJumpButtons = false, // TODO add preference
modifier = Modifier.fillMaxSize(),
initialPosition = 0,
positionCallback = { _, _ -> },

View file

@ -60,7 +60,7 @@ import org.jellyfin.sdk.model.extensions.ticks
data class HomeRow(
val section: HomeSection,
val items: List<BaseItem>,
val items: List<BaseItem?>,
val title: String? = null,
)
@ -82,13 +82,13 @@ fun HomePage(
LoadingState.Success -> {
val homeRows by viewModel.homeRows.observeAsState(listOf())
MainPageContent(navigationManager, homeRows, modifier)
HomePageContent(navigationManager, homeRows, modifier)
}
}
}
@Composable
fun MainPageContent(
fun HomePageContent(
navigationManager: NavigationManager,
homeRows: List<HomeRow>,
modifier: Modifier = Modifier,

View file

@ -6,15 +6,15 @@ import androidx.lifecycle.viewModelScope
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.DefaultItemFields
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.util.LoadingExceptionHandler
import com.github.damontecres.dolphin.util.LoadingState
import com.github.damontecres.dolphin.util.supportItemKinds
import com.github.damontecres.dolphin.util.supportedCollectionTypes
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi
import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
import org.jellyfin.sdk.api.client.extensions.userApi
@ -44,19 +44,33 @@ class HomeViewModel
"Error loading home page",
),
) {
val user = api.userApi.getCurrentUser().content
val displayPrefs =
api.displayPreferencesApi
.getDisplayPreferences(
displayPreferencesId = "usersettings",
client = "emby",
).content
val user by api.userApi.getCurrentUser()
// val displayPrefs =
// api.displayPreferencesApi
// .getDisplayPreferences(
// displayPreferencesId = "usersettings",
// client = "emby",
// ).content
val homeSections =
displayPrefs.customPrefs.entries
.filter { it.key.startsWith("homesection") && it.value.isNotNullOrBlank() }
.sortedBy { it.key }
.map { HomeSection.fromKey(it.value ?: "") }
.filterNot { it == HomeSection.NONE }
listOf(
HomeSection.RESUME,
HomeSection.NEXT_UP,
HomeSection.LATEST_MEDIA,
)
// TODO use display preferences?
// displayPrefs.customPrefs.entries
// .filter { it.key.startsWith("homesection") && it.value.isNotNullOrBlank() }
// .sortedBy { it.key }
// .map { HomeSection.fromKey(it.value ?: "") }
// .filter {
// it in
// setOf(
// HomeSection.LATEST_MEDIA,
// HomeSection.NEXT_UP,
// HomeSection.RESUME,
// )
// }
val latestMediaIncludes =
user.configuration?.orderedViews.orEmpty().toMutableList().apply {
@ -71,29 +85,38 @@ class HomeViewModel
Timber.Forest.v("Loading section: %s", section.name)
when (section) {
HomeSection.LATEST_MEDIA -> {
latestMediaIncludes.map { viewId ->
val title =
views.items.firstOrNull { it.id == viewId }?.name?.let {
"Recently Added in $it"
}
val request =
GetLatestMediaRequest(
fields = DefaultItemFields,
imageTypeLimit = 1,
parentId = viewId,
groupItems = true,
limit = limit,
latestMediaIncludes.mapNotNull { viewId ->
val view =
views.items
.firstOrNull { it.id == viewId }
if (view?.collectionType in supportedCollectionTypes) {
val title =
view
?.name
?.let {
"Recently Added in $it"
}
val request =
GetLatestMediaRequest(
fields = DefaultItemFields,
imageTypeLimit = 1,
parentId = viewId,
groupItems = true,
limit = limit,
)
val latest =
api.userLibraryApi
.getLatestMedia(request)
.content
.map { BaseItem.from(it, api, true) }
HomeRow(
section = section,
items = latest,
title = title,
)
val latest =
api.userLibraryApi
.getLatestMedia(request)
.content
.map { BaseItem.from(it, api, true) }
HomeRow(
section = section,
items = latest,
title = title,
)
} else {
null
}
}
}
@ -103,7 +126,7 @@ class HomeViewModel
userId = user.id,
fields = DefaultItemFields,
limit = limit,
// TODO, more params?
includeItemTypes = supportItemKinds,
)
val items =
api.itemsApi

View file

@ -16,10 +16,14 @@ import kotlinx.coroutines.sync.withLock
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.Response
import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.api.client.extensions.suggestionsApi
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest
import timber.log.Timber
import java.util.function.Predicate
@ -76,6 +80,63 @@ val GetEpisodesRequestHandler =
): Response<BaseItemDtoQueryResult> = api.tvShowsApi.getEpisodes(request)
}
val GetResumeItemsRequestHandler =
object : RequestHandler<GetResumeItemsRequest> {
override fun prepare(
request: GetResumeItemsRequest,
startIndex: Int,
limit: Int,
enableTotalRecordCount: Boolean,
): GetResumeItemsRequest =
request.copy(
startIndex = startIndex,
limit = limit,
)
override suspend fun execute(
api: ApiClient,
request: GetResumeItemsRequest,
): Response<BaseItemDtoQueryResult> = api.itemsApi.getResumeItems(request)
}
val GetNextUpRequestHandler =
object : RequestHandler<GetNextUpRequest> {
override fun prepare(
request: GetNextUpRequest,
startIndex: Int,
limit: Int,
enableTotalRecordCount: Boolean,
): GetNextUpRequest =
request.copy(
startIndex = startIndex,
limit = limit,
)
override suspend fun execute(
api: ApiClient,
request: GetNextUpRequest,
): Response<BaseItemDtoQueryResult> = api.tvShowsApi.getNextUp(request)
}
val GetSuggestionsRequestHandler =
object : RequestHandler<GetSuggestionsRequest> {
override fun prepare(
request: GetSuggestionsRequest,
startIndex: Int,
limit: Int,
enableTotalRecordCount: Boolean,
): GetSuggestionsRequest =
request.copy(
startIndex = startIndex,
limit = limit,
)
override suspend fun execute(
api: ApiClient,
request: GetSuggestionsRequest,
): Response<BaseItemDtoQueryResult> = api.suggestionsApi.getSuggestions(request)
}
class ApiRequestPager<T>(
val api: ApiClient,
val request: T,

View file

@ -0,0 +1,30 @@
package com.github.damontecres.dolphin.util
import com.github.damontecres.dolphin.ui.main.HomeSection
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
val supportedHomeSection =
setOf(
HomeSection.LATEST_MEDIA,
HomeSection.NEXT_UP,
HomeSection.RESUME,
)
val supportItemKinds =
setOf(
BaseItemKind.MOVIE,
BaseItemKind.EPISODE,
BaseItemKind.SERIES,
BaseItemKind.VIDEO,
BaseItemKind.SEASON,
BaseItemKind.COLLECTION_FOLDER,
BaseItemKind.USER_VIEW,
)
val supportedCollectionTypes =
setOf(
CollectionType.MOVIES,
CollectionType.TVSHOWS,
CollectionType.HOMEVIDEOS,
)

View file

@ -47,4 +47,13 @@
<string name="playback_debug_info">Show playback debug info</string>
<string name="auto_play_next_delay">Delay before playing next up</string>
<string name="auto_play_next">Auto play next up</string>
<string name="sort_by_name">Name</string>
<string name="sort_by_random">Random</string>
<string name="sort_by_date_episode_added">Date Episode Added</string>
<string name="sort_by_date_added">Date Added</string>
<string name="sort_by_date_played">Date Played</string>
<string name="sort_by_date_released">Date Released</string>
</resources>