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 5dc0993c..9f1d5f51 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 @@ -1,6 +1,5 @@ package com.github.damontecres.wholphin.ui.detail.movie -import android.content.Intent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -82,6 +81,7 @@ import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState +import com.github.damontecres.wholphin.util.TrailerService import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.MediaType @@ -277,22 +277,8 @@ fun MovieDetails( items = items, ) }, - trailerOnClick = { trailer -> - when (trailer) { - is LocalTrailer -> - viewModel.navigateTo( - Destination.Playback( - itemId = trailer.baseItem.id, - item = trailer.baseItem, - positionMs = 0L, - ), - ) - - is RemoteTrailer -> { - val intent = Intent(Intent.ACTION_VIEW, trailer.url.toUri()) - context.startActivity(intent) - } - } + trailerOnClick = { + TrailerService.onClick(context, it, viewModel::navigateTo) }, modifier = modifier, ) @@ -567,7 +553,7 @@ fun TrailerRow( LazyRow( state = state, horizontalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp), modifier = Modifier .fillMaxWidth() 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 0c29ae3c..30a75ef8 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 @@ -4,16 +4,13 @@ import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.ItemPlaybackRepository import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.ItemPlayback -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.preferences.ThemeSongVolume import com.github.damontecres.wholphin.ui.SlimItemFields @@ -27,6 +24,7 @@ import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.PeopleFavorites import com.github.damontecres.wholphin.util.ThemeSongPlayer +import com.github.damontecres.wholphin.util.TrailerService import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -57,6 +55,7 @@ class MovieViewModel private val themeSongPlayer: ThemeSongPlayer, private val favoriteWatchManager: FavoriteWatchManager, private val peopleFavorites: PeopleFavorites, + private val trailerService: TrailerService, @Assisted val itemId: UUID, ) : ViewModel() { @AssistedFactory @@ -108,31 +107,9 @@ class MovieViewModel loading.value = LoadingState.Success } viewModelScope.launchIO { - 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 -// ?.replace(item.name ?: "", "") -// ?.removePrefix(" - ") - ?: context.getString(R.string.trailer) - RemoteTrailer(name, url) - } - }.orEmpty() - .sortedBy { it.name } - val localTrailerCount = item.data.localTrailerCount ?: 0 - val localTrailers = - if (localTrailerCount > 0) { - api.userLibraryApi.getLocalTrailers(itemId).content.map { - LocalTrailer(BaseItem.Companion.from(it, api)) - } - } else { - listOf() - } + val trailers = trailerService.getTrailers(item) withContext(Dispatchers.Main) { - this@MovieViewModel.trailers.value = localTrailers + remoteTrailers + this@MovieViewModel.trailers.value = trailers } } viewModelScope.launchIO { 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 983e5798..91377f91 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 @@ -46,6 +46,7 @@ import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Person +import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.ItemRow @@ -71,6 +72,7 @@ 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.isNotNullOrBlank import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination @@ -79,6 +81,7 @@ import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState +import com.github.damontecres.wholphin.util.TrailerService import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.MediaType @@ -102,6 +105,7 @@ fun SeriesDetails( val item by viewModel.item.observeAsState() val seasons by viewModel.seasons.observeAsState(listOf()) + val trailers by viewModel.trailers.observeAsState(listOf()) val people by viewModel.people.observeAsState(listOf()) val similar by viewModel.similar.observeAsState(listOf()) @@ -133,6 +137,7 @@ fun SeriesDetails( preferences = preferences, series = item, seasons = seasons, + trailers = trailers, people = people, similar = similar, played = played, @@ -174,6 +179,9 @@ fun SeriesDetails( val favorite = item.data.userData?.isFavorite ?: false viewModel.setFavorite(item.id, !favorite, null) }, + trailerOnClick = { + TrailerService.onClick(context, it, viewModel::navigateTo) + }, moreActions = MoreDialogActions( navigateTo = { viewModel.navigateTo(it) }, @@ -244,7 +252,8 @@ fun SeriesDetails( private const val HEADER_ROW = 0 private const val SEASONS_ROW = HEADER_ROW + 1 private const val PEOPLE_ROW = SEASONS_ROW + 1 -private const val SIMILAR_ROW = PEOPLE_ROW + 1 +private const val TRAILER_ROW = PEOPLE_ROW + 1 +private const val SIMILAR_ROW = TRAILER_ROW + 1 @Composable fun SeriesDetailsContent( @@ -252,6 +261,7 @@ fun SeriesDetailsContent( series: BaseItem, seasons: List, similar: List, + trailers: List, people: List, played: Boolean, favorite: Boolean, @@ -262,6 +272,7 @@ fun SeriesDetailsContent( playOnClick: () -> Unit, watchOnClick: () -> Unit, favoriteOnClick: () -> Unit, + trailerOnClick: (Trailer) -> Unit, moreActions: MoreDialogActions, modifier: Modifier = Modifier, ) { @@ -452,6 +463,21 @@ fun SeriesDetailsContent( ) } } + if (trailers.isNotEmpty()) { + item { + TrailerRow( + trailers = trailers, + onClickTrailer = { + position = TRAILER_ROW + trailerOnClick.invoke(it) + }, + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequesters[TRAILER_ROW]), + ) + } + } if (similar.isNotEmpty()) { item { ItemRow( 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 43ca6f55..8a1a96bf 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 @@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Person +import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.preferences.ThemeSongVolume import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.SlimItemFields @@ -29,6 +30,7 @@ import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.PeopleFavorites import com.github.damontecres.wholphin.util.ThemeSongPlayer +import com.github.damontecres.wholphin.util.TrailerService import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -62,12 +64,14 @@ class SeriesViewModel private val themeSongPlayer: ThemeSongPlayer, private val favoriteWatchManager: FavoriteWatchManager, private val peopleFavorites: PeopleFavorites, + private val trailerService: TrailerService, ) : ItemViewModel(api) { private lateinit var seriesId: UUID private lateinit var prefs: UserPreferences val loading = MutableLiveData(LoadingState.Loading) val seasons = MutableLiveData>(listOf()) val episodes = MutableLiveData(EpisodeList.Loading) + val trailers = MutableLiveData>(listOf()) val people = MutableLiveData>(listOf()) val similar = MutableLiveData>() @@ -111,6 +115,12 @@ class SeriesViewModel episodes.value = episodeInfo loading.value = LoadingState.Success } + viewModelScope.launchIO { + val trailers = trailerService.getTrailers(item) + withContext(Dispatchers.Main) { + this@SeriesViewModel.trailers.value = trailers + } + } viewModelScope.launchIO { val people = peopleFavorites.getPeopleFor(item) this@SeriesViewModel.people.setValueOnMain(people) diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/TrailerService.kt b/app/src/main/java/com/github/damontecres/wholphin/util/TrailerService.kt new file mode 100644 index 00000000..4a60dea5 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/TrailerService.kt @@ -0,0 +1,81 @@ +package com.github.damontecres.wholphin.util + +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.LocalTrailer +import com.github.damontecres.wholphin.data.model.RemoteTrailer +import com.github.damontecres.wholphin.data.model.Trailer +import com.github.damontecres.wholphin.ui.nav.Destination +import dagger.hilt.android.qualifiers.ApplicationContext +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.userLibraryApi +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TrailerService + @Inject + constructor( + @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 +// ?.replace(item.name ?: "", "") +// ?.removePrefix(" - ") + ?: context.getString(R.string.trailer) + RemoteTrailer(name, url) + } + }.orEmpty() + .sortedBy { it.name } + val localTrailerCount = item.data.localTrailerCount ?: 0 + val localTrailers = + if (localTrailerCount > 0) { + api.userLibraryApi.getLocalTrailers(item.id).content.map { + LocalTrailer(BaseItem.Companion.from(it, api)) + } + } else { + listOf() + } + return localTrailers + remoteTrailers + } + + companion object { + /** + * Note: This is explicitly not a member function because the injected Context is not an Activity. + * We want to start the intent without a new task which requires the Activity context + * + * This can be provided by LocalContext.current from Compose + */ + fun onClick( + context: Context, + trailer: Trailer, + navigateTo: (Destination) -> Unit, + ) { + when (trailer) { + is LocalTrailer -> + navigateTo.invoke( + Destination.Playback( + itemId = trailer.baseItem.id, + item = trailer.baseItem, + positionMs = 0L, + ), + ) + + is RemoteTrailer -> { + val intent = Intent(Intent.ACTION_VIEW, trailer.url.toUri()) + context.startActivity(intent) + } + } + } + } + }