mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
fix(android): stop home hero carousel reshuffling while loading
The home hero pool is rebuilt every time a catalog batch finishes loading, then shuffled with a one-shot Random seeded on the request key. Re-permuting the growing pool on each intermediate publish made the carousel visibly reshuffle/reorder several times on app open before settling. Order the hero candidates by a stable per-item hash instead, so the relative order of already-loaded items stays fixed while later items slot into place. The carousel now settles instead of reshuffling, and ordering remains deterministic per catalog set and stable across app opens. Fixes #1133
This commit is contained in:
parent
09cb8f8277
commit
bd474133fc
2 changed files with 76 additions and 2 deletions
|
|
@ -197,14 +197,14 @@ object HomeRepository {
|
|||
}
|
||||
|
||||
val catalogHeroItems = if (snapshot.heroEnabled) {
|
||||
val heroRandom = Random((requestKey?.hashCode() ?: 0).absoluteValue + 1)
|
||||
val heroOrderSeed = requestKey?.hashCode() ?: 0
|
||||
currentDefinitions
|
||||
.filter { definition -> preferences[definition.key]?.heroSourceEnabled != false }
|
||||
.mapNotNull { definition -> cachedSections[definition.key] }
|
||||
.map { section -> section.withReleaseFilter() }
|
||||
.flatMap { section -> section.items }
|
||||
.distinctBy { item -> "${item.type}:${item.id}" }
|
||||
.shuffled(heroRandom)
|
||||
.orderedForHero(heroOrderSeed) { item -> "${item.type}:${item.id}" }
|
||||
.take(HOME_HERO_ITEM_LIMIT)
|
||||
} else {
|
||||
emptyList()
|
||||
|
|
@ -433,6 +433,26 @@ private const val HOME_CATALOG_FETCH_BATCH_SIZE = 4
|
|||
private const val HOME_CATALOG_PREVIEW_FETCH_LIMIT = 18
|
||||
private const val HOME_CATALOG_PUBLISH_INTERVAL = 2
|
||||
|
||||
internal fun <T> List<T>.orderedForHero(seed: Int, keySelector: (T) -> String): List<T> =
|
||||
sortedWith(
|
||||
compareBy(
|
||||
{ item -> stableHeroOrderRank(seed, keySelector(item)) },
|
||||
{ item -> keySelector(item) },
|
||||
),
|
||||
)
|
||||
|
||||
internal fun stableHeroOrderRank(seed: Int, itemKey: String): Int {
|
||||
var hash = seed
|
||||
for (index in itemKey.indices) {
|
||||
hash = hash * 31 + itemKey[index].code
|
||||
}
|
||||
// Avalanche mix so items from the same catalog (similar ids) don't cluster together.
|
||||
hash = hash xor (hash ushr 16)
|
||||
hash *= 0x7feb352d
|
||||
hash = hash xor (hash ushr 15)
|
||||
return hash
|
||||
}
|
||||
|
||||
private fun prioritizeDefinitions(
|
||||
definitions: List<HomeCatalogDefinition>,
|
||||
snapshot: HomeCatalogSettingsSnapshot,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package com.nuvio.app.features.home
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class HomeHeroOrderingTest {
|
||||
|
||||
@Test
|
||||
fun `ordering is deterministic for a given seed`() {
|
||||
val items = listOf("series:a", "movie:b", "series:c", "movie:d", "series:e")
|
||||
|
||||
assertEquals(
|
||||
items.orderedForHero(seed = 42) { it },
|
||||
items.orderedForHero(seed = 42) { it },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ordering does not depend on the input order`() {
|
||||
val seed = 7
|
||||
val ascending = listOf("a", "b", "c", "d", "e").orderedForHero(seed) { it }
|
||||
val descending = listOf("e", "d", "c", "b", "a").orderedForHero(seed) { it }
|
||||
|
||||
assertEquals(ascending, descending)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `adding more items keeps the relative order of already-loaded items`() {
|
||||
// Regression guard for the hero carousel reshuffle bug: the hero pool is rebuilt as more
|
||||
// catalog batches load, so the ordering must keep already-shown items in place instead of
|
||||
// re-permuting the whole carousel. The previous Random.shuffled() implementation failed
|
||||
// this because shuffling lists of different sizes produces unrelated orders.
|
||||
val seed = 123
|
||||
val firstBatch = listOf("s1", "s2", "s3", "s4")
|
||||
val grownPool = firstBatch + listOf("s5", "s6", "s7", "s8", "s9")
|
||||
|
||||
val firstBatchOrder = firstBatch.orderedForHero(seed) { it }
|
||||
val grownOrderFilteredToFirstBatch = grownPool
|
||||
.orderedForHero(seed) { it }
|
||||
.filter { it in firstBatch }
|
||||
|
||||
assertEquals(firstBatchOrder, grownOrderFilteredToFirstBatch)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ordering is shuffled rather than sorted alphabetically`() {
|
||||
val items = (1..20).map { "series:tt$it" }
|
||||
val ordered = items.orderedForHero(seed = 99) { it }
|
||||
|
||||
assertTrue(ordered != items.sortedBy { it }, "hero order should not be a plain sort")
|
||||
assertEquals(items.toSet(), ordered.toSet(), "ordering must not drop or add items")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue