Choose Seerr quality profile & root folder (#1604)
## Description Adds option to select the quality profile and/or root folder when requesting from Seerr. The user needs "Advanced Requests" permission to use this feature. If not, the options won't show. ### Related issues Closes #863 Related to #702 ### Testing Emulator w/ different user permissions and different request configurations ## Screenshots <img width="1280" height="720" alt="request_movie Large" src="https://github.com/user-attachments/assets/2665dcbb-e6c3-4d26-857c-b465edff6a02" /> ## AI or LLM usage None
This commit is contained in:
parent
4431757705
commit
77d7571276
10 changed files with 899 additions and 138 deletions
|
|
@ -6,13 +6,21 @@ import com.github.damontecres.wholphin.api.seerr.model.CreditCrew
|
|||
import com.github.damontecres.wholphin.api.seerr.model.MediaInfo
|
||||
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
|
||||
import com.github.damontecres.wholphin.api.seerr.model.MovieResult
|
||||
import com.github.damontecres.wholphin.api.seerr.model.RootFolder
|
||||
import com.github.damontecres.wholphin.api.seerr.model.SearchGet200ResponseResultsInner
|
||||
import com.github.damontecres.wholphin.api.seerr.model.ServiceProfile
|
||||
import com.github.damontecres.wholphin.api.seerr.model.TvDetails
|
||||
import com.github.damontecres.wholphin.api.seerr.model.TvResult
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||
import com.github.damontecres.wholphin.data.model.SeerrAvailability
|
||||
import com.github.damontecres.wholphin.data.model.SeerrItemType
|
||||
import com.github.damontecres.wholphin.data.model.SeerrPermission
|
||||
import com.github.damontecres.wholphin.data.model.hasPermission
|
||||
import com.github.damontecres.wholphin.ui.detail.discover.SeerrProfile
|
||||
import com.github.damontecres.wholphin.ui.detail.discover.SeerrRequestData
|
||||
import com.github.damontecres.wholphin.ui.detail.discover.SeerrRootFolder
|
||||
import com.github.damontecres.wholphin.ui.formatBytes
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.toLocalDate
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -191,6 +199,126 @@ class SeerrService
|
|||
return "${base}${prefix}$path"
|
||||
}
|
||||
|
||||
suspend fun getProfilesAndFolders(type: SeerrItemType): SeerrRequestData {
|
||||
var result = SeerrRequestData()
|
||||
val current = seerrServerRepository.current.firstOrNull()
|
||||
if (current != null && current.config.hasPermission(SeerrPermission.REQUEST_ADVANCED)) {
|
||||
if (type == SeerrItemType.MOVIE) {
|
||||
val radarrs = api.serviceApi.serviceRadarrGet()
|
||||
val radarr =
|
||||
radarrs.firstOrNull { it.isDefault && !it.is4k }
|
||||
?: radarrs.firstOrNull { !it.is4k }
|
||||
val radarr4k =
|
||||
radarrs.firstOrNull { it.isDefault && it.is4k }
|
||||
?: radarrs.firstOrNull { it.is4k }
|
||||
|
||||
if (radarr != null && radarr.id != null) {
|
||||
val settings = api.serviceApi.serviceRadarrRadarrIdGet(radarr.id)
|
||||
|
||||
setProfilesAndFolder(
|
||||
settings.profiles,
|
||||
settings.rootFolders,
|
||||
settings.server?.activeProfileId,
|
||||
settings.server?.activeDirectory,
|
||||
) { profiles, rootFolders ->
|
||||
result =
|
||||
result.copy(
|
||||
serverId = radarr.id,
|
||||
profiles = profiles,
|
||||
rootFolders = rootFolders,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (radarr4k != null && radarr4k.id != null) {
|
||||
val settings = api.serviceApi.serviceRadarrRadarrIdGet(radarr4k.id)
|
||||
setProfilesAndFolder(
|
||||
settings.profiles,
|
||||
settings.rootFolders,
|
||||
settings.server?.activeProfileId,
|
||||
settings.server?.activeDirectory,
|
||||
) { profiles, rootFolders ->
|
||||
result =
|
||||
result.copy(
|
||||
server4kId = radarr4k.id,
|
||||
profiles4k = profiles,
|
||||
rootFolders4k = rootFolders,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val sonarrs = api.serviceApi.serviceSonarrGet()
|
||||
val sonarr =
|
||||
sonarrs.firstOrNull { it.isDefault && !it.is4k }
|
||||
?: sonarrs.firstOrNull { !it.is4k }
|
||||
val sonarr4k =
|
||||
sonarrs.firstOrNull { it.isDefault && it.is4k }
|
||||
?: sonarrs.firstOrNull { it.is4k }
|
||||
|
||||
if (sonarr != null && sonarr.id != null) {
|
||||
val settings = api.serviceApi.serviceSonarrSonarrIdGet(sonarr.id)
|
||||
|
||||
setProfilesAndFolder(
|
||||
settings.profiles,
|
||||
settings.rootFolders,
|
||||
settings.server?.activeProfileId,
|
||||
settings.server?.activeDirectory,
|
||||
) { profiles, rootFolders ->
|
||||
result =
|
||||
result.copy(
|
||||
serverId = sonarr.id,
|
||||
profiles = profiles,
|
||||
rootFolders = rootFolders,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (sonarr4k != null && sonarr4k.id != null) {
|
||||
val settings = api.serviceApi.serviceSonarrSonarrIdGet(sonarr4k.id)
|
||||
setProfilesAndFolder(
|
||||
settings.profiles,
|
||||
settings.rootFolders,
|
||||
settings.server?.activeProfileId,
|
||||
settings.server?.activeDirectory,
|
||||
) { profiles, rootFolders ->
|
||||
result =
|
||||
result.copy(
|
||||
server4kId = sonarr4k.id,
|
||||
profiles4k = profiles,
|
||||
rootFolders4k = rootFolders,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun setProfilesAndFolder(
|
||||
profiles: List<ServiceProfile>?,
|
||||
rootFolders: List<RootFolder>?,
|
||||
activeProfileId: Int?,
|
||||
activeDirectory: String?,
|
||||
setter: (List<SeerrProfile>, List<SeerrRootFolder>) -> Unit,
|
||||
) {
|
||||
val profiles =
|
||||
profiles.orEmpty().mapNotNull {
|
||||
if (it.id != null && it.name != null) {
|
||||
SeerrProfile(it.id, it.name, activeProfileId == it.id)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val rootFolders =
|
||||
rootFolders.orEmpty().mapNotNull {
|
||||
if (it.id != null && it.path != null) {
|
||||
val freeSpace = it.freeSpace?.let { formatBytes(it) } ?: ""
|
||||
SeerrRootFolder(it.id, it.path, freeSpace, activeDirectory == it.path)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
setter.invoke(profiles, rootFolders)
|
||||
}
|
||||
|
||||
suspend fun createDiscoverItem(item: Any): DiscoverItem =
|
||||
when (item) {
|
||||
is MovieResult -> createDiscoverItem(item)
|
||||
|
|
|
|||
|
|
@ -53,9 +53,6 @@ import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard
|
|||
import com.github.damontecres.wholphin.ui.cards.DiscoverPersonRow
|
||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||
|
|
@ -90,7 +87,7 @@ fun DiscoverMovieDetails(
|
|||
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
|
||||
|
||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var showRequestDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val requestStr = stringResource(R.string.request)
|
||||
val request4kStr = stringResource(R.string.request_4k)
|
||||
|
|
@ -119,28 +116,8 @@ fun DiscoverMovieDetails(
|
|||
similar = state.similar,
|
||||
recommended = state.recommended,
|
||||
requestOnClick = {
|
||||
movie.id?.let { id ->
|
||||
if (request4kEnabled) {
|
||||
moreDialog =
|
||||
DialogParams(
|
||||
fromLongClick = false,
|
||||
title = movie.title + " (${movie.releaseDate ?: ""})",
|
||||
items =
|
||||
listOf(
|
||||
DialogItem(
|
||||
text = requestStr,
|
||||
onClick = { viewModel.request(id, false) },
|
||||
),
|
||||
DialogItem(
|
||||
text = request4kStr,
|
||||
onClick = { viewModel.request(id, true) },
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
viewModel.request(id, false)
|
||||
}
|
||||
}
|
||||
viewModel.requestOnClick()
|
||||
showRequestDialog = true
|
||||
},
|
||||
cancelOnClick = {
|
||||
movie.id?.let { viewModel.cancelRequest(it) }
|
||||
|
|
@ -170,14 +147,7 @@ fun DiscoverMovieDetails(
|
|||
)
|
||||
}
|
||||
},
|
||||
moreOnClick = {
|
||||
moreDialog =
|
||||
DialogParams(
|
||||
fromLongClick = false,
|
||||
title = movie.title + " (${movie.releaseDate ?: ""})",
|
||||
items = listOf(),
|
||||
)
|
||||
},
|
||||
moreOnClick = {},
|
||||
onLongClickPerson = { index, person -> },
|
||||
onLongClickSimilar = { index, similar ->
|
||||
},
|
||||
|
|
@ -186,6 +156,19 @@ fun DiscoverMovieDetails(
|
|||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
if (showRequestDialog) {
|
||||
RequestMovieDialog(
|
||||
loading = state.profileLoading,
|
||||
data = state.requestData,
|
||||
request4kEnabled = request4kEnabled,
|
||||
movie = movie,
|
||||
onSubmit = {
|
||||
viewModel.request(it)
|
||||
showRequestDialog = false
|
||||
},
|
||||
onDismissRequest = { showRequestDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
overviewDialog?.let { info ->
|
||||
|
|
@ -195,16 +178,6 @@ fun DiscoverMovieDetails(
|
|||
onDismissRequest = { overviewDialog = null },
|
||||
)
|
||||
}
|
||||
moreDialog?.let { params ->
|
||||
DialogPopup(
|
||||
showDialog = true,
|
||||
title = params.title,
|
||||
dialogItems = params.items,
|
||||
onDismissRequest = { moreDialog = null },
|
||||
dismissOnClick = true,
|
||||
waitToLoad = params.fromLongClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val HEADER_ROW = 0
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ 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.SeerrItemType
|
||||
import com.github.damontecres.wholphin.data.model.SeerrPermission
|
||||
import com.github.damontecres.wholphin.data.model.Trailer
|
||||
import com.github.damontecres.wholphin.data.model.hasPermission
|
||||
|
|
@ -24,6 +25,7 @@ import com.github.damontecres.wholphin.ui.launchIO
|
|||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||
import com.github.damontecres.wholphin.util.successValue
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -164,23 +166,33 @@ class DiscoverMovieViewModel
|
|||
navigationManager.navigateTo(destination)
|
||||
}
|
||||
|
||||
fun request(
|
||||
id: Int,
|
||||
is4k: Boolean,
|
||||
) {
|
||||
fun request(request: MovieRequest) {
|
||||
viewModelScope.launchIO {
|
||||
if (request.movieId == null) {
|
||||
Timber.w("Null movie ID")
|
||||
return@launchIO
|
||||
}
|
||||
try {
|
||||
seerrService.api.requestApi.requestPost(
|
||||
RequestPostRequest(
|
||||
is4k = is4k,
|
||||
mediaId = id,
|
||||
is4k = request.is4k,
|
||||
mediaId = request.movieId,
|
||||
mediaType = RequestPostRequest.MediaType.MOVIE,
|
||||
serverId =
|
||||
when {
|
||||
request.profileId == null && request.folder == null -> null
|
||||
request.is4k -> request.data.server4kId
|
||||
else -> request.data.serverId
|
||||
},
|
||||
profileId = request.profileId,
|
||||
rootFolder = request.folder,
|
||||
tags = emptyList(),
|
||||
),
|
||||
)
|
||||
} catch (ex: kotlinx.coroutines.CancellationException) {
|
||||
} catch (ex: CancellationException) {
|
||||
throw ex
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error requesting %s", id)
|
||||
Timber.e(ex, "Error requesting %s", request.movieId)
|
||||
showToast(context, "An error occurred")
|
||||
}
|
||||
fetchAndSetItem().await()
|
||||
|
|
@ -205,6 +217,28 @@ class DiscoverMovieViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestOnClick() {
|
||||
viewModelScope.launchIO {
|
||||
try {
|
||||
val data = seerrService.getProfilesAndFolders(SeerrItemType.MOVIE)
|
||||
_state.update {
|
||||
it.copy(
|
||||
profileLoading = LoadingState.Success,
|
||||
requestData = data,
|
||||
)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error getting profiles & folders")
|
||||
showToast(context, "Error getting profiles & folders: ${ex.localizedMessage}")
|
||||
_state.update {
|
||||
it.copy(
|
||||
profileLoading = LoadingState.Success,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun canUserCancelRequest(
|
||||
|
|
@ -226,4 +260,6 @@ data class DiscoverMovieState(
|
|||
val similar: List<DiscoverItem> = emptyList(),
|
||||
val recommended: List<DiscoverItem> = emptyList(),
|
||||
val canCancelRequest: Boolean = false,
|
||||
val profileLoading: LoadingState = LoadingState.Pending,
|
||||
val requestData: SeerrRequestData = SeerrRequestData(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -92,9 +92,6 @@ fun DiscoverSeriesDetails(
|
|||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var showRequestSeasonDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val requestStr = stringResource(R.string.request)
|
||||
val request4kStr = stringResource(R.string.request_4k)
|
||||
|
||||
when (val st = state.tvSeries) {
|
||||
is DataLoadingState.Error -> {
|
||||
ErrorMessage(st, modifier)
|
||||
|
|
@ -150,9 +147,8 @@ fun DiscoverSeriesDetails(
|
|||
},
|
||||
trailers = state.trailers,
|
||||
requestOnClick = {
|
||||
item.id?.let { id ->
|
||||
showRequestSeasonDialog = true
|
||||
}
|
||||
viewModel.requestOnClick()
|
||||
showRequestSeasonDialog = true
|
||||
},
|
||||
cancelOnClick = {
|
||||
item.id?.let { viewModel.cancelRequest(it) }
|
||||
|
|
@ -192,15 +188,16 @@ fun DiscoverSeriesDetails(
|
|||
}
|
||||
if (showRequestSeasonDialog) {
|
||||
RequestSeasonsDialog(
|
||||
id = state.tvSeries.successValue?.id ?: -1,
|
||||
title = state.tvSeries.successValue?.name ?: "",
|
||||
seasons = state.seasons,
|
||||
request4kEnabled = request4kEnabled,
|
||||
onSubmit = { seasons, is4k ->
|
||||
state.tvSeries.successValue
|
||||
?.id
|
||||
?.let { viewModel.request(it, seasons, is4k) }
|
||||
onSubmit = {
|
||||
showRequestSeasonDialog = false
|
||||
viewModel.request(it)
|
||||
},
|
||||
loading = state.profileLoading,
|
||||
data = state.requestData,
|
||||
onDismissRequest = { showRequestSeasonDialog = false },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ 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.SeerrAvailability
|
||||
import com.github.damontecres.wholphin.data.model.SeerrItemType
|
||||
import com.github.damontecres.wholphin.data.model.Trailer
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
|
|
@ -22,6 +23,7 @@ import com.github.damontecres.wholphin.ui.launchIO
|
|||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||
import com.github.damontecres.wholphin.util.successValue
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -198,11 +200,29 @@ class DiscoverSeriesViewModel
|
|||
_state.update { it.copy(canCancelRequest = canCancel) }
|
||||
}
|
||||
|
||||
fun request(
|
||||
id: Int,
|
||||
seasons: Set<Int>,
|
||||
is4k: Boolean,
|
||||
) {
|
||||
fun requestOnClick() {
|
||||
viewModelScope.launchIO {
|
||||
try {
|
||||
val data = seerrService.getProfilesAndFolders(SeerrItemType.TV)
|
||||
_state.update {
|
||||
it.copy(
|
||||
profileLoading = LoadingState.Success,
|
||||
requestData = data,
|
||||
)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error getting profiles & folders")
|
||||
showToast(context, "Error getting profiles & folders: ${ex.localizedMessage}")
|
||||
_state.update {
|
||||
it.copy(
|
||||
profileLoading = LoadingState.Success,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun request(request: TvRequest) {
|
||||
viewModelScope.launchIO {
|
||||
state.value.tvSeries.successValue?.let { tv ->
|
||||
val currentRequest =
|
||||
|
|
@ -217,26 +237,43 @@ class DiscoverSeriesViewModel
|
|||
requestId = currentRequest.id.toString(),
|
||||
requestRequestIdPutRequest =
|
||||
RequestRequestIdPutRequest(
|
||||
is4k = is4k,
|
||||
is4k = request.is4k,
|
||||
mediaType = RequestRequestIdPutRequest.MediaType.TV,
|
||||
seasons = seasons.toList(),
|
||||
seasons = request.seasons,
|
||||
serverId =
|
||||
when {
|
||||
request.profileId == null && request.folder == null -> null
|
||||
request.is4k -> request.data.server4kId
|
||||
else -> request.data.serverId
|
||||
},
|
||||
profileId = request.profileId,
|
||||
rootFolder = request.folder,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Timber.v("New request for %s seasons", seasons.size)
|
||||
Timber.v("New request for %s seasons", request.seasons.size)
|
||||
seerrService.api.requestApi.requestPost(
|
||||
RequestPostRequest(
|
||||
is4k = is4k,
|
||||
mediaId = id,
|
||||
is4k = request.is4k,
|
||||
mediaId = request.tvId,
|
||||
mediaType = RequestPostRequest.MediaType.TV,
|
||||
seasons = seasons.toList(),
|
||||
seasons = request.seasons,
|
||||
serverId =
|
||||
when {
|
||||
request.profileId == null && request.folder == null -> null
|
||||
request.is4k -> request.data.server4kId
|
||||
else -> request.data.serverId
|
||||
},
|
||||
profileId = request.profileId,
|
||||
rootFolder = request.folder,
|
||||
tags = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
} catch (ex: CancellationException) {
|
||||
throw ex
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error requesting %s", id)
|
||||
Timber.e(ex, "Error requesting %s", request.tvId)
|
||||
showToast(context, "An error occurred")
|
||||
}
|
||||
|
||||
|
|
@ -279,4 +316,6 @@ data class DiscoverSeriesState(
|
|||
val similar: List<DiscoverItem> = emptyList(),
|
||||
val recommended: List<DiscoverItem> = emptyList(),
|
||||
val canCancelRequest: Boolean = false,
|
||||
val profileLoading: LoadingState = LoadingState.Pending,
|
||||
val requestData: SeerrRequestData = SeerrRequestData(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,397 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.discover
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.ListItem
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopupContent
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
|
||||
@Composable
|
||||
fun RequestMovieDialog(
|
||||
loading: LoadingState,
|
||||
data: SeerrRequestData,
|
||||
request4kEnabled: Boolean,
|
||||
movie: MovieDetails,
|
||||
onSubmit: (MovieRequest) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
BasicDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
RequestMovie(
|
||||
loading = loading,
|
||||
data = data,
|
||||
request4kEnabled = request4kEnabled,
|
||||
movie = movie,
|
||||
onSubmit = onSubmit,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RequestMovie(
|
||||
loading: LoadingState,
|
||||
data: SeerrRequestData,
|
||||
request4kEnabled: Boolean,
|
||||
movie: MovieDetails,
|
||||
onSubmit: (MovieRequest) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (loading) {
|
||||
is LoadingState.Error -> {
|
||||
ErrorMessage(loading, modifier)
|
||||
}
|
||||
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> {
|
||||
LoadingPage(modifier)
|
||||
}
|
||||
|
||||
LoadingState.Success -> {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
val requestStr = stringResource(R.string.request)
|
||||
val request4kStr = stringResource(R.string.request_4k)
|
||||
if (data.profiles.isEmpty() && data.rootFolders.isEmpty() &&
|
||||
data.profiles4k.isEmpty() && data.rootFolders4k.isEmpty()
|
||||
) {
|
||||
if (request4kEnabled) {
|
||||
val items =
|
||||
remember {
|
||||
listOf(
|
||||
DialogItem(
|
||||
text = requestStr,
|
||||
onClick = {
|
||||
onSubmit.invoke(MovieRequest(data, movie.id, false, null, null))
|
||||
},
|
||||
),
|
||||
DialogItem(
|
||||
text = request4kStr,
|
||||
onClick = {
|
||||
onSubmit.invoke(MovieRequest(data, movie.id, true, null, null))
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
DialogPopupContent(
|
||||
title = movie.title + " (${movie.releaseDate ?: ""})",
|
||||
dialogItems = items,
|
||||
waiting = false,
|
||||
onDismissRequest = {},
|
||||
dismissOnClick = false,
|
||||
modifier = modifier.focusRequester(focusRequester),
|
||||
)
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
onSubmit.invoke(MovieRequest(data, movie.id, false, null, null))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
RequestMovieWithOptions(
|
||||
data = data,
|
||||
request4kEnabled = request4kEnabled,
|
||||
movie = movie,
|
||||
onSubmit = onSubmit,
|
||||
modifier = modifier.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RequestMovieWithOptions(
|
||||
data: SeerrRequestData,
|
||||
request4kEnabled: Boolean,
|
||||
movie: MovieDetails,
|
||||
onSubmit: (MovieRequest) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var is4k by remember { mutableStateOf(request4kEnabled) }
|
||||
var profile by remember(is4k) {
|
||||
mutableStateOf(
|
||||
if (is4k) {
|
||||
data.profiles4k.firstOrNull { it.default } ?: data.profiles4k.firstOrNull()
|
||||
} else {
|
||||
data.profiles.firstOrNull { it.default } ?: data.profiles.firstOrNull()
|
||||
},
|
||||
)
|
||||
}
|
||||
var folder by remember(is4k) {
|
||||
mutableStateOf(
|
||||
if (is4k) {
|
||||
data.rootFolders4k.firstOrNull { it.default } ?: data.rootFolders4k.firstOrNull()
|
||||
} else {
|
||||
data.rootFolders.firstOrNull { it.default } ?: data.rootFolders.firstOrNull()
|
||||
},
|
||||
)
|
||||
}
|
||||
val profiles = remember(is4k, data) { if (is4k) data.profiles4k else data.profiles }
|
||||
val folders = remember(is4k, data) { if (is4k) data.rootFolders4k else data.rootFolders }
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = movie.title + " (${movie.releaseDate ?: ""})",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
modifier = Modifier,
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
) {
|
||||
if (request4kEnabled) {
|
||||
ClickSwitch(
|
||||
label = stringResource(R.string.request_4k),
|
||||
checked = is4k,
|
||||
onClick = { is4k = !is4k },
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
onSubmit.invoke(MovieRequest(data, movie.id, is4k, profile?.id, folder?.path))
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.submit),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (profiles.isNotEmpty()) {
|
||||
profile?.let {
|
||||
item {
|
||||
ChooseProfile(
|
||||
selectedProfile = it,
|
||||
profiles = profiles,
|
||||
onClickProfile = { profile = it },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (folders.isNotEmpty()) {
|
||||
folder?.let {
|
||||
item {
|
||||
ChooseFolder(
|
||||
selectedFolder = it,
|
||||
folders = folders,
|
||||
onClickFolder = { folder = it },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChooseProfile(
|
||||
selectedProfile: SeerrProfile,
|
||||
profiles: List<SeerrProfile>,
|
||||
onClickProfile: (SeerrProfile) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val defaultStr = stringResource(R.string.is_default)
|
||||
val qualityProfileStr = stringResource(R.string.quality_profile)
|
||||
var showProfileDialog by remember { mutableStateOf(false) }
|
||||
ListItem(
|
||||
selected = false,
|
||||
headlineContent = {
|
||||
Text(qualityProfileStr)
|
||||
},
|
||||
supportingContent = {
|
||||
val text =
|
||||
remember(selectedProfile) {
|
||||
if (selectedProfile.default) "${selectedProfile.name} ($defaultStr)" else selectedProfile.name
|
||||
}
|
||||
Text(text)
|
||||
},
|
||||
onClick = { showProfileDialog = true },
|
||||
modifier = modifier,
|
||||
)
|
||||
if (showProfileDialog) {
|
||||
val params =
|
||||
remember {
|
||||
val items =
|
||||
profiles.map {
|
||||
DialogItem(
|
||||
headlineContent = {
|
||||
Text(it.name)
|
||||
},
|
||||
supportingContent = {
|
||||
if (it.default) {
|
||||
Text(defaultStr)
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
SelectedLeadingContent(it == selectedProfile)
|
||||
},
|
||||
onClick = { onClickProfile.invoke(it) },
|
||||
)
|
||||
}
|
||||
DialogParams(
|
||||
fromLongClick = false,
|
||||
title = qualityProfileStr,
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
DialogPopup(
|
||||
params = params,
|
||||
onDismissRequest = { showProfileDialog = false },
|
||||
dismissOnClick = true,
|
||||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChooseFolder(
|
||||
selectedFolder: SeerrRootFolder,
|
||||
folders: List<SeerrRootFolder>,
|
||||
onClickFolder: (SeerrRootFolder) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val defaultStr = stringResource(R.string.is_default)
|
||||
val rootFolderStr = stringResource(R.string.root_folder)
|
||||
var showFolderDialog by remember { mutableStateOf(false) }
|
||||
ListItem(
|
||||
selected = false,
|
||||
headlineContent = {
|
||||
Text(rootFolderStr)
|
||||
},
|
||||
supportingContent = {
|
||||
val text =
|
||||
remember(selectedFolder) {
|
||||
if (selectedFolder.default) "${selectedFolder.path} ($defaultStr)" else selectedFolder.path
|
||||
}
|
||||
Text(text)
|
||||
},
|
||||
trailingContent = {
|
||||
Text(selectedFolder.freeSpace)
|
||||
},
|
||||
onClick = { showFolderDialog = true },
|
||||
modifier = modifier,
|
||||
)
|
||||
if (showFolderDialog) {
|
||||
val params =
|
||||
remember {
|
||||
val items =
|
||||
folders.map {
|
||||
DialogItem(
|
||||
headlineContent = {
|
||||
Text(it.path)
|
||||
},
|
||||
supportingContent = {
|
||||
if (it.default) {
|
||||
Text(defaultStr)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Text(it.freeSpace)
|
||||
},
|
||||
leadingContent = {
|
||||
SelectedLeadingContent(it == selectedFolder)
|
||||
},
|
||||
onClick = { onClickFolder.invoke(it) },
|
||||
)
|
||||
}
|
||||
DialogParams(
|
||||
fromLongClick = false,
|
||||
title = rootFolderStr,
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
DialogPopup(
|
||||
params = params,
|
||||
onDismissRequest = { showFolderDialog = false },
|
||||
dismissOnClick = true,
|
||||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewTvSpec
|
||||
@Composable
|
||||
fun MovieRequestPreview() {
|
||||
WholphinTheme {
|
||||
RequestMovie(
|
||||
loading = LoadingState.Success,
|
||||
data =
|
||||
SeerrRequestData(
|
||||
profiles =
|
||||
listOf(
|
||||
SeerrProfile(1, "HD", true),
|
||||
SeerrProfile(2, "Ultra HD", false),
|
||||
),
|
||||
rootFolders =
|
||||
listOf(
|
||||
SeerrRootFolder(1, "/movies", "400GB", true),
|
||||
),
|
||||
profiles4k =
|
||||
listOf(
|
||||
SeerrProfile(1, "HD", true),
|
||||
SeerrProfile(2, "Ultra HD", false),
|
||||
),
|
||||
rootFolders4k =
|
||||
listOf(
|
||||
SeerrRootFolder(1, "/movies", "400GB", true),
|
||||
),
|
||||
),
|
||||
request4kEnabled = false,
|
||||
movie = MovieDetails(id = 1, title = "Movie Name", releaseDate = "2026"),
|
||||
onSubmit = {},
|
||||
modifier =
|
||||
Modifier
|
||||
.width(320.dp)
|
||||
.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,9 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableStateSetOf
|
||||
|
|
@ -20,9 +22,10 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.Button
|
||||
|
|
@ -40,7 +43,11 @@ import com.github.damontecres.wholphin.ui.cards.AvailableIndicator
|
|||
import com.github.damontecres.wholphin.ui.cards.PartiallyAvailableIndicator
|
||||
import com.github.damontecres.wholphin.ui.cards.PendingIndicator
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
|
||||
data class RequestSeason(
|
||||
val season: Season,
|
||||
|
|
@ -49,14 +56,16 @@ data class RequestSeason(
|
|||
|
||||
@Composable
|
||||
fun RequestSeasons(
|
||||
id: Int,
|
||||
title: String,
|
||||
seasons: List<RequestSeason>,
|
||||
onSubmit: (Set<Int>, Boolean) -> Unit,
|
||||
data: SeerrRequestData,
|
||||
request4kEnabled: Boolean,
|
||||
modifier: Modifier,
|
||||
onSubmit: (TvRequest) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val allSeasonNumbers = remember(seasons) { seasons.mapNotNull { it.season.seasonNumber }.toSet() }
|
||||
val selected =
|
||||
val selectedSeasons =
|
||||
remember {
|
||||
mutableStateSetOf<Int>(
|
||||
*seasons
|
||||
|
|
@ -69,39 +78,74 @@ fun RequestSeasons(
|
|||
}.toTypedArray(),
|
||||
)
|
||||
}
|
||||
var is4k by remember { mutableStateOf(false) }
|
||||
var is4k by remember { mutableStateOf(request4kEnabled) }
|
||||
|
||||
var profile by remember(is4k) {
|
||||
mutableStateOf(
|
||||
if (is4k) {
|
||||
data.profiles4k.firstOrNull { it.default } ?: data.profiles4k.firstOrNull()
|
||||
} else {
|
||||
data.profiles.firstOrNull { it.default } ?: data.profiles.firstOrNull()
|
||||
},
|
||||
)
|
||||
}
|
||||
var folder by remember(is4k) {
|
||||
mutableStateOf(
|
||||
if (is4k) {
|
||||
data.rootFolders4k.firstOrNull { it.default } ?: data.rootFolders4k.firstOrNull()
|
||||
} else {
|
||||
data.rootFolders.firstOrNull { it.default } ?: data.rootFolders.firstOrNull()
|
||||
},
|
||||
)
|
||||
}
|
||||
val profiles = remember(is4k, data) { if (is4k) data.profiles4k else data.profiles }
|
||||
val folders = remember(is4k, data) { if (is4k) data.rootFolders4k else data.rootFolders }
|
||||
|
||||
fun submit() {
|
||||
onSubmit.invoke(
|
||||
TvRequest(
|
||||
data = data,
|
||||
tvId = id,
|
||||
seasons = selectedSeasons.toList(),
|
||||
is4k = is4k,
|
||||
profileId = profile?.id,
|
||||
folder = folder?.path,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier,
|
||||
)
|
||||
LazyColumn {
|
||||
LazyColumn(
|
||||
modifier = Modifier,
|
||||
) {
|
||||
item {
|
||||
val isSelected = selected.containsAll(allSeasonNumbers)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
ClickSwitch(
|
||||
label = stringResource(R.string.select_all),
|
||||
checked = isSelected,
|
||||
onClick = {
|
||||
if (isSelected) {
|
||||
selected.removeAll(allSeasonNumbers)
|
||||
} else {
|
||||
selected.addAll(allSeasonNumbers)
|
||||
}
|
||||
},
|
||||
)
|
||||
if (request4kEnabled) {
|
||||
ClickSwitch(
|
||||
label = stringResource(R.string.request_4k),
|
||||
checked = is4k,
|
||||
onClick = { is4k = !is4k },
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = { onSubmit.invoke(selected, is4k) },
|
||||
onClick = ::submit,
|
||||
enabled = selectedSeasons.isNotEmpty(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.submit),
|
||||
|
|
@ -109,39 +153,80 @@ fun RequestSeasons(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (request4kEnabled) {
|
||||
item {
|
||||
ClickSwitch(
|
||||
label = stringResource(R.string.request_4k),
|
||||
checked = is4k,
|
||||
onClick = { is4k = !is4k },
|
||||
)
|
||||
if (profiles.isNotEmpty()) {
|
||||
profile?.let {
|
||||
item {
|
||||
ChooseProfile(
|
||||
selectedProfile = it,
|
||||
profiles = profiles,
|
||||
onClickProfile = { profile = it },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (folders.isNotEmpty()) {
|
||||
folder?.let {
|
||||
item {
|
||||
ChooseFolder(
|
||||
selectedFolder = it,
|
||||
folders = folders,
|
||||
onClickFolder = { folder = it },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.tv_seasons),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
val isSelected = selectedSeasons.containsAll(allSeasonNumbers)
|
||||
ClickSwitch(
|
||||
label = stringResource(R.string.select_all),
|
||||
checked = isSelected,
|
||||
onClick = {
|
||||
if (isSelected) {
|
||||
selectedSeasons.removeAll(allSeasonNumbers)
|
||||
} else {
|
||||
selectedSeasons.addAll(allSeasonNumbers)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
itemsIndexed(seasons) { index, season ->
|
||||
val seasonNumber = season.season.seasonNumber
|
||||
val isSelected = seasonNumber in selected
|
||||
val isSelected = seasonNumber in selectedSeasons
|
||||
SeasonListItem(
|
||||
season = season,
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (isSelected) {
|
||||
selected.remove(seasonNumber)
|
||||
selectedSeasons.remove(seasonNumber)
|
||||
} else {
|
||||
seasonNumber?.let { selected.add(it) }
|
||||
seasonNumber?.let { selectedSeasons.add(it) }
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
if (seasons.size > 7) {
|
||||
if (seasons.size > 3) {
|
||||
item {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
Button(
|
||||
onClick = { onSubmit.invoke(selected, is4k) },
|
||||
onClick = ::submit,
|
||||
enabled = selectedSeasons.isNotEmpty(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.submit),
|
||||
|
|
@ -237,7 +322,7 @@ private fun ClickSurface(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ClickSwitch(
|
||||
fun ClickSwitch(
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
onClick: () -> Unit,
|
||||
|
|
@ -267,22 +352,46 @@ private fun ClickSwitch(
|
|||
|
||||
@Composable
|
||||
fun RequestSeasonsDialog(
|
||||
id: Int,
|
||||
title: String,
|
||||
loading: LoadingState,
|
||||
data: SeerrRequestData,
|
||||
seasons: List<RequestSeason>,
|
||||
request4kEnabled: Boolean,
|
||||
onSubmit: (Set<Int>, Boolean) -> Unit,
|
||||
onSubmit: (TvRequest) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
BasicDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
RequestSeasons(
|
||||
title = title,
|
||||
seasons = seasons,
|
||||
request4kEnabled = request4kEnabled,
|
||||
onSubmit = onSubmit,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
when (loading) {
|
||||
is LoadingState.Error -> {
|
||||
ErrorMessage(loading, Modifier)
|
||||
}
|
||||
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> {
|
||||
LoadingPage(Modifier)
|
||||
}
|
||||
|
||||
LoadingState.Success -> {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
RequestSeasons(
|
||||
id = id,
|
||||
title = title,
|
||||
data = data,
|
||||
seasons = seasons,
|
||||
request4kEnabled = request4kEnabled,
|
||||
onSubmit = onSubmit,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(16.dp)
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -313,10 +422,23 @@ fun RequestSeasonsPreview() {
|
|||
|
||||
WholphinTheme {
|
||||
RequestSeasons(
|
||||
id = 1,
|
||||
title = "Series title",
|
||||
seasons = seasons,
|
||||
data =
|
||||
SeerrRequestData(
|
||||
profiles4k =
|
||||
listOf(
|
||||
SeerrProfile(1, "HD", true),
|
||||
SeerrProfile(2, "Ultra HD", false),
|
||||
),
|
||||
rootFolders4k =
|
||||
listOf(
|
||||
SeerrRootFolder(1, "/tv", "400GB", true),
|
||||
),
|
||||
),
|
||||
request4kEnabled = true,
|
||||
onSubmit = { _, _ -> },
|
||||
onSubmit = { },
|
||||
modifier = Modifier.width(400.dp),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.discover
|
||||
|
||||
data class SeerrRequestData(
|
||||
val serverId: Int = -1,
|
||||
val server4kId: Int = -1,
|
||||
val profiles: List<SeerrProfile> = emptyList(),
|
||||
val rootFolders: List<SeerrRootFolder> = emptyList(),
|
||||
val profiles4k: List<SeerrProfile> = emptyList(),
|
||||
val rootFolders4k: List<SeerrRootFolder> = emptyList(),
|
||||
)
|
||||
|
||||
data class SeerrProfile(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val default: Boolean,
|
||||
)
|
||||
|
||||
data class SeerrRootFolder(
|
||||
val id: Int,
|
||||
val path: String,
|
||||
val freeSpace: String,
|
||||
val default: Boolean,
|
||||
)
|
||||
|
||||
data class MovieRequest(
|
||||
val data: SeerrRequestData,
|
||||
val movieId: Int?,
|
||||
val is4k: Boolean,
|
||||
val profileId: Int?,
|
||||
val folder: String?,
|
||||
)
|
||||
|
||||
data class TvRequest(
|
||||
val data: SeerrRequestData,
|
||||
val tvId: Int,
|
||||
val seasons: List<Int>,
|
||||
val is4k: Boolean,
|
||||
val profileId: Int?,
|
||||
val folder: String?,
|
||||
)
|
||||
|
|
@ -821,4 +821,7 @@
|
|||
<string name="tomatoes">Tomatoes</string>
|
||||
<string name="sort">Sort</string>
|
||||
<string name="split_into_separate_rows">Split into separate rows</string>
|
||||
<string name="quality_profile">Quality profile</string>
|
||||
<string name="is_default">Default</string>
|
||||
<string name="root_folder">Root folder</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -603,15 +603,14 @@ components:
|
|||
example: false
|
||||
required:
|
||||
- name
|
||||
- hostname
|
||||
- port
|
||||
- apiKey
|
||||
- useSsl
|
||||
# - hostname
|
||||
# - port
|
||||
# - useSsl
|
||||
- activeProfileId
|
||||
- activeProfileName
|
||||
# - activeProfileName
|
||||
- activeDirectory
|
||||
- is4k
|
||||
- minimumAvailability
|
||||
# - minimumAvailability
|
||||
- isDefault
|
||||
SonarrSettings:
|
||||
type: object
|
||||
|
|
@ -682,15 +681,15 @@ components:
|
|||
example: false
|
||||
required:
|
||||
- name
|
||||
- hostname
|
||||
- port
|
||||
- apiKey
|
||||
- useSsl
|
||||
# - hostname
|
||||
# - port
|
||||
# - apiKey
|
||||
# - useSsl
|
||||
- activeProfileId
|
||||
- activeProfileName
|
||||
# - activeProfileName
|
||||
- activeDirectory
|
||||
- is4k
|
||||
- enableSeasonFolders
|
||||
# - enableSeasonFolders
|
||||
- isDefault
|
||||
ServarrTag:
|
||||
type: object
|
||||
|
|
@ -1396,6 +1395,17 @@ components:
|
|||
name:
|
||||
type: string
|
||||
example: 720p/1080p
|
||||
RootFolder:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
example: 1
|
||||
freeSpace:
|
||||
type: long
|
||||
path:
|
||||
type: string
|
||||
example: /movies
|
||||
PageInfo:
|
||||
type: object
|
||||
properties:
|
||||
|
|
@ -6154,9 +6164,9 @@ paths:
|
|||
is4k:
|
||||
type: boolean
|
||||
example: false
|
||||
serverid:
|
||||
serverId:
|
||||
type: integer
|
||||
profileid:
|
||||
profileId:
|
||||
type: integer
|
||||
rootFolder:
|
||||
type: string
|
||||
|
|
@ -6166,6 +6176,10 @@ paths:
|
|||
userid:
|
||||
type: integer
|
||||
nullable: true
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
required:
|
||||
- mediaType
|
||||
- mediaId
|
||||
|
|
@ -6261,9 +6275,9 @@ paths:
|
|||
is4k:
|
||||
type: boolean
|
||||
example: false
|
||||
serverid:
|
||||
serverId:
|
||||
type: integer
|
||||
profileid:
|
||||
profileId:
|
||||
type: integer
|
||||
rootFolder:
|
||||
type: string
|
||||
|
|
@ -7061,7 +7075,13 @@ paths:
|
|||
server:
|
||||
$ref: '#/components/schemas/RadarrSettings'
|
||||
profiles:
|
||||
$ref: '#/components/schemas/ServiceProfile'
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ServiceProfile'
|
||||
rootFolders:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RootFolder'
|
||||
/service/sonarr:
|
||||
get:
|
||||
summary: Get non-sensitive Sonarr server list
|
||||
|
|
@ -7101,7 +7121,13 @@ paths:
|
|||
server:
|
||||
$ref: '#/components/schemas/SonarrSettings'
|
||||
profiles:
|
||||
$ref: '#/components/schemas/ServiceProfile'
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ServiceProfile'
|
||||
rootFolders:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RootFolder'
|
||||
/service/sonarr/lookup/{tmdbId}:
|
||||
get:
|
||||
summary: Get series from Sonarr
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue