mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +02:00
Display episode even if season metadata is missing (#329)
Adds a simple fallback page to show an episode in cases when it doesn't have an associated season due to missing metadata,see #280. The missing metadata is probably a server bug, but it's good to have a fallback anyway.
This commit is contained in:
parent
70f569208c
commit
10c08c5515
13 changed files with 760 additions and 117 deletions
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
|
import com.github.damontecres.wholphin.ui.timeRemaining
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MovieQuickDetails(
|
||||||
|
dto: BaseItemDto?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val details =
|
||||||
|
remember(dto) {
|
||||||
|
buildList {
|
||||||
|
dto?.productionYear?.let { add(it.toString()) }
|
||||||
|
dto?.runTimeTicks?.ticks?.roundMinutes?.let {
|
||||||
|
add(it.toString())
|
||||||
|
}
|
||||||
|
dto?.timeRemaining?.roundMinutes?.let {
|
||||||
|
add("$it left")
|
||||||
|
}
|
||||||
|
dto?.officialRating?.let(::add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DotSeparatedRow(
|
||||||
|
texts = details,
|
||||||
|
rating = dto?.communityRating,
|
||||||
|
textStyle = MaterialTheme.typography.titleSmall,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.ui.formatDateTime
|
||||||
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
|
import com.github.damontecres.wholphin.ui.seasonEpisode
|
||||||
|
import com.github.damontecres.wholphin.ui.timeRemaining
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SeriesName(
|
||||||
|
seriesName: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = seriesName ?: "",
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EpisodeName(
|
||||||
|
episodeName: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = episodeName ?: "",
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EpisodeName(
|
||||||
|
episode: BaseItemDto?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) = EpisodeName(episode?.episodeTitle ?: episode?.name, modifier)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EpisodeQuickDetails(
|
||||||
|
dto: BaseItemDto?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val details =
|
||||||
|
remember(dto) {
|
||||||
|
buildList {
|
||||||
|
dto?.seasonEpisode?.let(::add)
|
||||||
|
dto?.premiereDate?.let { add(formatDateTime(it)) }
|
||||||
|
val duration = dto?.runTimeTicks?.ticks
|
||||||
|
duration
|
||||||
|
?.roundMinutes
|
||||||
|
?.toString()
|
||||||
|
?.let(::add)
|
||||||
|
dto?.timeRemaining?.roundMinutes?.let { add("$it left") }
|
||||||
|
dto?.officialRating?.let(::add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DotSeparatedRow(
|
||||||
|
texts = details,
|
||||||
|
rating = dto?.communityRating,
|
||||||
|
textStyle = MaterialTheme.typography.titleSmall,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ data class MoreDialogActions(
|
||||||
fun buildMoreDialogItems(
|
fun buildMoreDialogItems(
|
||||||
context: Context,
|
context: Context,
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
series: BaseItem?,
|
seriesId: UUID?,
|
||||||
sourceId: UUID?,
|
sourceId: UUID?,
|
||||||
watched: Boolean,
|
watched: Boolean,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
|
|
@ -144,7 +144,7 @@ fun buildMoreDialogItems(
|
||||||
actions.onClickFavorite.invoke(item.id, !favorite)
|
actions.onClickFavorite.invoke(item.id, !favorite)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
series?.let {
|
seriesId?.let {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.go_to_series),
|
context.getString(R.string.go_to_series),
|
||||||
|
|
@ -152,9 +152,8 @@ fun buildMoreDialogItems(
|
||||||
) {
|
) {
|
||||||
actions.navigateTo(
|
actions.navigateTo(
|
||||||
Destination.MediaItem(
|
Destination.MediaItem(
|
||||||
series.id,
|
seriesId,
|
||||||
BaseItemKind.SERIES,
|
BaseItemKind.SERIES,
|
||||||
series,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.episode
|
||||||
|
|
||||||
|
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.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
|
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||||
|
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.chooseSource
|
||||||
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
|
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
|
||||||
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.Optional
|
||||||
|
import com.github.damontecres.wholphin.ui.components.chooseStream
|
||||||
|
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
|
||||||
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EpisodeDetails(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
destination: Destination.MediaItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: EpisodeViewModel =
|
||||||
|
hiltViewModel<EpisodeViewModel, EpisodeViewModel.Factory>(
|
||||||
|
creationCallback = { it.create(destination.itemId) },
|
||||||
|
),
|
||||||
|
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
LifecycleResumeEffect(Unit) {
|
||||||
|
viewModel.init()
|
||||||
|
onPauseOrDispose { }
|
||||||
|
}
|
||||||
|
val item by viewModel.item.observeAsState()
|
||||||
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
|
val chosenStreams by viewModel.chosenStreams.observeAsState(null)
|
||||||
|
|
||||||
|
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||||
|
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
|
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
|
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||||
|
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||||
|
|
||||||
|
val moreActions =
|
||||||
|
MoreDialogActions(
|
||||||
|
navigateTo = viewModel::navigateTo,
|
||||||
|
onClickWatch = { itemId, watched ->
|
||||||
|
viewModel.setWatched(itemId, watched)
|
||||||
|
},
|
||||||
|
onClickFavorite = { itemId, favorite ->
|
||||||
|
viewModel.setFavorite(itemId, favorite)
|
||||||
|
},
|
||||||
|
onClickAddPlaylist = { itemId ->
|
||||||
|
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||||
|
showPlaylistDialog.makePresent(itemId)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
when (val state = loading) {
|
||||||
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Pending,
|
||||||
|
-> LoadingPage()
|
||||||
|
|
||||||
|
LoadingState.Success -> {
|
||||||
|
item?.let { ep ->
|
||||||
|
LifecycleStartEffect(destination.itemId) {
|
||||||
|
viewModel.maybePlayThemeSong(
|
||||||
|
destination.itemId,
|
||||||
|
preferences.appPreferences.interfacePreferences.playThemeSongs,
|
||||||
|
)
|
||||||
|
onStopOrDispose {
|
||||||
|
viewModel.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EpisodeDetailsContent(
|
||||||
|
preferences = preferences,
|
||||||
|
ep = ep,
|
||||||
|
chosenStreams = chosenStreams,
|
||||||
|
playOnClick = {
|
||||||
|
viewModel.navigateTo(
|
||||||
|
Destination.Playback(
|
||||||
|
ep.id,
|
||||||
|
it.inWholeMilliseconds,
|
||||||
|
ep,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
overviewOnClick = {
|
||||||
|
overviewDialog =
|
||||||
|
ItemDetailsDialogInfo(
|
||||||
|
title = ep.name ?: context.getString(R.string.unknown),
|
||||||
|
overview = ep.data.overview,
|
||||||
|
files = ep.data.mediaSources.orEmpty(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
moreOnClick = {
|
||||||
|
moreDialog =
|
||||||
|
DialogParams(
|
||||||
|
fromLongClick = false,
|
||||||
|
title = ep.name + " (${ep.data.productionYear ?: ""})",
|
||||||
|
items =
|
||||||
|
buildMoreDialogItems(
|
||||||
|
context = context,
|
||||||
|
item = ep,
|
||||||
|
watched = ep.data.userData?.played ?: false,
|
||||||
|
favorite = ep.data.userData?.isFavorite ?: false,
|
||||||
|
seriesId = ep.data.seriesId,
|
||||||
|
sourceId = chosenStreams?.sourceId,
|
||||||
|
actions = moreActions,
|
||||||
|
onChooseVersion = {
|
||||||
|
chooseVersion =
|
||||||
|
chooseVersionParams(
|
||||||
|
context,
|
||||||
|
ep.data.mediaSources!!,
|
||||||
|
) { idx ->
|
||||||
|
val source = ep.data.mediaSources!![idx]
|
||||||
|
viewModel.savePlayVersion(
|
||||||
|
ep,
|
||||||
|
source.id!!.toUUID(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
moreDialog = null
|
||||||
|
},
|
||||||
|
onChooseTracks = { type ->
|
||||||
|
chooseSource(
|
||||||
|
ep.data,
|
||||||
|
chosenStreams?.itemPlayback,
|
||||||
|
)?.let { source ->
|
||||||
|
chooseVersion =
|
||||||
|
chooseStream(
|
||||||
|
context = context,
|
||||||
|
streams = source.mediaStreams.orEmpty(),
|
||||||
|
type = type,
|
||||||
|
onClick = { trackIndex ->
|
||||||
|
viewModel.saveTrackSelection(
|
||||||
|
ep,
|
||||||
|
chosenStreams?.itemPlayback,
|
||||||
|
trackIndex,
|
||||||
|
type,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
watchOnClick = {
|
||||||
|
viewModel.setWatched(ep.id, !ep.played)
|
||||||
|
},
|
||||||
|
favoriteOnClick = {
|
||||||
|
viewModel.setFavorite(ep.id, !ep.favorite)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overviewDialog?.let { info ->
|
||||||
|
ItemDetailsDialog(
|
||||||
|
info = info,
|
||||||
|
showFilePath =
|
||||||
|
viewModel.serverRepository.currentUserDto.value
|
||||||
|
?.policy
|
||||||
|
?.isAdministrator == true,
|
||||||
|
onDismissRequest = { overviewDialog = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
moreDialog?.let { params ->
|
||||||
|
DialogPopup(
|
||||||
|
showDialog = true,
|
||||||
|
title = params.title,
|
||||||
|
dialogItems = params.items,
|
||||||
|
onDismissRequest = { moreDialog = null },
|
||||||
|
dismissOnClick = true,
|
||||||
|
waitToLoad = params.fromLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
chooseVersion?.let { params ->
|
||||||
|
DialogPopup(
|
||||||
|
showDialog = true,
|
||||||
|
title = params.title,
|
||||||
|
dialogItems = params.items,
|
||||||
|
onDismissRequest = { chooseVersion = null },
|
||||||
|
dismissOnClick = true,
|
||||||
|
waitToLoad = params.fromLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showPlaylistDialog.compose { itemId ->
|
||||||
|
PlaylistDialog(
|
||||||
|
title = stringResource(R.string.add_to_playlist),
|
||||||
|
state = playlistState,
|
||||||
|
onDismissRequest = { showPlaylistDialog.makeAbsent() },
|
||||||
|
onClick = {
|
||||||
|
playlistViewModel.addToPlaylist(it.id, itemId)
|
||||||
|
showPlaylistDialog.makeAbsent()
|
||||||
|
},
|
||||||
|
createEnabled = true,
|
||||||
|
onCreatePlaylist = {
|
||||||
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
|
showPlaylistDialog.makeAbsent()
|
||||||
|
},
|
||||||
|
elevation = 3.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val HEADER_ROW = 0
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EpisodeDetailsContent(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
ep: BaseItem,
|
||||||
|
chosenStreams: ChosenStreams?,
|
||||||
|
playOnClick: (Duration) -> Unit,
|
||||||
|
overviewOnClick: () -> Unit,
|
||||||
|
watchOnClick: () -> Unit,
|
||||||
|
favoriteOnClick: () -> Unit,
|
||||||
|
moreOnClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var position by rememberInt(0)
|
||||||
|
val focusRequesters = remember { List(1) { FocusRequester() } }
|
||||||
|
val dto = ep.data
|
||||||
|
val backdropImageUrl = ep.backdropImageUrl
|
||||||
|
val resumePosition = dto.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO
|
||||||
|
|
||||||
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||||
|
}
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
DetailsBackdropImage(backdropImageUrl)
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.bringIntoViewRequester(bringIntoViewRequester),
|
||||||
|
) {
|
||||||
|
EpisodeDetailsHeader(
|
||||||
|
preferences = preferences,
|
||||||
|
ep = ep,
|
||||||
|
chosenStreams = chosenStreams,
|
||||||
|
bringIntoViewRequester = bringIntoViewRequester,
|
||||||
|
overviewOnClick = overviewOnClick,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 32.dp, bottom = 16.dp),
|
||||||
|
)
|
||||||
|
ExpandablePlayButtons(
|
||||||
|
resumePosition = resumePosition,
|
||||||
|
watched = dto.userData?.played ?: false,
|
||||||
|
favorite = dto.userData?.isFavorite ?: false,
|
||||||
|
playOnClick = {
|
||||||
|
position = HEADER_ROW
|
||||||
|
playOnClick.invoke(it)
|
||||||
|
},
|
||||||
|
moreOnClick = moreOnClick,
|
||||||
|
watchOnClick = watchOnClick,
|
||||||
|
favoriteOnClick = favoriteOnClick,
|
||||||
|
buttonOnFocusChanged = {
|
||||||
|
if (it.isFocused) {
|
||||||
|
position = HEADER_ROW
|
||||||
|
scope.launch(ExceptionHandler()) {
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.focusRequester(focusRequesters[HEADER_ROW]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.episode
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
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.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.ui.components.EpisodeName
|
||||||
|
import com.github.damontecres.wholphin.ui.components.EpisodeQuickDetails
|
||||||
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
|
import com.github.damontecres.wholphin.ui.components.SeriesName
|
||||||
|
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||||
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
|
import org.jellyfin.sdk.model.api.PersonKind
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EpisodeDetailsHeader(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
ep: BaseItem,
|
||||||
|
chosenStreams: ChosenStreams?,
|
||||||
|
bringIntoViewRequester: BringIntoViewRequester,
|
||||||
|
overviewOnClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val dto = ep.data
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
SeriesName(dto.seriesName, Modifier.fillMaxWidth(.75f))
|
||||||
|
EpisodeName(dto, Modifier.fillMaxWidth(.75f))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
|
) {
|
||||||
|
val padding = 8.dp
|
||||||
|
EpisodeQuickDetails(dto)
|
||||||
|
|
||||||
|
VideoStreamDetails(
|
||||||
|
preferences = preferences,
|
||||||
|
dto = dto,
|
||||||
|
itemPlayback = chosenStreams?.itemPlayback,
|
||||||
|
modifier = Modifier.padding(bottom = padding),
|
||||||
|
)
|
||||||
|
dto.taglines?.firstOrNull()?.let { tagline ->
|
||||||
|
Text(
|
||||||
|
text = tagline,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
dto.overview?.let { overview ->
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val focused = interactionSource.collectIsFocusedAsState().value
|
||||||
|
LaunchedEffect(focused) {
|
||||||
|
if (focused) bringIntoViewRequester.bringIntoView()
|
||||||
|
}
|
||||||
|
OverviewText(
|
||||||
|
overview = overview,
|
||||||
|
maxLines = 3,
|
||||||
|
onClick = overviewOnClick,
|
||||||
|
textBoxHeight = Dp.Unspecified,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ep.data.people
|
||||||
|
?.filter { it.type == PersonKind.DIRECTOR && it.name.isNotNullOrBlank() }
|
||||||
|
?.joinToString(", ") { it.name!! }
|
||||||
|
?.let {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.directed_by, it),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.episode
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
|
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||||
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
|
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||||
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.services.ThemeSongPlayer
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@HiltViewModel(assistedFactory = EpisodeViewModel.Factory::class)
|
||||||
|
class EpisodeViewModel
|
||||||
|
@AssistedInject
|
||||||
|
constructor(
|
||||||
|
private val api: ApiClient,
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
|
private val navigationManager: NavigationManager,
|
||||||
|
val serverRepository: ServerRepository,
|
||||||
|
val itemPlaybackRepository: ItemPlaybackRepository,
|
||||||
|
private val themeSongPlayer: ThemeSongPlayer,
|
||||||
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
|
@Assisted val itemId: UUID,
|
||||||
|
) : ViewModel() {
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(itemId: UUID): EpisodeViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
val item = MutableLiveData<BaseItem?>(null)
|
||||||
|
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchAndSetItem(): Deferred<BaseItem> =
|
||||||
|
viewModelScope.async(
|
||||||
|
Dispatchers.IO +
|
||||||
|
LoadingExceptionHandler(
|
||||||
|
loading,
|
||||||
|
"Error fetching movie",
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val item =
|
||||||
|
api.userLibraryApi.getItem(itemId).content.let {
|
||||||
|
BaseItem.from(it, api)
|
||||||
|
}
|
||||||
|
this@EpisodeViewModel.item.setValueOnMain(item)
|
||||||
|
item
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(): Job =
|
||||||
|
viewModelScope.launch(
|
||||||
|
Dispatchers.IO +
|
||||||
|
LoadingExceptionHandler(
|
||||||
|
loading,
|
||||||
|
"Error fetching movie",
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val item = fetchAndSetItem().await()
|
||||||
|
val result = itemPlaybackRepository.getSelectedTracks(item.id, item)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
this@EpisodeViewModel.item.value = item
|
||||||
|
chosenStreams.value = result
|
||||||
|
loading.value = LoadingState.Success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWatched(
|
||||||
|
itemId: UUID,
|
||||||
|
played: Boolean,
|
||||||
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
|
favoriteWatchManager.setWatched(itemId, played)
|
||||||
|
fetchAndSetItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFavorite(
|
||||||
|
itemId: UUID,
|
||||||
|
favorite: Boolean,
|
||||||
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
|
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||||
|
fetchAndSetItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun savePlayVersion(
|
||||||
|
item: BaseItem,
|
||||||
|
sourceId: UUID,
|
||||||
|
) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
|
||||||
|
val chosen =
|
||||||
|
result?.let {
|
||||||
|
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
chosenStreams.value = chosen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveTrackSelection(
|
||||||
|
item: BaseItem,
|
||||||
|
itemPlayback: ItemPlayback?,
|
||||||
|
trackIndex: Int,
|
||||||
|
type: MediaStreamType,
|
||||||
|
) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val result =
|
||||||
|
itemPlaybackRepository.saveTrackSelection(
|
||||||
|
item = item,
|
||||||
|
itemPlayback = itemPlayback,
|
||||||
|
trackIndex = trackIndex,
|
||||||
|
type = type,
|
||||||
|
)
|
||||||
|
val chosen =
|
||||||
|
result?.let {
|
||||||
|
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
chosenStreams.value = chosen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun maybePlayThemeSong(
|
||||||
|
seriesId: UUID,
|
||||||
|
playThemeSongs: ThemeSongVolume,
|
||||||
|
) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
themeSongPlayer.playThemeFor(seriesId, playThemeSongs)
|
||||||
|
addCloseable {
|
||||||
|
themeSongPlayer.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
themeSongPlayer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateTo(destination: Destination) {
|
||||||
|
release()
|
||||||
|
navigationManager.navigateTo(destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -196,7 +196,7 @@ fun MovieDetails(
|
||||||
item = movie,
|
item = movie,
|
||||||
watched = movie.data.userData?.played ?: false,
|
watched = movie.data.userData?.played ?: false,
|
||||||
favorite = movie.data.userData?.isFavorite ?: false,
|
favorite = movie.data.userData?.isFavorite ?: false,
|
||||||
series = null,
|
seriesId = null,
|
||||||
sourceId = chosenStreams?.sourceId,
|
sourceId = chosenStreams?.sourceId,
|
||||||
actions = moreActions,
|
actions = moreActions,
|
||||||
onChooseVersion = {
|
onChooseVersion = {
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,11 @@ import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
import com.github.damontecres.wholphin.ui.components.MovieQuickDetails
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
|
||||||
import com.github.damontecres.wholphin.ui.timeRemaining
|
|
||||||
import org.jellyfin.sdk.model.api.PersonKind
|
import org.jellyfin.sdk.model.api.PersonKind
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MovieDetailsHeader(
|
fun MovieDetailsHeader(
|
||||||
|
|
@ -64,25 +61,7 @@ fun MovieDetailsHeader(
|
||||||
modifier = Modifier.fillMaxWidth(.60f),
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
) {
|
) {
|
||||||
val padding = 8.dp
|
val padding = 8.dp
|
||||||
val details =
|
MovieQuickDetails(dto, Modifier.padding(bottom = padding))
|
||||||
remember(dto) {
|
|
||||||
buildList {
|
|
||||||
dto.productionYear?.let { add(it.toString()) }
|
|
||||||
val duration = dto.runTimeTicks?.ticks
|
|
||||||
duration
|
|
||||||
?.roundMinutes
|
|
||||||
?.toString()
|
|
||||||
?.let(::add)
|
|
||||||
dto.timeRemaining?.roundMinutes?.let { add("$it left") }
|
|
||||||
dto.officialRating?.let(::add)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DotSeparatedRow(
|
|
||||||
texts = details,
|
|
||||||
rating = dto.communityRating,
|
|
||||||
textStyle = MaterialTheme.typography.titleMedium,
|
|
||||||
modifier = Modifier.padding(bottom = padding),
|
|
||||||
)
|
|
||||||
|
|
||||||
VideoStreamDetails(
|
VideoStreamDetails(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
|
|
@ -114,9 +93,15 @@ fun MovieDetailsHeader(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val directorName =
|
||||||
|
remember(movie.data.people) {
|
||||||
movie.data.people
|
movie.data.people
|
||||||
?.filter { it.type == PersonKind.DIRECTOR && it.name.isNotNullOrBlank() }
|
?.filter { it.type == PersonKind.DIRECTOR && it.name.isNotNullOrBlank() }
|
||||||
?.joinToString(", ") { it.name!! }
|
?.joinToString(", ") { it.name!! }
|
||||||
|
}
|
||||||
|
|
||||||
|
directorName
|
||||||
?.let {
|
?.let {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.directed_by, it),
|
text = stringResource(R.string.directed_by, it),
|
||||||
|
|
|
||||||
|
|
@ -3,24 +3,16 @@ package com.github.damontecres.wholphin.ui.detail.series
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
|
||||||
import androidx.tv.material3.Text
|
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
import com.github.damontecres.wholphin.ui.components.EpisodeName
|
||||||
|
import com.github.damontecres.wholphin.ui.components.EpisodeQuickDetails
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||||
import com.github.damontecres.wholphin.ui.formatDateTime
|
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
|
||||||
import com.github.damontecres.wholphin.ui.seasonEpisode
|
|
||||||
import com.github.damontecres.wholphin.ui.timeRemaining
|
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FocusedEpisodeHeader(
|
fun FocusedEpisodeHeader(
|
||||||
|
|
@ -36,32 +28,9 @@ fun FocusedEpisodeHeader(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
EpisodeName(dto, modifier = Modifier)
|
||||||
text = dto?.episodeTitle ?: dto?.name ?: "",
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
EpisodeQuickDetails(dto)
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier,
|
|
||||||
)
|
|
||||||
val details =
|
|
||||||
remember(dto) {
|
|
||||||
buildList {
|
|
||||||
dto?.seasonEpisode?.let(::add)
|
|
||||||
dto?.premiereDate?.let { add(formatDateTime(it)) }
|
|
||||||
val duration = dto?.runTimeTicks?.ticks
|
|
||||||
duration
|
|
||||||
?.roundMinutes
|
|
||||||
?.toString()
|
|
||||||
?.let(::add)
|
|
||||||
dto?.timeRemaining?.roundMinutes?.let { add("$it left") }
|
|
||||||
dto?.officialRating?.let(::add)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DotSeparatedRow(
|
|
||||||
texts = details,
|
|
||||||
rating = dto?.communityRating,
|
|
||||||
textStyle = MaterialTheme.typography.titleSmall,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (dto != null) {
|
if (dto != null) {
|
||||||
VideoStreamDetails(
|
VideoStreamDetails(
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ fun SeriesOverview(
|
||||||
item = ep,
|
item = ep,
|
||||||
watched = ep.data.userData?.played ?: false,
|
watched = ep.data.userData?.played ?: false,
|
||||||
favorite = ep.data.userData?.isFavorite ?: false,
|
favorite = ep.data.userData?.isFavorite ?: false,
|
||||||
series = series,
|
seriesId = series.id,
|
||||||
sourceId = chosenStreams?.sourceId,
|
sourceId = chosenStreams?.sourceId,
|
||||||
actions =
|
actions =
|
||||||
MoreDialogActions(
|
MoreDialogActions(
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,7 @@ import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
|
||||||
import androidx.tv.material3.Text
|
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
|
@ -48,6 +45,7 @@ import com.github.damontecres.wholphin.ui.cards.BannerCard
|
||||||
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.SeriesName
|
||||||
import com.github.damontecres.wholphin.ui.components.TabRow
|
import com.github.damontecres.wholphin.ui.components.TabRow
|
||||||
import com.github.damontecres.wholphin.ui.formatDateTime
|
import com.github.damontecres.wholphin.ui.formatDateTime
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
|
|
@ -133,15 +131,7 @@ fun SeriesOverviewContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
series.name?.let {
|
SeriesName(series.name, Modifier)
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
// Episode header
|
// Episode header
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,10 @@ import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
import com.github.damontecres.wholphin.ui.components.EpisodeQuickDetails
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.MovieQuickDetails
|
||||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
|
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
|
||||||
|
|
@ -67,19 +68,14 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||||
import com.github.damontecres.wholphin.ui.formatDateTime
|
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
|
||||||
import com.github.damontecres.wholphin.ui.seasonEpisode
|
|
||||||
import com.github.damontecres.wholphin.ui.timeRemaining
|
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
@ -457,30 +453,6 @@ fun HomePageHeader(
|
||||||
val title = if (isEpisode) dto.seriesName ?: item.name else item.name
|
val title = if (isEpisode) dto.seriesName ?: item.name else item.name
|
||||||
val subtitle = if (isEpisode) dto.name else null
|
val subtitle = if (isEpisode) dto.name else null
|
||||||
val overview = dto.overview
|
val overview = dto.overview
|
||||||
val details =
|
|
||||||
buildList {
|
|
||||||
if (isEpisode) {
|
|
||||||
val se = dto.seasonEpisode
|
|
||||||
if (se != null) {
|
|
||||||
add(se)
|
|
||||||
} else if (dto.parentIndexNumber != null) {
|
|
||||||
// Maybe a daily episode, so just show season, the date is added below
|
|
||||||
add("S${dto.parentIndexNumber}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isEpisode) {
|
|
||||||
dto.premiereDate?.let { add(formatDateTime(it)) }
|
|
||||||
} else {
|
|
||||||
dto.productionYear?.let { add(it.toString()) }
|
|
||||||
}
|
|
||||||
dto.runTimeTicks?.ticks?.roundMinutes?.let {
|
|
||||||
add(it.toString())
|
|
||||||
}
|
|
||||||
dto.timeRemaining?.roundMinutes?.let {
|
|
||||||
add("$it left")
|
|
||||||
}
|
|
||||||
dto.officialRating?.let(::add)
|
|
||||||
}
|
|
||||||
title?.let {
|
title?.let {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
|
|
@ -499,13 +471,10 @@ fun HomePageHeader(
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (details.isNotEmpty()) {
|
if (isEpisode) {
|
||||||
DotSeparatedRow(
|
EpisodeQuickDetails(dto, Modifier)
|
||||||
texts = details,
|
} else {
|
||||||
rating = item.data.communityRating,
|
MovieQuickDetails(dto, Modifier)
|
||||||
textStyle = MaterialTheme.typography.bodyLarge,
|
|
||||||
modifier = Modifier,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
val overviewModifier =
|
val overviewModifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import com.github.damontecres.wholphin.ui.detail.DebugPage
|
||||||
import com.github.damontecres.wholphin.ui.detail.FavoritesPage
|
import com.github.damontecres.wholphin.ui.detail.FavoritesPage
|
||||||
import com.github.damontecres.wholphin.ui.detail.PersonPage
|
import com.github.damontecres.wholphin.ui.detail.PersonPage
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDetails
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDetails
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.episode.EpisodeDetails
|
||||||
import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails
|
import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeriesDetails
|
import com.github.damontecres.wholphin.ui.detail.series.SeriesDetails
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeriesOverview
|
import com.github.damontecres.wholphin.ui.detail.series.SeriesOverview
|
||||||
|
|
@ -101,6 +102,13 @@ fun DestinationContent(
|
||||||
modifier,
|
modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
BaseItemKind.EPISODE ->
|
||||||
|
EpisodeDetails(
|
||||||
|
preferences,
|
||||||
|
destination,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
|
||||||
BaseItemKind.BOX_SET ->
|
BaseItemKind.BOX_SET ->
|
||||||
CollectionFolderBoxSet(
|
CollectionFolderBoxSet(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue