Better discover cards

This commit is contained in:
Damontecres 2025-12-20 15:51:44 -05:00
parent 99d1ccf7e7
commit 76c1fd4b15
No known key found for this signature in database
7 changed files with 214 additions and 35 deletions

View file

@ -3,13 +3,34 @@ package com.github.damontecres.wholphin.data.model
import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingsGet200Response 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.MovieResult
import com.github.damontecres.wholphin.api.seerr.model.TvResult import com.github.damontecres.wholphin.api.seerr.model.TvResult
import com.github.damontecres.wholphin.services.SeerrSearchResult
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
enum class SeerrItemType { enum class SeerrItemType {
@SerialName("movie")
MOVIE, MOVIE,
@SerialName("tv")
TV, TV,
@SerialName("person")
PERSON, PERSON,
@SerialName("unknown")
UNKNOWN,
;
companion object {
fun fromString(str: String?) =
when (str) {
"movie" -> MOVIE
"tv" -> TV
"person" -> PERSON
else -> UNKNOWN
}
}
} }
@Serializable @Serializable
@ -58,6 +79,18 @@ data class DiscoverItem(
posterPath = tv.posterPath, posterPath = tv.posterPath,
backdropPath = tv.backdropPath, backdropPath = tv.backdropPath,
) )
constructor(search: SeerrSearchResult) : this(
id = search.id,
type = SeerrItemType.fromString(search.mediaType),
title = search.title ?: search.name,
availability =
SeerrAvailability.from(search.mediaInfo?.status)
?: SeerrAvailability.UNKNOWN,
releaseDate = search.releaseDate ?: search.firstAirDate,
posterPath = search.posterPath,
backdropPath = search.backdropPath,
)
} }
data class DiscoverRating( data class DiscoverRating(

View file

@ -1,12 +1,43 @@
package com.github.damontecres.wholphin.ui.cards package com.github.damontecres.wholphin.ui.cards
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.data.model.DiscoverItem
import com.github.damontecres.wholphin.data.model.SeerrItemType
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.enableMarquee
import kotlinx.coroutines.delay
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
@ -14,20 +45,125 @@ fun DiscoverItemCard(
item: DiscoverItem?, item: DiscoverItem?,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
showOverlay: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) = SeasonCard( ) {
title = item?.title, val focused by interactionSource.collectIsFocusedAsState()
subtitle = item?.releaseDate, val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
name = item?.title, val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
imageUrl = item?.posterPath?.let { "https://image.tmdb.org/t/p/w500$it" }, // TODO var focusedAfterDelay by remember { mutableStateOf(false) }
isFavorite = false,
isPlayed = false, val hideOverlayDelay = 500L
unplayedItemCount = -1, if (focused) {
playedPercentage = -1.0, LaunchedEffect(Unit) {
delay(hideOverlayDelay)
if (focused) {
focusedAfterDelay = true
} else {
focusedAfterDelay = false
}
}
} else {
focusedAfterDelay = false
}
val width = Cards.height2x3 * AspectRatios.TALL
val height = Dp.Unspecified * (1f / AspectRatios.TALL)
Column(
verticalArrangement = Arrangement.spacedBy(spaceBetween),
modifier = modifier.size(width, height),
) {
Card(
modifier =
Modifier
.size(Dp.Unspecified, Cards.height2x3)
.aspectRatio(AspectRatios.TALL),
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
imageHeight = Cards.height2x3,
modifier = modifier,
interactionSource = interactionSource, interactionSource = interactionSource,
colors =
CardDefaults.colors(
containerColor = Color.Transparent,
),
) {
Box(
modifier =
Modifier
.fillMaxSize(),
) {
ItemCardImage(
imageUrl = item?.posterPath?.let { "https://image.tmdb.org/t/p/w500$it" },
name = item?.title,
showOverlay = false,
favorite = false,
watched = false,
unwatchedCount = 0,
watchedPercent = null,
useFallbackText = false,
contentScale = ContentScale.FillBounds,
modifier =
Modifier
.fillMaxSize(),
) )
if (showOverlay) {
// TODO better colors
val color =
when (item?.type) {
SeerrItemType.MOVIE -> Color.Blue
SeerrItemType.TV -> Color.Red
SeerrItemType.PERSON -> Color.Green
SeerrItemType.UNKNOWN -> Color.Black
null -> Color.Black
}.copy(alpha = .5f)
when (item?.type) {
SeerrItemType.MOVIE -> R.string.movies
SeerrItemType.TV -> R.string.tv_shows
SeerrItemType.PERSON -> R.string.people
SeerrItemType.UNKNOWN -> null
null -> null
}?.let {
Text(
text = stringResource(it),
modifier =
Modifier
.align(Alignment.TopStart)
.padding(4.dp)
.background(
color = color,
shape = CircleShape,
).padding(4.dp),
)
}
}
}
}
Column(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier =
Modifier
.padding(bottom = spaceBelow)
.fillMaxWidth(),
) {
Text(
text = item?.title ?: "",
maxLines = 1,
textAlign = TextAlign.Center,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
.enableMarquee(focusedAfterDelay),
)
Text(
text = item?.releaseDate ?: "",
maxLines = 1,
textAlign = TextAlign.Center,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
.enableMarquee(focusedAfterDelay),
)
}
}
}

View file

@ -49,7 +49,6 @@ 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.PersonRow import com.github.damontecres.wholphin.ui.cards.PersonRow
import com.github.damontecres.wholphin.ui.cards.SeasonCard 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.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
@ -227,9 +226,6 @@ fun DiscoverMovieDetailsContent(
focusRequesters.getOrNull(position)?.tryRequestFocus() focusRequesters.getOrNull(position)?.tryRequestFocus()
} }
Box(modifier = modifier) { Box(modifier = modifier) {
movie.backdropPath?.let {
DetailsBackdropImage("https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/$it")
}
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp), contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
@ -326,6 +322,7 @@ fun DiscoverMovieDetailsContent(
item = item, item = item,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
showOverlay = false,
modifier = mod, modifier = mod,
) )
}, },
@ -354,6 +351,7 @@ fun DiscoverMovieDetailsContent(
item = item, item = item,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
showOverlay = true,
modifier = mod, modifier = mod,
) )
}, },

View file

@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.DiscoverRating
import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.RemoteTrailer import com.github.damontecres.wholphin.data.model.RemoteTrailer
import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.NavigationManager 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.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
@ -42,6 +43,7 @@ class DiscoverMovieViewModel
private val api: ApiClient, private val api: ApiClient,
@param:ApplicationContext private val context: Context, @param:ApplicationContext private val context: Context,
private val navigationManager: NavigationManager, private val navigationManager: NavigationManager,
private val backdropService: BackdropService,
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
val seerrService: SeerrService, val seerrService: SeerrService,
@Assisted val item: DiscoverItem, @Assisted val item: DiscoverItem,
@ -86,6 +88,9 @@ class DiscoverMovieViewModel
), ),
) { ) {
val movie = fetchAndSetItem().await() val movie = fetchAndSetItem().await()
// TODO backdrop url = "https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/$it"
// backdropService.submit(movie)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
loading.value = LoadingState.Success loading.value = LoadingState.Success
} }

View file

@ -39,12 +39,14 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.DiscoverItem
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.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SeerrSearchResult
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
import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard
import com.github.damontecres.wholphin.ui.cards.EpisodeCard import com.github.damontecres.wholphin.ui.cards.EpisodeCard
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.cards.SeasonCard
@ -144,7 +146,11 @@ class SearchViewModel
if (seerrService.active) { if (seerrService.active) {
viewModelScope.launchIO { viewModelScope.launchIO {
seerrResults.setValueOnMain(SearchResult.Searching) seerrResults.setValueOnMain(SearchResult.Searching)
val results = seerrService.search(query) val results =
seerrService
.search(query)
.map { DiscoverItem(it) }
.filter { it.type == SeerrItemType.MOVIE || it.type == SeerrItemType.TV }
seerrResults.setValueOnMain(SearchResult.SuccessSeerr(results)) seerrResults.setValueOnMain(SearchResult.SuccessSeerr(results))
} }
} }
@ -170,7 +176,7 @@ sealed interface SearchResult {
) : SearchResult ) : SearchResult
data class SuccessSeerr( data class SuccessSeerr(
val items: List<SeerrSearchResult>, val items: List<DiscoverItem>,
) : SearchResult ) : SearchResult
} }
@ -416,19 +422,12 @@ fun LazyListScope.searchResultRow(
onClickItem = { index, item -> }, onClickItem = { index, item -> },
onLongClickItem = { _, _ -> }, onLongClickItem = { _, _ -> },
modifier = modifier, modifier = modifier,
cardContent = { index: Int, item: SeerrSearchResult?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit -> cardContent = { index: Int, item: DiscoverItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
SeasonCard( DiscoverItemCard(
title = item?.title ?: item?.name, item = item,
subtitle = null,
name = item?.title ?: item?.name,
imageUrl = item?.posterPath?.let { "https://image.tmdb.org/t/p/w500$it" }, // TODO
isFavorite = false,
isPlayed = false,
unplayedItemCount = -1,
playedPercentage = -1.0,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
imageHeight = Cards.height2x3, showOverlay = true,
modifier = mod, modifier = mod,
) )
}, },

View file

@ -284,6 +284,10 @@ fun DestinationContent(
SeerrItemType.PERSON -> { SeerrItemType.PERSON -> {
TODO() TODO()
} }
SeerrItemType.UNKNOWN -> {
TODO()
}
} }
} }
} }

View file

@ -9,11 +9,13 @@ 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
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel 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.R
import com.github.damontecres.wholphin.api.seerr.MediaApi import com.github.damontecres.wholphin.api.seerr.MediaApi
import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.data.model.DiscoverItem
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
@ -83,7 +85,7 @@ fun SeerrDiscoverPage(
.padding(16.dp), .padding(16.dp),
) { ) {
ItemRow( ItemRow(
title = "Movies", title = stringResource(R.string.movies),
items = movies, items = movies,
onClickItem = { index, item -> onClickItem = { index, item ->
viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item)) viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item))
@ -94,12 +96,13 @@ fun SeerrDiscoverPage(
item = item, item = item,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
showOverlay = false,
modifier = mod, modifier = mod,
) )
}, },
) )
ItemRow( ItemRow(
title = "TV", title = stringResource(R.string.tv_shows),
items = tv, items = tv,
onClickItem = { index, item -> onClickItem = { index, item ->
viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item)) viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item))
@ -110,6 +113,7 @@ fun SeerrDiscoverPage(
item = item, item = item,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
showOverlay = false,
modifier = mod, modifier = mod,
) )
}, },