mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Basic up next implementation for tv shows
This commit is contained in:
parent
25859a05f7
commit
3a255fcd8a
6 changed files with 338 additions and 133 deletions
|
|
@ -42,7 +42,9 @@ sealed class Destination(
|
|||
val type: BaseItemKind,
|
||||
@Transient val item: BaseItem? = null,
|
||||
val seasonEpisode: SeasonEpisode? = null,
|
||||
) : Destination()
|
||||
) : Destination() {
|
||||
override fun toString(): String = "SeriesOverview(itemId=$itemId, type=$type, seasonEpisode=$seasonEpisode)"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MediaItem(
|
||||
|
|
@ -50,14 +52,18 @@ sealed class Destination(
|
|||
val type: BaseItemKind,
|
||||
@Transient val item: BaseItem? = null,
|
||||
val seasonEpisode: SeasonEpisode? = null,
|
||||
) : Destination()
|
||||
) : Destination() {
|
||||
override fun toString(): String = "MediaItem(itemId=$itemId, type=$type, seasonEpisode=$seasonEpisode)"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Playback(
|
||||
val itemId: UUID,
|
||||
val positionMs: Long,
|
||||
@Transient val item: BaseItem? = null,
|
||||
) : Destination(true)
|
||||
) : Destination(true) {
|
||||
override fun toString(): String = "Playback(itemId=$itemId, positionMs=$positionMs)"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object License : Destination(true)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package com.github.damontecres.dolphin.ui.playback
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.Card
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||
|
||||
@Composable
|
||||
fun NextUpEpisode(
|
||||
ep: BaseItem,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = modifier.focusRequester(focusRequester),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Up Next...",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier,
|
||||
) {
|
||||
Box {
|
||||
AsyncImage(
|
||||
model = ep.imageUrl,
|
||||
contentDescription = ep.name,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
) {
|
||||
Text(
|
||||
text = ep.data.name ?: "",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier,
|
||||
)
|
||||
Text(
|
||||
text = ep.data.overview ?: "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.github.damontecres.dolphin.ui.playback
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -91,6 +93,8 @@ fun PlaybackContent(
|
|||
var cues by remember { mutableStateOf<List<Cue>>(listOf()) }
|
||||
var showDebugInfo by remember { mutableStateOf(prefs.showDebugInfo) }
|
||||
|
||||
val nextUp by viewModel.nextUpEpisode.observeAsState(null)
|
||||
|
||||
// TODO move to viewmodel?
|
||||
val cueListener =
|
||||
remember {
|
||||
|
|
@ -155,7 +159,7 @@ fun PlaybackContent(
|
|||
val keyHandler =
|
||||
PlaybackKeyHandler(
|
||||
player = player,
|
||||
controlsEnabled = true,
|
||||
controlsEnabled = nextUp == null,
|
||||
skipWithLeftRight = true,
|
||||
seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds,
|
||||
seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds,
|
||||
|
|
@ -170,130 +174,162 @@ fun PlaybackContent(
|
|||
.focusRequester(focusRequester)
|
||||
.focusable(),
|
||||
) {
|
||||
PlayerSurface(
|
||||
player = player,
|
||||
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
|
||||
modifier = scaledModifier,
|
||||
)
|
||||
if (presentationState.coverSurface) {
|
||||
Box(
|
||||
val playerSize by animateFloatAsState(if (nextUp == null) 1f else .66f)
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.background(Color.Black),
|
||||
) {
|
||||
LoadingPage()
|
||||
}
|
||||
}
|
||||
|
||||
if (!controllerViewState.controlsVisible && skipIndicatorDuration != 0L) {
|
||||
SkipIndicator(
|
||||
durationMs = skipIndicatorDuration,
|
||||
onFinish = {
|
||||
skipIndicatorDuration = 0L
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 70.dp),
|
||||
.fillMaxSize(playerSize)
|
||||
.align(Alignment.TopCenter),
|
||||
) {
|
||||
PlayerSurface(
|
||||
player = player,
|
||||
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
|
||||
modifier = scaledModifier,
|
||||
)
|
||||
val showSkipProgress = true // TODO get from preferences
|
||||
if (showSkipProgress) {
|
||||
duration?.let {
|
||||
val percent = (skipPosition.milliseconds / it).toFloat()
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.background(MaterialTheme.colorScheme.border)
|
||||
.clip(RectangleShape)
|
||||
.height(3.dp)
|
||||
.fillMaxWidth(percent),
|
||||
) {
|
||||
// No-op
|
||||
if (presentationState.coverSurface) {
|
||||
Box(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.background(Color.Black),
|
||||
) {
|
||||
LoadingPage()
|
||||
}
|
||||
}
|
||||
|
||||
if (!controllerViewState.controlsVisible && skipIndicatorDuration != 0L) {
|
||||
SkipIndicator(
|
||||
durationMs = skipIndicatorDuration,
|
||||
onFinish = {
|
||||
skipIndicatorDuration = 0L
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 70.dp),
|
||||
)
|
||||
val showSkipProgress = true // TODO get from preferences
|
||||
if (showSkipProgress) {
|
||||
duration?.let {
|
||||
val percent = (skipPosition.milliseconds / it).toFloat()
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.background(MaterialTheme.colorScheme.border)
|
||||
.clip(RectangleShape)
|
||||
.height(3.dp)
|
||||
.fillMaxWidth(percent),
|
||||
) {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
controllerViewState.controlsVisible,
|
||||
Modifier,
|
||||
slideInVertically { it },
|
||||
slideOutVertically { it },
|
||||
) {
|
||||
PlaybackOverlay(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(WindowInsets.systemBars.asPaddingValues())
|
||||
.fillMaxSize()
|
||||
.background(Color.Transparent),
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
subtitleStreams = subtitleStreams,
|
||||
playerControls = player,
|
||||
controllerViewState = controllerViewState,
|
||||
showPlay = playPauseState.showPlay,
|
||||
previousEnabled = previousState.isEnabled,
|
||||
nextEnabled = nextState.isEnabled,
|
||||
seekEnabled = true,
|
||||
seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds,
|
||||
seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds,
|
||||
onPlaybackActionClick = {
|
||||
when (it) {
|
||||
is PlaybackAction.PlaybackSpeed -> {
|
||||
playbackSpeed = it.value
|
||||
}
|
||||
|
||||
is PlaybackAction.Scale -> {
|
||||
contentScale = it.scale
|
||||
}
|
||||
|
||||
PlaybackAction.ShowDebug -> {
|
||||
showDebugInfo = !showDebugInfo
|
||||
}
|
||||
|
||||
PlaybackAction.ShowPlaylist -> TODO()
|
||||
PlaybackAction.ShowVideoFilterDialog -> TODO()
|
||||
is PlaybackAction.ToggleAudio -> {
|
||||
viewModel.changeAudioStream(it.index)
|
||||
}
|
||||
|
||||
is PlaybackAction.ToggleCaptions -> {
|
||||
viewModel.changeSubtitleStream(it.index)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSeekBarChange = seekBarState::onValueChange,
|
||||
showDebugInfo = showDebugInfo,
|
||||
scale = contentScale,
|
||||
playbackSpeed = playbackSpeed,
|
||||
moreButtonOptions = MoreButtonOptions(mapOf()),
|
||||
currentPlayback = currentPlayback,
|
||||
audioStreams = audioStreams,
|
||||
trickplayInfo = trickplay,
|
||||
trickplayUrlFor = viewModel::getTrickplayUrl,
|
||||
chapters = chapters,
|
||||
)
|
||||
}
|
||||
|
||||
if (!controllerViewState.controlsVisible && skipIndicatorDuration == 0L && currentPlayback?.subtitleIndex != null) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
SubtitleView(context).apply {
|
||||
setUserDefaultStyle()
|
||||
setUserDefaultTextSize()
|
||||
}
|
||||
},
|
||||
update = {
|
||||
it.setCues(cues)
|
||||
},
|
||||
onReset = {
|
||||
it.setCues(null)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Transparent),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(nextUp != null) {
|
||||
viewModel.cancelUpNextEpisode()
|
||||
}
|
||||
AnimatedVisibility(
|
||||
controllerViewState.controlsVisible,
|
||||
Modifier,
|
||||
slideInVertically { it },
|
||||
slideOutVertically { it },
|
||||
nextUp != null,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter),
|
||||
) {
|
||||
PlaybackOverlay(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(WindowInsets.systemBars.asPaddingValues())
|
||||
.fillMaxSize()
|
||||
.background(Color.Transparent),
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
subtitleStreams = subtitleStreams,
|
||||
playerControls = player,
|
||||
controllerViewState = controllerViewState,
|
||||
showPlay = playPauseState.showPlay,
|
||||
previousEnabled = previousState.isEnabled,
|
||||
nextEnabled = nextState.isEnabled,
|
||||
seekEnabled = true,
|
||||
seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds,
|
||||
seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds,
|
||||
onPlaybackActionClick = {
|
||||
when (it) {
|
||||
is PlaybackAction.PlaybackSpeed -> {
|
||||
playbackSpeed = it.value
|
||||
}
|
||||
|
||||
is PlaybackAction.Scale -> {
|
||||
contentScale = it.scale
|
||||
}
|
||||
|
||||
PlaybackAction.ShowDebug -> {
|
||||
showDebugInfo = !showDebugInfo
|
||||
}
|
||||
PlaybackAction.ShowPlaylist -> TODO()
|
||||
PlaybackAction.ShowVideoFilterDialog -> TODO()
|
||||
is PlaybackAction.ToggleAudio -> {
|
||||
viewModel.changeAudioStream(it.index)
|
||||
}
|
||||
|
||||
is PlaybackAction.ToggleCaptions -> {
|
||||
viewModel.changeSubtitleStream(it.index)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSeekBarChange = seekBarState::onValueChange,
|
||||
showDebugInfo = showDebugInfo,
|
||||
scale = contentScale,
|
||||
playbackSpeed = playbackSpeed,
|
||||
moreButtonOptions = MoreButtonOptions(mapOf()),
|
||||
currentPlayback = currentPlayback,
|
||||
audioStreams = audioStreams,
|
||||
trickplayInfo = trickplay,
|
||||
trickplayUrlFor = viewModel::getTrickplayUrl,
|
||||
chapters = chapters,
|
||||
)
|
||||
}
|
||||
|
||||
if (!controllerViewState.controlsVisible && skipIndicatorDuration == 0L && currentPlayback?.subtitleIndex != null) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
SubtitleView(context).apply {
|
||||
setUserDefaultStyle()
|
||||
setUserDefaultTextSize()
|
||||
}
|
||||
},
|
||||
update = {
|
||||
it.setCues(cues)
|
||||
},
|
||||
onReset = {
|
||||
it.setCues(null)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Transparent),
|
||||
)
|
||||
nextUp?.let {
|
||||
NextUpEpisode(
|
||||
ep = it,
|
||||
onClick = { viewModel.playUpNextEpisode() },
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(16.dp)
|
||||
.height(128.dp)
|
||||
.fillMaxWidth(.4f)
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,14 @@ import androidx.media3.common.TrackSelectionOverride
|
|||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.data.model.Chapter
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.DefaultItemFields
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
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.TrackActivityPlaybackListener
|
||||
import com.github.damontecres.dolphin.util.TrackSupport
|
||||
import com.github.damontecres.dolphin.util.checkForSupport
|
||||
|
|
@ -26,6 +30,8 @@ import com.github.damontecres.dolphin.util.subtitleMimeTypes
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
|
|
@ -41,12 +47,16 @@ import org.jellyfin.sdk.model.api.MediaStreamType
|
|||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||
import org.jellyfin.sdk.model.api.SubtitlePlaybackMode
|
||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
enum class TranscodeType {
|
||||
DIRECT_PLAY,
|
||||
|
|
@ -86,9 +96,15 @@ class PlaybackViewModel
|
|||
val trickplay = MutableLiveData<TrickplayInfo?>(null)
|
||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||
|
||||
private lateinit var preferences: UserPreferences
|
||||
private lateinit var deviceProfile: DeviceProfile
|
||||
private lateinit var itemId: UUID
|
||||
private lateinit var dto: BaseItemDto
|
||||
|
||||
private val episodes = MutableLiveData<ApiRequestPager<GetEpisodesRequest>>()
|
||||
private var currentEpisodeIndex = Int.MAX_VALUE
|
||||
val nextUpEpisode = MutableLiveData<BaseItem?>()
|
||||
|
||||
init {
|
||||
addCloseable { player.release() }
|
||||
}
|
||||
|
|
@ -98,8 +114,11 @@ class PlaybackViewModel
|
|||
deviceProfile: DeviceProfile,
|
||||
preferences: UserPreferences,
|
||||
) {
|
||||
nextUpEpisode.value = null
|
||||
this.preferences = preferences
|
||||
this.deviceProfile = deviceProfile
|
||||
val itemId = destination.itemId
|
||||
this.itemId = itemId
|
||||
val item = destination.item
|
||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||
val base = item?.data ?: api.userLibraryApi.getItem(itemId).content
|
||||
|
|
@ -234,6 +253,9 @@ class PlaybackViewModel
|
|||
this@PlaybackViewModel.chapters.value = Chapter.fromDto(dto, api)
|
||||
Timber.v("chapters=${this@PlaybackViewModel.chapters.value}")
|
||||
}
|
||||
if (base.type == BaseItemKind.EPISODE) {
|
||||
base.seriesId?.let(::getEpisodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -411,6 +433,54 @@ class PlaybackViewModel
|
|||
mediaSourceId,
|
||||
)
|
||||
}
|
||||
|
||||
fun getEpisodes(seriesId: UUID) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val episodes =
|
||||
if (!this@PlaybackViewModel.episodes.isInitialized) {
|
||||
val request =
|
||||
GetEpisodesRequest(seriesId = seriesId, fields = DefaultItemFields)
|
||||
val pager =
|
||||
ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope)
|
||||
pager.init()
|
||||
currentEpisodeIndex = pager.indexOfBlocking { it?.id == itemId }
|
||||
pager
|
||||
} else {
|
||||
this@PlaybackViewModel.episodes.value!!
|
||||
}
|
||||
Timber.v("Current episode is $currentEpisodeIndex of ${episodes.size}")
|
||||
val nextIndex = currentEpisodeIndex + 1
|
||||
if (nextIndex < episodes.size) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
while (this.isActive) {
|
||||
delay(5.seconds)
|
||||
val remaining =
|
||||
withContext(Dispatchers.Main) {
|
||||
(player.duration - player.currentPosition).milliseconds
|
||||
}
|
||||
if (remaining < 2.minutes) { // TODO time & preference
|
||||
val nextItem = episodes.getBlocking(nextIndex)
|
||||
Timber.v("Setting next up episode to ${nextItem?.id}")
|
||||
withContext(Dispatchers.Main) {
|
||||
nextUpEpisode.value = nextItem
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun playUpNextEpisode() {
|
||||
nextUpEpisode.value?.let {
|
||||
init(Destination.Playback(it.id, 0, it), deviceProfile, preferences)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelUpNextEpisode() {
|
||||
nextUpEpisode.value = null
|
||||
}
|
||||
}
|
||||
|
||||
data class CurrentPlayback(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ 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
|
||||
|
|
@ -22,6 +21,7 @@ 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
|
||||
import java.util.function.Predicate
|
||||
|
||||
interface RequestHandler<T> {
|
||||
fun prepare(
|
||||
|
|
@ -83,7 +83,8 @@ class ApiRequestPager<T>(
|
|||
private val scope: CoroutineScope,
|
||||
private val pageSize: Int = DEFAULT_PAGE_SIZE,
|
||||
cacheSize: Long = 8,
|
||||
) : AbstractList<BaseItem?>() {
|
||||
) : AbstractList<BaseItem?>(),
|
||||
BlockingList<BaseItem?> {
|
||||
private var items by mutableStateOf(ItemList<BaseItem>(0, pageSize, mapOf()))
|
||||
private var totalCount by mutableIntStateOf(-1)
|
||||
private val mutex = Mutex()
|
||||
|
|
@ -93,12 +94,13 @@ class ApiRequestPager<T>(
|
|||
.maximumSize(cacheSize)
|
||||
.build<Int, List<BaseItem>>()
|
||||
|
||||
suspend fun init() {
|
||||
suspend fun init(): ApiRequestPager<T> {
|
||||
if (totalCount < 0) {
|
||||
val newRequest = requestHandler.prepare(request, 0, 1, true)
|
||||
val result = requestHandler.execute(api, newRequest).content
|
||||
totalCount = result.totalRecordCount
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override operator fun get(index: Int): BaseItem? {
|
||||
|
|
@ -113,7 +115,7 @@ class ApiRequestPager<T>(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getBlocking(position: Int): BaseItem? {
|
||||
override suspend fun getBlocking(position: Int): BaseItem? {
|
||||
if (position in 0..<totalCount) {
|
||||
val item = items[position]
|
||||
if (item == null) {
|
||||
|
|
@ -126,6 +128,17 @@ class ApiRequestPager<T>(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun indexOfBlocking(predicate: Predicate<BaseItem?>): Int {
|
||||
init()
|
||||
for (i in 0 until totalCount) {
|
||||
val currentItem = getBlocking(i)
|
||||
if (currentItem != null && predicate.test(currentItem)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = totalCount
|
||||
|
||||
|
|
@ -150,16 +163,6 @@ class ApiRequestPager<T>(
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package com.github.damontecres.dolphin.util
|
||||
|
||||
import java.util.function.Predicate
|
||||
|
||||
interface BlockingList<T> : List<T> {
|
||||
suspend fun getBlocking(index: Int): T
|
||||
|
||||
suspend fun indexOfBlocking(predicate: Predicate<T>): Int
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue