mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Add backdrops to discover pages
This commit is contained in:
parent
f27415de0f
commit
22348a5adc
5 changed files with 111 additions and 22 deletions
|
|
@ -1,11 +1,18 @@
|
||||||
|
@file:UseSerializers(UUIDSerializer::class)
|
||||||
|
|
||||||
package com.github.damontecres.wholphin.data.model
|
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.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 com.github.damontecres.wholphin.services.SeerrSearchResult
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
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
|
@Serializable
|
||||||
enum class SeerrItemType {
|
enum class SeerrItemType {
|
||||||
|
|
@ -59,7 +66,11 @@ data class DiscoverItem(
|
||||||
val releaseDate: String?,
|
val releaseDate: String?,
|
||||||
val posterPath: String?,
|
val posterPath: String?,
|
||||||
val backdropPath: 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(
|
constructor(movie: MovieResult) : this(
|
||||||
id = movie.id,
|
id = movie.id,
|
||||||
type = SeerrItemType.MOVIE,
|
type = SeerrItemType.MOVIE,
|
||||||
|
|
@ -68,6 +79,18 @@ data class DiscoverItem(
|
||||||
releaseDate = movie.releaseDate,
|
releaseDate = movie.releaseDate,
|
||||||
posterPath = movie.posterPath,
|
posterPath = movie.posterPath,
|
||||||
backdropPath = movie.backdropPath,
|
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(
|
constructor(tv: TvResult) : this(
|
||||||
|
|
@ -78,6 +101,7 @@ data class DiscoverItem(
|
||||||
releaseDate = tv.firstAirDate,
|
releaseDate = tv.firstAirDate,
|
||||||
posterPath = tv.posterPath,
|
posterPath = tv.posterPath,
|
||||||
backdropPath = tv.backdropPath,
|
backdropPath = tv.backdropPath,
|
||||||
|
jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(search: SeerrSearchResult) : this(
|
constructor(search: SeerrSearchResult) : this(
|
||||||
|
|
@ -90,6 +114,7 @@ data class DiscoverItem(
|
||||||
releaseDate = search.releaseDate ?: search.firstAirDate,
|
releaseDate = search.releaseDate ?: search.firstAirDate,
|
||||||
posterPath = search.posterPath,
|
posterPath = search.posterPath,
|
||||||
backdropPath = search.backdropPath,
|
backdropPath = search.backdropPath,
|
||||||
|
jellyfinItemId = search.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import coil3.request.SuccessResult
|
||||||
import coil3.request.allowHardware
|
import coil3.request.allowHardware
|
||||||
import coil3.request.bitmapConfig
|
import coil3.request.bitmapConfig
|
||||||
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.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.BackdropStyle
|
import com.github.damontecres.wholphin.preferences.BackdropStyle
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
|
@ -27,7 +28,6 @@ import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -47,15 +47,24 @@ class BackdropService
|
||||||
|
|
||||||
suspend fun submit(item: BaseItem) =
|
suspend fun submit(item: BaseItem) =
|
||||||
withContext(Dispatchers.IO) {
|
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) {
|
if (backdropFlow.firstOrNull()?.imageUrl != imageUrl) {
|
||||||
_backdropFlow.update {
|
_backdropFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
itemId = item.id,
|
itemId = itemId,
|
||||||
imageUrl = null,
|
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)
|
delay(500)
|
||||||
val imageUrl = imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
|
|
||||||
val backdropStyle =
|
val backdropStyle =
|
||||||
preferences.data
|
preferences.data
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
|
|
@ -83,9 +94,9 @@ class BackdropService
|
||||||
ExtractedColors.DEFAULT
|
ExtractedColors.DEFAULT
|
||||||
}
|
}
|
||||||
_backdropFlow.update {
|
_backdropFlow.update {
|
||||||
if (it.itemId == item.id) {
|
if (it.itemId == itemId) {
|
||||||
BackdropResult(
|
BackdropResult(
|
||||||
itemId = item.id,
|
itemId = itemId,
|
||||||
imageUrl = imageUrl,
|
imageUrl = imageUrl,
|
||||||
primaryColor = primaryColor,
|
primaryColor = primaryColor,
|
||||||
secondaryColor = secondaryColor,
|
secondaryColor = secondaryColor,
|
||||||
|
|
@ -208,7 +219,7 @@ class BackdropService
|
||||||
}
|
}
|
||||||
|
|
||||||
data class BackdropResult(
|
data class BackdropResult(
|
||||||
val itemId: UUID?,
|
val itemId: String?,
|
||||||
val imageUrl: String?,
|
val imageUrl: String?,
|
||||||
val primaryColor: Color = Color.Unspecified,
|
val primaryColor: Color = Color.Unspecified,
|
||||||
val secondaryColor: Color = Color.Unspecified,
|
val secondaryColor: Color = Color.Unspecified,
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ fun DiscoverItemCard(
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
ItemCardImage(
|
ItemCardImage(
|
||||||
imageUrl = item?.posterPath?.let { "https://image.tmdb.org/t/p/w500$it" },
|
imageUrl = item?.posterUrl,
|
||||||
name = item?.title,
|
name = item?.title,
|
||||||
showOverlay = false,
|
showOverlay = false,
|
||||||
favorite = false,
|
favorite = false,
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,8 @@ 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"
|
val discoveredItem = DiscoverItem(movie)
|
||||||
// backdropService.submit(movie)
|
backdropService.submit(discoveredItem)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
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.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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.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
|
||||||
|
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.cards.DiscoverItemCard
|
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.data.RowColumn
|
||||||
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.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -42,6 +48,7 @@ class SeerrDiscoverViewModel
|
||||||
private val seerrService: SeerrService,
|
private val seerrService: SeerrService,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
|
private val backdropService: BackdropService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val recentlyAdded = MutableLiveData<List<DiscoverItem>>(listOf())
|
val recentlyAdded = MutableLiveData<List<DiscoverItem>>(listOf())
|
||||||
val discoverMovies = MutableLiveData<List<DiscoverItem>>(listOf())
|
val discoverMovies = MutableLiveData<List<DiscoverItem>>(listOf())
|
||||||
|
|
@ -49,7 +56,7 @@ class SeerrDiscoverViewModel
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
backdropService.clearBackdrop()
|
||||||
val tv =
|
val tv =
|
||||||
seerrService.api.mediaApi
|
seerrService.api.mediaApi
|
||||||
.mediaGet(
|
.mediaGet(
|
||||||
|
|
@ -69,6 +76,14 @@ class SeerrDiscoverViewModel
|
||||||
discoverTv.setValueOnMain(tv)
|
discoverTv.setValueOnMain(tv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateBackdrop(item: DiscoverItem?) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
if (item != null) {
|
||||||
|
backdropService.submit("discover_${item.id}", item.backDropUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -83,6 +98,16 @@ fun SeerrDiscoverPage(
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(movies) { if (movies.isNotEmpty()) focusRequester.tryRequestFocus() }
|
LaunchedEffect(movies) { if (movies.isNotEmpty()) focusRequester.tryRequestFocus() }
|
||||||
val scrollState = rememberScrollState()
|
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(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -94,7 +119,16 @@ fun SeerrDiscoverPage(
|
||||||
title = stringResource(R.string.movies),
|
title = stringResource(R.string.movies),
|
||||||
items = movies,
|
items = movies,
|
||||||
onClickItem = { index, item ->
|
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))
|
viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLongClickItem = { index, item -> },
|
onLongClickItem = { index, item -> },
|
||||||
cardContent = { index: Int, item: DiscoverItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
|
cardContent = { index: Int, item: DiscoverItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
|
||||||
|
|
@ -106,13 +140,27 @@ fun SeerrDiscoverPage(
|
||||||
modifier = mod,
|
modifier = mod,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
cardOnFocus = { isFocused, index ->
|
||||||
|
if (isFocused) {
|
||||||
|
position = RowColumn(0, index)
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
)
|
)
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = stringResource(R.string.tv_shows),
|
title = stringResource(R.string.tv_shows),
|
||||||
items = tv,
|
items = tv,
|
||||||
onClickItem = { index, item ->
|
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))
|
viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLongClickItem = { index, item -> },
|
onLongClickItem = { index, item -> },
|
||||||
cardContent = { index: Int, item: DiscoverItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
|
cardContent = { index: Int, item: DiscoverItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
|
||||||
|
|
@ -124,6 +172,11 @@ fun SeerrDiscoverPage(
|
||||||
modifier = mod,
|
modifier = mod,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
cardOnFocus = { isFocused, index ->
|
||||||
|
if (isFocused) {
|
||||||
|
position = RowColumn(1, index)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue