mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Show people in epsisodes on series overview page (#373)
On the series overview page, show rows below the play/watch/favorite buttons for cast & crew and guest stars in the episode 
This commit is contained in:
parent
8ea84b3efe
commit
887b7b597e
8 changed files with 263 additions and 34 deletions
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.damontecres.wholphin.ui.cards
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
|
|
@ -28,6 +29,7 @@ fun PersonRow(
|
|||
people: List<Person>,
|
||||
onClick: (Person) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@StringRes title: Int = R.string.people,
|
||||
onLongClick: ((Int, Person) -> Unit)? = null,
|
||||
) {
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
|
|
@ -36,7 +38,7 @@ fun PersonRow(
|
|||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.people),
|
||||
text = stringResource(title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
|
|
@ -50,7 +52,7 @@ fun PersonRow(
|
|||
.fillMaxWidth()
|
||||
.focusRestorer(firstFocus),
|
||||
) {
|
||||
itemsIndexed(people) { index, item ->
|
||||
itemsIndexed(people, key = { _, item -> item.id }) { index, item ->
|
||||
PersonCard(
|
||||
item = item,
|
||||
onClick = { onClick.invoke(item) },
|
||||
|
|
@ -58,7 +60,8 @@ fun PersonRow(
|
|||
modifier =
|
||||
Modifier
|
||||
.width(120.dp)
|
||||
.ifElse(index == 0, Modifier.focusRequester(firstFocus)),
|
||||
.ifElse(index == 0, Modifier.focusRequester(firstFocus))
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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.FocusState
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
|
|
@ -24,6 +25,7 @@ fun FocusedEpisodeFooter(
|
|||
watchOnClick: () -> Unit,
|
||||
favoriteOnClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
buttonOnFocusChanged: (FocusState) -> Unit = {},
|
||||
) {
|
||||
val dto = ep.data
|
||||
val resumePosition = dto.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO
|
||||
|
|
@ -41,7 +43,7 @@ fun FocusedEpisodeFooter(
|
|||
moreOnClick = moreOnClick,
|
||||
watchOnClick = watchOnClick,
|
||||
favoriteOnClick = favoriteOnClick,
|
||||
buttonOnFocusChanged = {},
|
||||
buttonOnFocusChanged = buttonOnFocusChanged,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusState
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
|
|
@ -20,6 +22,7 @@ fun FocusedEpisodeHeader(
|
|||
ep: BaseItem?,
|
||||
chosenStreams: ChosenStreams?,
|
||||
overviewOnClick: () -> Unit,
|
||||
overviewOnFocus: (FocusState) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -44,6 +47,7 @@ fun FocusedEpisodeHeader(
|
|||
overview = dto?.overview ?: "",
|
||||
maxLines = 3,
|
||||
onClick = overviewOnClick,
|
||||
modifier = Modifier.onFocusChanged(overviewOnFocus),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.lifecycle.map
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||
|
|
@ -39,13 +40,16 @@ 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.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.seasonEpisode
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.api.PersonKind
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
|
|
@ -85,6 +89,8 @@ fun SeriesOverview(
|
|||
val context = LocalContext.current
|
||||
val firstItemFocusRequester = remember { FocusRequester() }
|
||||
val episodeRowFocusRequester = remember { FocusRequester() }
|
||||
val castCrewRowFocusRequester = remember { FocusRequester() }
|
||||
val guestStarRowFocusRequester = remember { FocusRequester() }
|
||||
|
||||
var initialLoadDone by rememberSaveable { mutableStateOf(false) }
|
||||
OneTimeLaunchedEffect {
|
||||
|
|
@ -103,6 +109,7 @@ fun SeriesOverview(
|
|||
val series by viewModel.item.observeAsState(null)
|
||||
val seasons by viewModel.seasons.observeAsState(listOf())
|
||||
val episodes by viewModel.episodes.observeAsState(EpisodeList.Loading)
|
||||
val peopleInEpisode by viewModel.peopleInEpisode.map { it.people }.observeAsState(listOf())
|
||||
val episodeList = (episodes as? EpisodeList.Success)?.episodes
|
||||
|
||||
var position by rememberSaveable(
|
||||
|
|
@ -138,6 +145,8 @@ fun SeriesOverview(
|
|||
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
|
||||
var rowFocused by rememberInt()
|
||||
|
||||
LaunchedEffect(episodes) {
|
||||
episodes?.let { episodes ->
|
||||
if (episodes is EpisodeList.Success) {
|
||||
|
|
@ -152,13 +161,16 @@ fun SeriesOverview(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(position) {
|
||||
(episodes as? EpisodeList.Success)
|
||||
?.episodes
|
||||
?.getOrNull(position.episodeRowIndex)
|
||||
?.let {
|
||||
viewModel.lookUpChosenTracks(it.id, it)
|
||||
}
|
||||
LaunchedEffect(position, episodes) {
|
||||
val focusedEpisode =
|
||||
(episodes as? EpisodeList.Success)
|
||||
?.episodes
|
||||
?.getOrNull(position.episodeRowIndex)
|
||||
|
||||
focusedEpisode?.let {
|
||||
viewModel.lookUpChosenTracks(it.id, it)
|
||||
viewModel.lookupPeopleInEpisode(it)
|
||||
}
|
||||
}
|
||||
val chosenStreams by viewModel.chosenStreams.observeAsState(null)
|
||||
|
||||
|
|
@ -171,7 +183,14 @@ fun SeriesOverview(
|
|||
|
||||
LoadingState.Success -> {
|
||||
series?.let { series ->
|
||||
LaunchedEffect(Unit) { episodeRowFocusRequester.tryRequestFocus() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
when (rowFocused) {
|
||||
EPISODE_ROW -> episodeRowFocusRequester.tryRequestFocus()
|
||||
CAST_AND_CREW_ROW -> castCrewRowFocusRequester.tryRequestFocus()
|
||||
GUEST_STAR_ROW -> guestStarRowFocusRequester.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
LifecycleStartEffect(destination.itemId) {
|
||||
viewModel.maybePlayThemeSong(
|
||||
destination.itemId,
|
||||
|
|
@ -263,6 +282,7 @@ fun SeriesOverview(
|
|||
seasons = seasons,
|
||||
episodes = episodes,
|
||||
chosenStreams = chosenStreams,
|
||||
peopleInEpisode = peopleInEpisode,
|
||||
position = position,
|
||||
backdropImageUrl =
|
||||
remember {
|
||||
|
|
@ -273,6 +293,8 @@ fun SeriesOverview(
|
|||
},
|
||||
firstItemFocusRequester = firstItemFocusRequester,
|
||||
episodeRowFocusRequester = episodeRowFocusRequester,
|
||||
castCrewRowFocusRequester = castCrewRowFocusRequester,
|
||||
guestStarRowFocusRequester = guestStarRowFocusRequester,
|
||||
onFocus = {
|
||||
if (it.seasonTabIndex != position.seasonTabIndex) {
|
||||
seasons.getOrNull(it.seasonTabIndex)?.let { season ->
|
||||
|
|
@ -282,6 +304,7 @@ fun SeriesOverview(
|
|||
position = it
|
||||
},
|
||||
onClick = {
|
||||
rowFocused = EPISODE_ROW
|
||||
val resumePosition =
|
||||
it.data.userData
|
||||
?.playbackPositionTicks
|
||||
|
|
@ -298,6 +321,7 @@ fun SeriesOverview(
|
|||
moreDialog = buildMoreForEpisode(ep, true)
|
||||
},
|
||||
playOnClick = { resume ->
|
||||
rowFocused = EPISODE_ROW
|
||||
episodeList?.getOrNull(position.episodeRowIndex)?.let {
|
||||
viewModel.release()
|
||||
viewModel.navigateTo(
|
||||
|
|
@ -337,6 +361,16 @@ fun SeriesOverview(
|
|||
)
|
||||
}
|
||||
},
|
||||
personOnClick = {
|
||||
rowFocused =
|
||||
if (it.type == PersonKind.GUEST_STAR) GUEST_STAR_ROW else CAST_AND_CREW_ROW
|
||||
viewModel.navigateTo(
|
||||
Destination.MediaItem(
|
||||
it.id,
|
||||
BaseItemKind.PERSON,
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -391,3 +425,7 @@ fun SeriesOverview(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val EPISODE_ROW = 0
|
||||
private const val CAST_AND_CREW_ROW = EPISODE_ROW + 1
|
||||
private const val GUEST_STAR_ROW = CAST_AND_CREW_ROW + 1
|
||||
|
|
|
|||
|
|
@ -1,22 +1,28 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.series
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -24,6 +30,7 @@ import androidx.compose.runtime.key
|
|||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -33,15 +40,18 @@ import androidx.compose.ui.focus.focusRequester
|
|||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.Person
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
|
|
@ -51,6 +61,9 @@ import com.github.damontecres.wholphin.ui.formatDateTime
|
|||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.logTab
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.ui.util.rememberDelayedNestedScroll
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.PersonKind
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Composable
|
||||
|
|
@ -60,10 +73,13 @@ fun SeriesOverviewContent(
|
|||
seasons: List<BaseItem?>,
|
||||
episodes: EpisodeList,
|
||||
chosenStreams: ChosenStreams?,
|
||||
peopleInEpisode: List<Person>,
|
||||
position: SeriesOverviewPosition,
|
||||
backdropImageUrl: String?,
|
||||
firstItemFocusRequester: FocusRequester,
|
||||
episodeRowFocusRequester: FocusRequester,
|
||||
castCrewRowFocusRequester: FocusRequester,
|
||||
guestStarRowFocusRequester: FocusRequester,
|
||||
onFocus: (SeriesOverviewPosition) -> Unit,
|
||||
onClick: (BaseItem) -> Unit,
|
||||
onLongClick: (BaseItem) -> Unit,
|
||||
|
|
@ -72,8 +88,10 @@ fun SeriesOverviewContent(
|
|||
favoriteOnClick: () -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
overviewOnClick: () -> Unit,
|
||||
personOnClick: (Person) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
var selectedTabIndex by rememberSaveable(position) { mutableIntStateOf(position.seasonTabIndex) }
|
||||
LaunchedEffect(selectedTabIndex) {
|
||||
|
|
@ -87,24 +105,32 @@ fun SeriesOverviewContent(
|
|||
var cardRowHasFocus by remember { mutableStateOf(false) }
|
||||
val dimming by animateFloatAsState(if (pageHasFocus && !cardRowHasFocus) .4f else 1f)
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val scrollConnection = rememberDelayedNestedScroll()
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
// .fillMaxHeight(.33f)
|
||||
.height(460.dp)
|
||||
.bringIntoViewRequester(bringIntoViewRequester),
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
DetailsBackdropImage(backdropImageUrl)
|
||||
LazyColumn(
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRestorer(episodeRowFocusRequester)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.focusGroup()
|
||||
.nestedScroll(scrollConnection)
|
||||
.verticalScroll(scrollState)
|
||||
.onFocusChanged { pageHasFocus = it.hasFocus },
|
||||
) {
|
||||
item {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusGroup()
|
||||
.bringIntoViewRequester(bringIntoViewRequester),
|
||||
) {
|
||||
val paddingValues =
|
||||
if (preferences.appPreferences.interfacePreferences.showClock) {
|
||||
PaddingValues(start = 16.dp, end = 100.dp)
|
||||
|
|
@ -129,21 +155,22 @@ fun SeriesOverviewContent(
|
|||
.padding(paddingValues)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item {
|
||||
SeriesName(series.name, Modifier)
|
||||
}
|
||||
item {
|
||||
// Episode header
|
||||
FocusedEpisodeHeader(
|
||||
preferences = preferences,
|
||||
ep = focusedEpisode,
|
||||
chosenStreams = chosenStreams,
|
||||
overviewOnClick = overviewOnClick,
|
||||
overviewOnFocus = {
|
||||
if (it.isFocused) {
|
||||
scope.launch {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(.6f),
|
||||
)
|
||||
}
|
||||
item {
|
||||
|
||||
key(position.seasonTabIndex) {
|
||||
when (val eps = episodes) {
|
||||
EpisodeList.Loading -> LoadingPage()
|
||||
|
|
@ -216,7 +243,13 @@ fun SeriesOverviewContent(
|
|||
Color.Black,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
).alpha(dimming),
|
||||
),
|
||||
).onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
scope.launch {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
}
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
cardHeight = 120.dp,
|
||||
)
|
||||
|
|
@ -225,8 +258,7 @@ fun SeriesOverviewContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
|
||||
focusedEpisode?.let { ep ->
|
||||
FocusedEpisodeFooter(
|
||||
preferences = preferences,
|
||||
|
|
@ -239,6 +271,13 @@ fun SeriesOverviewContent(
|
|||
episodeRowFocusRequester.tryRequestFocus()
|
||||
},
|
||||
favoriteOnClick = favoriteOnClick,
|
||||
buttonOnFocusChanged = {
|
||||
if (it.isFocused) {
|
||||
scope.launch {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -246,6 +285,53 @@ fun SeriesOverviewContent(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
val castAndCrew =
|
||||
remember(peopleInEpisode) {
|
||||
peopleInEpisode.filterNot {
|
||||
it.type == PersonKind.GUEST_STAR
|
||||
}
|
||||
}
|
||||
val guestStars =
|
||||
remember(peopleInEpisode) {
|
||||
peopleInEpisode.filter {
|
||||
it.type == PersonKind.GUEST_STAR
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = peopleInEpisode.isNotEmpty(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
if (castAndCrew.isNotEmpty()) {
|
||||
PersonRow(
|
||||
title = R.string.cast_and_crew,
|
||||
people = castAndCrew,
|
||||
onClick = personOnClick,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(castCrewRowFocusRequester),
|
||||
)
|
||||
}
|
||||
if (guestStars.isNotEmpty()) {
|
||||
PersonRow(
|
||||
title = R.string.guest_stars,
|
||||
people = guestStars,
|
||||
onClick = personOnClick,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(guestStarRowFocusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.github.damontecres.wholphin.ui.SlimItemFields
|
|||
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.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
|
|
@ -37,6 +38,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
|
|
@ -80,6 +82,8 @@ class SeriesViewModel
|
|||
val people = MutableLiveData<List<Person>>(listOf())
|
||||
val similar = MutableLiveData<List<BaseItem>>()
|
||||
|
||||
val peopleInEpisode = MutableLiveData<PeopleInItem>(PeopleInItem())
|
||||
|
||||
fun init(
|
||||
prefs: UserPreferences,
|
||||
itemId: UUID,
|
||||
|
|
@ -218,6 +222,7 @@ class SeriesViewModel
|
|||
ItemFields.CUSTOM_RATING,
|
||||
ItemFields.TRICKPLAY,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.PEOPLE,
|
||||
),
|
||||
)
|
||||
val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope)
|
||||
|
|
@ -237,11 +242,15 @@ class SeriesViewModel
|
|||
0
|
||||
}
|
||||
Timber.v("Loaded ${pager.size} episodes for season $seasonId, initialIndex=$initialIndex")
|
||||
return EpisodeList.Success(pager, initialIndex)
|
||||
return EpisodeList.Success(seasonId, pager, initialIndex)
|
||||
}
|
||||
|
||||
fun loadEpisodes(seasonId: UUID) {
|
||||
this@SeriesViewModel.episodes.value = EpisodeList.Loading
|
||||
val currentEpisodes = (this@SeriesViewModel.episodes.value as? EpisodeList.Success)
|
||||
if (currentEpisodes == null || currentEpisodes.seasonId != seasonId) {
|
||||
this@SeriesViewModel.peopleInEpisode.value = PeopleInItem()
|
||||
this@SeriesViewModel.episodes.value = EpisodeList.Loading
|
||||
}
|
||||
viewModelScope.launchIO(ExceptionHandler(true)) {
|
||||
val episodes =
|
||||
try {
|
||||
|
|
@ -253,6 +262,12 @@ class SeriesViewModel
|
|||
withContext(Dispatchers.Main) {
|
||||
this@SeriesViewModel.episodes.value = episodes
|
||||
}
|
||||
if (currentEpisodes == null || currentEpisodes.seasonId != seasonId) {
|
||||
(episodes as? EpisodeList.Success)
|
||||
?.let {
|
||||
it.episodes.getOrNull(it.initialIndex)
|
||||
}?.let { lookupPeopleInEpisode(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -403,6 +418,24 @@ class SeriesViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var peopleInEpisodeJob: Job? = null
|
||||
|
||||
suspend fun lookupPeopleInEpisode(item: BaseItem) {
|
||||
peopleInEpisodeJob?.cancel()
|
||||
if (peopleInEpisode.value?.itemId != item.id) {
|
||||
peopleInEpisode.setValueOnMain(PeopleInItem())
|
||||
peopleInEpisodeJob =
|
||||
viewModelScope.launch(ExceptionHandler()) {
|
||||
delay(250)
|
||||
val people =
|
||||
item.data.people
|
||||
?.letNotEmpty { it.map { Person.fromDto(it, api) } }
|
||||
.orEmpty()
|
||||
peopleInEpisode.setValueOnMain(PeopleInItem(item.id, people))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface EpisodeList {
|
||||
|
|
@ -416,7 +449,13 @@ sealed interface EpisodeList {
|
|||
}
|
||||
|
||||
data class Success(
|
||||
val seasonId: UUID,
|
||||
val episodes: ApiRequestPager<GetEpisodesRequest>,
|
||||
val initialIndex: Int,
|
||||
) : EpisodeList
|
||||
}
|
||||
|
||||
data class PeopleInItem(
|
||||
val itemId: UUID? = null,
|
||||
val people: List<Person> = listOf(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package com.github.damontecres.wholphin.ui.util
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
|
||||
class DelayedNestedScrollConnection(
|
||||
@param:FloatRange(0.0, 1.0)
|
||||
private val xDelay: Float = 0f,
|
||||
@param:FloatRange(0.0, 1.0)
|
||||
private val yDelay: Float = .6f,
|
||||
) : NestedScrollConnection {
|
||||
override fun onPreScroll(
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset =
|
||||
if (source == NestedScrollSource.UserInput) {
|
||||
Offset(x = available.x * xDelay, y = available.y * yDelay)
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a delay to the scroll speed via user interaction
|
||||
*
|
||||
* In some cases, slowing down scrolling makes it feel smoother and less jerky visually
|
||||
*
|
||||
* ```kotlin
|
||||
* val scrollState = rememberScrollState()
|
||||
* val scrollConnection = rememberDelayedNestedScroll(.5f, .5f)
|
||||
* Column(
|
||||
* modifier =
|
||||
* Modifier
|
||||
* .nestedScroll(scrollConnection)
|
||||
* .verticalScroll(scrollState)
|
||||
* ){ content() }
|
||||
* ```
|
||||
*
|
||||
* @param xDelay how much to delay left-right scrolling
|
||||
* @param yDelay how much to delay up-down scrolling
|
||||
*
|
||||
* @see rememberDelayedNestedScroll
|
||||
* @see androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
*/
|
||||
@Composable
|
||||
fun rememberDelayedNestedScroll(
|
||||
@FloatRange(0.0, 1.0)
|
||||
xDelay: Float = 0f,
|
||||
@FloatRange(0.0, 1.0)
|
||||
yDelay: Float = .33f,
|
||||
) = remember(xDelay, yDelay) { DelayedNestedScrollConnection(xDelay, yDelay) }
|
||||
|
|
@ -347,6 +347,8 @@
|
|||
<string name="aspect_ratio">Aspect Ratio</string>
|
||||
<string name="show_details">Show details</string>
|
||||
<string name="image_type">Image type</string>
|
||||
<string name="cast_and_crew"><![CDATA[Cast & Crew]]></string>
|
||||
<string name="guest_stars">Guest stars</string>
|
||||
|
||||
|
||||
<string-array name="theme_song_volume">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue