Better handling for Seerr request permissions & minor discover UI tweaks (#2084)

## Description
No longer assume that if a user has "Request 4K" then they must also
have regular "Request". Now the requesting dialogs will handle:
- User has only "Request" (including just "Request Movie" vs "Request
TV")
- User has only "Request 4K"
- User has both

Also fixes an issue where the request button would be disabled if a
previous request was declined.

And also because it helped me with testing this and is useful, the
dialog in settings to remove a Seerr server now also shows the username
in addition to URL.

### Related issues

https://github.com/damontecres/Wholphin/issues/702#issuecomment-6132914691

### Testing
Manually on the emulator with the test cases above. I also tested with
and without "Advanced Requests" permission

## Screenshots
N/A, the dialog change is minor

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-10-19 13:35:51 -04:00 • committed by GitHub
parent ebf44699ca
commit e273195e93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 170 additions and 70 deletions

View file

@ -54,3 +54,11 @@ fun SeerrUserConfig?.hasPermission(permission: SeerrPermission): Boolean {
permission.parents.any { hasPermission(it) } || permission.parents.any { hasPermission(it) } ||
SeerrPermission.ADMIN.hasPermission(permissions) SeerrPermission.ADMIN.hasPermission(permissions)
} }
val SeerrUserConfig?.canRequestMovies: Boolean
get() =
hasPermission(SeerrPermission.REQUEST_MOVIE) || hasPermission(SeerrPermission.REQUEST_4K_MOVIE)
val SeerrUserConfig?.canRequestTv: Boolean
get() =
hasPermission(SeerrPermission.REQUEST_TV) || hasPermission(SeerrPermission.REQUEST_4K_TV)

View file

@ -205,11 +205,15 @@ data class CurrentSeerr(
val config: SeerrUserConfig, val config: SeerrUserConfig,
val serverConfig: PublicSettings, val serverConfig: PublicSettings,
) { ) {
val requestMovieEnabled: Boolean
get() = config.hasPermission(SeerrPermission.REQUEST_MOVIE)
val request4kMovieEnabled: Boolean val request4kMovieEnabled: Boolean
get() = get() =
(serverConfig.movie4kEnabled ?: false) && (serverConfig.movie4kEnabled ?: false) &&
config.hasPermission(SeerrPermission.REQUEST_4K_MOVIE) config.hasPermission(SeerrPermission.REQUEST_4K_MOVIE)
val requestTvEnabled: Boolean
get() = config.hasPermission(SeerrPermission.REQUEST_TV)
val request4kTvEnabled: Boolean val request4kTvEnabled: Boolean
get() = get() =
(serverConfig.series4kEnabled ?: false) && (serverConfig.series4kEnabled ?: false) &&

View file

@ -231,28 +231,35 @@ fun DialogPopup(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
properties = properties, properties = properties,
) { ) {
val elevatedContainerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
DialogPopupContent( DialogPopupContent(
title = title, title = title,
dialogItems = dialogItems, dialogItems = dialogItems,
waiting = waiting, waiting = waiting,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
dismissOnClick = dismissOnClick, dismissOnClick = dismissOnClick,
elevation = elevation,
modifier = modifier =
Modifier.onKeyEvent { event -> Modifier
val code = event.nativeKeyEvent.keyCode .onKeyEvent { event ->
if (event.nativeKeyEvent.action == KeyEvent.ACTION_UP && val code = event.nativeKeyEvent.keyCode
code in if (event.nativeKeyEvent.action == KeyEvent.ACTION_UP &&
setOf( code in
KeyEvent.KEYCODE_ENTER, setOf(
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_DPAD_CENTER,
) KeyEvent.KEYCODE_NUMPAD_ENTER,
) { )
waiting = false ) {
} waiting = false
false }
}, false
}.shadow(elevation = elevation, shape = RoundedCornerShape(28.0.dp))
.graphicsLayer {
this.clip = true
this.shape = RoundedCornerShape(28.0.dp)
}.drawBehind { drawRect(color = elevatedContainerColor) }
.padding(PaddingValues(24.dp)),
) )
} }
} }
@ -266,20 +273,10 @@ fun DialogPopupContent(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
dismissOnClick: Boolean = true, dismissOnClick: Boolean = true,
elevation: Dp = 8.dp,
) { ) {
val elevatedContainerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier = modifier,
modifier
.shadow(elevation = elevation, shape = RoundedCornerShape(28.0.dp))
.graphicsLayer {
this.clip = true
this.shape = RoundedCornerShape(28.0.dp)
}.drawBehind { drawRect(color = elevatedContainerColor) }
.padding(PaddingValues(24.dp)),
) { ) {
Text( Text(
text = title, text = title,

View file

@ -42,9 +42,8 @@ import com.github.damontecres.wholphin.data.model.DiscoverRating
import com.github.damontecres.wholphin.data.model.LocalTrailer import com.github.damontecres.wholphin.data.model.LocalTrailer
import com.github.damontecres.wholphin.data.model.RemoteTrailer import com.github.damontecres.wholphin.data.model.RemoteTrailer
import com.github.damontecres.wholphin.data.model.SeerrAvailability import com.github.damontecres.wholphin.data.model.SeerrAvailability
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.data.model.canRequestMovies
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.SeerrUserConfig import com.github.damontecres.wholphin.services.SeerrUserConfig
import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.services.TrailerService
@ -84,14 +83,12 @@ fun DiscoverMovieDetails(
} }
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val userConfig by viewModel.userConfig.collectAsState(null) val userConfig by viewModel.userConfig.collectAsState(null)
val requestEnabled by viewModel.requestEnabled.collectAsState(false)
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false) val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showRequestDialog by remember { mutableStateOf(false) } var showRequestDialog by remember { mutableStateOf(false) }
val requestStr = stringResource(R.string.request)
val request4kStr = stringResource(R.string.request_4k)
when (val st = state.movie) { when (val st = state.movie) {
is DataLoadingState.Error -> { is DataLoadingState.Error -> {
ErrorMessage(st, modifier) ErrorMessage(st, modifier)
@ -153,6 +150,7 @@ fun DiscoverMovieDetails(
RequestMovieDialog( RequestMovieDialog(
loading = state.profileLoading, loading = state.profileLoading,
data = state.requestData, data = state.requestData,
requestEnabled = requestEnabled,
request4kEnabled = request4kEnabled, request4kEnabled = request4kEnabled,
movie = movie, movie = movie,
onSubmit = { onSubmit = {
@ -253,7 +251,7 @@ fun DiscoverMovieDetailsContent(
} }
} }
}, },
canRequest = userConfig.hasPermission(SeerrPermission.REQUEST), canRequest = userConfig.canRequestMovies,
canCancel = canCancel, canCancel = canCancel,
trailers = trailers, trailers = trailers,
trailerOnClick = trailerOnClick, trailerOnClick = trailerOnClick,

View file

@ -72,6 +72,8 @@ class DiscoverMovieViewModel
val state: StateFlow<DiscoverMovieState> = _state val state: StateFlow<DiscoverMovieState> = _state
val userConfig = seerrServerRepository.current.map { it?.config } val userConfig = seerrServerRepository.current.map { it?.config }
val requestEnabled =
seerrServerRepository.current.map { it?.requestMovieEnabled ?: false }
val request4kEnabled = val request4kEnabled =
seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false } seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false }
@ -191,7 +193,7 @@ class DiscoverMovieViewModel
return@launchIO return@launchIO
} }
try { try {
seerrService.api.requestApi.requestPost( val postRequest =
RequestPostRequest( RequestPostRequest(
is4k = request.is4k, is4k = request.is4k,
mediaId = request.movieId, mediaId = request.movieId,
@ -205,8 +207,9 @@ class DiscoverMovieViewModel
profileId = request.profileId, profileId = request.profileId,
rootFolder = request.folder, rootFolder = request.folder,
tags = emptyList(), tags = emptyList(),
), )
) Timber.v("postRequest=%s", postRequest)
seerrService.api.requestApi.requestPost(postRequest)
} catch (ex: CancellationException) { } catch (ex: CancellationException) {
throw ex throw ex
} catch (ex: Exception) { } catch (ex: Exception) {

View file

@ -36,9 +36,8 @@ import com.github.damontecres.wholphin.data.model.BaseItem
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.SeerrAvailability import com.github.damontecres.wholphin.data.model.SeerrAvailability
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.data.model.canRequestTv
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.SeerrUserConfig import com.github.damontecres.wholphin.services.SeerrUserConfig
import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.services.TrailerService
@ -87,6 +86,7 @@ fun DiscoverSeriesDetails(
val context = LocalContext.current val context = LocalContext.current
val resources = LocalResources.current val resources = LocalResources.current
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val requestEnabled by viewModel.requestEnabled.collectAsState()
val request4kEnabled by viewModel.request4kEnabled.collectAsState() val request4kEnabled by viewModel.request4kEnabled.collectAsState()
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
@ -111,6 +111,7 @@ fun DiscoverSeriesDetails(
DiscoverSeriesDetailsContent( DiscoverSeriesDetailsContent(
preferences = preferences, preferences = preferences,
series = item, series = item,
availability = state.availability,
userConfig = userConfig, userConfig = userConfig,
rating = state.rating, rating = state.rating,
canCancel = state.canCancelRequest, canCancel = state.canCancelRequest,
@ -187,6 +188,7 @@ fun DiscoverSeriesDetails(
title = state.tvSeries.successValue?.name ?: "", title = state.tvSeries.successValue?.name ?: "",
seasons = state.seasons, seasons = state.seasons,
seasons4k = state.seasons4k, seasons4k = state.seasons4k,
requestEnabled = requestEnabled,
request4kEnabled = request4kEnabled, request4kEnabled = request4kEnabled,
onSubmit = { onSubmit = {
showRequestSeasonDialog = false showRequestSeasonDialog = false
@ -211,6 +213,7 @@ fun DiscoverSeriesDetailsContent(
preferences: UserPreferences, preferences: UserPreferences,
userConfig: SeerrUserConfig?, userConfig: SeerrUserConfig?,
series: TvDetails, series: TvDetails,
availability: SeerrAvailability,
rating: DiscoverRating?, rating: DiscoverRating?,
canCancel: Boolean, canCancel: Boolean,
seasons: List<RequestSeason>, seasons: List<RequestSeason>,
@ -274,9 +277,7 @@ fun DiscoverSeriesDetailsContent(
.padding(top = HeaderUtils.topPadding, bottom = 16.dp), .padding(top = HeaderUtils.topPadding, bottom = 16.dp),
) )
ExpandableDiscoverButtons( ExpandableDiscoverButtons(
availability = availability = availability,
SeerrAvailability.from(series.mediaInfo?.status)
?: SeerrAvailability.UNKNOWN,
requestOnClick = requestOnClick, requestOnClick = requestOnClick,
pendingOnClick = requestOnClick, pendingOnClick = requestOnClick,
cancelOnClick = cancelOnClick, cancelOnClick = cancelOnClick,
@ -290,7 +291,7 @@ fun DiscoverSeriesDetailsContent(
} }
} }
}, },
canRequest = userConfig.hasPermission(SeerrPermission.REQUEST), canRequest = userConfig.canRequestTv,
canCancel = canCancel, canCancel = canCancel,
trailers = trailers, trailers = trailers,
trailerOnClick = trailerOnClick, trailerOnClick = trailerOnClick,

View file

@ -72,6 +72,10 @@ class DiscoverSeriesViewModel
val state: StateFlow<DiscoverSeriesState> = _state val state: StateFlow<DiscoverSeriesState> = _state
val userConfig = seerrServerRepository.current.map { it?.config } val userConfig = seerrServerRepository.current.map { it?.config }
val requestEnabled =
seerrServerRepository.current
.map { it?.requestTvEnabled ?: false }
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
val request4kEnabled = val request4kEnabled =
seerrServerRepository.current seerrServerRepository.current
.map { it?.request4kTvEnabled ?: false } .map { it?.request4kTvEnabled ?: false }
@ -105,7 +109,7 @@ class DiscoverSeriesViewModel
backdropService.submit(discoveredItem) backdropService.submit(discoveredItem)
updateSeasonStatus(tv) updateSeasonStatus(tv)
updateCanCancel() updateAvailability()
viewModelScope.launchIO { viewModelScope.launchIO {
val rating = val rating =
@ -185,6 +189,7 @@ class DiscoverSeriesViewModel
tv: TvDetails, tv: TvDetails,
is4k: Boolean, is4k: Boolean,
) { ) {
Timber.v("updateSeasonStatus: id=%s, is4k=%s", tv.id, is4k)
val currentUserId = seerrServerRepository.currentUserId.first() val currentUserId = seerrServerRepository.currentUserId.first()
val seasonStatus = mutableMapOf<Int, RequestStatus>() val seasonStatus = mutableMapOf<Int, RequestStatus>()
val seasonAvailability = mutableMapOf<Int, SeerrAvailability>() val seasonAvailability = mutableMapOf<Int, SeerrAvailability>()
@ -225,7 +230,10 @@ class DiscoverSeriesViewModel
seasonStatus[season.seasonNumber] = new seasonStatus[season.seasonNumber] = new
} }
editable[season.seasonNumber] = editable[season.seasonNumber] =
currentUserId == req.requestedBy?.id && req.status == RequestStatus.PENDING.status currentUserId == req.requestedBy?.id && (
req.status == RequestStatus.PENDING.status ||
req.status == RequestStatus.DECLINED.status
)
} }
} }
} }
@ -294,7 +302,7 @@ class DiscoverSeriesViewModel
} }
} }
private suspend fun updateCanCancel() { private suspend fun updateAvailability() {
val user = userConfig.firstOrNull() val user = userConfig.firstOrNull()
val canCancel = val canCancel =
canUserCancelRequest( canUserCancelRequest(
@ -303,7 +311,32 @@ class DiscoverSeriesViewModel
?.mediaInfo ?.mediaInfo
?.requests, ?.requests,
) )
_state.update { it.copy(canCancelRequest = canCancel) } val status =
state.value.tvSeries.successValue?.let { tv ->
val overallStatus = SeerrAvailability.from(tv.mediaInfo?.status)
if (overallStatus == SeerrAvailability.UNKNOWN) {
val userRequests =
tv.mediaInfo
?.requests
?.filter { it.requestedBy?.id == user?.id }
.orEmpty()
if (userRequests.any { RequestStatus.from(it.status) == RequestStatus.PENDING }) {
SeerrAvailability.PENDING
} else if (userRequests.any { RequestStatus.from(it.status) == RequestStatus.APPROVED }) {
SeerrAvailability.PROCESSING
} else {
SeerrAvailability.UNKNOWN
}
} else {
overallStatus
}
}
_state.update {
it.copy(
canCancelRequest = canCancel,
availability = status ?: SeerrAvailability.UNKNOWN,
)
}
} }
fun requestOnClick() { fun requestOnClick() {
@ -386,7 +419,7 @@ class DiscoverSeriesViewModel
fetchAndSetItem().await()?.let { fetchAndSetItem().await()?.let {
updateSeasonStatus(it) updateSeasonStatus(it)
updateCanCancel() updateAvailability()
} }
} }
} }
@ -407,7 +440,7 @@ class DiscoverSeriesViewModel
fetchAndSetItem().await()?.let { fetchAndSetItem().await()?.let {
updateSeasonStatus(it) updateSeasonStatus(it)
updateCanCancel() updateAvailability()
} }
} }
} }
@ -426,4 +459,5 @@ data class DiscoverSeriesState(
val canCancelRequest: Boolean = false, val canCancelRequest: Boolean = false,
val profileLoading: LoadingState = LoadingState.Pending, val profileLoading: LoadingState = LoadingState.Pending,
val requestData: SeerrRequestData = SeerrRequestData(), val requestData: SeerrRequestData = SeerrRequestData(),
val availability: SeerrAvailability = SeerrAvailability.UNKNOWN,
) )

View file

@ -43,6 +43,7 @@ import com.github.damontecres.wholphin.util.LoadingState
fun RequestMovieDialog( fun RequestMovieDialog(
loading: LoadingState, loading: LoadingState,
data: SeerrRequestData, data: SeerrRequestData,
requestEnabled: Boolean,
request4kEnabled: Boolean, request4kEnabled: Boolean,
movie: MovieDetails, movie: MovieDetails,
onSubmit: (MovieRequest) -> Unit, onSubmit: (MovieRequest) -> Unit,
@ -54,10 +55,11 @@ fun RequestMovieDialog(
RequestMovie( RequestMovie(
loading = loading, loading = loading,
data = data, data = data,
requestEnabled = requestEnabled,
request4kEnabled = request4kEnabled, request4kEnabled = request4kEnabled,
movie = movie, movie = movie,
onSubmit = onSubmit, onSubmit = onSubmit,
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(24.dp),
) )
} }
} }
@ -66,6 +68,7 @@ fun RequestMovieDialog(
fun RequestMovie( fun RequestMovie(
loading: LoadingState, loading: LoadingState,
data: SeerrRequestData, data: SeerrRequestData,
requestEnabled: Boolean,
request4kEnabled: Boolean, request4kEnabled: Boolean,
movie: MovieDetails, movie: MovieDetails,
onSubmit: (MovieRequest) -> Unit, onSubmit: (MovieRequest) -> Unit,
@ -90,7 +93,8 @@ fun RequestMovie(
if (data.profiles.isEmpty() && data.rootFolders.isEmpty() && if (data.profiles.isEmpty() && data.rootFolders.isEmpty() &&
data.profiles4k.isEmpty() && data.rootFolders4k.isEmpty() data.profiles4k.isEmpty() && data.rootFolders4k.isEmpty()
) { ) {
if (request4kEnabled) { // Both enabled, show choice
if (requestEnabled && request4kEnabled) {
val items = val items =
remember { remember {
listOf( listOf(
@ -118,12 +122,13 @@ fun RequestMovie(
) )
} else { } else {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
onSubmit.invoke(MovieRequest(data, movie.id, false, null, null)) onSubmit.invoke(MovieRequest(data, movie.id, request4kEnabled, null, null))
} }
} }
} else { } else {
RequestMovieWithOptions( RequestMovieWithOptions(
data = data, data = data,
requestEnabled = requestEnabled,
request4kEnabled = request4kEnabled, request4kEnabled = request4kEnabled,
movie = movie, movie = movie,
onSubmit = onSubmit, onSubmit = onSubmit,
@ -137,12 +142,13 @@ fun RequestMovie(
@Composable @Composable
private fun RequestMovieWithOptions( private fun RequestMovieWithOptions(
data: SeerrRequestData, data: SeerrRequestData,
requestEnabled: Boolean,
request4kEnabled: Boolean, request4kEnabled: Boolean,
movie: MovieDetails, movie: MovieDetails,
onSubmit: (MovieRequest) -> Unit, onSubmit: (MovieRequest) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var is4k by remember { mutableStateOf(request4kEnabled) } var is4k by remember { mutableStateOf(!requestEnabled) }
var profile by remember(is4k) { var profile by remember(is4k) {
mutableStateOf( mutableStateOf(
if (is4k) { if (is4k) {
@ -185,7 +191,7 @@ private fun RequestMovieWithOptions(
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 8.dp), .padding(bottom = 8.dp),
) { ) {
if (request4kEnabled) { if (requestEnabled && request4kEnabled) {
ClickSwitch( ClickSwitch(
label = stringResource(R.string.request_4k), label = stringResource(R.string.request_4k),
checked = is4k, checked = is4k,
@ -377,7 +383,7 @@ fun MovieRequestPreview() {
), ),
profiles4k = profiles4k =
listOf( listOf(
SeerrProfile(1, "HD", true), SeerrProfile(1, "4K", true),
SeerrProfile(2, "Ultra HD", false), SeerrProfile(2, "Ultra HD", false),
), ),
rootFolders4k = rootFolders4k =
@ -385,7 +391,8 @@ fun MovieRequestPreview() {
SeerrRootFolder(1, "/movies", "400GB", true), SeerrRootFolder(1, "/movies", "400GB", true),
), ),
), ),
request4kEnabled = false, requestEnabled = true,
request4kEnabled = true,
movie = MovieDetails(id = 1, title = "Movie Name", releaseDate = "2026"), movie = MovieDetails(id = 1, title = "Movie Name", releaseDate = "2026"),
onSubmit = {}, onSubmit = {},
modifier = modifier =

View file

@ -65,11 +65,12 @@ fun RequestSeasons(
seasons: List<RequestSeason>, seasons: List<RequestSeason>,
seasons4k: List<RequestSeason>, seasons4k: List<RequestSeason>,
data: SeerrRequestData, data: SeerrRequestData,
requestEnabled: Boolean,
request4kEnabled: Boolean, request4kEnabled: Boolean,
onSubmit: (TvRequest) -> Unit, onSubmit: (TvRequest) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var is4k by remember { mutableStateOf(request4kEnabled) } var is4k by remember { mutableStateOf(!requestEnabled) }
val seasons = remember(is4k, seasons, seasons4k) { if (is4k) seasons4k else seasons } val seasons = remember(is4k, seasons, seasons4k) { if (is4k) seasons4k else seasons }
val allSeasonNumbers = val allSeasonNumbers =
@ -161,7 +162,7 @@ fun RequestSeasons(
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp), .padding(vertical = 8.dp),
) { ) {
if (request4kEnabled) { if (requestEnabled && request4kEnabled) {
ClickSwitch( ClickSwitch(
label = stringResource(R.string.request_4k), label = stringResource(R.string.request_4k),
checked = is4k, checked = is4k,
@ -391,6 +392,7 @@ fun RequestSeasonsDialog(
data: SeerrRequestData, data: SeerrRequestData,
seasons: List<RequestSeason>, seasons: List<RequestSeason>,
seasons4k: List<RequestSeason>, seasons4k: List<RequestSeason>,
requestEnabled: Boolean,
request4kEnabled: Boolean, request4kEnabled: Boolean,
onSubmit: (TvRequest) -> Unit, onSubmit: (TvRequest) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
@ -418,6 +420,7 @@ fun RequestSeasonsDialog(
data = data, data = data,
seasons = seasons, seasons = seasons,
seasons4k = seasons4k, seasons4k = seasons4k,
requestEnabled = requestEnabled,
request4kEnabled = request4kEnabled, request4kEnabled = request4kEnabled,
onSubmit = onSubmit, onSubmit = onSubmit,
modifier = modifier =
@ -475,6 +478,7 @@ fun RequestSeasonsPreview() {
SeerrRootFolder(1, "/tv", "400GB", true), SeerrRootFolder(1, "/tv", "400GB", true),
), ),
), ),
requestEnabled = true,
request4kEnabled = true, request4kEnabled = true,
onSubmit = { }, onSubmit = { },
modifier = Modifier.width(400.dp), modifier = Modifier.width(400.dp),

View file

@ -1,6 +1,13 @@
package com.github.damontecres.wholphin.ui.preferences package com.github.damontecres.wholphin.ui.preferences
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
import com.github.damontecres.wholphin.data.model.SeerrServer
import com.github.damontecres.wholphin.data.model.SeerrUser
import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreference
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -45,3 +52,40 @@ enum class PreferenceScreenOption {
fun fromString(name: String?) = entries.firstOrNull { it.name == name } ?: BASIC fun fromString(name: String?) = entries.firstOrNull { it.name == name } ?: BASIC
} }
} }
@Composable
fun rememberSeerrDialogBody(
user: SeerrUser,
server: SeerrServer,
ex: Exception? = null,
): String {
val username =
when (user.authMethod) {
SeerrAuthMethod.LOCAL,
SeerrAuthMethod.JELLYFIN,
-> user.username ?: stringResource(R.string.unknown)
SeerrAuthMethod.API_KEY -> stringResource(R.string.api_key)
}
val method =
when (user.authMethod) {
SeerrAuthMethod.LOCAL -> R.string.seerr_local_user
SeerrAuthMethod.JELLYFIN -> R.string.seerr_jellyfin_user
SeerrAuthMethod.API_KEY -> 0
}.let {
if (it != 0) " (${stringResource(it)})" else ""
}
val body =
"""
${stringResource(R.string.url)}: ${server.url}
${stringResource(R.string.username)}: $username$method
""".trimIndent() +
if (ex != null) {
"\n\n${stringResource(R.string.voice_error_server)}: ${ex.localizedMessage}"
} else {
""
}
return remember { body }
}

View file

@ -46,6 +46,8 @@ import androidx.tv.material3.surfaceColorAtElevation
import coil3.SingletonImageLoader import coil3.SingletonImageLoader
import coil3.imageLoader import coil3.imageLoader
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.SeerrServer
import com.github.damontecres.wholphin.data.model.SeerrUser
import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.AppSwitchPreference import com.github.damontecres.wholphin.preferences.AppSwitchPreference
@ -62,6 +64,7 @@ import com.github.damontecres.wholphin.preferences.experimentalPreferences
import com.github.damontecres.wholphin.preferences.lazyListWrapScrolling import com.github.damontecres.wholphin.preferences.lazyListWrapScrolling
import com.github.damontecres.wholphin.preferences.screensaverPreferences import com.github.damontecres.wholphin.preferences.screensaverPreferences
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
import com.github.damontecres.wholphin.services.CurrentSeerr
import com.github.damontecres.wholphin.services.Release import com.github.damontecres.wholphin.services.Release
import com.github.damontecres.wholphin.services.SeerrConnectionStatus import com.github.damontecres.wholphin.services.SeerrConnectionStatus
import com.github.damontecres.wholphin.services.UpdateChecker import com.github.damontecres.wholphin.services.UpdateChecker
@ -478,7 +481,8 @@ fun PreferencesContent(
when (val conn = seerrConnection) { when (val conn = seerrConnection) {
is SeerrConnectionStatus.Error -> { is SeerrConnectionStatus.Error -> {
SeerrDialogMode.Error( SeerrDialogMode.Error(
conn.server.url, conn.server,
conn.user,
conn.ex, conn.ex,
) )
} }
@ -489,7 +493,7 @@ fun PreferencesContent(
is SeerrConnectionStatus.Success -> { is SeerrConnectionStatus.Success -> {
SeerrDialogMode.Remove( SeerrDialogMode.Remove(
conn.current.server.url, conn.current,
) )
} }
} }
@ -701,14 +705,16 @@ fun PreferencesContent(
} }
when (val mode = seerrDialogMode) { when (val mode = seerrDialogMode) {
is SeerrDialogMode.Remove -> { is SeerrDialogMode.Remove -> {
val body = rememberSeerrDialogBody(mode.current.user, mode.current.server)
ConfirmDialog( ConfirmDialog(
title = stringResource(R.string.remove_seerr_server), title = stringResource(R.string.remove_seerr_server),
body = mode.serverUrl, body = body,
onCancel = { seerrDialogMode = SeerrDialogMode.None }, onCancel = { seerrDialogMode = SeerrDialogMode.None },
onConfirm = { onConfirm = {
seerrVm.removeServer() seerrVm.removeServer()
seerrDialogMode = SeerrDialogMode.None seerrDialogMode = SeerrDialogMode.None
}, },
elevation = 3.dp,
) )
} }
@ -732,15 +738,7 @@ fun PreferencesContent(
} }
is SeerrDialogMode.Error -> { is SeerrDialogMode.Error -> {
val errorStr = stringResource(R.string.voice_error_server) val body = rememberSeerrDialogBody(mode.user, mode.server, mode.ex)
val body =
remember(mode) {
"""
${mode.serverUrl}
$errorStr: ${mode.ex.localizedMessage}
""".trimIndent()
}
ConfirmDialog( ConfirmDialog(
title = stringResource(R.string.remove_seerr_server), title = stringResource(R.string.remove_seerr_server),
body = body, body = body,
@ -750,6 +748,7 @@ fun PreferencesContent(
seerrDialogMode = SeerrDialogMode.None seerrDialogMode = SeerrDialogMode.None
}, },
bodyColor = MaterialTheme.colorScheme.error, bodyColor = MaterialTheme.colorScheme.error,
elevation = 3.dp,
) )
} }
@ -865,11 +864,12 @@ private sealed interface SeerrDialogMode {
data object Add : SeerrDialogMode data object Add : SeerrDialogMode
data class Remove( data class Remove(
val serverUrl: String, val current: CurrentSeerr,
) : SeerrDialogMode ) : SeerrDialogMode
data class Error( data class Error(
val serverUrl: String, val server: SeerrServer,
val user: SeerrUser,
val ex: Exception, val ex: Exception,
) : SeerrDialogMode ) : SeerrDialogMode
} }