From 5b96be6662aeac1d4d7686e5ba2fed0a3da79ef0 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:35:55 -0500 Subject: [PATCH] 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 --- .../damontecres/wholphin/ui/Formatting.kt | 57 ---- .../ui/components/DetailsBackdropImage.kt | 50 +++ .../wholphin/ui/components/TitleValueText.kt | 3 +- .../ui/components/VideoStreamDetails.kt | 321 ++++++++++++++++++ .../wholphin/ui/detail/movie/MovieDetails.kt | 61 +--- .../ui/detail/movie/MovieDetailsHeader.kt | 78 +---- .../ui/detail/series/FocusedEpisodeFooter.kt | 52 +-- .../ui/detail/series/FocusedEpisodeHeader.kt | 6 +- .../ui/detail/series/SeriesDetails.kt | 39 +-- .../ui/detail/series/SeriesOverviewContent.kt | 42 +-- .../ui/theme/colors/PurpleThemeColors.kt | 2 +- .../damontecres/wholphin/util/TrackSupport.kt | 4 +- app/src/main/res/values/fa_strings.xml | 1 + app/src/main/res/values/strings.xml | 2 + 14 files changed, 427 insertions(+), 291 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/components/DetailsBackdropImage.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt index 621133b0..34f7f134 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt @@ -2,19 +2,9 @@ package com.github.damontecres.wholphin.ui import android.os.Build 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.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.MediaSegmentType -import org.jellyfin.sdk.model.api.MediaStream -import org.jellyfin.sdk.model.api.MediaStreamType import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -81,53 +71,6 @@ val BaseItemDto.seasonEpisodePadded: String? null } -fun formatSubtitleLang(mediaStreams: List?): 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") /** diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/DetailsBackdropImage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/DetailsBackdropImage.kt new file mode 100644 index 00000000..bb7ef78f --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/DetailsBackdropImage.kt @@ -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, + ), + ) + }, + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleValueText.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleValueText.kt index 998aa340..341978c8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleValueText.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleValueText.kt @@ -52,6 +52,7 @@ fun TitleValueText( playSoundOnFocus: Boolean = false, onClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, + valueTextOverflow: TextOverflow = TextOverflow.Ellipsis, ) { MaybeClickColumn( onClick = onClick, @@ -70,7 +71,7 @@ fun TitleValueText( style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Normal), color = MaterialTheme.colorScheme.onSurface, maxLines = 1, - overflow = TextOverflow.Ellipsis, + overflow = valueTextOverflow, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt new file mode 100644 index 00000000..7b05366e --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt @@ -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 + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 28d84003..fcec24a5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -4,10 +4,8 @@ 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.fillMaxHeight 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.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed @@ -22,16 +20,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -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.focus.FocusRequester import androidx.compose.ui.focus.focusRequester 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.res.stringResource import androidx.compose.ui.unit.Dp @@ -42,7 +34,6 @@ import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleStartEffect import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ChosenStreams 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.PersonRow 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.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.VideoStreamDetails import com.github.damontecres.wholphin.ui.components.chooseStream import com.github.damontecres.wholphin.ui.components.chooseVersionParams 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.buildMoreDialogItemsForHome 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.rememberInt import com.github.damontecres.wholphin.ui.tryRequestFocus @@ -386,35 +378,7 @@ fun MovieDetailsContent( focusRequesters.getOrNull(position)?.tryRequestFocus() } Box(modifier = modifier) { - if (backdropImageUrl.isNotNullOrBlank()) { - 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, - ), - ) - }, - ) - } + DetailsBackdropImage(backdropImageUrl) LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp), @@ -422,11 +386,12 @@ fun MovieDetailsContent( ) { item { Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(0.dp), modifier = Modifier - .bringIntoViewRequester(bringIntoViewRequester) - .padding(bottom = 56.dp), + .fillMaxWidth() + .bringIntoViewRequester(bringIntoViewRequester), +// .padding(bottom = 16.dp), ) { MovieDetailsHeader( preferences = preferences, @@ -434,9 +399,7 @@ fun MovieDetailsContent( chosenStreams = chosenStreams, bringIntoViewRequester = bringIntoViewRequester, overviewOnClick = overviewOnClick, - Modifier - .fillMaxWidth(.75f) - .padding(bottom = 8.dp), + modifier = Modifier.fillMaxWidth(), ) ExpandablePlayButtons( resumePosition = resumePosition, @@ -461,6 +424,14 @@ fun MovieDetailsContent( ) } } + item { + VideoStreamDetails( + preferences = preferences, + dto = dto, + itemPlayback = chosenStreams?.itemPlayback, + modifier = Modifier, + ) + } if (people.isNotEmpty()) { item { PersonRow( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt index c4d12ff7..f326a133 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt @@ -7,15 +7,13 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth 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.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource 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.data.ChosenStreams 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.ui.components.DotSeparatedRow import com.github.damontecres.wholphin.ui.components.OverviewText 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.roundMinutes 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.extensions.ticks @@ -64,11 +57,12 @@ fun MovieDetailsHeader( style = MaterialTheme.typography.displayMedium, maxLines = 2, overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(.75f), ) Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.alpha(0.75f), + modifier = Modifier.fillMaxWidth(.60f), ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -92,10 +86,15 @@ fun MovieDetailsHeader( modifier = Modifier, ) } - SimpleStarRating( - dto.communityRating, - Modifier.height(20.dp), - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + SimpleStarRating( + dto.communityRating, + Modifier.height(20.dp), + ) + } dto.taglines?.firstOrNull()?.let { tagline -> Text( text = tagline, @@ -128,59 +127,6 @@ fun MovieDetailsHeader( 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), -// ) -// } - } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt index 42d8d991..cb312824 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt @@ -2,25 +2,16 @@ package com.github.damontecres.wholphin.ui.detail.series import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.data.model.chooseStream import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons -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 org.jellyfin.sdk.model.api.MediaStreamType +import com.github.damontecres.wholphin.ui.components.VideoStreamDetails import org.jellyfin.sdk.model.extensions.ticks import kotlin.time.Duration @@ -54,40 +45,11 @@ fun FocusedEpisodeFooter( buttonOnFocusChanged = {}, modifier = Modifier, ) - - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - chooseStream(dto, chosenStreams?.itemPlayback, MediaStreamType.VIDEO, preferences) - ?.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), - ) - } - } - } + VideoStreamDetails( + preferences = preferences, + dto = dto, + itemPlayback = chosenStreams?.itemPlayback, + modifier = Modifier, + ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt index 664aab2f..fc56c21e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt @@ -12,7 +12,9 @@ import androidx.compose.ui.text.style.TextOverflow 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.model.BaseItem +import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.DotSeparatedRow import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.SimpleStarRating @@ -24,14 +26,16 @@ import org.jellyfin.sdk.model.extensions.ticks @Composable fun FocusedEpisodeHeader( + preferences: UserPreferences, ep: BaseItem?, + chosenStreams: ChosenStreams?, overviewOnClick: () -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current val dto = ep?.data Column( - verticalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier, ) { Text( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 844d593a..ef7e11e8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -24,16 +23,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -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.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged -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.res.stringResource import androidx.compose.ui.unit.Dp @@ -42,7 +36,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.LifecycleStartEffect import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ExtrasItem 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.SeasonCard 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.DialogParams 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.buildMoreDialogItemsForPerson 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.nav.Destination import com.github.damontecres.wholphin.ui.rememberInt @@ -319,35 +312,7 @@ fun SeriesDetailsContent( Box( modifier = modifier, ) { - if (series.backdropImageUrl.isNotNullOrBlank()) { - 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, - ), - ) - }, - ) - } + DetailsBackdropImage(series.backdropImageUrl) Column( modifier = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index 979cca0b..5d75a081 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -27,23 +26,18 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -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.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ChosenStreams 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.OneTimeLaunchedEffect 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.LoadingPage import com.github.damontecres.wholphin.ui.components.TabRow import com.github.damontecres.wholphin.ui.formatDateTime 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.tryRequestFocus import kotlin.time.Duration @@ -103,35 +97,7 @@ fun SeriesOverviewContent( .height(460.dp) .bringIntoViewRequester(bringIntoViewRequester), ) { - if (backdropImageUrl.isNotNullOrBlank()) { - 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, - ), - ) - }, - ) - } + DetailsBackdropImage(backdropImageUrl) LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(16.dp), @@ -180,9 +146,11 @@ fun SeriesOverviewContent( item { // Episode header FocusedEpisodeHeader( + preferences = preferences, ep = focusedEpisode, + chosenStreams = chosenStreams, overviewOnClick = overviewOnClick, - modifier = Modifier.fillMaxWidth(.66f), + modifier = Modifier.fillMaxWidth(.6f), ) } item { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/theme/colors/PurpleThemeColors.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/theme/colors/PurpleThemeColors.kt index d0850fa3..813aaf79 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/theme/colors/PurpleThemeColors.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/theme/colors/PurpleThemeColors.kt @@ -40,7 +40,7 @@ val PurpleThemeColors = val onPrimaryContainerDark = Color(0xFFEADCFF) val secondaryDark = Color(0xFFD2BCFF) val onSecondaryDark = Color(0xFF3B167D) - val secondaryContainerDark = Color(0xFF533295) + val secondaryContainerDark = Color(0xFF48415B) val onSecondaryContainerDark = Color(0xFFC2A6FF) val tertiaryDark = Color(0xFFA071F8) val onTertiaryDark = Color(0xFF5E0052) diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/TrackSupport.kt b/app/src/main/java/com/github/damontecres/wholphin/util/TrackSupport.kt index 3b42b750..ed7abe57 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/TrackSupport.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/TrackSupport.kt @@ -146,7 +146,9 @@ fun checkForSupport(tracks: Tracks): List = } fun languageName(code: String?): String = - if (code != null) { + if (code == "und") { + "Unknown" + } else if (code != null) { try { Locale(code).displayLanguage } catch (ex: Exception) { diff --git a/app/src/main/res/values/fa_strings.xml b/app/src/main/res/values/fa_strings.xml index c91db2c9..ad36a929 100644 --- a/app/src/main/res/values/fa_strings.xml +++ b/app/src/main/res/values/fa_strings.xml @@ -36,6 +36,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c9f6f65..5c92e74e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -324,6 +324,8 @@ Year Decade Remove + Dolby Vision + Dolby Atmos Disabled