mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Updates to details pages including videos' stream details display (#279)
## Details This PR updates the detail pages for Movies & TV Shows. - Increase backdrop image size & brightness (now matches home page) - Increase max width for titles - Decrease max width for detail text - Switch to using a customized display labels for video, audio, & subtitle streams Additional change for movie pages: - Don't dim movie details text - Move video stream details below play buttons - Move first row up higher ### Labels Instead of using the server provided `DisplayTitle`, create display labels in-app. In general, this shortens the text without losing significant detail. It is similar to the official app. Some of the changes: - Don't show "SDR", only "HDR" - Simplify audio channel info, eg "Surround 5.1" => "5.1" - Show chosen subtitle language - Show chosen subtitle codec - Show "(+#)" if there are additional audio or subtitle tracks - Use gray background slightly tinted by the theme color ## Issues Closes #239
This commit is contained in:
parent
7be4fb5fdf
commit
5b96be6662
14 changed files with 427 additions and 291 deletions
|
|
@ -2,19 +2,9 @@ package com.github.damontecres.wholphin.ui
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
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.model.ItemPlayback
|
|
||||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
|
||||||
import com.github.damontecres.wholphin.data.model.chooseStream
|
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
|
||||||
import com.github.damontecres.wholphin.util.languageName
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.MediaSegmentType
|
import org.jellyfin.sdk.model.api.MediaSegmentType
|
||||||
import org.jellyfin.sdk.model.api.MediaStream
|
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
@ -81,53 +71,6 @@ val BaseItemDto.seasonEpisodePadded: String?
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatSubtitleLang(mediaStreams: List<MediaStream>?): String? =
|
|
||||||
mediaStreams
|
|
||||||
?.filter { it.type == MediaStreamType.SUBTITLE && it.language.isNotNullOrBlank() }
|
|
||||||
?.mapNotNull { it.language }
|
|
||||||
?.distinct()
|
|
||||||
?.joinToString(", ") { languageName(it) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the selected audio display title for the given item & chosen streams
|
|
||||||
*/
|
|
||||||
fun getAudioDisplay(
|
|
||||||
item: BaseItemDto,
|
|
||||||
chosenStreams: ChosenStreams?,
|
|
||||||
preferences: UserPreferences,
|
|
||||||
) = getAudioDisplay(item, chosenStreams?.itemPlayback, preferences)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the selected audio display title for the given item & chosen streams
|
|
||||||
*/
|
|
||||||
fun getAudioDisplay(
|
|
||||||
item: BaseItemDto,
|
|
||||||
itemPlayback: ItemPlayback?,
|
|
||||||
preferences: UserPreferences,
|
|
||||||
) = chooseStream(item, itemPlayback, MediaStreamType.AUDIO, preferences)
|
|
||||||
?.displayTitle
|
|
||||||
?.replace(" - Default", "")
|
|
||||||
?.ifBlank { null }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the selected subtitle language for the given item & chosen streams
|
|
||||||
*
|
|
||||||
* If none are chosen, returns a concatenated list of languages available
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getSubtitleDisplay(
|
|
||||||
item: BaseItemDto,
|
|
||||||
chosenStreams: ChosenStreams?,
|
|
||||||
) = if (chosenStreams?.subtitlesDisabled == true) {
|
|
||||||
stringResource(R.string.disabled)
|
|
||||||
} else if (chosenStreams?.subtitleStream != null) {
|
|
||||||
languageName(chosenStreams.subtitleStream.language)
|
|
||||||
} else {
|
|
||||||
chooseSource(item, chosenStreams?.itemPlayback)?.let {
|
|
||||||
formatSubtitleLang(it.mediaStreams)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val abbrevSuffixes = listOf("", "K", "M", "B")
|
private val abbrevSuffixes = listOf("", "K", "M", "B")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DetailsBackdropImage(
|
||||||
|
backdropImageUrl: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (backdropImageUrl.isNotNullOrBlank()) {
|
||||||
|
val gradientColor = MaterialTheme.colorScheme.background
|
||||||
|
AsyncImage(
|
||||||
|
model = backdropImageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
alignment = Alignment.TopEnd,
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.fillMaxHeight(.85f)
|
||||||
|
.alpha(.75f)
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawRect(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(Color.Transparent, gradientColor),
|
||||||
|
startY = size.height * .5f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
drawRect(
|
||||||
|
Brush.horizontalGradient(
|
||||||
|
colors = listOf(Color.Transparent, gradientColor),
|
||||||
|
endX = 0f,
|
||||||
|
startX = size.width * .75f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,7 @@ fun TitleValueText(
|
||||||
playSoundOnFocus: Boolean = false,
|
playSoundOnFocus: Boolean = false,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
valueTextOverflow: TextOverflow = TextOverflow.Ellipsis,
|
||||||
) {
|
) {
|
||||||
MaybeClickColumn(
|
MaybeClickColumn(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
@ -70,7 +71,7 @@ fun TitleValueText(
|
||||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Normal),
|
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Normal),
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = valueTextOverflow,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,321 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.ProvideTextStyle
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
|
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||||
|
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||||
|
import com.github.damontecres.wholphin.data.model.chooseStream
|
||||||
|
import com.github.damontecres.wholphin.preferences.AppThemeColors
|
||||||
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||||
|
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||||
|
import com.github.damontecres.wholphin.ui.playback.audioStreamCount
|
||||||
|
import com.github.damontecres.wholphin.ui.playback.embeddedSubtitleCount
|
||||||
|
import com.github.damontecres.wholphin.ui.playback.externalSubtitlesCount
|
||||||
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
|
import com.github.damontecres.wholphin.util.languageName
|
||||||
|
import com.github.damontecres.wholphin.util.profile.Codec
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
|
import org.jellyfin.sdk.model.api.VideoRange
|
||||||
|
import org.jellyfin.sdk.model.api.VideoRangeType
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoStreamDetails(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
dto: BaseItemDto,
|
||||||
|
itemPlayback: ItemPlayback?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
val source = remember(dto, itemPlayback) { chooseSource(dto, itemPlayback) }
|
||||||
|
|
||||||
|
val videoStream =
|
||||||
|
remember(dto, itemPlayback) {
|
||||||
|
chooseStream(
|
||||||
|
dto,
|
||||||
|
itemPlayback,
|
||||||
|
MediaStreamType.VIDEO,
|
||||||
|
preferences,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val video =
|
||||||
|
remember(videoStream) {
|
||||||
|
videoStream
|
||||||
|
?.let {
|
||||||
|
val width = it.width
|
||||||
|
val height = it.height
|
||||||
|
val resName =
|
||||||
|
if (width != null && height != null) {
|
||||||
|
resolutionString(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
videoStream.isInterlaced,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val range = formatVideoRange(context, it.videoRange, it.videoRangeType)
|
||||||
|
listOfNotNull(
|
||||||
|
resName.concatWithSpace(range),
|
||||||
|
it.codec?.uppercase(),
|
||||||
|
)
|
||||||
|
}.orEmpty()
|
||||||
|
}
|
||||||
|
video.forEach {
|
||||||
|
StreamLabel(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val audioStream =
|
||||||
|
remember(dto, itemPlayback) {
|
||||||
|
chooseStream(
|
||||||
|
dto,
|
||||||
|
itemPlayback,
|
||||||
|
MediaStreamType.AUDIO,
|
||||||
|
preferences,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val audioCount = remember(source) { source?.audioStreamCount ?: 0 }
|
||||||
|
val audio =
|
||||||
|
if (audioCount == 0 || audioStream == null) {
|
||||||
|
stringResource(R.string.none)
|
||||||
|
} else {
|
||||||
|
listOfNotNull(
|
||||||
|
languageName(audioStream.language),
|
||||||
|
formatAudioCodec(context, audioStream.codec, audioStream.profile),
|
||||||
|
audioStream.channelLayout,
|
||||||
|
).joinToString(" ")
|
||||||
|
}
|
||||||
|
StreamLabel(
|
||||||
|
text = audio,
|
||||||
|
count = audioCount,
|
||||||
|
icon = R.string.fa_volume_high,
|
||||||
|
modifier = Modifier.widthIn(max = 200.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
val subtitleStream =
|
||||||
|
remember(dto, itemPlayback) {
|
||||||
|
chooseStream(
|
||||||
|
dto,
|
||||||
|
itemPlayback,
|
||||||
|
MediaStreamType.SUBTITLE,
|
||||||
|
preferences,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val subtitleCount =
|
||||||
|
remember(source) {
|
||||||
|
(source?.embeddedSubtitleCount ?: 0) + (source?.externalSubtitlesCount ?: 0)
|
||||||
|
}
|
||||||
|
val subtitle =
|
||||||
|
if (itemPlayback?.subtitleIndex == TrackIndex.DISABLED) {
|
||||||
|
stringResource(R.string.disabled)
|
||||||
|
} else if (subtitleCount == 0 || subtitleStream == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
listOfNotNull(
|
||||||
|
languageName(subtitleStream.language),
|
||||||
|
"SDH".takeIf { subtitleStream.isHearingImpaired },
|
||||||
|
formatSubtitleCodec(subtitleStream.codec),
|
||||||
|
).joinToString(" ")
|
||||||
|
}
|
||||||
|
subtitle?.let {
|
||||||
|
StreamLabel(
|
||||||
|
text = it,
|
||||||
|
count = subtitleCount,
|
||||||
|
icon = R.string.fa_closed_captioning,
|
||||||
|
modifier = Modifier.widthIn(max = 160.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interlaced(interlaced: Boolean) = if (interlaced) "i" else "p"
|
||||||
|
|
||||||
|
// Adapted from https://github.com/jellyfin/jellyfin/blob/aa4ddd139a7c01889a99561fc314121ba198dd70/MediaBrowser.Model/Entities/MediaStream.cs#L714
|
||||||
|
fun resolutionString(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
interlaced: Boolean,
|
||||||
|
): String =
|
||||||
|
if (height > width) {
|
||||||
|
// Vertical video
|
||||||
|
resolutionString(height, width, interlaced)
|
||||||
|
} else {
|
||||||
|
when {
|
||||||
|
width <= 256 && height <= 144 -> "144" + interlaced(interlaced)
|
||||||
|
width <= 426 && height <= 240 -> "240" + interlaced(interlaced)
|
||||||
|
width <= 640 && height <= 360 -> "360" + interlaced(interlaced)
|
||||||
|
width <= 682 && height <= 384 -> "384" + interlaced(interlaced)
|
||||||
|
width <= 720 && height <= 404 -> "404" + interlaced(interlaced)
|
||||||
|
width <= 854 && height <= 480 -> "480" + interlaced(interlaced)
|
||||||
|
width <= 960 && height <= 544 -> "540" + interlaced(interlaced)
|
||||||
|
width <= 1024 && height <= 576 -> "576" + interlaced(interlaced)
|
||||||
|
width <= 1280 && height <= 962 -> "720" + interlaced(interlaced)
|
||||||
|
width <= 2560 && height <= 1440 -> "1080" + interlaced(interlaced)
|
||||||
|
width <= 4096 && height <= 3072 -> "4K"
|
||||||
|
width <= 8192 && height <= 6144 -> "8K"
|
||||||
|
else -> height.toString() + interlaced(interlaced)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StreamLabel(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
@StringRes icon: Int? = null,
|
||||||
|
count: Int = 0,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
// MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
).padding(vertical = 4.dp, horizontal = 6.dp),
|
||||||
|
) {
|
||||||
|
ProvideTextStyle(
|
||||||
|
TextStyle(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
if (icon != null) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(icon),
|
||||||
|
fontFamily = FontAwesome,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
if (count > 1) {
|
||||||
|
Text(
|
||||||
|
text = "(+${count - 1})",
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewTvSpec
|
||||||
|
@Composable
|
||||||
|
private fun StreamLabelPreview() {
|
||||||
|
WholphinTheme(appThemeColors = AppThemeColors.PURPLE) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.padding(8.dp),
|
||||||
|
) {
|
||||||
|
StreamLabel("1080p")
|
||||||
|
StreamLabel("H264")
|
||||||
|
StreamLabel("HDR")
|
||||||
|
StreamLabel("AC3 5.1", icon = R.string.fa_volume_high, count = 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatVideoRange(
|
||||||
|
context: Context,
|
||||||
|
videoRange: VideoRange?,
|
||||||
|
type: VideoRangeType?,
|
||||||
|
): String? =
|
||||||
|
when (videoRange) {
|
||||||
|
VideoRange.UNKNOWN,
|
||||||
|
VideoRange.SDR, null,
|
||||||
|
-> null
|
||||||
|
|
||||||
|
VideoRange.HDR ->
|
||||||
|
when (type) {
|
||||||
|
VideoRangeType.UNKNOWN,
|
||||||
|
VideoRangeType.SDR,
|
||||||
|
null,
|
||||||
|
-> null
|
||||||
|
|
||||||
|
VideoRangeType.HDR10 -> "HDR10"
|
||||||
|
VideoRangeType.HDR10_PLUS -> "HDR10+"
|
||||||
|
VideoRangeType.HLG -> "HLG"
|
||||||
|
|
||||||
|
VideoRangeType.DOVI,
|
||||||
|
VideoRangeType.DOVI_WITH_HDR10,
|
||||||
|
VideoRangeType.DOVI_WITH_HLG,
|
||||||
|
VideoRangeType.DOVI_WITH_SDR,
|
||||||
|
-> context.getString(R.string.dolby_vision)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatAudioCodec(
|
||||||
|
context: Context,
|
||||||
|
codec: String?,
|
||||||
|
profile: String?,
|
||||||
|
): String? =
|
||||||
|
when {
|
||||||
|
profile?.contains("Dolby Atmos", true) == true -> context.getString(R.string.dolby_atmos)
|
||||||
|
profile?.contains("DTS:X", true) == true -> "DTS:X"
|
||||||
|
profile?.contains("DTS:HD", true) == true -> "DTS:HD"
|
||||||
|
else ->
|
||||||
|
when (codec?.lowercase()) {
|
||||||
|
Codec.Audio.TRUEHD -> "TrueHD"
|
||||||
|
Codec.Audio.OGG,
|
||||||
|
Codec.Audio.OPUS,
|
||||||
|
Codec.Audio.VORBIS,
|
||||||
|
-> codec.replaceFirstChar { it.uppercase() }
|
||||||
|
|
||||||
|
null -> null
|
||||||
|
else -> codec.uppercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatSubtitleCodec(codec: String?): String? =
|
||||||
|
when (codec?.lowercase()) {
|
||||||
|
Codec.Subtitle.DVBSUB -> "DVB"
|
||||||
|
Codec.Subtitle.DVDSUB -> "DVD"
|
||||||
|
Codec.Subtitle.PGSSUB -> "PGS"
|
||||||
|
Codec.Subtitle.SUBRIP -> "SRT"
|
||||||
|
null -> null
|
||||||
|
else -> codec.uppercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String?.concatWithSpace(str: String?): String? =
|
||||||
|
when {
|
||||||
|
this != null && str != null -> "$this $str"
|
||||||
|
this == null -> str
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
|
@ -4,10 +4,8 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
|
@ -22,16 +20,10 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
@ -42,7 +34,6 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
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.ExtrasItem
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
|
|
@ -63,12 +54,14 @@ import com.github.damontecres.wholphin.ui.cards.ExtrasRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||||
|
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
||||||
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.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
|
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.Optional
|
import com.github.damontecres.wholphin.ui.components.Optional
|
||||||
|
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||||
import com.github.damontecres.wholphin.ui.components.chooseStream
|
import com.github.damontecres.wholphin.ui.components.chooseStream
|
||||||
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
|
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
|
||||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
|
|
@ -80,7 +73,6 @@ import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
||||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -386,35 +378,7 @@ fun MovieDetailsContent(
|
||||||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||||
}
|
}
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
if (backdropImageUrl.isNotNullOrBlank()) {
|
DetailsBackdropImage(backdropImageUrl)
|
||||||
val gradientColor = MaterialTheme.colorScheme.background
|
|
||||||
AsyncImage(
|
|
||||||
model = backdropImageUrl,
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
alignment = Alignment.TopEnd,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxHeight(.75f)
|
|
||||||
.alpha(.5f)
|
|
||||||
.drawWithContent {
|
|
||||||
drawContent()
|
|
||||||
drawRect(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(Color.Transparent, gradientColor),
|
|
||||||
startY = size.height * .5f,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
drawRect(
|
|
||||||
Brush.horizontalGradient(
|
|
||||||
colors = listOf(Color.Transparent, gradientColor),
|
|
||||||
endX = 0f,
|
|
||||||
startX = size.width * .75f,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
|
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
|
||||||
|
|
@ -422,11 +386,12 @@ fun MovieDetailsContent(
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.bringIntoViewRequester(bringIntoViewRequester)
|
.fillMaxWidth()
|
||||||
.padding(bottom = 56.dp),
|
.bringIntoViewRequester(bringIntoViewRequester),
|
||||||
|
// .padding(bottom = 16.dp),
|
||||||
) {
|
) {
|
||||||
MovieDetailsHeader(
|
MovieDetailsHeader(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
|
|
@ -434,9 +399,7 @@ fun MovieDetailsContent(
|
||||||
chosenStreams = chosenStreams,
|
chosenStreams = chosenStreams,
|
||||||
bringIntoViewRequester = bringIntoViewRequester,
|
bringIntoViewRequester = bringIntoViewRequester,
|
||||||
overviewOnClick = overviewOnClick,
|
overviewOnClick = overviewOnClick,
|
||||||
Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth(.75f)
|
|
||||||
.padding(bottom = 8.dp),
|
|
||||||
)
|
)
|
||||||
ExpandablePlayButtons(
|
ExpandablePlayButtons(
|
||||||
resumePosition = resumePosition,
|
resumePosition = resumePosition,
|
||||||
|
|
@ -461,6 +424,14 @@ fun MovieDetailsContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
VideoStreamDetails(
|
||||||
|
preferences = preferences,
|
||||||
|
dto = dto,
|
||||||
|
itemPlayback = chosenStreams?.itemPlayback,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (people.isNotEmpty()) {
|
if (people.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
PersonRow(
|
PersonRow(
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,13 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
@ -26,18 +24,13 @@ 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
|
||||||
import com.github.damontecres.wholphin.data.model.chooseStream
|
|
||||||
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.DotSeparatedRow
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
|
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
|
||||||
import com.github.damontecres.wholphin.ui.components.TitleValueText
|
|
||||||
import com.github.damontecres.wholphin.ui.getAudioDisplay
|
|
||||||
import com.github.damontecres.wholphin.ui.getSubtitleDisplay
|
|
||||||
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.roundMinutes
|
||||||
import com.github.damontecres.wholphin.ui.timeRemaining
|
import com.github.damontecres.wholphin.ui.timeRemaining
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
|
||||||
import org.jellyfin.sdk.model.api.PersonKind
|
import org.jellyfin.sdk.model.api.PersonKind
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
|
||||||
|
|
@ -64,11 +57,12 @@ fun MovieDetailsHeader(
|
||||||
style = MaterialTheme.typography.displayMedium,
|
style = MaterialTheme.typography.displayMedium,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.fillMaxWidth(.75f),
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier.alpha(0.75f),
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
|
@ -92,10 +86,15 @@ fun MovieDetailsHeader(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SimpleStarRating(
|
Row(
|
||||||
dto.communityRating,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
Modifier.height(20.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
)
|
) {
|
||||||
|
SimpleStarRating(
|
||||||
|
dto.communityRating,
|
||||||
|
Modifier.height(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
dto.taglines?.firstOrNull()?.let { tagline ->
|
dto.taglines?.firstOrNull()?.let { tagline ->
|
||||||
Text(
|
Text(
|
||||||
text = tagline,
|
text = tagline,
|
||||||
|
|
@ -128,59 +127,6 @@ fun MovieDetailsHeader(
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Key-Values
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(start = 16.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
) {
|
|
||||||
chooseStream(dto, chosenStreams?.itemPlayback, MediaStreamType.VIDEO, preferences)
|
|
||||||
?.displayTitle
|
|
||||||
?.let {
|
|
||||||
TitleValueText(
|
|
||||||
stringResource(R.string.video),
|
|
||||||
it,
|
|
||||||
modifier = Modifier.widthIn(max = 200.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val audioDisplay = getAudioDisplay(movie.data, chosenStreams, preferences)
|
|
||||||
audioDisplay
|
|
||||||
?.let {
|
|
||||||
TitleValueText(
|
|
||||||
stringResource(R.string.audio),
|
|
||||||
it,
|
|
||||||
modifier = Modifier.widthIn(max = 200.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubtitleDisplay(movie.data, chosenStreams)
|
|
||||||
?.let {
|
|
||||||
if (it.isNotNullOrBlank()) {
|
|
||||||
TitleValueText(
|
|
||||||
stringResource(R.string.subtitles),
|
|
||||||
it,
|
|
||||||
modifier = Modifier.widthIn(max = 200.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO add writers, studio, etc to overview dialog
|
|
||||||
// dto.studios?.letNotEmpty {
|
|
||||||
// TitleValueText(
|
|
||||||
// stringResource(R.string.studios),
|
|
||||||
// it.joinToString(", ") { s -> s.name ?: "" },
|
|
||||||
// modifier = Modifier.widthIn(max = 80.dp),
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// dto.genres?.letNotEmpty {
|
|
||||||
// TitleValueText(
|
|
||||||
// stringResource(R.string.genres),
|
|
||||||
// it.joinToString(", "),
|
|
||||||
// modifier = Modifier.widthIn(max = 80.dp),
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,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.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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.ChosenStreams
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.chooseStream
|
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
|
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
|
||||||
import com.github.damontecres.wholphin.ui.components.TitleValueText
|
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||||
import com.github.damontecres.wholphin.ui.getAudioDisplay
|
|
||||||
import com.github.damontecres.wholphin.ui.getSubtitleDisplay
|
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
|
@ -54,40 +45,11 @@ fun FocusedEpisodeFooter(
|
||||||
buttonOnFocusChanged = {},
|
buttonOnFocusChanged = {},
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
|
VideoStreamDetails(
|
||||||
Row(
|
preferences = preferences,
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
dto = dto,
|
||||||
) {
|
itemPlayback = chosenStreams?.itemPlayback,
|
||||||
chooseStream(dto, chosenStreams?.itemPlayback, MediaStreamType.VIDEO, preferences)
|
modifier = Modifier,
|
||||||
?.displayTitle
|
)
|
||||||
?.let {
|
|
||||||
TitleValueText(
|
|
||||||
stringResource(R.string.video),
|
|
||||||
it,
|
|
||||||
modifier = Modifier.widthIn(max = 160.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val audioDisplay = getAudioDisplay(ep.data, chosenStreams, preferences)
|
|
||||||
audioDisplay?.let {
|
|
||||||
TitleValueText(
|
|
||||||
stringResource(R.string.audio),
|
|
||||||
it,
|
|
||||||
modifier = Modifier.widthIn(max = 200.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val subtitleText = getSubtitleDisplay(ep.data, chosenStreams)
|
|
||||||
subtitleText
|
|
||||||
?.let {
|
|
||||||
if (it.isNotNullOrBlank()) {
|
|
||||||
TitleValueText(
|
|
||||||
stringResource(R.string.subtitles),
|
|
||||||
it,
|
|
||||||
modifier = Modifier.widthIn(max = 160.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ 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.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
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.ui.components.DotSeparatedRow
|
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
|
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
|
||||||
|
|
@ -24,14 +26,16 @@ import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FocusedEpisodeHeader(
|
fun FocusedEpisodeHeader(
|
||||||
|
preferences: UserPreferences,
|
||||||
ep: BaseItem?,
|
ep: BaseItem?,
|
||||||
|
chosenStreams: ChosenStreams?,
|
||||||
overviewOnClick: () -> Unit,
|
overviewOnClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val dto = ep?.data
|
val dto = ep?.data
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -24,16 +23,11 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
@ -42,7 +36,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ExtrasItem
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
|
@ -56,6 +49,7 @@ import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||||
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
|
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||||
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
|
||||||
|
|
@ -76,7 +70,6 @@ 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.detail.buildMoreDialogItemsForPerson
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
|
||||||
import com.github.damontecres.wholphin.ui.detail.movie.TrailerRow
|
import com.github.damontecres.wholphin.ui.detail.movie.TrailerRow
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
|
||||||
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
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
|
|
@ -319,35 +312,7 @@ fun SeriesDetailsContent(
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
if (series.backdropImageUrl.isNotNullOrBlank()) {
|
DetailsBackdropImage(series.backdropImageUrl)
|
||||||
val gradientColor = MaterialTheme.colorScheme.background
|
|
||||||
AsyncImage(
|
|
||||||
model = series.backdropImageUrl,
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
alignment = Alignment.TopEnd,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxHeight(.75f)
|
|
||||||
.alpha(.5f)
|
|
||||||
.drawWithContent {
|
|
||||||
drawContent()
|
|
||||||
drawRect(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(Color.Transparent, gradientColor),
|
|
||||||
startY = size.height * .5f,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
drawRect(
|
|
||||||
Brush.horizontalGradient(
|
|
||||||
colors = listOf(Color.Transparent, gradientColor),
|
|
||||||
endX = 0f,
|
|
||||||
startX = size.width * .75f,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -27,23 +26,18 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
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
|
||||||
|
|
@ -51,12 +45,12 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||||
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
||||||
|
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.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
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
|
||||||
import com.github.damontecres.wholphin.ui.logTab
|
import com.github.damontecres.wholphin.ui.logTab
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
@ -103,35 +97,7 @@ fun SeriesOverviewContent(
|
||||||
.height(460.dp)
|
.height(460.dp)
|
||||||
.bringIntoViewRequester(bringIntoViewRequester),
|
.bringIntoViewRequester(bringIntoViewRequester),
|
||||||
) {
|
) {
|
||||||
if (backdropImageUrl.isNotNullOrBlank()) {
|
DetailsBackdropImage(backdropImageUrl)
|
||||||
val gradientColor = MaterialTheme.colorScheme.background
|
|
||||||
AsyncImage(
|
|
||||||
model = backdropImageUrl,
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
alignment = Alignment.TopEnd,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxHeight(.6f)
|
|
||||||
.alpha(.4f)
|
|
||||||
.drawWithContent {
|
|
||||||
drawContent()
|
|
||||||
drawRect(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(Color.Transparent, gradientColor),
|
|
||||||
startY = 500f,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
drawRect(
|
|
||||||
Brush.horizontalGradient(
|
|
||||||
colors = listOf(gradientColor, Color.Transparent),
|
|
||||||
endX = 400f,
|
|
||||||
startX = 100f,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
|
@ -180,9 +146,11 @@ fun SeriesOverviewContent(
|
||||||
item {
|
item {
|
||||||
// Episode header
|
// Episode header
|
||||||
FocusedEpisodeHeader(
|
FocusedEpisodeHeader(
|
||||||
|
preferences = preferences,
|
||||||
ep = focusedEpisode,
|
ep = focusedEpisode,
|
||||||
|
chosenStreams = chosenStreams,
|
||||||
overviewOnClick = overviewOnClick,
|
overviewOnClick = overviewOnClick,
|
||||||
modifier = Modifier.fillMaxWidth(.66f),
|
modifier = Modifier.fillMaxWidth(.6f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ val PurpleThemeColors =
|
||||||
val onPrimaryContainerDark = Color(0xFFEADCFF)
|
val onPrimaryContainerDark = Color(0xFFEADCFF)
|
||||||
val secondaryDark = Color(0xFFD2BCFF)
|
val secondaryDark = Color(0xFFD2BCFF)
|
||||||
val onSecondaryDark = Color(0xFF3B167D)
|
val onSecondaryDark = Color(0xFF3B167D)
|
||||||
val secondaryContainerDark = Color(0xFF533295)
|
val secondaryContainerDark = Color(0xFF48415B)
|
||||||
val onSecondaryContainerDark = Color(0xFFC2A6FF)
|
val onSecondaryContainerDark = Color(0xFFC2A6FF)
|
||||||
val tertiaryDark = Color(0xFFA071F8)
|
val tertiaryDark = Color(0xFFA071F8)
|
||||||
val onTertiaryDark = Color(0xFF5E0052)
|
val onTertiaryDark = Color(0xFF5E0052)
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,9 @@ fun checkForSupport(tracks: Tracks): List<TrackSupport> =
|
||||||
}
|
}
|
||||||
|
|
||||||
fun languageName(code: String?): String =
|
fun languageName(code: String?): String =
|
||||||
if (code != null) {
|
if (code == "und") {
|
||||||
|
"Unknown"
|
||||||
|
} else if (code != null) {
|
||||||
try {
|
try {
|
||||||
Locale(code).displayLanguage
|
Locale(code).displayLanguage
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
<string name="fa_list_ul" translatable="false"></string>
|
<string name="fa_list_ul" translatable="false"></string>
|
||||||
<string name="fa_file_video" translatable="false"></string>
|
<string name="fa_file_video" translatable="false"></string>
|
||||||
<string name="fa_volume_low" translatable="false"></string>
|
<string name="fa_volume_low" translatable="false"></string>
|
||||||
|
<string name="fa_volume_high" translatable="false"></string>
|
||||||
<string name="fa_ellipsis" translatable="false"></string>
|
<string name="fa_ellipsis" translatable="false"></string>
|
||||||
<string name="fa_ellipsis_vertical" translatable="false"></string>
|
<string name="fa_ellipsis_vertical" translatable="false"></string>
|
||||||
<string name="fa_filter" translatable="false"></string>
|
<string name="fa_filter" translatable="false"></string>
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,8 @@
|
||||||
<string name="year">Year</string>
|
<string name="year">Year</string>
|
||||||
<string name="decade">Decade</string>
|
<string name="decade">Decade</string>
|
||||||
<string name="remove">Remove</string>
|
<string name="remove">Remove</string>
|
||||||
|
<string name="dolby_vision">Dolby Vision</string>
|
||||||
|
<string name="dolby_atmos">Dolby Atmos</string>
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue