From 0fb1a69556274d76b21feee244c62c7f6070d7a2 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:40:13 -0500 Subject: [PATCH] Fix only loading 100 episode in a season (#187) Fixes #184 Data was being fetched, but the UI wasn't being properly notified to display the data. This PR also adjusts so that "focused episode header" still takes up the space even if the focused episode hasn't fully loaded yet. This prevents the UI from jumping up and down. Also #42 is _not_ fixed by this PR because it is a different issue. --- .../ui/detail/series/FocusedEpisodeHeader.kt | 20 ++++++------ .../ui/detail/series/SeriesOverviewContent.kt | 12 +++---- .../ui/detail/series/SeriesViewModel.kt | 12 ++----- .../wholphin/util/ApiRequestPager.kt | 31 +++++++++++++++++-- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt index 78775e03..77d7e386 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt @@ -24,18 +24,18 @@ import org.jellyfin.sdk.model.extensions.ticks @Composable fun FocusedEpisodeHeader( - ep: BaseItem, + ep: BaseItem?, overviewOnClick: () -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current - val dto = ep.data + val dto = ep?.data Column( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier, ) { Text( - text = dto.episodeTitle ?: dto.name ?: "", + text = dto?.episodeTitle ?: dto?.name ?: "", style = MaterialTheme.typography.headlineSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -46,17 +46,17 @@ fun FocusedEpisodeHeader( ) { val details = buildList { - dto.seasonEpisode?.let(::add) - dto.premiereDate?.let { add(formatDateTime(it)) } - val duration = dto.runTimeTicks?.ticks + dto?.seasonEpisode?.let(::add) + dto?.premiereDate?.let { add(formatDateTime(it)) } + val duration = dto?.runTimeTicks?.ticks duration ?.roundMinutes ?.toString() ?.let { add(it) } - dto.officialRating?.let(::add) - dto.timeRemaining?.roundMinutes?.let { add("$it left") } + dto?.officialRating?.let(::add) + dto?.timeRemaining?.roundMinutes?.let { add("$it left") } } DotSeparatedRow( texts = details, @@ -68,12 +68,12 @@ fun FocusedEpisodeHeader( verticalAlignment = Alignment.CenterVertically, ) { SimpleStarRating( - dto.communityRating, + dto?.communityRating, Modifier.height(20.dp), ) } OverviewText( - overview = dto.overview ?: "", + overview = dto?.overview ?: "", maxLines = 3, onClick = overviewOnClick, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index f2fde9f3..ce73e872 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -162,13 +162,11 @@ fun SeriesOverviewContent( } item { // Episode header - focusedEpisode?.let { ep -> - FocusedEpisodeHeader( - ep = ep, - overviewOnClick = overviewOnClick, - modifier = Modifier.fillMaxWidth(.66f), - ) - } + FocusedEpisodeHeader( + ep = focusedEpisode, + overviewOnClick = overviewOnClick, + modifier = Modifier.fillMaxWidth(.66f), + ) } item { key(position.seasonTabIndex) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index 8acb295e..36b8b124 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -293,17 +293,11 @@ class SeriesViewModel itemId: UUID, listIndex: Int, ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { - val base = api.userLibraryApi.getItem(itemId).content - val item = BaseItem.Companion.from(base, api) val eps = episodes.value if (eps is EpisodeList.Success) { - val newItems = - eps.episodes.toMutableList().apply { - this[listIndex] = item - } - val newValue = eps.copy(episodes = newItems) + eps.episodes.refreshItem(listIndex, itemId) withContext(Dispatchers.Main) { - episodes.value = newValue + episodes.value = eps } } } @@ -409,7 +403,7 @@ sealed interface EpisodeList { } data class Success( - val episodes: List, + val episodes: ApiRequestPager, val initialIndex: Int, ) : EpisodeList } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt b/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt index bde82618..2cc5bb3d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt @@ -21,6 +21,7 @@ import org.jellyfin.sdk.api.client.extensions.liveTvApi import org.jellyfin.sdk.api.client.extensions.playlistsApi import org.jellyfin.sdk.api.client.extensions.suggestionsApi import org.jellyfin.sdk.api.client.extensions.tvShowsApi +import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult import org.jellyfin.sdk.model.api.GetProgramsDto import org.jellyfin.sdk.model.api.request.GetEpisodesRequest @@ -31,6 +32,7 @@ import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest import timber.log.Timber +import java.util.UUID import java.util.function.Predicate /** @@ -58,7 +60,7 @@ class ApiRequestPager( CacheBuilder .newBuilder() .maximumSize(cacheSize) - .build>() + .build>() suspend fun init(): ApiRequestPager { if (totalCount < 0) { @@ -122,13 +124,38 @@ class ApiRequestPager( false, ) val result = requestHandler.execute(api, newRequest).content - val data = result.items.map { BaseItem.from(it, api, useSeriesForPrimary) } + val data = mutableListOf() + result.items.forEach { data.add(BaseItem.from(it, api, useSeriesForPrimary)) } cachedPages.put(pageNumber, data) items = ItemList(totalCount, pageSize, cachedPages.asMap()) } } } + suspend fun refreshItem( + position: Int, + itemId: UUID, + ) { + val item = + api.userLibraryApi.getItem(itemId).content.let { + BaseItem.from( + it, + api, + useSeriesForPrimary, + ) + } + val pageNumber = position / pageSize + val index = position - pageNumber * pageSize + mutex.withLock { + val page = cachedPages.getIfPresent(pageNumber) + if (page != null && index in page.indices) { + page[index] = item + cachedPages.put(pageNumber, page) + items = ItemList(totalCount, pageSize, cachedPages.asMap()) + } + } + } + companion object { private const val DEBUG = false }