Fixes a few discover page UI bugs (#710)

## Description
- Fixes when to show/hide Request & Cancel buttons
- Removes the More button
- Uses a larger 16x9 backdrop so that the fade is better
- Fix reloading the backdrop on series pages

### Related issues
Fixes #706
This commit is contained in:
Ray 2026-01-16 13:43:59 -05:00 committed by GitHub
parent ccd8c80d5e
commit 03b332e4c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 73 additions and 47 deletions

View file

@ -91,7 +91,7 @@ data class DiscoverItem(
override val playable: Boolean = false override val playable: Boolean = false
override val sortName: String get() = title ?: "" override val sortName: String get() = title ?: ""
val backDropUrl: String? get() = backdropPath?.let { "https://image.tmdb.org/t/p/w1920_and_h800_multi_faces$it" } val backDropUrl: String? get() = backdropPath?.let { "https://image.tmdb.org/t/p/w1920_and_h1080_multi_faces$it" }
val posterUrl: String? get() = posterPath?.let { "https://image.tmdb.org/t/p/w500$it" } val posterUrl: String? get() = posterPath?.let { "https://image.tmdb.org/t/p/w500$it" }
val destination: Destination val destination: Destination

View file

@ -94,6 +94,7 @@ fun DiscoverMovieDetails(
val loading by viewModel.loading.observeAsState(LoadingState.Loading) val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val userConfig by viewModel.userConfig.collectAsState(null) val userConfig by viewModel.userConfig.collectAsState(null)
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false) val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
val canCancel by viewModel.canCancelRequest.collectAsState()
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) } var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
@ -127,6 +128,7 @@ fun DiscoverMovieDetails(
movie = movie, movie = movie,
userConfig = userConfig, userConfig = userConfig,
rating = rating, rating = rating,
canCancel = canCancel,
people = people, people = people,
trailers = trailers, trailers = trailers,
similar = similar, similar = similar,
@ -234,6 +236,7 @@ fun DiscoverMovieDetailsContent(
userConfig: SeerrUserConfig?, userConfig: SeerrUserConfig?,
movie: MovieDetails, movie: MovieDetails,
rating: DiscoverRating?, rating: DiscoverRating?,
canCancel: Boolean,
people: List<DiscoverItem>, people: List<DiscoverItem>,
trailers: List<Trailer>, trailers: List<Trailer>,
similar: List<DiscoverItem>, similar: List<DiscoverItem>,
@ -284,15 +287,6 @@ fun DiscoverMovieDetailsContent(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 32.dp, bottom = 16.dp), .padding(top = 32.dp, bottom = 16.dp),
) )
val canCancel =
remember(movie, userConfig) {
(
// User requested this
userConfig.hasPermission(SeerrPermission.REQUEST) &&
movie.mediaInfo?.requests?.any { it.requestedBy?.id == userConfig?.id } == true
) ||
userConfig.hasPermission(SeerrPermission.MANAGE_REQUESTS)
}
ExpandableDiscoverButtons( ExpandableDiscoverButtons(
availability = availability =
SeerrAvailability.from(movie.mediaInfo?.status) SeerrAvailability.from(movie.mediaInfo?.status)

View file

@ -4,6 +4,7 @@ import android.content.Context
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.MediaRequest
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
import com.github.damontecres.wholphin.api.seerr.model.RelatedVideo import com.github.damontecres.wholphin.api.seerr.model.RelatedVideo
import com.github.damontecres.wholphin.api.seerr.model.RequestPostRequest import com.github.damontecres.wholphin.api.seerr.model.RequestPostRequest
@ -11,11 +12,14 @@ import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.data.model.DiscoverItem
import com.github.damontecres.wholphin.data.model.DiscoverRating import com.github.damontecres.wholphin.data.model.DiscoverRating
import com.github.damontecres.wholphin.data.model.RemoteTrailer import com.github.damontecres.wholphin.data.model.RemoteTrailer
import com.github.damontecres.wholphin.data.model.SeerrPermission
import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.data.model.hasPermission
import com.github.damontecres.wholphin.services.BackdropService 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.SeerrServerRepository import com.github.damontecres.wholphin.services.SeerrServerRepository
import com.github.damontecres.wholphin.services.SeerrService import com.github.damontecres.wholphin.services.SeerrService
import com.github.damontecres.wholphin.services.SeerrUserConfig
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
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
@ -31,7 +35,10 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -62,6 +69,7 @@ class DiscoverMovieViewModel
val people = MutableLiveData<List<DiscoverItem>>(listOf()) val people = MutableLiveData<List<DiscoverItem>>(listOf())
val similar = MutableLiveData<List<DiscoverItem>>(listOf()) val similar = MutableLiveData<List<DiscoverItem>>(listOf())
val recommended = MutableLiveData<List<DiscoverItem>>(listOf()) val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
val canCancelRequest = MutableStateFlow(false)
val userConfig = seerrServerRepository.current.map { it?.config } val userConfig = seerrServerRepository.current.map { it?.config }
val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false } val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false }
@ -95,6 +103,8 @@ class DiscoverMovieViewModel
val discoveredItem = DiscoverItem(movie) val discoveredItem = DiscoverItem(movie)
backdropService.submit(discoveredItem) backdropService.submit(discoveredItem)
updateCanCancel()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
loading.value = LoadingState.Success loading.value = LoadingState.Success
} }
@ -142,6 +152,12 @@ class DiscoverMovieViewModel
this@DiscoverMovieViewModel.trailers.setValueOnMain(trailers) this@DiscoverMovieViewModel.trailers.setValueOnMain(trailers)
} }
private suspend fun updateCanCancel() {
val user = userConfig.firstOrNull()
val canCancel = canUserCancelRequest(user, movie.value?.mediaInfo?.requests)
canCancelRequest.update { canCancel }
}
fun navigateTo(destination: Destination) { fun navigateTo(destination: Destination) {
navigationManager.navigateTo(destination) navigationManager.navigateTo(destination)
} }
@ -160,6 +176,7 @@ class DiscoverMovieViewModel
), ),
) )
fetchAndSetItem().await() fetchAndSetItem().await()
updateCanCancel()
} }
} }
@ -169,7 +186,18 @@ class DiscoverMovieViewModel
// TODO handle multiple requests? Or just delete self's request? // TODO handle multiple requests? Or just delete self's request?
seerrService.api.requestApi.requestRequestIdDelete(it.id.toString()) seerrService.api.requestApi.requestRequestIdDelete(it.id.toString())
fetchAndSetItem().await() fetchAndSetItem().await()
updateCanCancel()
} }
} }
} }
} }
fun canUserCancelRequest(
user: SeerrUserConfig?,
requests: List<MediaRequest>?,
) = user.hasPermission(SeerrPermission.MANAGE_REQUESTS) ||
(
// User requested this
user.hasPermission(SeerrPermission.REQUEST) &&
requests?.any { it.requestedBy?.id == user?.id } == true
)

View file

@ -93,6 +93,7 @@ fun DiscoverSeriesDetails(
val recommended by viewModel.recommended.observeAsState(listOf()) val recommended by viewModel.recommended.observeAsState(listOf())
val userConfig by viewModel.userConfig.collectAsState(null) val userConfig by viewModel.userConfig.collectAsState(null)
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false) val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
val canCancel by viewModel.canCancelRequest.collectAsState()
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) } var seasonDialog by remember { mutableStateOf<DialogParams?>(null) }
@ -120,6 +121,7 @@ fun DiscoverSeriesDetails(
series = item, series = item,
userConfig = userConfig, userConfig = userConfig,
rating = rating, rating = rating,
canCancel = canCancel,
seasons = seasons, seasons = seasons,
people = people, people = people,
similar = similar, similar = similar,
@ -230,6 +232,7 @@ fun DiscoverSeriesDetailsContent(
userConfig: SeerrUserConfig?, userConfig: SeerrUserConfig?,
series: TvDetails, series: TvDetails,
rating: DiscoverRating?, rating: DiscoverRating?,
canCancel: Boolean,
seasons: List<Season>, seasons: List<Season>,
similar: List<DiscoverItem>, similar: List<DiscoverItem>,
recommended: List<DiscoverItem>, recommended: List<DiscoverItem>,
@ -290,15 +293,6 @@ fun DiscoverSeriesDetailsContent(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 32.dp, bottom = 16.dp), .padding(top = 32.dp, bottom = 16.dp),
) )
val canCancel =
remember(series, userConfig) {
(
// User requested this
userConfig.hasPermission(SeerrPermission.REQUEST) &&
series.mediaInfo?.requests?.any { it.requestedBy?.id == userConfig?.id } == true
) ||
userConfig.hasPermission(SeerrPermission.MANAGE_REQUESTS)
}
ExpandableDiscoverButtons( ExpandableDiscoverButtons(
availability = availability =
SeerrAvailability.from(series.mediaInfo?.status) SeerrAvailability.from(series.mediaInfo?.status)

View file

@ -29,7 +29,10 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -61,6 +64,7 @@ class DiscoverSeriesViewModel
val people = MutableLiveData<List<DiscoverItem>>(listOf()) val people = MutableLiveData<List<DiscoverItem>>(listOf())
val similar = MutableLiveData<List<DiscoverItem>>(listOf()) val similar = MutableLiveData<List<DiscoverItem>>(listOf())
val recommended = MutableLiveData<List<DiscoverItem>>(listOf()) val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
val canCancelRequest = MutableStateFlow(false)
val userConfig = seerrServerRepository.current.map { it?.config } val userConfig = seerrServerRepository.current.map { it?.config }
val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false } val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false }
@ -94,6 +98,8 @@ class DiscoverSeriesViewModel
val discoveredItem = DiscoverItem(tv) val discoveredItem = DiscoverItem(tv)
backdropService.submit(discoveredItem) backdropService.submit(discoveredItem)
updateCanCancel()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
loading.value = LoadingState.Success loading.value = LoadingState.Success
} }
@ -137,6 +143,12 @@ class DiscoverSeriesViewModel
navigationManager.navigateTo(destination) navigationManager.navigateTo(destination)
} }
private suspend fun updateCanCancel() {
val user = userConfig.firstOrNull()
val canCancel = canUserCancelRequest(user, tvSeries.value?.mediaInfo?.requests)
canCancelRequest.update { canCancel }
}
fun request( fun request(
id: Int, id: Int,
is4k: Boolean, is4k: Boolean,
@ -152,6 +164,7 @@ class DiscoverSeriesViewModel
), ),
) )
fetchAndSetItem().await() fetchAndSetItem().await()
updateCanCancel()
} }
} }
@ -161,6 +174,7 @@ class DiscoverSeriesViewModel
// TODO handle multiple requests? Or just delete self's request? // TODO handle multiple requests? Or just delete self's request?
seerrService.api.requestApi.requestRequestIdDelete(it.id.toString()) seerrService.api.requestApi.requestRequestIdDelete(it.id.toString())
fetchAndSetItem().await() fetchAndSetItem().await()
updateCanCancel()
} }
} }
} }

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -140,15 +139,16 @@ fun ExpandableDiscoverButtons(
} }
// More button // More button
item("more") { // No functionality yet
ExpandablePlayButton( // item("more") {
R.string.more, // ExpandablePlayButton(
Duration.ZERO, // R.string.more,
Icons.Default.MoreVert, // Duration.ZERO,
{ moreOnClick.invoke() }, // Icons.Default.MoreVert,
Modifier // { moreOnClick.invoke() },
.onFocusChanged(buttonOnFocusChanged), // Modifier
) // .onFocusChanged(buttonOnFocusChanged),
} // )
// }
} }
} }

View file

@ -131,10 +131,8 @@ fun SeriesDetails(
LoadingState.Success -> { LoadingState.Success -> {
item?.let { item -> item?.let { item ->
LifecycleResumeEffect(destination.itemId) { LifecycleResumeEffect(destination.itemId) {
viewModel.maybePlayThemeSong( viewModel.onResumePage()
destination.itemId,
preferences.appPreferences.interfacePreferences.playThemeSongs,
)
onPauseOrDispose { onPauseOrDispose {
viewModel.release() viewModel.release()
} }

View file

@ -170,10 +170,8 @@ fun SeriesOverview(
"series_overview", "series_overview",
) )
LifecycleResumeEffect(destination.itemId) { LifecycleResumeEffect(destination.itemId) {
viewModel.maybePlayThemeSong( viewModel.onResumePage()
destination.itemId,
preferences.appPreferences.interfacePreferences.playThemeSongs,
)
onPauseOrDispose { onPauseOrDispose {
viewModel.release() viewModel.release()
} }

View file

@ -13,7 +13,6 @@ import com.github.damontecres.wholphin.data.model.DiscoverItem
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.ExtrasService import com.github.damontecres.wholphin.services.ExtrasService
import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.FavoriteWatchManager
@ -222,15 +221,16 @@ class SeriesViewModel
} }
} }
/** fun onResumePage() {
* If the series has a theme song & app settings allow, play it
*/
fun maybePlayThemeSong(
seriesId: UUID,
playThemeSongs: ThemeSongVolume,
) {
viewModelScope.launchIO { viewModelScope.launchIO {
themeSongPlayer.playThemeFor(seriesId, playThemeSongs) item.value?.let {
backdropService.submit(it)
val playThemeSongs =
userPreferencesService
.getCurrent()
.appPreferences.interfacePreferences.playThemeSongs
themeSongPlayer.playThemeFor(seriesId, playThemeSongs)
}
} }
} }