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 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 destination: Destination

View file

@ -94,6 +94,7 @@ fun DiscoverMovieDetails(
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val userConfig by viewModel.userConfig.collectAsState(null)
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
val canCancel by viewModel.canCancelRequest.collectAsState()
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
@ -127,6 +128,7 @@ fun DiscoverMovieDetails(
movie = movie,
userConfig = userConfig,
rating = rating,
canCancel = canCancel,
people = people,
trailers = trailers,
similar = similar,
@ -234,6 +236,7 @@ fun DiscoverMovieDetailsContent(
userConfig: SeerrUserConfig?,
movie: MovieDetails,
rating: DiscoverRating?,
canCancel: Boolean,
people: List<DiscoverItem>,
trailers: List<Trailer>,
similar: List<DiscoverItem>,
@ -284,15 +287,6 @@ fun DiscoverMovieDetailsContent(
.fillMaxWidth()
.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(
availability =
SeerrAvailability.from(movie.mediaInfo?.status)

View file

@ -4,6 +4,7 @@ import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
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.RelatedVideo
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.DiscoverRating
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.hasPermission
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SeerrServerRepository
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.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
@ -31,7 +35,10 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
@ -62,6 +69,7 @@ class DiscoverMovieViewModel
val people = MutableLiveData<List<DiscoverItem>>(listOf())
val similar = MutableLiveData<List<DiscoverItem>>(listOf())
val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
val canCancelRequest = MutableStateFlow(false)
val userConfig = seerrServerRepository.current.map { it?.config }
val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false }
@ -95,6 +103,8 @@ class DiscoverMovieViewModel
val discoveredItem = DiscoverItem(movie)
backdropService.submit(discoveredItem)
updateCanCancel()
withContext(Dispatchers.Main) {
loading.value = LoadingState.Success
}
@ -142,6 +152,12 @@ class DiscoverMovieViewModel
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) {
navigationManager.navigateTo(destination)
}
@ -160,6 +176,7 @@ class DiscoverMovieViewModel
),
)
fetchAndSetItem().await()
updateCanCancel()
}
}
@ -169,7 +186,18 @@ class DiscoverMovieViewModel
// TODO handle multiple requests? Or just delete self's request?
seerrService.api.requestApi.requestRequestIdDelete(it.id.toString())
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 userConfig by viewModel.userConfig.collectAsState(null)
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
val canCancel by viewModel.canCancelRequest.collectAsState()
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) }
@ -120,6 +121,7 @@ fun DiscoverSeriesDetails(
series = item,
userConfig = userConfig,
rating = rating,
canCancel = canCancel,
seasons = seasons,
people = people,
similar = similar,
@ -230,6 +232,7 @@ fun DiscoverSeriesDetailsContent(
userConfig: SeerrUserConfig?,
series: TvDetails,
rating: DiscoverRating?,
canCancel: Boolean,
seasons: List<Season>,
similar: List<DiscoverItem>,
recommended: List<DiscoverItem>,
@ -290,15 +293,6 @@ fun DiscoverSeriesDetailsContent(
.fillMaxWidth()
.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(
availability =
SeerrAvailability.from(series.mediaInfo?.status)

View file

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

View file

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

View file

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

View file

@ -170,10 +170,8 @@ fun SeriesOverview(
"series_overview",
)
LifecycleResumeEffect(destination.itemId) {
viewModel.maybePlayThemeSong(
destination.itemId,
preferences.appPreferences.interfacePreferences.playThemeSongs,
)
viewModel.onResumePage()
onPauseOrDispose {
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.Person
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.ExtrasService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
@ -222,17 +221,18 @@ class SeriesViewModel
}
}
/**
* If the series has a theme song & app settings allow, play it
*/
fun maybePlayThemeSong(
seriesId: UUID,
playThemeSongs: ThemeSongVolume,
) {
fun onResumePage() {
viewModelScope.launchIO {
item.value?.let {
backdropService.submit(it)
val playThemeSongs =
userPreferencesService
.getCurrent()
.appPreferences.interfacePreferences.playThemeSongs
themeSongPlayer.playThemeFor(seriesId, playThemeSongs)
}
}
}
fun release() {
themeSongPlayer.stop()