From 6e676b9474f0f97e3e070251a033db536f417f07 Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:05:11 -0500 Subject: [PATCH] Fixes overscrolling on recommended tabs (#881) ## Description If the continue watching row is empty on recommended pages, it would scroll to the first successfully loaded row even if previous ones are still pending. This PR fixes that so the page waits until the right first row is ready. ### Related issues Fixes #839 ### Testing Emulator & shield 2019 ## Screenshots N/A ## AI or LLM usage None --- .../ui/components/RecommendedContent.kt | 10 ++++---- .../ui/components/RecommendedMovie.kt | 23 +++++++++++++++---- .../ui/components/RecommendedTvShow.kt | 21 +++++++++++++---- .../damontecres/wholphin/util/LoadingState.kt | 3 +++ 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt index 24b3d502..6c577c9b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt @@ -37,9 +37,10 @@ import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.LoadingState +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.MediaType import java.util.UUID @@ -99,13 +100,13 @@ abstract class RecommendedViewModel( abstract fun update( @StringRes title: Int, row: HomeRowLoadingState, - ) + ): HomeRowLoadingState fun update( @StringRes title: Int, block: suspend () -> List, - ) { - viewModelScope.launch(Dispatchers.IO) { + ): Deferred = + viewModelScope.async(Dispatchers.IO) { val titleStr = context.getString(title) val row = try { @@ -115,7 +116,6 @@ abstract class RecommendedViewModel( } update(title, row) } - } } @Composable diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt index e44c72f7..a1a5be80 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt @@ -32,6 +32,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.firstOrNull @@ -123,6 +124,8 @@ class RecommendedMovieViewModel } } + val jobs = mutableListOf>() + update(R.string.recently_released) { val request = GetItemsRequest( @@ -138,7 +141,7 @@ class RecommendedMovieViewModel enableTotalRecordCount = false, ) GetItemsRequestHandler.execute(api, request).toBaseItems(api, false) - } + }.also(jobs::add) update(R.string.recently_added) { val request = @@ -155,7 +158,7 @@ class RecommendedMovieViewModel enableTotalRecordCount = false, ) GetItemsRequestHandler.execute(api, request).toBaseItems(api, false) - } + }.also(jobs::add) update(R.string.top_unwatched) { val request = @@ -173,7 +176,7 @@ class RecommendedMovieViewModel enableTotalRecordCount = false, ) GetItemsRequestHandler.execute(api, request).toBaseItems(api, false) - } + }.also(jobs::add) viewModelScope.launch(Dispatchers.IO) { try { @@ -216,8 +219,17 @@ class RecommendedMovieViewModel } } + // If the continue watching row is empty, then wait until the first successful row + // is loaded before telling the UI that the page is loaded if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) { - loading.setValueOnMain(LoadingState.Success) + for (i in 0.. current.toMutableList().apply { set(rowTitles[title]!!, row) } } + return row } companion object { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt index 4e952033..00430a2c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt @@ -33,6 +33,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow @@ -170,6 +171,8 @@ class RecommendedTvShowViewModel } } + val jobs = mutableListOf>() + update(R.string.recently_released) { val request = GetItemsRequest( @@ -185,7 +188,7 @@ class RecommendedTvShowViewModel enableTotalRecordCount = false, ) GetItemsRequestHandler.execute(api, request).toBaseItems(api, true) - } + }.also(jobs::add) update(R.string.recently_added) { val request = @@ -202,7 +205,7 @@ class RecommendedTvShowViewModel enableTotalRecordCount = false, ) GetItemsRequestHandler.execute(api, request).toBaseItems(api, true) - } + }.also(jobs::add) update(R.string.top_unwatched) { val request = @@ -220,7 +223,7 @@ class RecommendedTvShowViewModel enableTotalRecordCount = false, ) GetItemsRequestHandler.execute(api, request).toBaseItems(api, true) - } + }.also(jobs::add) viewModelScope.launch(Dispatchers.IO) { try { @@ -264,7 +267,14 @@ class RecommendedTvShowViewModel } if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) { - loading.setValueOnMain(LoadingState.Success) + for (i in 0.. current.toMutableList().apply { set(rowTitles[title]!!, row) } } + return row } companion object { diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt b/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt index c35e9512..1a4ca1aa 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt @@ -48,6 +48,9 @@ sealed interface RowLoadingState { sealed interface HomeRowLoadingState { val title: String + val completed: Boolean + get() = this is Success || this is Error + data class Pending( override val title: String, ) : HomeRowLoadingState