mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
WIP translating season/episode to tab/row indexes
This commit is contained in:
parent
a7046a942e
commit
c5cfa876e4
7 changed files with 285 additions and 55 deletions
|
|
@ -22,6 +22,9 @@ data class BaseItem(
|
|||
|
||||
@Transient val name = data.name
|
||||
|
||||
@Transient
|
||||
val indexNumber = data.indexNumber
|
||||
|
||||
fun destination(): Destination.MediaItem {
|
||||
val result =
|
||||
// Redirect episodes & seasons to their series if possible
|
||||
|
|
|
|||
|
|
@ -15,11 +15,16 @@ 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.GetEpisodesRequestHandler
|
||||
import com.github.damontecres.dolphin.util.ItemPager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||
|
|
@ -28,6 +33,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
|||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
|
@ -40,6 +46,7 @@ class SeriesViewModel
|
|||
api: ApiClient,
|
||||
) : ItemViewModel<Video>(api) {
|
||||
val seasons = MutableLiveData<List<BaseItem?>>(listOf())
|
||||
val episodes = MutableLiveData<List<BaseItem?>>(listOf())
|
||||
|
||||
override fun init(
|
||||
itemId: UUID,
|
||||
|
|
@ -67,6 +74,13 @@ class SeriesViewModel
|
|||
pager.init()
|
||||
seasons.value = pager
|
||||
Timber.v("Loaded ${pager.size} seasons for series ${item.id}")
|
||||
val pairs =
|
||||
pager.mapIndexed { index, _ ->
|
||||
val season = pager.getBlocking(index)
|
||||
Pair(season?.indexNumber!!, index)
|
||||
}
|
||||
mapOf(*pairs.toTypedArray())
|
||||
mapOf(*pairs.map { Pair(it.second, it.first) }.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,32 +88,27 @@ class SeriesViewModel
|
|||
itemId: UUID,
|
||||
potential: BaseItem?,
|
||||
season: Int?,
|
||||
episode: Int?,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
init(itemId, potential).join()
|
||||
season?.let {
|
||||
season?.let { seasonNum ->
|
||||
val targetSeasonPosition =
|
||||
(seasons.value!! as ItemPager)
|
||||
.getBlocking(season)
|
||||
?.let {
|
||||
loadEpisodes(it.id)
|
||||
}
|
||||
.toBlockingList()
|
||||
.indexOfFirstOrNull { it.indexNumber == seasonNum }
|
||||
loadEpisodes(seasonNum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val episodes = MutableLiveData<List<BaseItem?>>(listOf())
|
||||
|
||||
fun loadEpisodes(seasonId: UUID): Job {
|
||||
Timber.v("Loading episodes for season $seasonId")
|
||||
episodes.value = listOf()
|
||||
return viewModelScope.launch {
|
||||
fun loadEpisodes(season: Int): Deferred<ApiRequestPager<*>> =
|
||||
viewModelScope.async {
|
||||
val request =
|
||||
GetItemsRequest(
|
||||
parentId = seasonId,
|
||||
recursive = false,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
sortBy = listOf(ItemSortBy.INDEX_NUMBER),
|
||||
sortOrder = listOf(SortOrder.ASCENDING),
|
||||
GetEpisodesRequest(
|
||||
seriesId = item.value!!.id,
|
||||
season = season,
|
||||
sortBy = ItemSortBy.INDEX_NUMBER,
|
||||
fields =
|
||||
listOf(
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
|
|
@ -109,11 +118,11 @@ class SeriesViewModel
|
|||
ItemFields.TRICKPLAY,
|
||||
),
|
||||
)
|
||||
val pager = ItemPager(api, request, viewModelScope)
|
||||
val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope)
|
||||
pager.init()
|
||||
Timber.v("Loaded ${pager.size} episodes for season $seasonId")
|
||||
Timber.v("Loaded ${pager.size} episodes for season $season")
|
||||
episodes.value = pager
|
||||
}
|
||||
pager
|
||||
}
|
||||
|
||||
fun setWatched(
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ data class SeasonEpisode(
|
|||
val episode: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SeriesOverviewPosition(
|
||||
val seasonTabIndex: Int,
|
||||
val episodeRowIndex: Int,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SeriesOverview(
|
||||
preferences: UserPreferences,
|
||||
|
|
@ -45,19 +51,33 @@ fun SeriesOverview(
|
|||
|
||||
OneTimeLaunchedEffect {
|
||||
Timber.v("SeriesDetailParent: itemId=${destination.itemId}, initialSeasonEpisode=$initialSeasonEpisode")
|
||||
viewModel.init(destination.itemId, destination.item, initialSeasonEpisode.season)
|
||||
viewModel.init(
|
||||
destination.itemId,
|
||||
destination.item,
|
||||
initialSeasonEpisode.season,
|
||||
initialSeasonEpisode.episode,
|
||||
)
|
||||
}
|
||||
val series by viewModel.item.observeAsState(null)
|
||||
val seasons by viewModel.seasons.observeAsState(listOf())
|
||||
val episodes by viewModel.episodes.observeAsState(listOf())
|
||||
var seasonEpisode by rememberSaveable(
|
||||
|
||||
// TODO need to translate season/episode into tab/row index in case of missing seasons/episodes
|
||||
var position by rememberSaveable(
|
||||
destination,
|
||||
stateSaver =
|
||||
Saver(
|
||||
save = { listOf(it.season, it.episode) },
|
||||
restore = { SeasonEpisode(it[0], it[1]) },
|
||||
save = { listOf(it.seasonTabIndex, it.episodeRowIndex) },
|
||||
restore = { SeriesOverviewPosition(it[0], it[1]) },
|
||||
),
|
||||
) { mutableStateOf(initialSeasonEpisode) }
|
||||
) {
|
||||
mutableStateOf(
|
||||
SeriesOverviewPosition(
|
||||
initialSeasonEpisode.season,
|
||||
initialSeasonEpisode.episode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||
|
||||
|
|
@ -65,8 +85,8 @@ fun SeriesOverview(
|
|||
if (episodes.isNotEmpty()) {
|
||||
// TODO focus on first episode when changing seasons
|
||||
// firstItemFocusRequester.requestFocus()
|
||||
episodes.getOrNull(seasonEpisode.episode)?.let {
|
||||
viewModel.refreshEpisode(it.id, seasonEpisode.episode)
|
||||
episodes.getOrNull(position.episodeRowIndex)?.let {
|
||||
viewModel.refreshEpisode(it.id, position.episodeRowIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -83,14 +103,14 @@ fun SeriesOverview(
|
|||
series = series,
|
||||
seasons = seasons,
|
||||
episodes = episodes,
|
||||
seasonEpisode = seasonEpisode,
|
||||
position = position,
|
||||
backdropImageUrl = remember { viewModel.imageUrl(series.id, ImageType.BACKDROP) },
|
||||
firstItemFocusRequester = firstItemFocusRequester,
|
||||
onFocus = {
|
||||
if (it.season != seasonEpisode.season) {
|
||||
viewModel.loadEpisodes(seasons[it.season]!!.id)
|
||||
if (it.seasonTabIndex != position.seasonTabIndex) {
|
||||
viewModel.loadEpisodes(seasons[it.seasonTabIndex]!!.indexNumber!!)
|
||||
}
|
||||
seasonEpisode = it
|
||||
position = it
|
||||
},
|
||||
onClick = {
|
||||
val resumePosition =
|
||||
|
|
@ -103,7 +123,7 @@ fun SeriesOverview(
|
|||
// TODO
|
||||
},
|
||||
playOnClick = { resume ->
|
||||
episodes.getOrNull(seasonEpisode.episode)?.let {
|
||||
episodes.getOrNull(position.episodeRowIndex)?.let {
|
||||
navigationManager.navigateTo(
|
||||
Destination.Playback(
|
||||
it.id,
|
||||
|
|
@ -114,18 +134,16 @@ fun SeriesOverview(
|
|||
}
|
||||
},
|
||||
watchOnClick = {
|
||||
// TODO toggle watched state
|
||||
episodes.getOrNull(seasonEpisode.episode)?.let {
|
||||
episodes.getOrNull(position.episodeRowIndex)?.let {
|
||||
val played = it.data.userData?.played ?: false
|
||||
// TODO map indexes
|
||||
viewModel.setWatched(it.id, !played, seasonEpisode.episode)
|
||||
viewModel.setWatched(it.id, !played, position.episodeRowIndex)
|
||||
}
|
||||
},
|
||||
moreOnClick = {
|
||||
// TODO show more actions
|
||||
},
|
||||
overviewOnClick = {
|
||||
episodes.getOrNull(seasonEpisode.episode)?.let {
|
||||
episodes.getOrNull(position.episodeRowIndex)?.let {
|
||||
overviewDialog =
|
||||
ItemDetailsDialogInfo(
|
||||
title = it.name ?: "Unknown",
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ fun SeriesOverviewContent(
|
|||
series: BaseItem,
|
||||
seasons: List<BaseItem?>,
|
||||
episodes: List<BaseItem?>,
|
||||
seasonEpisode: SeasonEpisode,
|
||||
position: SeriesOverviewPosition,
|
||||
backdropImageUrl: String?,
|
||||
firstItemFocusRequester: FocusRequester,
|
||||
onFocus: (SeasonEpisode) -> Unit,
|
||||
onFocus: (SeriesOverviewPosition) -> Unit,
|
||||
onClick: (BaseItem) -> Unit,
|
||||
onLongClick: (BaseItem) -> Unit,
|
||||
playOnClick: (Duration) -> Unit,
|
||||
|
|
@ -64,12 +64,12 @@ fun SeriesOverviewContent(
|
|||
) {
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
// TODO need to map between season index and tab index in case of missing seasons
|
||||
var selectedTabIndex by rememberSaveable(seasonEpisode) { mutableIntStateOf(seasonEpisode.season) }
|
||||
var selectedTabIndex by rememberSaveable(position) { mutableIntStateOf(position.seasonTabIndex) }
|
||||
val focusRequesters = remember(seasons.size) { List(seasons.size) { FocusRequester() } }
|
||||
var resolvedTabIndex by remember { mutableIntStateOf(selectedTabIndex) }
|
||||
val tabRowFocusRequester = remember { FocusRequester() }
|
||||
|
||||
val focusedEpisode = episodes.getOrNull(seasonEpisode.episode)
|
||||
val focusedEpisode = episodes.getOrNull(position.episodeRowIndex)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
|
|
@ -132,7 +132,7 @@ fun SeriesOverviewContent(
|
|||
onFocus = {},
|
||||
onClick = {
|
||||
selectedTabIndex = index
|
||||
onFocus.invoke(SeasonEpisode(index, 0))
|
||||
onFocus.invoke(SeriesOverviewPosition(index, 0))
|
||||
},
|
||||
colors =
|
||||
TabDefaults.pillIndicatorTabColors(
|
||||
|
|
@ -171,7 +171,7 @@ fun SeriesOverviewContent(
|
|||
}
|
||||
}
|
||||
item {
|
||||
key(seasonEpisode.season) {
|
||||
key(position.seasonTabIndex) {
|
||||
val state = rememberLazyListState()
|
||||
LazyRow(
|
||||
state = state,
|
||||
|
|
@ -179,10 +179,15 @@ fun SeriesOverviewContent(
|
|||
contentPadding = PaddingValues(start = 16.dp),
|
||||
modifier = modifier.focusRestorer(firstItemFocusRequester),
|
||||
) {
|
||||
itemsIndexed(episodes) { index, episode ->
|
||||
itemsIndexed(episodes) { episodeIndex, episode ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
if (interactionSource.collectIsFocusedAsState().value) {
|
||||
onFocus.invoke(SeasonEpisode(selectedTabIndex, index))
|
||||
onFocus.invoke(
|
||||
SeriesOverviewPosition(
|
||||
selectedTabIndex,
|
||||
episodeIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
BannerCard(
|
||||
imageUrl = episode?.imageUrl,
|
||||
|
|
@ -193,7 +198,7 @@ fun SeriesOverviewContent(
|
|||
onLongClick = { if (episode != null) onLongClick.invoke(episode) },
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
index == 0,
|
||||
episodeIndex == 0,
|
||||
Modifier.focusRequester(firstItemFocusRequester),
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
|
|
|
|||
|
|
@ -15,12 +15,9 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.Player
|
||||
import androidx.tv.material3.Text
|
||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
fun PlaybackOverlay(
|
||||
|
|
@ -78,12 +75,7 @@ fun PlaybackOverlay(
|
|||
val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
|
||||
val index =
|
||||
(seekProgressMs / trickplayInfo.interval).toInt() / tilesPerImage
|
||||
|
||||
val yOffsetDp = 180.dp + 160.dp
|
||||
val heightPx = with(LocalDensity.current) { yOffsetDp.toPx().toInt() }
|
||||
|
||||
val imageUrl = trickplayUrlFor(index)
|
||||
Timber.v("Trickplay imageUrl: $imageUrl")
|
||||
|
||||
if (imageUrl != null) {
|
||||
SeekPreviewImage(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
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.Response
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
|
||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import timber.log.Timber
|
||||
|
||||
interface RequestHandler<T> {
|
||||
fun prepare(
|
||||
request: T,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): T
|
||||
|
||||
suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: T,
|
||||
): Response<BaseItemDtoQueryResult>
|
||||
}
|
||||
|
||||
val GetItemsRequestHandler =
|
||||
object : RequestHandler<GetItemsRequest> {
|
||||
override fun prepare(
|
||||
request: GetItemsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetItemsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetItemsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.itemsApi.getItems(request)
|
||||
}
|
||||
|
||||
val GetEpisodesRequestHandler =
|
||||
object : RequestHandler<GetEpisodesRequest> {
|
||||
override fun prepare(
|
||||
request: GetEpisodesRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetEpisodesRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetEpisodesRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.tvShowsApi.getEpisodes(request)
|
||||
}
|
||||
|
||||
class ApiRequestPager<T>(
|
||||
val api: ApiClient,
|
||||
val request: T,
|
||||
val requestHandler: RequestHandler<T>,
|
||||
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 newRequest = requestHandler.prepare(request, 0, 1, true)
|
||||
val result = requestHandler.execute(api, newRequest).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(Dispatchers.IO) {
|
||||
mutex.withLock {
|
||||
val pageNumber = position / pageSize
|
||||
if (cachedPages.getIfPresent(pageNumber) == null) {
|
||||
if (DEBUG) Timber.v("fetchPage: $pageNumber")
|
||||
val newRequest =
|
||||
requestHandler.prepare(
|
||||
request,
|
||||
pageNumber * pageSize,
|
||||
pageSize,
|
||||
false,
|
||||
)
|
||||
val result = requestHandler.execute(api, newRequest).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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ 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
|
||||
|
|
@ -100,6 +101,16 @@ class ItemPager(
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue