mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
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:
parent
b8e8a1d913
commit
0fe8e5d988
12 changed files with 218 additions and 170 deletions
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.gestures.scrollBy
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -239,47 +240,48 @@ fun DialogPopupContent(
|
|||
) {
|
||||
val elevatedContainerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
|
||||
LazyColumn(
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
modifier
|
||||
// .widthIn(min = 520.dp, max = 300.dp)
|
||||
// .dialogFocusable()
|
||||
.graphicsLayer {
|
||||
this.clip = true
|
||||
this.shape = RoundedCornerShape(28.0.dp)
|
||||
}.drawBehind { drawRect(color = elevatedContainerColor) }
|
||||
.padding(PaddingValues(24.dp)),
|
||||
) {
|
||||
stickyHeader {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
items(dialogItems) {
|
||||
when (it) {
|
||||
is DialogItemDivider -> {
|
||||
HorizontalDivider(Modifier.height(16.dp))
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier,
|
||||
) {
|
||||
items(dialogItems) {
|
||||
when (it) {
|
||||
is DialogItemDivider -> {
|
||||
HorizontalDivider(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
is DialogItem -> {
|
||||
ListItem(
|
||||
selected = false,
|
||||
enabled = !waiting && it.enabled,
|
||||
onClick = {
|
||||
if (dismissOnClick) {
|
||||
onDismissRequest.invoke()
|
||||
}
|
||||
it.onClick.invoke()
|
||||
},
|
||||
headlineContent = it.headlineContent,
|
||||
overlineContent = it.overlineContent,
|
||||
supportingContent = it.supportingContent,
|
||||
leadingContent = it.leadingContent,
|
||||
trailingContent = it.trailingContent,
|
||||
modifier = Modifier,
|
||||
)
|
||||
is DialogItem -> {
|
||||
ListItem(
|
||||
selected = false,
|
||||
enabled = !waiting && it.enabled,
|
||||
onClick = {
|
||||
if (dismissOnClick) {
|
||||
onDismissRequest.invoke()
|
||||
}
|
||||
it.onClick.invoke()
|
||||
},
|
||||
headlineContent = it.headlineContent,
|
||||
overlineContent = it.overlineContent,
|
||||
supportingContent = it.supportingContent,
|
||||
leadingContent = it.leadingContent,
|
||||
trailingContent = it.trailingContent,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ import androidx.compose.material.icons.filled.MoreVert
|
|||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -40,6 +43,7 @@ import androidx.tv.material3.Icon
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
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.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||
|
|
@ -59,10 +63,12 @@ fun ExpandablePlayButtons(
|
|||
resumePosition: Duration,
|
||||
watched: Boolean,
|
||||
favorite: Boolean,
|
||||
trailers: List<Trailer>?,
|
||||
playOnClick: (position: Duration) -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
favoriteOnClick: () -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
trailerOnClick: (Trailer) -> Unit,
|
||||
buttonOnFocusChanged: (FocusState) -> Unit,
|
||||
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
|
||||
item("more") {
|
||||
ExpandablePlayButton(
|
||||
|
|
@ -213,6 +229,7 @@ fun ExpandableFaButton(
|
|||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
iconColor: Color = Color.Unspecified,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
Button(
|
||||
|
|
@ -225,6 +242,7 @@ fun ExpandableFaButton(
|
|||
),
|
||||
contentPadding = DefaultButtonPadding,
|
||||
interactionSource = interactionSource,
|
||||
enabled = enabled,
|
||||
) {
|
||||
Box(
|
||||
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
|
||||
@Composable
|
||||
private fun ExpandablePlayButtonsPreview() {
|
||||
|
|
@ -264,6 +318,8 @@ private fun ExpandablePlayButtonsPreview() {
|
|||
favoriteOnClick = {},
|
||||
moreOnClick = {},
|
||||
buttonOnFocusChanged = {},
|
||||
trailers = listOf(),
|
||||
trailerOnClick = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -334,6 +334,8 @@ fun EpisodeDetailsContent(
|
|||
}
|
||||
}
|
||||
},
|
||||
trailers = null,
|
||||
trailerOnClick = {},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@ 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
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -23,24 +20,18 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
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.RemoteTrailer
|
||||
import com.github.damontecres.wholphin.data.model.Trailer
|
||||
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
|
|
@ -436,6 +427,11 @@ fun MovieDetailsContent(
|
|||
}
|
||||
}
|
||||
},
|
||||
trailers = trailers,
|
||||
trailerOnClick = {
|
||||
position = TRAILER_ROW
|
||||
trailerOnClick.invoke(it)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.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()) {
|
||||
item {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import com.github.damontecres.wholphin.services.TrailerService
|
|||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
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.setValueOnMain
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
|
|
@ -116,16 +117,19 @@ class MovieViewModel
|
|||
item,
|
||||
userPreferencesService.getCurrent(),
|
||||
)
|
||||
val remoteTrailers = trailerService.getRemoteTrailers(item)
|
||||
withContext(Dispatchers.Main) {
|
||||
this@MovieViewModel.item.value = item
|
||||
chosenStreams.value = result
|
||||
this@MovieViewModel.trailers.value = remoteTrailers
|
||||
loading.value = LoadingState.Success
|
||||
backdropService.submit(item)
|
||||
}
|
||||
viewModelScope.launchIO {
|
||||
val trailers = trailerService.getTrailers(item)
|
||||
withContext(Dispatchers.Main) {
|
||||
this@MovieViewModel.trailers.value = trailers
|
||||
trailerService.getLocalTrailers(item).letNotEmpty { localTrailers ->
|
||||
withContext(Dispatchers.Main) {
|
||||
this@MovieViewModel.trailers.value = localTrailers + remoteTrailers
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launchIO {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ fun FocusedEpisodeFooter(
|
|||
watchOnClick = watchOnClick,
|
||||
favoriteOnClick = favoriteOnClick,
|
||||
buttonOnFocusChanged = buttonOnFocusChanged,
|
||||
trailers = null,
|
||||
trailerOnClick = {},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.OverviewText
|
||||
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.ItemDetailsDialog
|
||||
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.buildMoreDialogItemsForHome
|
||||
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.nav.Destination
|
||||
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 {
|
||||
|
|
@ -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()) {
|
||||
item {
|
||||
ExtrasRow(
|
||||
|
|
|
|||
|
|
@ -165,8 +165,9 @@ class SeriesViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
val remoteTrailers = trailerService.getRemoteTrailers(item)
|
||||
withContext(Dispatchers.Main) {
|
||||
this@SeriesViewModel.trailers.value = remoteTrailers
|
||||
this@SeriesViewModel.position.update {
|
||||
it.copy(
|
||||
episodeRowIndex =
|
||||
|
|
@ -179,9 +180,10 @@ class SeriesViewModel
|
|||
}
|
||||
if (seriesPageType == SeriesPageType.DETAILS) {
|
||||
viewModelScope.launchIO {
|
||||
val trailers = trailerService.getTrailers(item)
|
||||
withContext(Dispatchers.Main) {
|
||||
this@SeriesViewModel.trailers.value = trailers
|
||||
trailerService.getLocalTrailers(item).letNotEmpty { localTrailers ->
|
||||
withContext(Dispatchers.Main) {
|
||||
this@SeriesViewModel.trailers.value = localTrailers + remoteTrailers
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launchIO {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue