Use ApiRequestPager

This commit is contained in:
Damontecres 2025-10-02 16:45:49 -04:00
parent 9265a06fd7
commit 41e3a19cd4
No known key found for this signature in database
5 changed files with 25 additions and 157 deletions

View file

@ -52,7 +52,6 @@ import com.github.damontecres.dolphin.ui.playback.isForwardButton
import com.github.damontecres.dolphin.ui.playback.isPlayKeyUp
import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.ItemPager
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.math.max
@ -61,7 +60,7 @@ private const val DEBUG = false
@Composable
fun CardGrid(
pager: ItemPager,
pager: List<BaseItem?>,
itemOnClick: (BaseItem) -> Unit,
longClicker: (BaseItem) -> Unit,
letterPosition: suspend (Char) -> Int,

View file

@ -16,8 +16,9 @@ import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.ApiRequestPager
import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.ItemPager
import com.github.damontecres.dolphin.util.GetItemsRequestHandler
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@ -38,7 +39,7 @@ class CollectionFolderViewModel
constructor(
api: ApiClient,
) : ItemViewModel<Library>(api) {
val pager = MutableLiveData<ItemPager?>()
val pager = MutableLiveData<ApiRequestPager<GetItemsRequest>?>()
override fun init(
itemId: UUID,
@ -81,7 +82,8 @@ class CollectionFolderViewModel
sortOrder = listOf(SortOrder.ASCENDING),
fields = listOf(ItemFields.PRIMARY_IMAGE_ASPECT_RATIO),
)
val newPager = ItemPager(api, request, viewModelScope)
val newPager =
ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)
newPager.init()
pager.value = newPager
}
@ -162,7 +164,7 @@ fun TVShowCollectionDetails(
navigationManager: NavigationManager,
library: Library,
item: BaseItem,
pager: ItemPager,
pager: List<BaseItem?>,
modifier: Modifier = Modifier,
) {
val gridFocusRequester = remember { FocusRequester() }

View file

@ -21,8 +21,9 @@ import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.ApiRequestPager
import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.ItemPager
import com.github.damontecres.dolphin.util.GetItemsRequestHandler
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@ -60,7 +61,13 @@ class SeasonViewModel
fields = listOf(ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, ItemFields.CHILD_COUNT),
)
val pager =
ItemPager(api, request, viewModelScope, itemCount = item.data.childCount)
ApiRequestPager(
api,
request,
GetItemsRequestHandler,
viewModelScope,
itemCount = item.data.childCount,
)
pager.init()
episodes.value = pager
}

View file

@ -15,13 +15,12 @@ import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.data.model.Video
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.indexOfFirstOrNull
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.ApiRequestPager
import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.GetEpisodesRequestHandler
import com.github.damontecres.dolphin.util.ItemPager
import com.github.damontecres.dolphin.util.GetItemsRequestHandler
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
@ -71,7 +70,13 @@ class SeriesViewModel
),
)
val pager =
ItemPager(api, request, viewModelScope, itemCount = item.data.childCount)
ApiRequestPager(
api,
request,
GetItemsRequestHandler,
viewModelScope,
itemCount = item.data.childCount,
)
pager.init()
seasons.value = pager
Timber.v("Loaded ${pager.size} seasons for series ${item.id}")
@ -94,10 +99,7 @@ class SeriesViewModel
viewModelScope.launch(ExceptionHandler()) {
init(itemId, potential).join()
season?.let { seasonNum ->
val targetSeasonPosition =
(seasons.value!! as ItemPager)
.toBlockingList()
.indexOfFirstOrNull { it.indexNumber == seasonNum }
// TODO map season number to index in list
loadEpisodes(seasonNum)
}
}

View file

@ -1,142 +0,0 @@
package com.github.damontecres.dolphin.util
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ui.DEFAULT_PAGE_SIZE
import com.google.common.cache.CacheBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import timber.log.Timber
class ItemPager(
val api: ApiClient,
val request: GetItemsRequest,
private val scope: CoroutineScope,
private val pageSize: Int = DEFAULT_PAGE_SIZE,
itemCount: Int? = null,
cacheSize: Long = 8,
) : AbstractList<BaseItem?>() {
private var items by mutableStateOf(ItemList<BaseItem>(0, pageSize, mapOf()))
private var totalCount by mutableIntStateOf(itemCount ?: -1)
private val mutex = Mutex()
private val cachedPages =
CacheBuilder
.newBuilder()
.maximumSize(cacheSize)
.build<Int, List<BaseItem>>()
suspend fun init() {
if (totalCount < 0) {
val result =
api.itemsApi
.getItems(
request.copy(
startIndex = 0,
limit = 1,
enableTotalRecordCount = true,
),
).content
totalCount = result.totalRecordCount
}
}
override operator fun get(index: Int): BaseItem? {
if (index in 0..<totalCount) {
val item = items[index]
if (item == null) {
fetchPage(index)
}
return item
} else {
throw IndexOutOfBoundsException("$index of $totalCount")
}
}
suspend fun getBlocking(position: Int): BaseItem? {
if (position in 0..<totalCount) {
val item = items[position]
if (item == null) {
fetchPage(position).join()
return items[position]
}
return item
} else {
throw IndexOutOfBoundsException("$position of $totalCount")
}
}
override val size: Int
get() = totalCount
private fun fetchPage(position: Int): Job =
scope.launch(ExceptionHandler() + Dispatchers.IO) {
mutex.withLock {
val pageNumber = position / pageSize
if (cachedPages.getIfPresent(pageNumber) == null) {
if (DEBUG) Timber.v("fetchPage: $pageNumber")
val result =
api.itemsApi
.getItems(
request.copy(
startIndex = pageNumber * pageSize,
limit = pageSize,
enableTotalRecordCount = false,
),
).content
val data = result.items.map { BaseItem.from(it, api) }
cachedPages.put(pageNumber, data)
items = ItemList(totalCount, pageSize, cachedPages.asMap())
}
}
}
suspend fun toBlockingList(): List<BaseItem> {
init()
return object : AbstractList<BaseItem>() {
override val size: Int
get() = totalCount
override fun get(index: Int): BaseItem = runBlocking { getBlocking(index)!! }
}
}
companion object {
private const val DEBUG = false
}
class ItemList<T>(
val size: Int,
val pageSize: Int,
val pages: Map<Int, List<T>>,
) {
operator fun get(position: Int): T? {
val page = position / pageSize
val data = pages[page]
if (data != null) {
val index = position % pageSize
if (index in data.indices) {
return data[index]
} else {
// This can happen when items are removed while scrolling
Timber.w(
"Index $index not in data: position=$position, data.size=${data.size}",
)
return null
}
} else {
return null
}
}
}
}