@@ -380,18 +380,18 @@ internal fun resolveHomeCategoryIndex(
380380internal fun resolveHomeItemIndex (
381381 itemKeys : List <String >,
382382 preferredItemKey : String? ,
383- fallbackIndex : Int ,
384- hasMore : Boolean
383+ fallbackIndex : Int
385384): Int {
386385 val preferredIndex = preferredItemKey?.let (itemKeys::indexOf) ? : - 1
387386 if (preferredIndex >= 0 ) return preferredIndex
387+ if (itemKeys.isEmpty()) return 0
388+ return fallbackIndex.coerceIn(0 , itemKeys.lastIndex)
389+ }
388390
389- val safeFallback = fallbackIndex.coerceAtLeast(0 )
390- if (itemKeys.isEmpty() || safeFallback <= itemKeys.lastIndex) return safeFallback
391-
392- // A paged row can temporarily contain fewer items while it is refreshing.
393- // Preserve the intended index until the page arrives instead of snapping left.
394- return if (hasMore) safeFallback else itemKeys.lastIndex
391+ internal fun clampHomeItemIndex (items : List <MediaItem >, index : Int ): Int {
392+ val realItemCount = items.count { ! it.isPlaceholder }
393+ val navigableItemCount = if (realItemCount > 0 ) realItemCount else items.size
394+ return if (navigableItemCount == 0 ) 0 else index.coerceIn(0 , navigableItemCount - 1 )
395395}
396396
397397@androidx.compose.runtime.Immutable
@@ -2455,17 +2455,15 @@ private fun HomeInputLayer(
24552455 stableHomeRowItemKeys(focusedCategory.id, focusedCategory.items)
24562456 }
24572457 }
2458- val focusedRowHasMore = focusedCategoryId?.let { categoryHasMoreMap[it] == true } == true
2459-
2460- // Restore the same title when a row is reordered or refreshed. Empty and
2461- // partial paged results keep the pending index instead of resetting to zero.
2462- LaunchedEffect (focusedCategoryId, focusedItemKeys, focusedRowHasMore) {
2458+ // Restore the same title when a row is reordered or refreshed. Always clamp
2459+ // to a real item: an out-of-range index can otherwise scroll a paged rail
2460+ // entirely into its loading placeholders.
2461+ LaunchedEffect (focusedCategoryId, focusedItemKeys) {
24632462 val categoryId = focusedCategoryId ? : return @LaunchedEffect
24642463 val resolvedIndex = resolveHomeItemIndex(
24652464 itemKeys = focusedItemKeys,
24662465 preferredItemKey = focusState.rowItemKeysByCategoryId[categoryId],
2467- fallbackIndex = focusState.currentItemIndex,
2468- hasMore = focusedRowHasMore
2466+ fallbackIndex = focusState.currentItemIndex
24692467 )
24702468 if (focusState.currentItemIndex != resolvedIndex) {
24712469 focusState.currentItemIndex = resolvedIndex
@@ -2600,10 +2598,13 @@ private fun HomeInputLayer(
26002598 }
26012599 focusState.currentRowIndex--
26022600 // Restore saved position for the target row (or 0 if never visited)
2603- val targetCategoryId = categories.getOrNull(focusState.currentRowIndex)?.id
2604- focusState.currentItemIndex = targetCategoryId
2601+ val targetCategory = categories.getOrNull(focusState.currentRowIndex)
2602+ val restoredIndex = targetCategory?.id
26052603 ?.let (focusState.rowItemIndicesByCategoryId::get)
26062604 ? : 0
2605+ focusState.currentItemIndex = targetCategory
2606+ ?.let { clampHomeItemIndex(it.items, restoredIndex) }
2607+ ? : 0
26072608 focusState.lastNavEventTime = SystemClock .elapsedRealtime()
26082609 true
26092610 } else {
@@ -2620,10 +2621,13 @@ private fun HomeInputLayer(
26202621 focusState.userHasNavigated = true
26212622 if (focusState.isSidebarFocused) {
26222623 focusState.isSidebarFocused = false
2623- val targetCategoryId = categories.getOrNull(focusState.currentRowIndex)?.id
2624- focusState.currentItemIndex = targetCategoryId
2624+ val targetCategory = categories.getOrNull(focusState.currentRowIndex)
2625+ val restoredIndex = targetCategory?.id
26252626 ?.let (focusState.rowItemIndicesByCategoryId::get)
26262627 ? : focusState.currentItemIndex
2628+ focusState.currentItemIndex = targetCategory
2629+ ?.let { clampHomeItemIndex(it.items, restoredIndex) }
2630+ ? : 0
26272631 focusState.lastNavEventTime = SystemClock .elapsedRealtime()
26282632 true
26292633 } else if (! focusState.isSidebarFocused && focusState.currentRowIndex < categories.size - 1 ) {
@@ -2633,10 +2637,13 @@ private fun HomeInputLayer(
26332637 }
26342638 focusState.currentRowIndex++
26352639 // Restore saved position for the target row (or 0 if never visited)
2636- val targetCategoryId = categories.getOrNull(focusState.currentRowIndex)?.id
2637- focusState.currentItemIndex = targetCategoryId
2640+ val targetCategory = categories.getOrNull(focusState.currentRowIndex)
2641+ val restoredIndex = targetCategory?.id
26382642 ?.let (focusState.rowItemIndicesByCategoryId::get)
26392643 ? : 0
2644+ focusState.currentItemIndex = targetCategory
2645+ ?.let { clampHomeItemIndex(it.items, restoredIndex) }
2646+ ? : 0
26402647 focusState.lastNavEventTime = SystemClock .elapsedRealtime()
26412648 true
26422649 } else {
@@ -3354,7 +3361,11 @@ private fun TvHomeRowsLayer(
33543361 categoryHasMore = categoryHasMoreMap[category.id] == true ,
33553362 smoothScrolling = smoothScrolling,
33563363 onLoadMore = onRowLoadMore,
3357- focusedItemIndex = if (rowIsFocused) focusState.currentItemIndex else - 1 ,
3364+ focusedItemIndex = if (rowIsFocused) {
3365+ clampHomeItemIndex(category.items, focusState.currentItemIndex)
3366+ } else {
3367+ - 1
3368+ },
33583369 isFastScrolling = rowIsFocused && isFastScrolling,
33593370 featuredTrailerKey = if (rowIsFocused) featuredTrailerKey else null ,
33603371 featuredTrailerDelayMs = featuredTrailerDelayMs,
@@ -3594,7 +3605,7 @@ private fun ContentRow(
35943605 val cardAspectRatio = if (effectivePosterMode) 2f / 3f else 16f / 9f
35953606 val itemWidth = if (effectivePosterMode) 105 .dp else 210 .dp
35963607 val itemSpacing = 14 .dp
3597- val itemsToRender = remember(category.items, effectiveCategoryHasMore, effectivePosterMode ) {
3608+ val itemsToRender = remember(category.items) {
35983609 if (category.items.isEmpty()) {
35993610 (1 .. 8 ).map { index ->
36003611 MediaItem (
@@ -3604,17 +3615,9 @@ private fun ContentRow(
36043615 isPlaceholder = true
36053616 )
36063617 }
3607- } else if (effectiveCategoryHasMore) {
3608- val skeletonCount = if (effectivePosterMode) 12 else 7
3609- category.items + List (skeletonCount) { idx ->
3610- MediaItem (
3611- id = - 1000 - idx,
3612- title = " " ,
3613- isPlaceholder = true
3614- )
3615- }
36163618 } else {
3617- category.items
3619+ val realItems = category.items.filterNot { it.isPlaceholder }
3620+ realItems.ifEmpty { category.items }
36183621 }
36193622 }
36203623 val itemKeys = remember(category.id, itemsToRender) {
@@ -3812,12 +3815,8 @@ private fun ContentRow(
38123815 }
38133816 }
38143817 ) { index, item ->
3815- if (item.isPlaceholder) {
3816- LaunchedEffect (item.id) {
3817- onLoadMore()
3818- }
3819- } else if (effectiveCategoryHasMore && index >= category.items.size - 5 ) {
3820- LaunchedEffect (category.items.size) {
3818+ if (! item.isPlaceholder && effectiveCategoryHasMore && index >= itemsToRender.size - 5 ) {
3819+ LaunchedEffect (itemsToRender.size) {
38213820 onLoadMore()
38223821 }
38233822 }
0 commit comments