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:
damontecres 2025-11-24 15:35:55 -05:00 committed by GitHub
parent 7be4fb5fdf
commit 5b96be6662
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 427 additions and 291 deletions

View file

@ -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")
/** /**

View file

@ -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,
),
)
},
)
}
}

View file

@ -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,
) )
} }
} }

View file

@ -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
}

View file

@ -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(

View file

@ -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),
// )
// }
}
} }
} }
} }

View file

@ -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),
)
}
}
}
} }
} }

View file

@ -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(

View file

@ -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 =

View file

@ -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 {

View file

@ -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)

View file

@ -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) {

View file

@ -36,6 +36,7 @@
<string name="fa_list_ul" translatable="false">&#xf0ca;</string> <string name="fa_list_ul" translatable="false">&#xf0ca;</string>
<string name="fa_file_video" translatable="false">&#xf1c8;</string> <string name="fa_file_video" translatable="false">&#xf1c8;</string>
<string name="fa_volume_low" translatable="false">&#xf027;</string> <string name="fa_volume_low" translatable="false">&#xf027;</string>
<string name="fa_volume_high" translatable="false">&#xf028;</string>
<string name="fa_ellipsis" translatable="false">&#xf141;</string> <string name="fa_ellipsis" translatable="false">&#xf141;</string>
<string name="fa_ellipsis_vertical" translatable="false">&#xf142;</string> <string name="fa_ellipsis_vertical" translatable="false">&#xf142;</string>
<string name="fa_filter" translatable="false">&#xf0b0;</string> <string name="fa_filter" translatable="false">&#xf0b0;</string>

View file

@ -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>