diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt index bf82bbf3..943789db 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt @@ -14,4 +14,5 @@ data class LocalTrailer( data class RemoteTrailer( override val name: String, val url: String, + val subtitle: String?, ) : Trailer diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt index 1af38ddd..856493f6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt @@ -22,31 +22,53 @@ class TrailerService @param:ApplicationContext private val context: Context, private val api: ApiClient, ) { - suspend fun getTrailers(item: BaseItem): List { - val remoteTrailers = - item.data.remoteTrailers - ?.mapNotNull { t -> - t.url?.let { url -> - val name = - t.name - // TODO would be nice to clean up the trailer name + fun getRemoteTrailers(item: BaseItem): List = + item.data.remoteTrailers + ?.mapNotNull { t -> + t.url?.let { url -> + val name = + t.name + // TODO would be nice to clean up the trailer name // ?.replace(item.name ?: "", "") // ?.removePrefix(" - ") - ?: context.getString(R.string.trailer) - RemoteTrailer(name, url) - } - }.orEmpty() - .sortedBy { it.name } - val localTrailerCount = item.data.localTrailerCount ?: 0 + ?: context.getString(R.string.trailer) + val subtitle = + when (url.toUri().host) { + "youtube.com", "www.youtube.com" -> "YouTube" + else -> null + } + 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 { + val localTrailerCount = item.data.localTrailerCount ?: return emptyList() val localTrailers = if (localTrailerCount > 0) { api.userLibraryApi.getLocalTrailers(item.id).content.map { - LocalTrailer(BaseItem.Companion.from(it, api)) + LocalTrailer(BaseItem.from(it, api)) } } else { listOf() } - return localTrailers + remoteTrailers + return localTrailers } companion object { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index bfb098d5..4b985056 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -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, + ) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt index 0f99f8bc..747e726a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt @@ -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?, 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, + 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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TrailerDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TrailerDialog.kt new file mode 100644 index 00000000..0b17491b --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TrailerDialog.kt @@ -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, + 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, + ) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt index 06216eb4..c47570f6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt @@ -334,6 +334,8 @@ fun EpisodeDetailsContent( } } }, + trailers = null, + trailerOnClick = {}, modifier = Modifier .fillMaxWidth() 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 6c820128..6fc1cc19 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 @@ -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, - 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, - ) - } - } - } - } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt index be62b4aa..793ef8bb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt @@ -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 { 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 ddaedd88..9b6d81df 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 @@ -45,6 +45,8 @@ fun FocusedEpisodeFooter( watchOnClick = watchOnClick, favoriteOnClick = favoriteOnClick, buttonOnFocusChanged = buttonOnFocusChanged, + trailers = null, + trailerOnClick = {}, modifier = Modifier.fillMaxWidth(), ) } 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 90c9157b..7546ca56 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 @@ -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( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index 4f85cda3..181e5b98 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -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 { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4e4a859..db4e720d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -399,6 +399,9 @@ Subtitle delay Backdrop style Resolution switching + Local + Play trailer + No trailers Disabled