Show button for trailers instead of card row (#618)

## Description
On both movie & series detail pages, show a button to play trailers
instead of the row of cards below.

The button works like this:
- If no trailers, the button shows "No trailers" and is
disabled/non-clickable
- If only a single trailer, the button shows "Play trailer" and clicking
it plays the trailer
- Otherwise, shows "Trailers" and clicking displays a dialog to choose
one

Local trailers are shown first in the list. Then there is an attempt to
sort the trailers for the dialog so that names with "Official Trailer"
or "Official Theatrical Trailer" are listed first and "Teaser" is listed
last. This isn't perfect and doesn't work for non-English, but there's
no other metadata to use.

### Related issues
Closes #408
This commit is contained in:
Ray 2026-01-02 13:50:45 -05:00 committed by GitHub
parent b8e8a1d913
commit 0fe8e5d988
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 218 additions and 170 deletions

View file

@ -14,4 +14,5 @@ data class LocalTrailer(
data class RemoteTrailer( data class RemoteTrailer(
override val name: String, override val name: String,
val url: String, val url: String,
val subtitle: String?,
) : Trailer ) : Trailer

View file

@ -22,31 +22,53 @@ class TrailerService
@param:ApplicationContext private val context: Context, @param:ApplicationContext private val context: Context,
private val api: ApiClient, private val api: ApiClient,
) { ) {
suspend fun getTrailers(item: BaseItem): List<Trailer> { fun getRemoteTrailers(item: BaseItem): List<Trailer> =
val remoteTrailers = item.data.remoteTrailers
item.data.remoteTrailers ?.mapNotNull { t ->
?.mapNotNull { t -> t.url?.let { url ->
t.url?.let { url -> val name =
val name = t.name
t.name // TODO would be nice to clean up the trailer name
// TODO would be nice to clean up the trailer name
// ?.replace(item.name ?: "", "") // ?.replace(item.name ?: "", "")
// ?.removePrefix(" - ") // ?.removePrefix(" - ")
?: context.getString(R.string.trailer) ?: context.getString(R.string.trailer)
RemoteTrailer(name, url) val subtitle =
} when (url.toUri().host) {
}.orEmpty() "youtube.com", "www.youtube.com" -> "YouTube"
.sortedBy { it.name } else -> null
val localTrailerCount = item.data.localTrailerCount ?: 0 }
RemoteTrailer(name, url, subtitle)
}
}.orEmpty()
.sortedWith(
compareBy(
{
// Try to show official trailers first & teasers last
when {
it.name.contains("Official Trailer", true) -> 0
it.name.contains("Official Theatrical Trailer", true) -> 0
it.name.contains("Teaser", true) -> 10
it.name.contains("Trailer", true) -> 1
else -> 5
}
},
{
it.name
},
),
)
suspend fun getLocalTrailers(item: BaseItem): List<Trailer> {
val localTrailerCount = item.data.localTrailerCount ?: return emptyList()
val localTrailers = val localTrailers =
if (localTrailerCount > 0) { if (localTrailerCount > 0) {
api.userLibraryApi.getLocalTrailers(item.id).content.map { api.userLibraryApi.getLocalTrailers(item.id).content.map {
LocalTrailer(BaseItem.Companion.from(it, api)) LocalTrailer(BaseItem.from(it, api))
} }
} else { } else {
listOf() listOf()
} }
return localTrailers + remoteTrailers return localTrailers
} }
companion object { companion object {

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.gestures.scrollBy
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.BoxScope import androidx.compose.foundation.layout.BoxScope
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.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -239,47 +240,48 @@ fun DialogPopupContent(
) { ) {
val elevatedContainerColor = val elevatedContainerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation) MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
LazyColumn( Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier =
modifier modifier
// .widthIn(min = 520.dp, max = 300.dp)
// .dialogFocusable()
.graphicsLayer { .graphicsLayer {
this.clip = true this.clip = true
this.shape = RoundedCornerShape(28.0.dp) this.shape = RoundedCornerShape(28.0.dp)
}.drawBehind { drawRect(color = elevatedContainerColor) } }.drawBehind { drawRect(color = elevatedContainerColor) }
.padding(PaddingValues(24.dp)), .padding(PaddingValues(24.dp)),
) { ) {
stickyHeader { Text(
Text( text = title,
text = title, style = MaterialTheme.typography.titleLarge,
style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurface,
color = MaterialTheme.colorScheme.onSurface, )
) LazyColumn(
} modifier = Modifier,
items(dialogItems) { ) {
when (it) { items(dialogItems) {
is DialogItemDivider -> { when (it) {
HorizontalDivider(Modifier.height(16.dp)) is DialogItemDivider -> {
} HorizontalDivider(Modifier.height(16.dp))
}
is DialogItem -> { is DialogItem -> {
ListItem( ListItem(
selected = false, selected = false,
enabled = !waiting && it.enabled, enabled = !waiting && it.enabled,
onClick = { onClick = {
if (dismissOnClick) { if (dismissOnClick) {
onDismissRequest.invoke() onDismissRequest.invoke()
} }
it.onClick.invoke() it.onClick.invoke()
}, },
headlineContent = it.headlineContent, headlineContent = it.headlineContent,
overlineContent = it.overlineContent, overlineContent = it.overlineContent,
supportingContent = it.supportingContent, supportingContent = it.supportingContent,
leadingContent = it.leadingContent, leadingContent = it.leadingContent,
trailingContent = it.trailingContent, trailingContent = it.trailingContent,
modifier = Modifier, modifier = Modifier,
) )
}
} }
} }
} }

View file

@ -21,7 +21,10 @@ import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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
@ -40,6 +43,7 @@ import androidx.tv.material3.Icon
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.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.data.SortAndDirection
@ -59,10 +63,12 @@ fun ExpandablePlayButtons(
resumePosition: Duration, resumePosition: Duration,
watched: Boolean, watched: Boolean,
favorite: Boolean, favorite: Boolean,
trailers: List<Trailer>?,
playOnClick: (position: Duration) -> Unit, playOnClick: (position: Duration) -> Unit,
watchOnClick: () -> Unit, watchOnClick: () -> Unit,
favoriteOnClick: () -> Unit, favoriteOnClick: () -> Unit,
moreOnClick: () -> Unit, moreOnClick: () -> Unit,
trailerOnClick: (Trailer) -> Unit,
buttonOnFocusChanged: (FocusState) -> Unit, buttonOnFocusChanged: (FocusState) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -134,6 +140,16 @@ fun ExpandablePlayButtons(
) )
} }
if (trailers != null) {
item("trailers") {
TrailerButton(
trailers = trailers,
trailerOnClick = trailerOnClick,
modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
)
}
}
// More button // More button
item("more") { item("more") {
ExpandablePlayButton( ExpandablePlayButton(
@ -213,6 +229,7 @@ fun ExpandableFaButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
iconColor: Color = Color.Unspecified, iconColor: Color = Color.Unspecified,
enabled: Boolean = true,
) { ) {
val isFocused = interactionSource.collectIsFocusedAsState().value val isFocused = interactionSource.collectIsFocusedAsState().value
Button( Button(
@ -225,6 +242,7 @@ fun ExpandableFaButton(
), ),
contentPadding = DefaultButtonPadding, contentPadding = DefaultButtonPadding,
interactionSource = interactionSource, interactionSource = interactionSource,
enabled = enabled,
) { ) {
Box( Box(
modifier = modifier =
@ -251,6 +269,42 @@ fun ExpandableFaButton(
} }
} }
@Composable
fun TrailerButton(
trailers: List<Trailer>,
trailerOnClick: (Trailer) -> Unit,
modifier: Modifier = Modifier,
) {
var showDialog by remember { mutableStateOf(false) }
ExpandableFaButton(
title =
if (trailers.isEmpty()) {
R.string.no_trailers
} else if (trailers.size == 1) {
R.string.play_trailer
} else {
R.string.trailers
},
iconStringRes = R.string.fa_film,
enabled = trailers.isNotEmpty(),
onClick = {
if (trailers.size == 1) {
trailerOnClick.invoke(trailers.first())
} else {
showDialog = true
}
},
modifier = modifier,
)
if (showDialog) {
TrailerDialog(
onDismissRequest = { showDialog = false },
trailers = trailers,
onClick = trailerOnClick,
)
}
}
@PreviewTvSpec @PreviewTvSpec
@Composable @Composable
private fun ExpandablePlayButtonsPreview() { private fun ExpandablePlayButtonsPreview() {
@ -264,6 +318,8 @@ private fun ExpandablePlayButtonsPreview() {
favoriteOnClick = {}, favoriteOnClick = {},
moreOnClick = {}, moreOnClick = {},
buttonOnFocusChanged = {}, buttonOnFocusChanged = {},
trailers = listOf(),
trailerOnClick = {},
modifier = Modifier, modifier = Modifier,
) )
} }

View file

@ -0,0 +1,51 @@
package com.github.damontecres.wholphin.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.LocalTrailer
import com.github.damontecres.wholphin.data.model.RemoteTrailer
import com.github.damontecres.wholphin.data.model.Trailer
@Composable
fun TrailerDialog(
onDismissRequest: () -> Unit,
trailers: List<Trailer>,
onClick: (Trailer) -> Unit,
) {
val trailersStr = stringResource(R.string.play_trailer)
val localStr = stringResource(R.string.local)
val externalStr = stringResource(R.string.external_track)
val params =
remember(trailers) {
DialogParams(
fromLongClick = false,
title = trailersStr,
items =
trailers.map { trailer ->
DialogItem(
headlineContent = {
Text(trailer.name)
},
supportingContent = {
val subtitle =
when (trailer) {
is LocalTrailer -> localStr
is RemoteTrailer -> trailer.subtitle ?: externalStr
}
Text(
text = subtitle,
)
},
onClick = { onClick.invoke(trailer) },
)
},
)
}
DialogPopup(
params = params,
onDismissRequest = onDismissRequest,
)
}

View file

@ -334,6 +334,8 @@ fun EpisodeDetailsContent(
} }
} }
}, },
trailers = null,
trailerOnClick = {},
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -8,9 +8,6 @@ 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.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.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -23,24 +20,18 @@ import androidx.compose.runtime.setValue
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.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
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
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.ExtrasItem import com.github.damontecres.wholphin.data.ExtrasItem
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.Chapter
import com.github.damontecres.wholphin.data.model.LocalTrailer
import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.RemoteTrailer
import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.data.model.aspectRatioFloat import com.github.damontecres.wholphin.data.model.aspectRatioFloat
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
@ -436,6 +427,11 @@ fun MovieDetailsContent(
} }
} }
}, },
trailers = trailers,
trailerOnClick = {
position = TRAILER_ROW
trailerOnClick.invoke(it)
},
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -463,21 +459,6 @@ fun MovieDetailsContent(
) )
} }
} }
if (trailers.isNotEmpty()) {
item {
TrailerRow(
trailers = trailers,
onClickTrailer = {
position = TRAILER_ROW
trailerOnClick.invoke(it)
},
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequesters[TRAILER_ROW]),
)
}
}
if (chapters.isNotEmpty()) { if (chapters.isNotEmpty()) {
item { item {
ChapterRow( ChapterRow(
@ -545,78 +526,3 @@ fun MovieDetailsContent(
} }
} }
} }
@Composable
fun TrailerRow(
trailers: List<Trailer>,
onClickTrailer: (Trailer) -> Unit,
modifier: Modifier = Modifier,
) {
val state = rememberLazyListState()
val firstFocus = remember { FocusRequester() }
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier,
) {
Text(
text = stringResource(R.string.trailers),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
)
LazyRow(
state = state,
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp),
modifier =
Modifier
.fillMaxWidth()
.focusRestorer(firstFocus),
) {
itemsIndexed(trailers) { index, item ->
val cardModifier =
if (index == 0) {
Modifier.focusRequester(firstFocus)
} else {
Modifier
}
when (item) {
is LocalTrailer -> {
SeasonCard(
item = item.baseItem,
onClick = { onClickTrailer.invoke(item) },
onLongClick = {},
imageHeight = Cards.height2x3,
imageWidth = Dp.Unspecified,
showImageOverlay = false,
modifier = cardModifier,
)
}
is RemoteTrailer -> {
val subtitle =
when (item.url.toUri().host) {
"youtube.com", "www.youtube.com" -> "YouTube"
else -> null
}
SeasonCard(
title = item.name,
subtitle = subtitle,
name = item.name,
imageUrl = null,
isFavorite = false,
isPlayed = false,
unplayedItemCount = 0,
playedPercentage = 0.0,
onClick = { onClickTrailer.invoke(item) },
onLongClick = {},
modifier = cardModifier,
showImageOverlay = false,
imageHeight = Cards.height2x3,
imageWidth = Dp.Unspecified,
)
}
}
}
}
}
}

View file

@ -25,6 +25,7 @@ import com.github.damontecres.wholphin.services.TrailerService
import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
@ -116,16 +117,19 @@ class MovieViewModel
item, item,
userPreferencesService.getCurrent(), userPreferencesService.getCurrent(),
) )
val remoteTrailers = trailerService.getRemoteTrailers(item)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@MovieViewModel.item.value = item this@MovieViewModel.item.value = item
chosenStreams.value = result chosenStreams.value = result
this@MovieViewModel.trailers.value = remoteTrailers
loading.value = LoadingState.Success loading.value = LoadingState.Success
backdropService.submit(item) backdropService.submit(item)
} }
viewModelScope.launchIO { viewModelScope.launchIO {
val trailers = trailerService.getTrailers(item) trailerService.getLocalTrailers(item).letNotEmpty { localTrailers ->
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@MovieViewModel.trailers.value = trailers this@MovieViewModel.trailers.value = localTrailers + remoteTrailers
}
} }
} }
viewModelScope.launchIO { viewModelScope.launchIO {

View file

@ -45,6 +45,8 @@ fun FocusedEpisodeFooter(
watchOnClick = watchOnClick, watchOnClick = watchOnClick,
favoriteOnClick = favoriteOnClick, favoriteOnClick = favoriteOnClick,
buttonOnFocusChanged = buttonOnFocusChanged, buttonOnFocusChanged = buttonOnFocusChanged,
trailers = null,
trailerOnClick = {},
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
} }

View file

@ -62,6 +62,7 @@ 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.OverviewText import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.SeriesQuickDetails import com.github.damontecres.wholphin.ui.components.SeriesQuickDetails
import com.github.damontecres.wholphin.ui.components.TrailerButton
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
@ -70,7 +71,6 @@ import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.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.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
@ -411,6 +411,18 @@ fun SeriesDetailsContent(
} }
}, },
) )
TrailerButton(
trailers = trailers,
trailerOnClick = trailerOnClick,
modifier =
Modifier.onFocusChanged {
if (it.isFocused) {
scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView()
}
}
},
)
} }
} }
item { item {
@ -472,21 +484,6 @@ fun SeriesDetailsContent(
) )
} }
} }
if (trailers.isNotEmpty()) {
item {
TrailerRow(
trailers = trailers,
onClickTrailer = {
position = TRAILER_ROW
trailerOnClick.invoke(it)
},
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequesters[TRAILER_ROW]),
)
}
}
if (extras.isNotEmpty()) { if (extras.isNotEmpty()) {
item { item {
ExtrasRow( ExtrasRow(

View file

@ -165,8 +165,9 @@ class SeriesViewModel
} }
} }
} }
val remoteTrailers = trailerService.getRemoteTrailers(item)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@SeriesViewModel.trailers.value = remoteTrailers
this@SeriesViewModel.position.update { this@SeriesViewModel.position.update {
it.copy( it.copy(
episodeRowIndex = episodeRowIndex =
@ -179,9 +180,10 @@ class SeriesViewModel
} }
if (seriesPageType == SeriesPageType.DETAILS) { if (seriesPageType == SeriesPageType.DETAILS) {
viewModelScope.launchIO { viewModelScope.launchIO {
val trailers = trailerService.getTrailers(item) trailerService.getLocalTrailers(item).letNotEmpty { localTrailers ->
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@SeriesViewModel.trailers.value = trailers this@SeriesViewModel.trailers.value = localTrailers + remoteTrailers
}
} }
} }
viewModelScope.launchIO { viewModelScope.launchIO {

View file

@ -399,6 +399,9 @@
<string name="subtitle_delay">Subtitle delay</string> <string name="subtitle_delay">Subtitle delay</string>
<string name="backdrop_display">Backdrop style</string> <string name="backdrop_display">Backdrop style</string>
<string name="resolution_switching">Resolution switching</string> <string name="resolution_switching">Resolution switching</string>
<string name="local">Local</string>
<string name="play_trailer">Play trailer</string>
<string name="no_trailers">No trailers</string>
<string-array name="theme_song_volume"> <string-array name="theme_song_volume">
<item>Disabled</item> <item>Disabled</item>