mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Fixes & optimizations to series pages (#106)
Simplifies the process for finding the right season id & episode id after clicking on an episode. This can also improve loading time for seasons that have a large number of episodes (hundreds). Also fixes handling for "unknown" seasons which don't have an index/season number.
This commit is contained in:
parent
808e379e02
commit
576c20edff
7 changed files with 130 additions and 118 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.data.model
|
package com.github.damontecres.wholphin.data.model
|
||||||
|
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.util.seasonEpisode
|
import com.github.damontecres.wholphin.util.seasonEpisode
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
@ -54,27 +54,23 @@ data class BaseItem(
|
||||||
// Redirect episodes & seasons to their series if possible
|
// Redirect episodes & seasons to their series if possible
|
||||||
when (type) {
|
when (type) {
|
||||||
BaseItemKind.EPISODE -> {
|
BaseItemKind.EPISODE -> {
|
||||||
indexNumber?.let { episode ->
|
data.seasonId?.let { seasonId ->
|
||||||
data.parentIndexNumber?.let { season ->
|
|
||||||
Destination.SeriesOverview(
|
|
||||||
data.seriesId!!,
|
|
||||||
BaseItemKind.SERIES,
|
|
||||||
this,
|
|
||||||
SeasonEpisode(season, episode),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: Destination.MediaItem(id, type, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseItemKind.SEASON ->
|
|
||||||
data.indexNumber?.let { season ->
|
|
||||||
Destination.SeriesOverview(
|
Destination.SeriesOverview(
|
||||||
data.seriesId!!,
|
data.seriesId!!,
|
||||||
BaseItemKind.SERIES,
|
BaseItemKind.SERIES,
|
||||||
this,
|
this,
|
||||||
SeasonEpisode(season, 0),
|
SeasonEpisodeIds(seasonId, data.parentIndexNumber, id, indexNumber),
|
||||||
)
|
)
|
||||||
} ?: Destination.MediaItem(id, type, this)
|
} ?: Destination.MediaItem(id, type, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseItemKind.SEASON ->
|
||||||
|
Destination.SeriesOverview(
|
||||||
|
data.seriesId!!,
|
||||||
|
BaseItemKind.SERIES,
|
||||||
|
this,
|
||||||
|
SeasonEpisodeIds(id, indexNumber, null, null),
|
||||||
|
)
|
||||||
|
|
||||||
else -> Destination.MediaItem(id, type, this)
|
else -> Destination.MediaItem(id, type, this)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -375,3 +375,8 @@ suspend fun <T> MutableLiveData<T>.setValueOnMain(value: T) =
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@setValueOnMain.value = value
|
this@setValueOnMain.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun equalsNotNull(
|
||||||
|
a: Any?,
|
||||||
|
b: Any?,
|
||||||
|
) = a != null && b != null && a == b
|
||||||
|
|
|
||||||
|
|
@ -92,12 +92,12 @@ fun SeriesDetails(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.init(preferences, destination.itemId, destination.item, null, null)
|
viewModel.init(preferences, destination.itemId, destination.item, null)
|
||||||
}
|
}
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
|
|
||||||
val item by viewModel.item.observeAsState()
|
val item by viewModel.item.observeAsState()
|
||||||
val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty())
|
val seasons by viewModel.seasons.observeAsState(listOf())
|
||||||
val people by viewModel.people.observeAsState(listOf())
|
val people by viewModel.people.observeAsState(listOf())
|
||||||
val similar by viewModel.similar.observeAsState(listOf())
|
val similar by viewModel.similar.observeAsState(listOf())
|
||||||
|
|
||||||
|
|
@ -190,7 +190,7 @@ private const val SIMILAR_ROW = PEOPLE_ROW + 1
|
||||||
fun SeriesDetailsContent(
|
fun SeriesDetailsContent(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
series: BaseItem,
|
series: BaseItem,
|
||||||
seasons: ItemListAndMapping,
|
seasons: List<BaseItem>,
|
||||||
similar: List<BaseItem>,
|
similar: List<BaseItem>,
|
||||||
people: List<Person>,
|
people: List<Person>,
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
|
|
@ -332,7 +332,7 @@ fun SeriesDetailsContent(
|
||||||
item {
|
item {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = "Seasons",
|
title = "Seasons",
|
||||||
items = seasons.items,
|
items = seasons,
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
position = SEASONS_ROW
|
position = SEASONS_ROW
|
||||||
onClickItem.invoke(it)
|
onClickItem.invoke(it)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@file:UseSerializers(UUIDSerializer::class)
|
||||||
|
|
||||||
package com.github.damontecres.wholphin.ui.detail.series
|
package com.github.damontecres.wholphin.ui.detail.series
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -26,15 +28,20 @@ import com.github.damontecres.wholphin.ui.components.chooseVersionParams
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
||||||
|
import com.github.damontecres.wholphin.ui.equalsNotNull
|
||||||
|
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.seasonEpisode
|
import com.github.damontecres.wholphin.util.seasonEpisode
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.UseSerializers
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -43,6 +50,14 @@ data class SeasonEpisode(
|
||||||
val episode: Int,
|
val episode: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SeasonEpisodeIds(
|
||||||
|
val seasonId: UUID,
|
||||||
|
val seasonNumber: Int?,
|
||||||
|
val episodeId: UUID?,
|
||||||
|
val episodeNumber: Int?,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SeriesOverviewPosition(
|
data class SeriesOverviewPosition(
|
||||||
val seasonTabIndex: Int,
|
val seasonTabIndex: Int,
|
||||||
|
|
@ -55,7 +70,7 @@ fun SeriesOverview(
|
||||||
destination: Destination.SeriesOverview,
|
destination: Destination.SeriesOverview,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SeriesViewModel = hiltViewModel(),
|
viewModel: SeriesViewModel = hiltViewModel(),
|
||||||
initialSeasonEpisode: SeasonEpisode? = null,
|
initialSeasonEpisode: SeasonEpisodeIds? = null,
|
||||||
) {
|
) {
|
||||||
LifecycleStartEffect(destination.itemId) {
|
LifecycleStartEffect(destination.itemId) {
|
||||||
viewModel.maybePlayThemeSong(
|
viewModel.maybePlayThemeSong(
|
||||||
|
|
@ -76,8 +91,7 @@ fun SeriesOverview(
|
||||||
preferences,
|
preferences,
|
||||||
destination.itemId,
|
destination.itemId,
|
||||||
destination.item,
|
destination.item,
|
||||||
initialSeasonEpisode?.season,
|
initialSeasonEpisode,
|
||||||
initialSeasonEpisode?.episode,
|
|
||||||
)
|
)
|
||||||
initialLoadDone = true
|
initialLoadDone = true
|
||||||
}
|
}
|
||||||
|
|
@ -85,9 +99,9 @@ fun SeriesOverview(
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
|
|
||||||
val series by viewModel.item.observeAsState(null)
|
val series by viewModel.item.observeAsState(null)
|
||||||
val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty())
|
val seasons by viewModel.seasons.observeAsState(listOf())
|
||||||
val episodes by viewModel.episodes.observeAsState(EpisodeList.Loading)
|
val episodes by viewModel.episodes.observeAsState(EpisodeList.Loading)
|
||||||
val episodeList = (episodes as? EpisodeList.Success)?.episodes?.items
|
val episodeList = (episodes as? EpisodeList.Success)?.episodes
|
||||||
|
|
||||||
var position by rememberSaveable(
|
var position by rememberSaveable(
|
||||||
destination,
|
destination,
|
||||||
|
|
@ -100,18 +114,18 @@ fun SeriesOverview(
|
||||||
) {
|
) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
SeriesOverviewPosition(
|
SeriesOverviewPosition(
|
||||||
seasons.numberToIndex[initialSeasonEpisode?.season ?: 0] ?: 0,
|
seasons.indexOfFirstOrNull {
|
||||||
(episodes as? EpisodeList.Success)?.episodes?.numberToIndex[
|
equalsNotNull(it.id, initialSeasonEpisode?.seasonId) ||
|
||||||
initialSeasonEpisode?.episode
|
equalsNotNull(it.indexNumber, initialSeasonEpisode?.seasonNumber)
|
||||||
?: 0,
|
} ?: 0,
|
||||||
] ?: 0,
|
(episodes as? EpisodeList.Success)?.initialIndex ?: 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (initialLoadDone) {
|
if (initialLoadDone) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
seasons.indexToNumber[position.seasonTabIndex]?.let {
|
seasons.getOrNull(position.seasonTabIndex)?.let {
|
||||||
viewModel.loadEpisodes(it)
|
viewModel.loadEpisodes(it.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,10 +137,10 @@ fun SeriesOverview(
|
||||||
LaunchedEffect(episodes) {
|
LaunchedEffect(episodes) {
|
||||||
episodes?.let { episodes ->
|
episodes?.let { episodes ->
|
||||||
if (episodes is EpisodeList.Success) {
|
if (episodes is EpisodeList.Success) {
|
||||||
if (episodes.episodes.items.isNotEmpty()) {
|
if (episodes.episodes.isNotEmpty()) {
|
||||||
// TODO focus on first episode when changing seasons?
|
// TODO focus on first episode when changing seasons?
|
||||||
// firstItemFocusRequester.requestFocus()
|
// firstItemFocusRequester.requestFocus()
|
||||||
episodes.episodes.items.getOrNull(position.episodeRowIndex)?.let {
|
episodes.episodes.getOrNull(position.episodeRowIndex)?.let {
|
||||||
viewModel.refreshEpisode(it.id, position.episodeRowIndex)
|
viewModel.refreshEpisode(it.id, position.episodeRowIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +151,6 @@ fun SeriesOverview(
|
||||||
LaunchedEffect(position) {
|
LaunchedEffect(position) {
|
||||||
(episodes as? EpisodeList.Success)
|
(episodes as? EpisodeList.Success)
|
||||||
?.episodes
|
?.episodes
|
||||||
?.items
|
|
||||||
?.getOrNull(position.episodeRowIndex)
|
?.getOrNull(position.episodeRowIndex)
|
||||||
?.let {
|
?.let {
|
||||||
viewModel.lookUpChosenTracks(it.id, it)
|
viewModel.lookUpChosenTracks(it.id, it)
|
||||||
|
|
@ -230,7 +243,7 @@ fun SeriesOverview(
|
||||||
SeriesOverviewContent(
|
SeriesOverviewContent(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
series = series,
|
series = series,
|
||||||
seasons = seasons.items,
|
seasons = seasons,
|
||||||
episodes = episodes,
|
episodes = episodes,
|
||||||
chosenStreams = chosenStreams,
|
chosenStreams = chosenStreams,
|
||||||
position = position,
|
position = position,
|
||||||
|
|
@ -245,8 +258,8 @@ fun SeriesOverview(
|
||||||
episodeRowFocusRequester = episodeRowFocusRequester,
|
episodeRowFocusRequester = episodeRowFocusRequester,
|
||||||
onFocus = {
|
onFocus = {
|
||||||
if (it.seasonTabIndex != position.seasonTabIndex) {
|
if (it.seasonTabIndex != position.seasonTabIndex) {
|
||||||
seasons.indexToNumber[it.seasonTabIndex]?.let { seasonNumber ->
|
seasons.getOrNull(it.seasonTabIndex)?.let { season ->
|
||||||
viewModel.loadEpisodes(seasonNumber)
|
viewModel.loadEpisodes(season.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
position = it
|
position = it
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ fun SeriesOverviewContent(
|
||||||
val tabRowFocusRequester = remember { FocusRequester() }
|
val tabRowFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
val focusedEpisode =
|
val focusedEpisode =
|
||||||
(episodes as? EpisodeList.Success)?.episodes?.items?.getOrNull(position.episodeRowIndex)
|
(episodes as? EpisodeList.Success)?.episodes?.getOrNull(position.episodeRowIndex)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -229,7 +229,7 @@ fun SeriesOverviewContent(
|
||||||
.focusRestorer(firstItemFocusRequester)
|
.focusRestorer(firstItemFocusRequester)
|
||||||
.focusRequester(episodeRowFocusRequester),
|
.focusRequester(episodeRowFocusRequester),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(eps.episodes.items) { episodeIndex, episode ->
|
itemsIndexed(eps.episodes) { episodeIndex, episode ->
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
if (interactionSource.collectIsFocusedAsState().value) {
|
if (interactionSource.collectIsFocusedAsState().value) {
|
||||||
onFocus.invoke(
|
onFocus.invoke(
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
||||||
|
import com.github.damontecres.wholphin.ui.equalsNotNull
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
|
@ -68,7 +69,7 @@ class SeriesViewModel
|
||||||
private lateinit var seriesId: UUID
|
private lateinit var seriesId: UUID
|
||||||
private lateinit var prefs: UserPreferences
|
private lateinit var prefs: UserPreferences
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
val seasons = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty())
|
val seasons = MutableLiveData<List<BaseItem>>(listOf())
|
||||||
val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading)
|
val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading)
|
||||||
val people = MutableLiveData<List<Person>>(listOf())
|
val people = MutableLiveData<List<Person>>(listOf())
|
||||||
val similar = MutableLiveData<List<BaseItem>>()
|
val similar = MutableLiveData<List<BaseItem>>()
|
||||||
|
|
@ -77,8 +78,7 @@ class SeriesViewModel
|
||||||
prefs: UserPreferences,
|
prefs: UserPreferences,
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
potential: BaseItem?,
|
potential: BaseItem?,
|
||||||
season: Int?,
|
seasonEpisodeIds: SeasonEpisodeIds?,
|
||||||
episode: Int?,
|
|
||||||
) {
|
) {
|
||||||
this.seriesId = itemId
|
this.seriesId = itemId
|
||||||
this.prefs = prefs
|
this.prefs = prefs
|
||||||
|
|
@ -89,16 +89,28 @@ class SeriesViewModel
|
||||||
) + Dispatchers.IO,
|
) + Dispatchers.IO,
|
||||||
) {
|
) {
|
||||||
val item = fetchItem(seriesId, potential)
|
val item = fetchItem(seriesId, potential)
|
||||||
val seasonsInfo = getSeasons(item)
|
val seasons = getSeasons(item)
|
||||||
|
|
||||||
// If a particular season was requested, fetch those episodes, otherwise get the first season
|
// If a particular season was requested, fetch those episodes, otherwise get the first season
|
||||||
|
val initialSeason =
|
||||||
|
if (seasonEpisodeIds != null) {
|
||||||
|
seasons.firstOrNull {
|
||||||
|
equalsNotNull(it.id, seasonEpisodeIds.seasonId) ||
|
||||||
|
equalsNotNull(it.indexNumber, seasonEpisodeIds.seasonNumber)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seasons.firstOrNull()
|
||||||
|
}
|
||||||
val episodeInfo =
|
val episodeInfo =
|
||||||
(season ?: seasonsInfo.items.firstOrNull()?.indexNumber)
|
initialSeason?.let {
|
||||||
?.let { seasonNum ->
|
loadEpisodesInternal(
|
||||||
loadEpisodesInternal(seasonNum)
|
it.id,
|
||||||
} ?: EpisodeList.Error("Could not determine season")
|
seasonEpisodeIds?.episodeId,
|
||||||
|
seasonEpisodeIds?.episodeNumber,
|
||||||
|
)
|
||||||
|
} ?: EpisodeList.Error("Could not determine season for selected episode")
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
seasons.value = seasonsInfo
|
this@SeriesViewModel.seasons.value = seasons
|
||||||
episodes.value = episodeInfo
|
episodes.value = episodeInfo
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
people.value =
|
people.value =
|
||||||
|
|
@ -108,18 +120,20 @@ class SeriesViewModel
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
}
|
}
|
||||||
if (!similar.isInitialized) {
|
if (!similar.isInitialized) {
|
||||||
val similar =
|
viewModelScope.launchIO {
|
||||||
api.libraryApi
|
val similar =
|
||||||
.getSimilarItems(
|
api.libraryApi
|
||||||
GetSimilarItemsRequest(
|
.getSimilarItems(
|
||||||
userId = serverRepository.currentUser?.id,
|
GetSimilarItemsRequest(
|
||||||
itemId = itemId,
|
userId = serverRepository.currentUser?.id,
|
||||||
fields = SlimItemFields,
|
itemId = itemId,
|
||||||
limit = 25,
|
fields = SlimItemFields,
|
||||||
),
|
limit = 25,
|
||||||
).content.items
|
),
|
||||||
.map { BaseItem.from(it, api, true) }
|
).content.items
|
||||||
this@SeriesViewModel.similar.setValueOnMain(similar)
|
.map { BaseItem.from(it, api, true) }
|
||||||
|
this@SeriesViewModel.similar.setValueOnMain(similar)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -163,7 +177,7 @@ class SeriesViewModel
|
||||||
themeSongPlayer.stop()
|
themeSongPlayer.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSeasons(series: BaseItem): ItemListAndMapping {
|
private suspend fun getSeasons(series: BaseItem): List<BaseItem> {
|
||||||
val request =
|
val request =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
parentId = series.id,
|
parentId = series.id,
|
||||||
|
|
@ -178,30 +192,26 @@ class SeriesViewModel
|
||||||
ItemFields.SEASON_USER_DATA,
|
ItemFields.SEASON_USER_DATA,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val pager =
|
val seasons =
|
||||||
ApiRequestPager(
|
GetItemsRequestHandler.execute(api, request).content.items.map {
|
||||||
api,
|
BaseItem.from(
|
||||||
request,
|
it,
|
||||||
GetItemsRequestHandler,
|
api,
|
||||||
viewModelScope,
|
)
|
||||||
)
|
|
||||||
pager.init()
|
|
||||||
Timber.Forest.v("Loaded ${pager.size} seasons for series ${series.id}")
|
|
||||||
val pairs =
|
|
||||||
pager.mapIndexed { index, _ ->
|
|
||||||
val season = pager.getBlocking(index)
|
|
||||||
Pair(season?.indexNumber!!, index)
|
|
||||||
}
|
}
|
||||||
val seasonNumToIndex = mapOf(*pairs.toTypedArray())
|
Timber.v("Loaded ${seasons.size} seasons for series ${series.id}")
|
||||||
val indexToSeasonNum = mapOf(*pairs.map { Pair(it.second, it.first) }.toTypedArray())
|
return seasons
|
||||||
return ItemListAndMapping(pager, seasonNumToIndex, indexToSeasonNum)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadEpisodesInternal(season: Int): EpisodeList {
|
private suspend fun loadEpisodesInternal(
|
||||||
|
seasonId: UUID,
|
||||||
|
episodeId: UUID?,
|
||||||
|
episodeNumber: Int?,
|
||||||
|
): EpisodeList {
|
||||||
val request =
|
val request =
|
||||||
GetEpisodesRequest(
|
GetEpisodesRequest(
|
||||||
seriesId = item.value!!.id,
|
seriesId = seriesId,
|
||||||
season = season,
|
seasonId = seasonId,
|
||||||
sortBy = ItemSortBy.INDEX_NUMBER,
|
sortBy = ItemSortBy.INDEX_NUMBER,
|
||||||
fields =
|
fields =
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -215,18 +225,30 @@ class SeriesViewModel
|
||||||
)
|
)
|
||||||
val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope)
|
val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope)
|
||||||
pager.init()
|
pager.init()
|
||||||
Timber.v("Loaded ${pager.size} episodes for season $season")
|
val initialIndex =
|
||||||
return EpisodeList.Success(convertPager(pager))
|
if (episodeId != null || episodeNumber != null) {
|
||||||
|
pager
|
||||||
|
.indexOfBlocking {
|
||||||
|
equalsNotNull(it?.id, episodeId) ||
|
||||||
|
equalsNotNull(it?.indexNumber, episodeNumber)
|
||||||
|
}.coerceAtLeast(0)
|
||||||
|
} else {
|
||||||
|
// Force the first page to to be fetched
|
||||||
|
pager.getBlocking(0)
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Timber.v("Loaded ${pager.size} episodes for season $seasonId, initialIndex=$initialIndex")
|
||||||
|
return EpisodeList.Success(pager, initialIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadEpisodes(season: Int) {
|
fun loadEpisodes(seasonId: UUID) {
|
||||||
this@SeriesViewModel.episodes.value = EpisodeList.Loading
|
this@SeriesViewModel.episodes.value = EpisodeList.Loading
|
||||||
viewModelScope.launch(ExceptionHandler(true)) {
|
viewModelScope.launchIO(ExceptionHandler(true)) {
|
||||||
val episodes =
|
val episodes =
|
||||||
try {
|
try {
|
||||||
loadEpisodesInternal(season)
|
loadEpisodesInternal(seasonId, null, null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Error loading episodes for $seriesId for season $season")
|
Timber.e(e, "Error loading episodes for $seriesId for season $seasonId")
|
||||||
EpisodeList.Error(e)
|
EpisodeList.Error(e)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|
@ -295,13 +317,13 @@ class SeriesViewModel
|
||||||
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
val base = api.userLibraryApi.getItem(itemId).content
|
val base = api.userLibraryApi.getItem(itemId).content
|
||||||
val item = BaseItem.Companion.from(base, api)
|
val item = BaseItem.Companion.from(base, api)
|
||||||
val eps = episodes.value!!
|
val eps = episodes.value
|
||||||
if (eps is EpisodeList.Success) {
|
if (eps is EpisodeList.Success) {
|
||||||
val newItems =
|
val newItems =
|
||||||
eps.episodes.items.toMutableList().apply {
|
eps.episodes.toMutableList().apply {
|
||||||
this[listIndex] = item
|
this[listIndex] = item
|
||||||
}
|
}
|
||||||
val newValue = EpisodeList.Success(eps.episodes.copy(items = newItems))
|
val newValue = eps.copy(episodes = newItems)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
episodes.value = newValue
|
episodes.value = newValue
|
||||||
}
|
}
|
||||||
|
|
@ -398,32 +420,6 @@ class SeriesViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ItemListAndMapping(
|
|
||||||
val items: List<BaseItem?>,
|
|
||||||
val numberToIndex: Map<Int, Int>,
|
|
||||||
val indexToNumber: Map<Int, Int>,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun empty() = ItemListAndMapping(listOf(), mapOf(), mapOf())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the index<->season/ep number pairings
|
|
||||||
*
|
|
||||||
* This allows for handling of missing seasons
|
|
||||||
*/
|
|
||||||
private suspend fun convertPager(pager: ApiRequestPager<*>): ItemListAndMapping {
|
|
||||||
val pairs =
|
|
||||||
pager.mapIndexed { index, _ ->
|
|
||||||
val item = pager.getBlocking(index)
|
|
||||||
Pair(item?.indexNumber ?: index, index)
|
|
||||||
}
|
|
||||||
val seasonNumToIndex = mapOf(*pairs.toTypedArray())
|
|
||||||
val indexToSeasonNum = mapOf(*pairs.map { Pair(it.second, it.first) }.toTypedArray())
|
|
||||||
return ItemListAndMapping(pager, seasonNumToIndex, indexToSeasonNum)
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface EpisodeList {
|
sealed interface EpisodeList {
|
||||||
data object Loading : EpisodeList
|
data object Loading : EpisodeList
|
||||||
|
|
||||||
|
|
@ -435,6 +431,7 @@ sealed interface EpisodeList {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Success(
|
data class Success(
|
||||||
val episodes: ItemListAndMapping,
|
val episodes: List<BaseItem?>,
|
||||||
|
val initialIndex: Int,
|
||||||
) : EpisodeList
|
) : EpisodeList
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
|
|
@ -48,7 +49,7 @@ sealed class Destination(
|
||||||
val itemId: UUID,
|
val itemId: UUID,
|
||||||
val type: BaseItemKind,
|
val type: BaseItemKind,
|
||||||
@Transient val item: BaseItem? = null,
|
@Transient val item: BaseItem? = null,
|
||||||
val seasonEpisode: SeasonEpisode? = null,
|
val seasonEpisode: SeasonEpisodeIds? = null,
|
||||||
) : Destination() {
|
) : Destination() {
|
||||||
override fun toString(): String = "SeriesOverview(itemId=$itemId, type=$type, seasonEpisode=$seasonEpisode)"
|
override fun toString(): String = "SeriesOverview(itemId=$itemId, type=$type, seasonEpisode=$seasonEpisode)"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue