Fix always listing next up/continue watching rows first (#1288)

## Description
Previously the home page always tried to load the "watching rows" (Next
up, Continue watching, or combined) first. This meant that even if the
user's customized home page did not put these rows at the top, they
would always be listed first anyway.

This PR fixes this so rows on the home page are always listed in the
right order.

### Related issues
Fixes #1275

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-04-22 13:34:43 -04:00 committed by GitHub
parent c9cab7a5c2
commit 0e37084a16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -87,26 +88,30 @@ class HomeViewModel
// Refreshing if a load has already occurred and the rows haven't significantly changed // Refreshing if a load has already occurred and the rows haven't significantly changed
val refresh = val refresh =
state.loadingState == LoadingState.Success && state.settings == settings state.loadingState == LoadingState.Success && state.settings == settings
Timber.v("refresh=$refresh, state.loadingState=${state.loadingState}") Timber.v(
"refresh=%s, state.loadingState=%s, %s rows",
refresh,
state.loadingState,
settings.rows.size,
)
_state.update { _state.update {
it.copy( it.copy(
loadingState = if (refresh) LoadingState.Success else LoadingState.Loading, loadingState = if (refresh) LoadingState.Success else LoadingState.Loading,
refreshState = LoadingState.Loading, refreshState = LoadingState.Loading,
settings = settings, settings = settings,
homeRows =
if (refresh) {
it.homeRows
} else {
List(settings.rows.size) { HomeRowLoadingState.Pending("") }
},
) )
} }
val semaphore = Semaphore(4) val semaphore = Semaphore(4)
val watchingRowIndexes =
settings.rows
.mapIndexedNotNull { index, row ->
if (isWatchingRow(row.config)) index else null
}
val deferred = val deferred =
settings.rows settings.rows
// Load the watching rows first
.sortedByDescending { isWatchingRow(it.config) }
.map { row -> .map { row ->
viewModelScope.async(Dispatchers.IO) { viewModelScope.async(Dispatchers.IO) {
semaphore.withPermit { semaphore.withPermit {
@ -123,32 +128,53 @@ class HomeViewModel
) )
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e(ex, "Error on row %s", row) Timber.e(ex, "Error on row %s", row)
HomeRowLoadingState.Error(row.title, exception = ex) HomeRowLoadingState.Error(
row.title,
exception = ex,
)
} }
} }
} }
} }
if (refresh && state.homeRows.isNotEmpty() && watchingRowIndexes.isNotEmpty()) { if (refresh) {
// Replace watching rows first // Replace rows as they complete
Timber.v("Refreshing rows: %s", watchingRowIndexes) val remaining = deferred.withIndex().toMutableList()
val rows = while (remaining.isNotEmpty()) {
deferred val (rowIndex, rowData) =
.filterIndexed { index, _ -> index in watchingRowIndexes } select {
.awaitAll() // "Return" the first remaining that is completed
_state.update { remaining
.forEach { (rowIndex, deferred) ->
deferred.onAwait { rowIndex to it }
}
}
Timber.v("Got row data index=%s", rowIndex)
remaining.removeIf { it.index == rowIndex }
// Include only errors & non-empty successes
if (rowData is HomeRowLoadingState.Error ||
(rowData is HomeRowLoadingState.Success && rowData.items.isNotEmpty())
) {
_state.update { state ->
val newRows = val newRows =
it.homeRows.toMutableList().apply { state.homeRows.toMutableList().apply {
rows.forEachIndexed { index, row -> set(rowIndex, rowData)
set(watchingRowIndexes[index], row)
} }
} state.copy(
it.copy(
loadingState = LoadingState.Success,
homeRows = newRows, homeRows = newRows,
) )
} }
} else {
Timber.d("Skipping invalid row %s: %s", rowIndex, rowData)
} }
}
_state.update {
it.copy(
loadingState = LoadingState.Success,
refreshState = LoadingState.Success,
)
}
} else {
val rows = val rows =
deferred deferred
.awaitAll() .awaitAll()
@ -166,6 +192,8 @@ class HomeViewModel
) )
} }
} }
Timber.d("Home page load complete")
}
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e(ex, "Exception during home page loading") Timber.e(ex, "Exception during home page loading")
if (state.value.loadingState == LoadingState.Success) { if (state.value.loadingState == LoadingState.Success) {