From 1a4ddabff99b89053db6f9856f7fca39b94a8e4f Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 6 Oct 2025 17:15:24 -0400 Subject: [PATCH] Sort names, static home rows, & only show supported content --- .../damontecres/dolphin/ui/cards/ItemCard.kt | 7 ++ .../dolphin/ui/components/SortByButton.kt | 8 +- .../dolphin/ui/data/SortAndDirection.kt | 23 +++-- .../damontecres/dolphin/ui/detail/CardGrid.kt | 14 +-- .../ui/detail/CollectionFolderDetails.kt | 22 ++--- .../damontecres/dolphin/ui/main/HomePage.kt | 6 +- .../dolphin/ui/main/HomeViewModel.kt | 97 ++++++++++++------- .../dolphin/util/ApiRequestPager.kt | 61 ++++++++++++ .../damontecres/dolphin/util/Constants.kt | 30 ++++++ app/src/main/res/values/strings.xml | 9 ++ 10 files changed, 203 insertions(+), 74 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/dolphin/util/Constants.kt diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt index f4b63126..ec78e98a 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt @@ -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() diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt index 0c22c53a..57f17b91 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt @@ -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, ) }, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/data/SortAndDirection.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/data/SortAndDirection.kt index 7770c340..d4a54987 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/data/SortAndDirection.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/data/SortAndDirection.kt @@ -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") + } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt index 02443699..0c39b157 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt @@ -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( diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt index 4cc6f50a..55816c11 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt @@ -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 = { _, _ -> }, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt index 4bd520c3..bcc3c92f 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt @@ -60,7 +60,7 @@ import org.jellyfin.sdk.model.extensions.ticks data class HomeRow( val section: HomeSection, - val items: List, + val items: List, 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, modifier: Modifier = Modifier, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeViewModel.kt index f801420d..3b0ce429 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeViewModel.kt @@ -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 diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt b/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt index 755a3406..a4272517 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt @@ -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 = api.tvShowsApi.getEpisodes(request) } +val GetResumeItemsRequestHandler = + object : RequestHandler { + 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 = api.itemsApi.getResumeItems(request) + } + +val GetNextUpRequestHandler = + object : RequestHandler { + 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 = api.tvShowsApi.getNextUp(request) + } + +val GetSuggestionsRequestHandler = + object : RequestHandler { + 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 = api.suggestionsApi.getSuggestions(request) + } + class ApiRequestPager( val api: ApiClient, val request: T, diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/Constants.kt b/app/src/main/java/com/github/damontecres/dolphin/util/Constants.kt new file mode 100644 index 00000000..243b6bac --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/util/Constants.kt @@ -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, + ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5485eeea..23b4dee7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,4 +47,13 @@ Show playback debug info Delay before playing next up Auto play next up + + + Name + Random + Date Episode Added + Date Added + Date Played + Date Released +