Fix issue with scrolling to the right episode if there are specials in the season (#1827)
## Description Fixes an edge case where clicking on an episode to go to Series Overview wouldn't scroll to the episode if there is a special (season 0) episodes in the episode list. Dev note: to simplify things, if the entire dataset is cached, then just search with `indexOf`. Often the data is cached because 20 seasons or 100 episodes are fetched initially which covers many shows. ### Related issues Follow up to #1708 ### Testing Emulator and unit tests ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
c6d81dada8
commit
548ec09af3
3 changed files with 123 additions and 37 deletions
|
|
@ -177,10 +177,11 @@ class SeriesViewModel
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val index =
|
val index =
|
||||||
(seasons as? ApiRequestPager<*>)?.let {
|
(seasons as? ApiRequestPager<*>)?.let {
|
||||||
findIndexByNumberOrId(
|
findIndexByNumberOrIdFast(
|
||||||
seasonEpisodeIds.seasonNumber,
|
seasonEpisodeIds.seasonNumber,
|
||||||
seasonEpisodeIds.seasonId,
|
seasonEpisodeIds.seasonId,
|
||||||
it,
|
it,
|
||||||
|
null,
|
||||||
)
|
)
|
||||||
} ?: 0
|
} ?: 0
|
||||||
Timber.v("Got initial season index: $index")
|
Timber.v("Got initial season index: $index")
|
||||||
|
|
@ -313,6 +314,7 @@ class SeriesViewModel
|
||||||
includeItemTypes = listOf(BaseItemKind.SEASON),
|
includeItemTypes = listOf(BaseItemKind.SEASON),
|
||||||
sortBy = listOf(ItemSortBy.INDEX_NUMBER),
|
sortBy = listOf(ItemSortBy.INDEX_NUMBER),
|
||||||
sortOrder = listOf(SortOrder.ASCENDING),
|
sortOrder = listOf(SortOrder.ASCENDING),
|
||||||
|
enableUserData = seriesPageType == SeriesPageType.DETAILS,
|
||||||
fields =
|
fields =
|
||||||
if (seriesPageType == SeriesPageType.DETAILS) {
|
if (seriesPageType == SeriesPageType.DETAILS) {
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -331,16 +333,8 @@ class SeriesViewModel
|
||||||
request,
|
request,
|
||||||
GetItemsRequestHandler,
|
GetItemsRequestHandler,
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
pageSize = 10,
|
pageSize = 20,
|
||||||
).init(seasonNum ?: 0)
|
).init(seasonNum ?: 0)
|
||||||
// val seasons =
|
|
||||||
// GetItemsRequestHandler.execute(api, request).content.items.map {
|
|
||||||
// BaseItem.from(
|
|
||||||
// it,
|
|
||||||
// api,
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// Timber.v("Loaded ${seasons.size} seasons for series ${series.id}")
|
|
||||||
pager
|
pager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,6 +356,7 @@ class SeriesViewModel
|
||||||
ItemFields.CUSTOM_RATING,
|
ItemFields.CUSTOM_RATING,
|
||||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||||
ItemFields.CAN_DELETE,
|
ItemFields.CAN_DELETE,
|
||||||
|
ItemFields.PARENT_ID,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
Timber.v(
|
Timber.v(
|
||||||
|
|
@ -373,10 +368,10 @@ class SeriesViewModel
|
||||||
pager.init(episodeNumber ?: 0)
|
pager.init(episodeNumber ?: 0)
|
||||||
val initialIndex =
|
val initialIndex =
|
||||||
if (episodeId != null || episodeNumber != null) {
|
if (episodeId != null || episodeNumber != null) {
|
||||||
findIndexByNumberOrId(episodeNumber, episodeId, pager)
|
findIndexByNumberOrIdFast(episodeNumber, episodeId, pager, seasonId)
|
||||||
.coerceAtLeast(0)
|
.coerceAtLeast(0)
|
||||||
} else {
|
} else {
|
||||||
// Force the first page to to be fetched
|
// Force the first page to be fetched
|
||||||
if (pager.isNotEmpty()) {
|
if (pager.isNotEmpty()) {
|
||||||
pager.getBlocking(0)
|
pager.getBlocking(0)
|
||||||
}
|
}
|
||||||
|
|
@ -728,6 +723,32 @@ private fun checkNumberOrId(
|
||||||
equalsNotNull(indexNumber, targetNum)
|
equalsNotNull(indexNumber, targetNum)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the index in the [list] where the item's `indexNumber`==[targetNum] or `id`==[targetId]
|
||||||
|
*
|
||||||
|
* If the list is smaller than its page size, then the entire dataset is cached and
|
||||||
|
* [BlockingList.indexOfBlocking] is used, otherwise this function calls [findIndexByNumberOrId]
|
||||||
|
*/
|
||||||
|
suspend fun findIndexByNumberOrIdFast(
|
||||||
|
targetNum: Int?,
|
||||||
|
targetId: UUID?,
|
||||||
|
list: ApiRequestPager<*>,
|
||||||
|
parentId: UUID?,
|
||||||
|
): Int =
|
||||||
|
if (list.size <= list.pageSize) {
|
||||||
|
Timber.v("Using findIndexByNumberOrIdFast indexOfBlocking method")
|
||||||
|
list.indexOfBlocking {
|
||||||
|
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id) &&
|
||||||
|
if (parentId != null) {
|
||||||
|
it?.data?.parentId == parentId
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
findIndexByNumberOrId(targetNum, targetId, list as BlockingList<BaseItem?>, parentId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the index in the [list] where the item's `indexNumber`==[targetNum] or `id`==[targetId]
|
* Find the index in the [list] where the item's `indexNumber`==[targetNum] or `id`==[targetId]
|
||||||
*
|
*
|
||||||
|
|
@ -743,7 +764,9 @@ suspend fun findIndexByNumberOrId(
|
||||||
targetNum: Int?,
|
targetNum: Int?,
|
||||||
targetId: UUID?,
|
targetId: UUID?,
|
||||||
list: BlockingList<BaseItem?>,
|
list: BlockingList<BaseItem?>,
|
||||||
|
parentId: UUID? = null,
|
||||||
): Int {
|
): Int {
|
||||||
|
Timber.v("Using findIndexByNumberOrId")
|
||||||
// Adjust for 1-based numbers
|
// Adjust for 1-based numbers
|
||||||
val listIndex = targetNum?.minus(1)?.coerceAtLeast(0)
|
val listIndex = targetNum?.minus(1)?.coerceAtLeast(0)
|
||||||
val index =
|
val index =
|
||||||
|
|
@ -754,36 +777,55 @@ suspend fun findIndexByNumberOrId(
|
||||||
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id)
|
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id)
|
||||||
}.coerceAtLeast(0)
|
}.coerceAtLeast(0)
|
||||||
} else if (listIndex != null && listIndex in list.indices) {
|
} else if (listIndex != null && listIndex in list.indices) {
|
||||||
// Start searching from the target number and choose direction from there
|
searchList(listIndex, targetNum, targetId, list, parentId)
|
||||||
val num = list.getBlocking(listIndex)?.indexNumber
|
|
||||||
if (num.lt(targetNum)) {
|
|
||||||
for (i in listIndex + 1 until list.size) {
|
|
||||||
val item = list.getBlocking(i)
|
|
||||||
if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id)) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else if (num.gt(targetNum)) {
|
|
||||||
for (i in listIndex - 1 downTo 0) {
|
|
||||||
val item = list.getBlocking(i)
|
|
||||||
if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id)) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
list
|
|
||||||
.indexOfBlocking {
|
|
||||||
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id)
|
|
||||||
}.coerceAtLeast(0)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun searchList(
|
||||||
|
listIndex: Int,
|
||||||
|
targetNum: Int,
|
||||||
|
targetId: UUID?,
|
||||||
|
list: BlockingList<BaseItem?>,
|
||||||
|
parentId: UUID?,
|
||||||
|
): Int {
|
||||||
|
val item = list.getBlocking(listIndex)
|
||||||
|
if (parentId != null && item?.data?.parentId != parentId) {
|
||||||
|
return if (listIndex - 1 in list.indices) {
|
||||||
|
searchList(listIndex - 1, targetNum, targetId, list, parentId)
|
||||||
|
} else if (listIndex + 1 in list.indices) {
|
||||||
|
searchList(listIndex + 1, targetNum, targetId, list, parentId)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val num = item?.indexNumber
|
||||||
|
if (num.lt(targetNum)) {
|
||||||
|
for (i in listIndex + 1 until list.size) {
|
||||||
|
val item = list.getBlocking(i)
|
||||||
|
if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id)) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else if (num.gt(targetNum)) {
|
||||||
|
for (i in listIndex - 1 downTo 0) {
|
||||||
|
val item = list.getBlocking(i)
|
||||||
|
if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id)) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return list
|
||||||
|
.indexOfBlocking {
|
||||||
|
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id)
|
||||||
|
}.coerceAtLeast(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class SeriesState(
|
data class SeriesState(
|
||||||
val series: DataLoadingState<BaseItem> = DataLoadingState.Pending,
|
val series: DataLoadingState<BaseItem> = DataLoadingState.Pending,
|
||||||
val seasons: List<BaseItem?> = emptyList(),
|
val seasons: List<BaseItem?> = emptyList(),
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import java.util.function.Predicate
|
||||||
*/
|
*/
|
||||||
abstract class RequestPager<T>(
|
abstract class RequestPager<T>(
|
||||||
protected val scope: CoroutineScope,
|
protected val scope: CoroutineScope,
|
||||||
protected val pageSize: Int = DEFAULT_PAGE_SIZE,
|
val pageSize: Int = DEFAULT_PAGE_SIZE,
|
||||||
cacheSize: Long = 8,
|
cacheSize: Long = 8,
|
||||||
) : AbstractList<T?>(),
|
) : AbstractList<T?>(),
|
||||||
BlockingList<T?> {
|
BlockingList<T?> {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class TestFindIndexByNumberOrId {
|
class TestFindIndexByNumberOrId {
|
||||||
|
val season0Id = UUID.randomUUID()
|
||||||
|
val season2Id = UUID.randomUUID()
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
indexNumber: Int,
|
indexNumber: Int,
|
||||||
parentIndexNumber: Int,
|
parentIndexNumber: Int,
|
||||||
|
|
@ -20,6 +23,12 @@ class TestFindIndexByNumberOrId {
|
||||||
type = BaseItemKind.EPISODE,
|
type = BaseItemKind.EPISODE,
|
||||||
indexNumber = indexNumber,
|
indexNumber = indexNumber,
|
||||||
parentIndexNumber = parentIndexNumber,
|
parentIndexNumber = parentIndexNumber,
|
||||||
|
parentId =
|
||||||
|
when (parentIndexNumber) {
|
||||||
|
0 -> season0Id
|
||||||
|
2 -> season2Id
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -151,4 +160,39 @@ class TestFindIndexByNumberOrId {
|
||||||
Assert.assertEquals(2, index)
|
Assert.assertEquals(2, index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test with special in middle`() =
|
||||||
|
runTest {
|
||||||
|
val episodes =
|
||||||
|
BlockingList.of<BaseItem?>(
|
||||||
|
listOf(
|
||||||
|
epS02E01,
|
||||||
|
epS02E02,
|
||||||
|
epS02E03,
|
||||||
|
create(6, 0),
|
||||||
|
epS02E04,
|
||||||
|
epS02E05,
|
||||||
|
epS02E06,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
findIndexByNumberOrId(
|
||||||
|
targetNum = 3,
|
||||||
|
targetId = epS02E03.id,
|
||||||
|
list = episodes,
|
||||||
|
parentId = season2Id,
|
||||||
|
).let { index ->
|
||||||
|
Assert.assertEquals(2, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
findIndexByNumberOrId(
|
||||||
|
targetNum = 4,
|
||||||
|
targetId = epS02E04.id,
|
||||||
|
list = episodes,
|
||||||
|
parentId = season2Id,
|
||||||
|
).let { index ->
|
||||||
|
Assert.assertEquals(4, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue