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:
parent
ebf44699ca
commit
e273195e93
11 changed files with 170 additions and 70 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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) &&
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue