Add backdrops to discover pages

This commit is contained in:
Damontecres 2025-12-23 17:15:25 -05:00
parent f27415de0f
commit 22348a5adc
No known key found for this signature in database
5 changed files with 111 additions and 22 deletions

View file

@ -1,11 +1,18 @@
@file:UseSerializers(UUIDSerializer::class)
package com.github.damontecres.wholphin.data.model
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
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 com.github.damontecres.wholphin.services.SeerrSearchResult
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.serializer.UUIDSerializer
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import java.util.UUID
@Serializable
enum class SeerrItemType {
@ -59,7 +66,11 @@ data class DiscoverItem(
val releaseDate: String?,
val posterPath: String?,
val backdropPath: String?,
val jellyfinItemId: UUID?,
) {
val backDropUrl: String? get() = backdropPath?.let { "https://image.tmdb.org/t/p/w1920_and_h800_multi_faces$it" }
val posterUrl: String? get() = posterPath?.let { "https://image.tmdb.org/t/p/w500$it" }
constructor(movie: MovieResult) : this(
id = movie.id,
type = SeerrItemType.MOVIE,
@ -68,6 +79,18 @@ data class DiscoverItem(
releaseDate = movie.releaseDate,
posterPath = movie.posterPath,
backdropPath = movie.backdropPath,
jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
)
constructor(movie: MovieDetails) : this(
id = movie.id ?: -1,
type = SeerrItemType.MOVIE,
title = movie.title,
availability = SeerrAvailability.from(movie.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN,
releaseDate = movie.releaseDate,
posterPath = movie.posterPath,
backdropPath = movie.backdropPath,
jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
)
constructor(tv: TvResult) : this(
@ -78,6 +101,7 @@ data class DiscoverItem(
releaseDate = tv.firstAirDate,
posterPath = tv.posterPath,
backdropPath = tv.backdropPath,
jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
)
constructor(search: SeerrSearchResult) : this(
@ -90,6 +114,7 @@ data class DiscoverItem(
releaseDate = search.releaseDate ?: search.firstAirDate,
posterPath = search.posterPath,
backdropPath = search.backdropPath,
jellyfinItemId = search.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
)
}

View file

@ -15,6 +15,7 @@ import coil3.request.SuccessResult
import coil3.request.allowHardware
import coil3.request.bitmapConfig
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.DiscoverItem
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.BackdropStyle
import dagger.hilt.android.qualifiers.ApplicationContext
@ -27,7 +28,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.ImageType
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@ -47,15 +47,24 @@ class BackdropService
suspend fun submit(item: BaseItem) =
withContext(Dispatchers.IO) {
val imageUrl = imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
val imageUrl = imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)!!
submit(item.id.toString(), imageUrl)
}
suspend fun submit(item: DiscoverItem) = submit("discover_${item.id}", item.backDropUrl)
suspend fun submit(
itemId: String,
imageUrl: String?,
) = withContext(Dispatchers.IO) {
if (backdropFlow.firstOrNull()?.imageUrl != imageUrl) {
_backdropFlow.update {
it.copy(
itemId = item.id,
itemId = itemId,
imageUrl = null,
)
}
extractColors(item)
extractColors(itemId, imageUrl)
}
}
@ -65,9 +74,11 @@ class BackdropService
}
}
private suspend fun extractColors(item: BaseItem) {
private suspend fun extractColors(
itemId: String,
imageUrl: String?,
) {
delay(500)
val imageUrl = imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
val backdropStyle =
preferences.data
.firstOrNull()
@ -83,9 +94,9 @@ class BackdropService
ExtractedColors.DEFAULT
}
_backdropFlow.update {
if (it.itemId == item.id) {
if (it.itemId == itemId) {
BackdropResult(
itemId = item.id,
itemId = itemId,
imageUrl = imageUrl,
primaryColor = primaryColor,
secondaryColor = secondaryColor,
@ -208,7 +219,7 @@ class BackdropService
}
data class BackdropResult(
val itemId: UUID?,
val itemId: String?,
val imageUrl: String?,
val primaryColor: Color = Color.Unspecified,
val secondaryColor: Color = Color.Unspecified,

View file

@ -92,7 +92,7 @@ fun DiscoverItemCard(
.fillMaxSize(),
) {
ItemCardImage(
imageUrl = item?.posterPath?.let { "https://image.tmdb.org/t/p/w500$it" },
imageUrl = item?.posterUrl,
name = item?.title,
showOverlay = false,
favorite = false,

View file

@ -89,8 +89,8 @@ class DiscoverMovieViewModel
),
) {
val movie = fetchAndSetItem().await()
// TODO backdrop url = "https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/$it"
// backdropService.submit(movie)
val discoveredItem = DiscoverItem(movie)
backdropService.submit(discoveredItem)
withContext(Dispatchers.Main) {
loading.value = LoadingState.Success

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@ -23,16 +24,21 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.api.seerr.MediaApi
import com.github.damontecres.wholphin.data.model.DiscoverItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SeerrService
import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard
import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberPosition
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.tryRequestFocus
import dagger.hilt.android.lifecycle.HiltViewModel
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.api.BaseItemKind
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
@ -42,6 +48,7 @@ class SeerrDiscoverViewModel
private val seerrService: SeerrService,
val navigationManager: NavigationManager,
private val api: ApiClient,
private val backdropService: BackdropService,
) : ViewModel() {
val recentlyAdded = MutableLiveData<List<DiscoverItem>>(listOf())
val discoverMovies = MutableLiveData<List<DiscoverItem>>(listOf())
@ -49,7 +56,7 @@ class SeerrDiscoverViewModel
init {
viewModelScope.launchIO {
backdropService.clearBackdrop()
val tv =
seerrService.api.mediaApi
.mediaGet(
@ -69,6 +76,14 @@ class SeerrDiscoverViewModel
discoverTv.setValueOnMain(tv)
}
}
fun updateBackdrop(item: DiscoverItem?) {
viewModelScope.launchIO {
if (item != null) {
backdropService.submit("discover_${item.id}", item.backDropUrl)
}
}
}
}
@Composable
@ -83,6 +98,16 @@ fun SeerrDiscoverPage(
val focusRequester = remember { FocusRequester() }
LaunchedEffect(movies) { if (movies.isNotEmpty()) focusRequester.tryRequestFocus() }
val scrollState = rememberScrollState()
var position by rememberPosition(0, 0)
LaunchedEffect(position) {
position.let {
val item = if (it.row == 0) movies.getOrNull(it.column) else tv.getOrNull(it.column)
Timber.v("Backdrop for $item")
viewModel.updateBackdrop(item)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier =
@ -94,7 +119,16 @@ fun SeerrDiscoverPage(
title = stringResource(R.string.movies),
items = movies,
onClickItem = { index, item ->
if (item.jellyfinItemId != null) {
viewModel.navigationManager.navigateTo(
Destination.MediaItem(
itemId = item.jellyfinItemId,
type = BaseItemKind.MOVIE,
),
)
} else {
viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item))
}
},
onLongClickItem = { index, item -> },
cardContent = { index: Int, item: DiscoverItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
@ -106,13 +140,27 @@ fun SeerrDiscoverPage(
modifier = mod,
)
},
cardOnFocus = { isFocused, index ->
if (isFocused) {
position = RowColumn(0, index)
}
},
modifier = Modifier.focusRequester(focusRequester),
)
ItemRow(
title = stringResource(R.string.tv_shows),
items = tv,
onClickItem = { index, item ->
if (item.jellyfinItemId != null) {
viewModel.navigationManager.navigateTo(
Destination.MediaItem(
itemId = item.jellyfinItemId,
type = BaseItemKind.SERIES,
),
)
} else {
viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item))
}
},
onLongClickItem = { index, item -> },
cardContent = { index: Int, item: DiscoverItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
@ -124,6 +172,11 @@ fun SeerrDiscoverPage(
modifier = mod,
)
},
cardOnFocus = { isFocused, index ->
if (isFocused) {
position = RowColumn(1, index)
}
},
)
}
}