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
This commit is contained in:
Ray 2026-02-11 16:05:11 -05:00 committed by GitHub
parent 724d4d0da3
commit 6e676b9474
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 42 additions and 15 deletions

View file

@ -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<BaseItem>,
) {
viewModelScope.launch(Dispatchers.IO) {
): Deferred<HomeRowLoadingState> =
viewModelScope.async(Dispatchers.IO) {
val titleStr = context.getString(title)
val row =
try {
@ -115,7 +116,6 @@ abstract class RecommendedViewModel(
}
update(title, row)
}
}
}
@Composable

View file

@ -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<Deferred<HomeRowLoadingState>>()
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,19 +219,29 @@ 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) {
for (i in 0..<jobs.size) {
val result = jobs[i].await()
if (result.completed) {
Timber.v("First success")
loading.setValueOnMain(LoadingState.Success)
}
break
}
}
}
}
override fun update(
@StringRes title: Int,
row: HomeRowLoadingState,
) {
): HomeRowLoadingState {
rows.update { current ->
current.toMutableList().apply { set(rowTitles[title]!!, row) }
}
return row
}
companion object {

View file

@ -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<Deferred<HomeRowLoadingState>>()
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,18 +267,26 @@ class RecommendedTvShowViewModel
}
if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) {
for (i in 0..<jobs.size) {
val result = jobs[i].await()
if (result is HomeRowLoadingState.Success) {
Timber.v("First success")
loading.setValueOnMain(LoadingState.Success)
}
break
}
}
}
}
override fun update(
@StringRes title: Int,
row: HomeRowLoadingState,
) {
): HomeRowLoadingState {
rows.update { current ->
current.toMutableList().apply { set(rowTitles[title]!!, row) }
}
return row
}
companion object {

View file

@ -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