mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
PoC movie details
This commit is contained in:
parent
0281d00907
commit
69fb26a070
13 changed files with 997 additions and 39 deletions
|
|
@ -0,0 +1,71 @@
|
||||||
|
package com.github.damontecres.wholphin.data.model
|
||||||
|
|
||||||
|
import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingsGet200Response
|
||||||
|
import com.github.damontecres.wholphin.api.seerr.model.MovieResult
|
||||||
|
import com.github.damontecres.wholphin.api.seerr.model.TvResult
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class SeerrItemType {
|
||||||
|
MOVIE,
|
||||||
|
TV,
|
||||||
|
PERSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class SeerrAvailability(
|
||||||
|
val status: Int,
|
||||||
|
) {
|
||||||
|
UNKNOWN(1),
|
||||||
|
PENDING(2),
|
||||||
|
PROCESSING(3),
|
||||||
|
PARTIALLY_AVAILABLE(4),
|
||||||
|
AVAILABLE(5),
|
||||||
|
DELETED(6),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(status: Int?) = entries.firstOrNull { it.status == status }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DiscoverItem(
|
||||||
|
val id: Int,
|
||||||
|
val type: SeerrItemType,
|
||||||
|
val title: String?,
|
||||||
|
val availability: SeerrAvailability,
|
||||||
|
val releaseDate: String?,
|
||||||
|
val posterPath: String?,
|
||||||
|
val backdropPath: String?,
|
||||||
|
) {
|
||||||
|
constructor(movie: MovieResult) : this(
|
||||||
|
id = movie.id,
|
||||||
|
type = SeerrItemType.MOVIE,
|
||||||
|
title = movie.title,
|
||||||
|
availability = SeerrAvailability.from(movie.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN,
|
||||||
|
releaseDate = movie.releaseDate,
|
||||||
|
posterPath = movie.posterPath,
|
||||||
|
backdropPath = movie.backdropPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(tv: TvResult) : this(
|
||||||
|
id = tv.id!!,
|
||||||
|
type = SeerrItemType.MOVIE,
|
||||||
|
title = tv.name,
|
||||||
|
availability = SeerrAvailability.from(tv.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN,
|
||||||
|
releaseDate = tv.firstAirDate,
|
||||||
|
posterPath = tv.posterPath,
|
||||||
|
backdropPath = tv.backdropPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DiscoverRating(
|
||||||
|
val criticRating: Int?,
|
||||||
|
val audienceRating: Float?,
|
||||||
|
) {
|
||||||
|
constructor(rating: MovieMovieIdRatingsGet200Response) : this(
|
||||||
|
criticRating = rating.criticsScore,
|
||||||
|
audienceRating = rating.audienceScore?.div(10f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
|
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.SearchGet200ResponseResultsInner
|
import com.github.damontecres.wholphin.api.seerr.model.SearchGet200ResponseResultsInner
|
||||||
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -13,14 +14,10 @@ class SeerrService
|
||||||
constructor(
|
constructor(
|
||||||
private val seerApi: SeerrApi,
|
private val seerApi: SeerrApi,
|
||||||
) {
|
) {
|
||||||
private val api: SeerrApiClient get() = seerApi.api
|
val api: SeerrApiClient get() = seerApi.api
|
||||||
|
|
||||||
val active: Boolean get() = seerApi.active
|
val active: Boolean get() = seerApi.active
|
||||||
|
|
||||||
suspend fun init() {
|
|
||||||
api.searchApi.discoverTvGet(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun search(
|
suspend fun search(
|
||||||
query: String,
|
query: String,
|
||||||
page: Int = 1,
|
page: Int = 1,
|
||||||
|
|
@ -30,15 +27,17 @@ class SeerrService
|
||||||
.results
|
.results
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
|
||||||
suspend fun discoverTv(page: Int = 1) =
|
suspend fun discoverTv(page: Int = 1): List<DiscoverItem> =
|
||||||
api.searchApi
|
api.searchApi
|
||||||
.discoverTvGet(page = page)
|
.discoverTvGet(page = page)
|
||||||
.results
|
.results
|
||||||
|
?.map(::DiscoverItem)
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
|
||||||
suspend fun discoverMovies(page: Int = 1) =
|
suspend fun discoverMovies(page: Int = 1): List<DiscoverItem> =
|
||||||
api.searchApi
|
api.searchApi
|
||||||
.discoverMoviesGet(page = page)
|
.discoverMoviesGet(page = page)
|
||||||
.results
|
.results
|
||||||
|
?.map(::DiscoverItem)
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.cards
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.NonRestartableComposable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
fun DiscoverItemCard(
|
||||||
|
item: DiscoverItem?,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) = SeasonCard(
|
||||||
|
title = item?.title,
|
||||||
|
subtitle = item?.releaseDate,
|
||||||
|
name = item?.title,
|
||||||
|
imageUrl = item?.posterPath?.let { "https://image.tmdb.org/t/p/w500$it" }, // TODO
|
||||||
|
isFavorite = false,
|
||||||
|
isPlayed = false,
|
||||||
|
unplayedItemCount = -1,
|
||||||
|
playedPercentage = -1.0,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
imageHeight = Cards.height2x3,
|
||||||
|
modifier = modifier,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,456 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.discover
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
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
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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.api.seerr.model.MovieDetails
|
||||||
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
||||||
|
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.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.TrailerService
|
||||||
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
|
import com.github.damontecres.wholphin.ui.cards.ExtrasRow
|
||||||
|
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
|
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||||
|
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.DialogPopup
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
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 kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DiscoverMovieDetails(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
destination: Destination.DiscoveredItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: DiscoverMovieViewModel =
|
||||||
|
hiltViewModel<DiscoverMovieViewModel, DiscoverMovieViewModel.Factory>(
|
||||||
|
creationCallback = { it.create(destination.item) },
|
||||||
|
),
|
||||||
|
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
LifecycleResumeEffect(Unit) {
|
||||||
|
viewModel.init()
|
||||||
|
onPauseOrDispose { }
|
||||||
|
}
|
||||||
|
val item by viewModel.movie.observeAsState()
|
||||||
|
val rating by viewModel.rating.observeAsState(null)
|
||||||
|
val people by viewModel.people.observeAsState(listOf())
|
||||||
|
val trailers by viewModel.trailers.observeAsState(listOf())
|
||||||
|
val extras by viewModel.extras.observeAsState(listOf())
|
||||||
|
val similar by viewModel.similar.observeAsState(listOf())
|
||||||
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
|
|
||||||
|
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||||
|
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
|
|
||||||
|
val moreActions =
|
||||||
|
MoreDialogActions(
|
||||||
|
navigateTo = viewModel::navigateTo,
|
||||||
|
onClickWatch = { itemId, watched -> },
|
||||||
|
onClickFavorite = { itemId, favorite -> },
|
||||||
|
onClickAddPlaylist = { itemId -> },
|
||||||
|
)
|
||||||
|
|
||||||
|
when (val state = loading) {
|
||||||
|
is LoadingState.Error -> {
|
||||||
|
ErrorMessage(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Pending,
|
||||||
|
-> {
|
||||||
|
LoadingPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState.Success -> {
|
||||||
|
item?.let { movie ->
|
||||||
|
DiscoverMovieDetailsContent(
|
||||||
|
preferences = preferences,
|
||||||
|
movie = movie,
|
||||||
|
rating = rating,
|
||||||
|
people = people,
|
||||||
|
extras = extras,
|
||||||
|
trailers = trailers,
|
||||||
|
similar = similar,
|
||||||
|
requestOnClick = {
|
||||||
|
// TODO seerr
|
||||||
|
},
|
||||||
|
onClickItem = { index, item ->
|
||||||
|
viewModel.navigateTo(item.destination())
|
||||||
|
},
|
||||||
|
onClickPerson = {
|
||||||
|
},
|
||||||
|
overviewOnClick = {
|
||||||
|
overviewDialog =
|
||||||
|
ItemDetailsDialogInfo(
|
||||||
|
title = movie.title ?: context.getString(R.string.unknown),
|
||||||
|
overview = movie.overview,
|
||||||
|
genres = movie.genres?.mapNotNull { it.name }.orEmpty(),
|
||||||
|
files = listOf(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
moreOnClick = {
|
||||||
|
moreDialog =
|
||||||
|
DialogParams(
|
||||||
|
fromLongClick = false,
|
||||||
|
title = movie.title + " (${movie.releaseDate ?: ""})",
|
||||||
|
items = listOf(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onLongClickPerson = { index, person ->
|
||||||
|
val items =
|
||||||
|
buildMoreDialogItemsForPerson(
|
||||||
|
context = context,
|
||||||
|
person = person,
|
||||||
|
actions = moreActions,
|
||||||
|
)
|
||||||
|
moreDialog =
|
||||||
|
DialogParams(
|
||||||
|
fromLongClick = true,
|
||||||
|
title = person.name ?: "",
|
||||||
|
items = items,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onLongClickSimilar = { index, similar ->
|
||||||
|
val items =
|
||||||
|
buildMoreDialogItemsForHome(
|
||||||
|
context = context,
|
||||||
|
item = similar,
|
||||||
|
seriesId = null,
|
||||||
|
playbackPosition = similar.playbackPosition,
|
||||||
|
watched = similar.played,
|
||||||
|
favorite = similar.favorite,
|
||||||
|
actions = moreActions,
|
||||||
|
)
|
||||||
|
moreDialog =
|
||||||
|
DialogParams(
|
||||||
|
fromLongClick = true,
|
||||||
|
title = similar.title ?: "",
|
||||||
|
items = items,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailerOnClick = {
|
||||||
|
TrailerService.onClick(context, it, viewModel::navigateTo)
|
||||||
|
},
|
||||||
|
onClickExtra = { index, extra ->
|
||||||
|
viewModel.navigateTo(extra.destination)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overviewDialog?.let { info ->
|
||||||
|
ItemDetailsDialog(
|
||||||
|
info = info,
|
||||||
|
showFilePath = false,
|
||||||
|
onDismissRequest = { overviewDialog = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
moreDialog?.let { params ->
|
||||||
|
DialogPopup(
|
||||||
|
showDialog = true,
|
||||||
|
title = params.title,
|
||||||
|
dialogItems = params.items,
|
||||||
|
onDismissRequest = { moreDialog = null },
|
||||||
|
dismissOnClick = true,
|
||||||
|
waitToLoad = params.fromLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val HEADER_ROW = 0
|
||||||
|
private const val PEOPLE_ROW = HEADER_ROW + 1
|
||||||
|
private const val TRAILER_ROW = PEOPLE_ROW + 1
|
||||||
|
private const val CHAPTER_ROW = TRAILER_ROW + 1
|
||||||
|
private const val EXTRAS_ROW = CHAPTER_ROW + 1
|
||||||
|
private const val SIMILAR_ROW = EXTRAS_ROW + 1
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DiscoverMovieDetailsContent(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
movie: MovieDetails,
|
||||||
|
rating: DiscoverRating?,
|
||||||
|
people: List<Person>,
|
||||||
|
trailers: List<Trailer>,
|
||||||
|
extras: List<ExtrasItem>,
|
||||||
|
similar: List<BaseItem>,
|
||||||
|
requestOnClick: () -> Unit,
|
||||||
|
trailerOnClick: (Trailer) -> Unit,
|
||||||
|
overviewOnClick: () -> Unit,
|
||||||
|
moreOnClick: () -> Unit,
|
||||||
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
|
onClickPerson: (Person) -> Unit,
|
||||||
|
onLongClickPerson: (Int, Person) -> Unit,
|
||||||
|
onLongClickSimilar: (Int, BaseItem) -> Unit,
|
||||||
|
onClickExtra: (Int, ExtrasItem) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var position by rememberInt(0)
|
||||||
|
val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } }
|
||||||
|
|
||||||
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||||
|
}
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
movie.backdropPath?.let {
|
||||||
|
DetailsBackdropImage("https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/$it")
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.bringIntoViewRequester(bringIntoViewRequester),
|
||||||
|
) {
|
||||||
|
DiscoverMovieDetailsHeader(
|
||||||
|
preferences = preferences,
|
||||||
|
movie = movie,
|
||||||
|
rating = rating,
|
||||||
|
bringIntoViewRequester = bringIntoViewRequester,
|
||||||
|
overviewOnClick = overviewOnClick,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 32.dp, bottom = 16.dp),
|
||||||
|
)
|
||||||
|
ExpandableDiscoverButtons(
|
||||||
|
requestOnClick = requestOnClick,
|
||||||
|
moreOnClick = moreOnClick,
|
||||||
|
buttonOnFocusChanged = {
|
||||||
|
if (it.isFocused) {
|
||||||
|
position = HEADER_ROW
|
||||||
|
scope.launch(ExceptionHandler()) {
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.focusRequester(focusRequesters[HEADER_ROW]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (people.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
PersonRow(
|
||||||
|
people = people,
|
||||||
|
onClick = {
|
||||||
|
position = PEOPLE_ROW
|
||||||
|
onClickPerson.invoke(it)
|
||||||
|
},
|
||||||
|
onLongClick = { index, person ->
|
||||||
|
position = PEOPLE_ROW
|
||||||
|
onLongClickPerson.invoke(index, person)
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequesters[PEOPLE_ROW]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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(
|
||||||
|
extras = extras,
|
||||||
|
onClickItem = { index, item ->
|
||||||
|
position = EXTRAS_ROW
|
||||||
|
onClickExtra.invoke(index, item)
|
||||||
|
},
|
||||||
|
onLongClickItem = { _, _ -> },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequesters[EXTRAS_ROW]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (similar.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
ItemRow(
|
||||||
|
title = stringResource(R.string.more_like_this),
|
||||||
|
items = similar,
|
||||||
|
onClickItem = { index, item ->
|
||||||
|
position = SIMILAR_ROW
|
||||||
|
onClickItem.invoke(index, item)
|
||||||
|
},
|
||||||
|
onLongClickItem = { index, similar ->
|
||||||
|
position = SIMILAR_ROW
|
||||||
|
onLongClickSimilar.invoke(index, similar)
|
||||||
|
},
|
||||||
|
cardContent = { index, item, mod, onClick, onLongClick ->
|
||||||
|
SeasonCard(
|
||||||
|
item = item,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
modifier = mod,
|
||||||
|
showImageOverlay = true,
|
||||||
|
imageHeight = Cards.height2x3,
|
||||||
|
imageWidth = Dp.Unspecified,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequesters[SIMILAR_ROW]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.discover
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
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.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
|
||||||
|
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
||||||
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
||||||
|
import com.github.damontecres.wholphin.ui.components.GenreText
|
||||||
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DiscoverMovieDetailsHeader(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
movie: MovieDetails,
|
||||||
|
rating: DiscoverRating?,
|
||||||
|
bringIntoViewRequester: BringIntoViewRequester,
|
||||||
|
overviewOnClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
// Title
|
||||||
|
Text(
|
||||||
|
text = movie.title ?: "",
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.displaySmall,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.fillMaxWidth(.75f),
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
|
) {
|
||||||
|
val padding = 4.dp
|
||||||
|
val details =
|
||||||
|
remember(movie) {
|
||||||
|
buildList {
|
||||||
|
movie.releaseDate?.let(::add)
|
||||||
|
movie.runtime
|
||||||
|
?.toDouble()
|
||||||
|
?.minutes
|
||||||
|
?.roundMinutes
|
||||||
|
?.toString()
|
||||||
|
?.let(::add)
|
||||||
|
movie.releases
|
||||||
|
?.results
|
||||||
|
?.firstOrNull()
|
||||||
|
?.releaseDates
|
||||||
|
?.firstOrNull()
|
||||||
|
?.certification
|
||||||
|
?.let(::add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DotSeparatedRow(
|
||||||
|
texts = details,
|
||||||
|
communityRating = rating?.audienceRating,
|
||||||
|
criticRating = rating?.criticRating?.toFloat(),
|
||||||
|
textStyle = MaterialTheme.typography.titleSmall,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
movie.genres?.mapNotNull { it.name }?.letNotEmpty {
|
||||||
|
GenreText(it, Modifier.padding(bottom = padding))
|
||||||
|
}
|
||||||
|
|
||||||
|
movie.tagline?.let { tagline ->
|
||||||
|
Text(
|
||||||
|
text = tagline,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
movie.overview?.let { overview ->
|
||||||
|
OverviewText(
|
||||||
|
overview = overview,
|
||||||
|
maxLines = 3,
|
||||||
|
onClick = overviewOnClick,
|
||||||
|
textBoxHeight = Dp.Unspecified,
|
||||||
|
modifier =
|
||||||
|
Modifier.onFocusChanged {
|
||||||
|
if (it.isFocused) {
|
||||||
|
scope.launch(ExceptionHandler()) {
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val directorName =
|
||||||
|
remember(movie.credits?.crew) {
|
||||||
|
movie.credits
|
||||||
|
?.crew
|
||||||
|
?.filter { it.job == "Directing" }
|
||||||
|
?.joinToString(", ") { it.name!! }
|
||||||
|
}
|
||||||
|
|
||||||
|
directorName
|
||||||
|
?.let {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.directed_by, it),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.discover
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
|
||||||
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
|
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.DiscoverItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
||||||
|
import com.github.damontecres.wholphin.data.model.Person
|
||||||
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.services.SeerrService
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
import org.jellyfin.sdk.model.api.PersonKind
|
||||||
|
|
||||||
|
@HiltViewModel(assistedFactory = DiscoverMovieViewModel.Factory::class)
|
||||||
|
class DiscoverMovieViewModel
|
||||||
|
@AssistedInject
|
||||||
|
constructor(
|
||||||
|
private val api: ApiClient,
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
|
private val navigationManager: NavigationManager,
|
||||||
|
val serverRepository: ServerRepository,
|
||||||
|
val seerrService: SeerrService,
|
||||||
|
@Assisted val item: DiscoverItem,
|
||||||
|
) : ViewModel() {
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(item: DiscoverItem): DiscoverMovieViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
val movie = MutableLiveData<MovieDetails?>(null)
|
||||||
|
val rating = MutableLiveData<DiscoverRating?>(null)
|
||||||
|
|
||||||
|
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
||||||
|
val people = MutableLiveData<List<Person>>(listOf())
|
||||||
|
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||||
|
val extras = MutableLiveData<List<ExtrasItem>>(listOf())
|
||||||
|
val similar = MutableLiveData<List<BaseItem>>()
|
||||||
|
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchAndSetItem(): Deferred<MovieDetails> =
|
||||||
|
viewModelScope.async(
|
||||||
|
Dispatchers.IO +
|
||||||
|
LoadingExceptionHandler(
|
||||||
|
loading,
|
||||||
|
"Error fetching movie",
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val movie = seerrService.api.moviesApi.movieMovieIdGet(movieId = item.id)
|
||||||
|
this@DiscoverMovieViewModel.movie.setValueOnMain(movie)
|
||||||
|
movie
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(): Job =
|
||||||
|
viewModelScope.launch(
|
||||||
|
Dispatchers.IO +
|
||||||
|
LoadingExceptionHandler(
|
||||||
|
loading,
|
||||||
|
"Error fetching movie",
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val movie = fetchAndSetItem().await()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
loading.value = LoadingState.Success
|
||||||
|
}
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val result = seerrService.api.moviesApi.movieMovieIdRatingsGet(movieId = item.id)
|
||||||
|
rating.setValueOnMain(DiscoverRating(result))
|
||||||
|
}
|
||||||
|
val people =
|
||||||
|
movie.credits
|
||||||
|
?.cast
|
||||||
|
?.map {
|
||||||
|
Person(
|
||||||
|
id = UUID.randomUUID(),
|
||||||
|
name = it.name,
|
||||||
|
role = it.character,
|
||||||
|
type = PersonKind.UNKNOWN,
|
||||||
|
imageUrl = "https://image.tmdb.org/t/p/w600_and_h900_bestv2${it.profilePath}",
|
||||||
|
favorite = false,
|
||||||
|
)
|
||||||
|
}.orEmpty() +
|
||||||
|
movie.credits
|
||||||
|
?.crew
|
||||||
|
?.map {
|
||||||
|
Person(
|
||||||
|
id = UUID.randomUUID(),
|
||||||
|
name = it.name,
|
||||||
|
role = it.job,
|
||||||
|
type = PersonKind.UNKNOWN,
|
||||||
|
imageUrl = "https://image.tmdb.org/t/p/w600_and_h900_bestv2${it.profilePath}",
|
||||||
|
favorite = false,
|
||||||
|
)
|
||||||
|
}.orEmpty()
|
||||||
|
this@DiscoverMovieViewModel.people.setValueOnMain(people)
|
||||||
|
if (!similar.isInitialized) {
|
||||||
|
// val similar =
|
||||||
|
// api.libraryApi
|
||||||
|
// .getSimilarItems(
|
||||||
|
// GetSimilarItemsRequest(
|
||||||
|
// userId = serverRepository.currentUser.value?.id,
|
||||||
|
// itemId = itemId,
|
||||||
|
// fields = SlimItemFields,
|
||||||
|
// limit = 25,
|
||||||
|
// ),
|
||||||
|
// ).content.items
|
||||||
|
// .map { BaseItem.Companion.from(it, api) }
|
||||||
|
// this@DiscoverMovieViewModel.similar.setValueOnMain(similar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateTo(destination: Destination) {
|
||||||
|
navigationManager.navigateTo(destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.discover
|
||||||
|
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.FocusState
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandableDiscoverButtons(
|
||||||
|
requestOnClick: () -> Unit,
|
||||||
|
moreOnClick: () -> Unit,
|
||||||
|
buttonOnFocusChanged: (FocusState) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val firstFocus = remember { FocusRequester() }
|
||||||
|
LazyRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.focusGroup()
|
||||||
|
.focusRestorer(firstFocus),
|
||||||
|
) {
|
||||||
|
item("request") {
|
||||||
|
ExpandableFaButton(
|
||||||
|
title = R.string.request,
|
||||||
|
iconStringRes = R.string.fa_download,
|
||||||
|
onClick = requestOnClick,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.focusRequester(firstFocus)
|
||||||
|
.onFocusChanged(buttonOnFocusChanged)
|
||||||
|
.animateItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// More button
|
||||||
|
item("more") {
|
||||||
|
ExpandablePlayButton(
|
||||||
|
R.string.more,
|
||||||
|
Duration.ZERO,
|
||||||
|
Icons.Default.MoreVert,
|
||||||
|
{ moreOnClick.invoke() },
|
||||||
|
Modifier
|
||||||
|
.onFocusChanged(buttonOnFocusChanged)
|
||||||
|
.animateItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ package com.github.damontecres.wholphin.ui.nav
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
|
|
@ -120,6 +121,11 @@ sealed class Destination(
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Discover : Destination(false)
|
data object Discover : Destination(false)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DiscoveredItem(
|
||||||
|
val item: DiscoverItem,
|
||||||
|
) : Destination(false)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object UpdateApp : Destination(true)
|
data object UpdateApp : Destination(true)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.data.filter.DefaultForGenresFilterOptions
|
import com.github.damontecres.wholphin.data.filter.DefaultForGenresFilterOptions
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrItemType
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.ItemGrid
|
import com.github.damontecres.wholphin.ui.components.ItemGrid
|
||||||
import com.github.damontecres.wholphin.ui.components.LicenseInfo
|
import com.github.damontecres.wholphin.ui.components.LicenseInfo
|
||||||
|
|
@ -19,6 +20,7 @@ import com.github.damontecres.wholphin.ui.detail.DebugPage
|
||||||
import com.github.damontecres.wholphin.ui.detail.FavoritesPage
|
import com.github.damontecres.wholphin.ui.detail.FavoritesPage
|
||||||
import com.github.damontecres.wholphin.ui.detail.PersonPage
|
import com.github.damontecres.wholphin.ui.detail.PersonPage
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDetails
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDetails
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.discover.DiscoverMovieDetails
|
||||||
import com.github.damontecres.wholphin.ui.detail.episode.EpisodeDetails
|
import com.github.damontecres.wholphin.ui.detail.episode.EpisodeDetails
|
||||||
import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails
|
import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeriesDetails
|
import com.github.damontecres.wholphin.ui.detail.series.SeriesDetails
|
||||||
|
|
@ -249,6 +251,25 @@ fun DestinationContent(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is Destination.DiscoveredItem -> {
|
||||||
|
when (destination.item.type) {
|
||||||
|
SeerrItemType.MOVIE -> {
|
||||||
|
DiscoverMovieDetails(
|
||||||
|
preferences = preferences,
|
||||||
|
destination = destination,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SeerrItemType.TV -> {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
|
||||||
|
SeerrItemType.PERSON -> {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package com.github.damontecres.wholphin.ui.seerr
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
|
@ -12,13 +14,14 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.MovieResult
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.SeerrService
|
import com.github.damontecres.wholphin.services.SeerrService
|
||||||
import com.github.damontecres.wholphin.ui.Cards
|
import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard
|
||||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -27,15 +30,21 @@ import javax.inject.Inject
|
||||||
class SeerrDiscoverViewModel
|
class SeerrDiscoverViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
val seerrService: SeerrService,
|
private val seerrService: SeerrService,
|
||||||
|
val navigationManager: NavigationManager,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val discoverMovies = MutableLiveData<List<MovieResult>>(listOf())
|
val discoverMovies = MutableLiveData<List<DiscoverItem>>(listOf())
|
||||||
|
val discoverTv = MutableLiveData<List<DiscoverItem>>(listOf())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val movies = seerrService.discoverMovies()
|
val movies = seerrService.discoverMovies()
|
||||||
discoverMovies.setValueOnMain(movies)
|
discoverMovies.setValueOnMain(movies)
|
||||||
}
|
}
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val tv = seerrService.discoverTv()
|
||||||
|
discoverTv.setValueOnMain(tv)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,30 +55,44 @@ fun SeerrDiscoverPage(
|
||||||
viewModel: SeerrDiscoverViewModel = hiltViewModel(),
|
viewModel: SeerrDiscoverViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val movies by viewModel.discoverMovies.observeAsState(listOf())
|
val movies by viewModel.discoverMovies.observeAsState(listOf())
|
||||||
|
val tv by viewModel.discoverTv.observeAsState(listOf())
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
|
.verticalScroll(scrollState)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = "Movies",
|
title = "Movies",
|
||||||
items = movies,
|
items = movies,
|
||||||
onClickItem = { index, item -> },
|
onClickItem = { index, item ->
|
||||||
|
viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item))
|
||||||
|
},
|
||||||
onLongClickItem = { index, item -> },
|
onLongClickItem = { index, item -> },
|
||||||
cardContent = { index: Int, item: MovieResult?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
|
cardContent = { index: Int, item: DiscoverItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
|
||||||
SeasonCard(
|
DiscoverItemCard(
|
||||||
title = item?.title,
|
item = item,
|
||||||
subtitle = null,
|
onClick = onClick,
|
||||||
name = item?.title,
|
onLongClick = onLongClick,
|
||||||
imageUrl = item?.posterPath?.let { "https://image.tmdb.org/t/p/w500$it" }, // TODO
|
modifier = mod,
|
||||||
isFavorite = false,
|
)
|
||||||
isPlayed = false,
|
},
|
||||||
unplayedItemCount = -1,
|
)
|
||||||
playedPercentage = -1.0,
|
ItemRow(
|
||||||
|
title = "TV",
|
||||||
|
items = tv,
|
||||||
|
onClickItem = { index, item ->
|
||||||
|
viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item))
|
||||||
|
},
|
||||||
|
onLongClickItem = { index, item -> },
|
||||||
|
cardContent = { index: Int, item: DiscoverItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
|
||||||
|
DiscoverItemCard(
|
||||||
|
item = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
imageHeight = Cards.height2x3,
|
|
||||||
modifier = mod,
|
modifier = mod,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -43,4 +43,5 @@
|
||||||
<string name="fa_ellipsis_vertical" translatable="false"></string>
|
<string name="fa_ellipsis_vertical" translatable="false"></string>
|
||||||
<string name="fa_filter" translatable="false"></string>
|
<string name="fa_filter" translatable="false"></string>
|
||||||
<string name="fa_sliders" translatable="false"></string>
|
<string name="fa_sliders" translatable="false"></string>
|
||||||
|
<string name="fa_download" translatable="false"></string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,7 @@
|
||||||
<string name="username_or_password">Use username/password</string>
|
<string name="username_or_password">Use username/password</string>
|
||||||
<string name="login">Login</string>
|
<string name="login">Login</string>
|
||||||
<string name="discover">Discover</string>
|
<string name="discover">Discover</string>
|
||||||
|
<string name="request">Request</string>
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
|
|
|
||||||
|
|
@ -1993,8 +1993,6 @@ components:
|
||||||
webpush:
|
webpush:
|
||||||
type: integer
|
type: integer
|
||||||
WatchProviders:
|
WatchProviders:
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
iso_3166_1:
|
iso_3166_1:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue