mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Request individual seasons from Jellyseerr (#1060)
## Description Adds a dialog to select individual seasons for a TV show when requesting it via Discover/Jellyseerr If a request is already pending, it can be updated. If there's more than 7 seasons, a second submit button is added at the bottom of the dialog for easier access. Also, adds a button from Jellyfin series details to its Discover equivalent and added a request button for partially available series to request more seasons. ### Related issues Closes #961 ### Testing Emulator & Jellyseerr 2.7.3 ## Screenshots  ## AI or LLM usage None
This commit is contained in:
parent
49a6cd8d2f
commit
afc47f254b
12 changed files with 512 additions and 46 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.github.damontecres.wholphin.api.seerr.model.CreditCast
|
||||
import com.github.damontecres.wholphin.api.seerr.model.CreditCrew
|
||||
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
|
||||
|
|
@ -74,6 +75,7 @@ enum class SeerrAvailability(
|
|||
/**
|
||||
* An item provided by a discovery service (ie Seerr). It may exist on the JF server as well.
|
||||
*/
|
||||
@Stable
|
||||
@Serializable
|
||||
data class DiscoverItem(
|
||||
val id: Int,
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ class SeerrServerRepository
|
|||
connection.map { (it as? SeerrConnectionStatus.Success)?.current?.server }
|
||||
val currentUser: Flow<SeerrUser?> =
|
||||
connection.map { (it as? SeerrConnectionStatus.Success)?.current?.user }
|
||||
val currentUserId: Flow<Int?> = current.map { it?.config?.id }
|
||||
|
||||
/**
|
||||
* Whether Seerr integration is currently active of not
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.services
|
|||
|
||||
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
|
||||
import com.github.damontecres.wholphin.api.seerr.model.SearchGet200ResponseResultsInner
|
||||
import com.github.damontecres.wholphin.api.seerr.model.TvDetails
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -125,4 +126,16 @@ class SeerrService
|
|||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
suspend fun getTvSeries(item: BaseItem): TvDetails? =
|
||||
if (active.first()) {
|
||||
item.data.providerIds
|
||||
?.get("Tmdb")
|
||||
?.toIntOrNull()
|
||||
?.let { tvId ->
|
||||
api.tvApi.tvTvIdGet(tvId = tvId)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.api.seerr.model.Season
|
||||
import com.github.damontecres.wholphin.api.seerr.model.TvDetails
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||
|
|
@ -99,6 +98,7 @@ fun DiscoverSeriesDetails(
|
|||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
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)
|
||||
|
|
@ -159,26 +159,7 @@ fun DiscoverSeriesDetails(
|
|||
trailers = trailers,
|
||||
requestOnClick = {
|
||||
item.id?.let { id ->
|
||||
if (request4kEnabled) {
|
||||
moreDialog =
|
||||
DialogParams(
|
||||
fromLongClick = false,
|
||||
title = item.name ?: "",
|
||||
items =
|
||||
listOf(
|
||||
DialogItem(
|
||||
text = requestStr,
|
||||
onClick = { viewModel.request(id, false) },
|
||||
),
|
||||
DialogItem(
|
||||
text = request4kStr,
|
||||
onClick = { viewModel.request(id, true) },
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
viewModel.request(id, false)
|
||||
}
|
||||
showRequestSeasonDialog = true
|
||||
}
|
||||
},
|
||||
cancelOnClick = {
|
||||
|
|
@ -218,6 +199,18 @@ fun DiscoverSeriesDetails(
|
|||
waitToLoad = params.fromLongClick,
|
||||
)
|
||||
}
|
||||
if (showRequestSeasonDialog) {
|
||||
RequestSeasonsDialog(
|
||||
title = item?.name ?: "",
|
||||
seasons = seasons,
|
||||
request4kEnabled = request4kEnabled,
|
||||
onSubmit = { seasons, is4k ->
|
||||
item?.id?.let { viewModel.request(it, seasons, is4k) }
|
||||
showRequestSeasonDialog = false
|
||||
},
|
||||
onDismissRequest = { showRequestSeasonDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val HEADER_ROW = 0
|
||||
|
|
@ -234,7 +227,7 @@ fun DiscoverSeriesDetailsContent(
|
|||
series: TvDetails,
|
||||
rating: DiscoverRating?,
|
||||
canCancel: Boolean,
|
||||
seasons: List<Season>,
|
||||
seasons: List<RequestSeason>,
|
||||
similar: List<DiscoverItem>,
|
||||
recommended: List<DiscoverItem>,
|
||||
trailers: List<Trailer>,
|
||||
|
|
@ -299,6 +292,7 @@ fun DiscoverSeriesDetailsContent(
|
|||
SeerrAvailability.from(series.mediaInfo?.status)
|
||||
?: SeerrAvailability.UNKNOWN,
|
||||
requestOnClick = requestOnClick,
|
||||
pendingOnClick = requestOnClick,
|
||||
cancelOnClick = cancelOnClick,
|
||||
moreOnClick = moreOnClick,
|
||||
goToOnClick = goToOnClick,
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.api.seerr.model.RelatedVideo
|
||||
import com.github.damontecres.wholphin.api.seerr.model.RequestPostRequest
|
||||
import com.github.damontecres.wholphin.api.seerr.model.Season
|
||||
import com.github.damontecres.wholphin.api.seerr.model.RequestRequestIdPutRequest
|
||||
import com.github.damontecres.wholphin.api.seerr.model.TvDetails
|
||||
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.SeerrAvailability
|
||||
import com.github.damontecres.wholphin.data.model.Trailer
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
|
|
@ -33,6 +34,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
|
@ -63,7 +65,7 @@ class DiscoverSeriesViewModel
|
|||
val tvSeries = MutableLiveData<TvDetails?>(null)
|
||||
val rating = MutableLiveData<DiscoverRating?>(null)
|
||||
|
||||
val seasons = MutableLiveData<List<Season>>(listOf())
|
||||
val seasons = MutableLiveData<List<RequestSeason>>(listOf())
|
||||
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
||||
val people = MutableLiveData<List<DiscoverItem>>(listOf())
|
||||
val similar = MutableLiveData<List<DiscoverItem>>()
|
||||
|
|
@ -103,6 +105,7 @@ class DiscoverSeriesViewModel
|
|||
val discoveredItem = DiscoverItem(tv)
|
||||
backdropService.submit(discoveredItem)
|
||||
|
||||
updateSeasonStatus()
|
||||
updateCanCancel()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
@ -157,6 +160,43 @@ class DiscoverSeriesViewModel
|
|||
navigationManager.navigateTo(destination)
|
||||
}
|
||||
|
||||
private suspend fun updateSeasonStatus() {
|
||||
tvSeries.value?.let { tv ->
|
||||
val seasonStatus = mutableMapOf<Int, SeerrAvailability>()
|
||||
tv.seasons?.forEach {
|
||||
it.seasonNumber?.let {
|
||||
seasonStatus[it] = SeerrAvailability.UNKNOWN
|
||||
}
|
||||
}
|
||||
val tvStatus =
|
||||
SeerrAvailability.from(tv.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN
|
||||
tv.mediaInfo
|
||||
?.requests
|
||||
?.forEach {
|
||||
it.seasons?.mapNotNull { season ->
|
||||
season.seasonNumber?.let {
|
||||
val current = seasonStatus[season.seasonNumber]
|
||||
val new =
|
||||
SeerrAvailability
|
||||
.from(season.status)
|
||||
?.takeIf { it != SeerrAvailability.UNKNOWN } ?: tvStatus
|
||||
if (current == null || new.status > current.status) {
|
||||
seasonStatus[season.seasonNumber] = new
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Timber.v("seasonStatus=%s", seasonStatus)
|
||||
val requestSeasons =
|
||||
seasonStatus.mapNotNull { (seasonNumber, availability) ->
|
||||
tv.seasons?.firstOrNull { it.seasonNumber == seasonNumber }?.let {
|
||||
RequestSeason(it, availability)
|
||||
}
|
||||
}
|
||||
seasons.setValueOnMain(requestSeasons)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateCanCancel() {
|
||||
val user = userConfig.firstOrNull()
|
||||
val canCancel = canUserCancelRequest(user, tvSeries.value?.mediaInfo?.requests)
|
||||
|
|
@ -165,20 +205,43 @@ class DiscoverSeriesViewModel
|
|||
|
||||
fun request(
|
||||
id: Int,
|
||||
seasons: Set<Int>,
|
||||
is4k: Boolean,
|
||||
) {
|
||||
viewModelScope.launchIO {
|
||||
val request =
|
||||
seerrService.api.requestApi.requestPost(
|
||||
RequestPostRequest(
|
||||
is4k = is4k,
|
||||
mediaId = id,
|
||||
mediaType = RequestPostRequest.MediaType.TV,
|
||||
seasons = RequestPostRequest.Seasons.ALL, // TODO handle picking seasons
|
||||
),
|
||||
)
|
||||
fetchAndSetItem().await()
|
||||
updateCanCancel()
|
||||
tvSeries.value?.let { tv ->
|
||||
val currentRequest =
|
||||
tv.mediaInfo?.requests?.firstOrNull {
|
||||
it.requestedBy?.id ==
|
||||
seerrServerRepository.currentUserId.first()
|
||||
}
|
||||
if (currentRequest != null) {
|
||||
Timber.v("User has pending request, will update")
|
||||
seerrService.api.requestApi.requestRequestIdPut(
|
||||
requestId = currentRequest.id.toString(),
|
||||
requestRequestIdPutRequest =
|
||||
RequestRequestIdPutRequest(
|
||||
is4k = is4k,
|
||||
mediaType = RequestRequestIdPutRequest.MediaType.TV,
|
||||
seasons = seasons.toList(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Timber.v("New request for %s seasons", seasons.size)
|
||||
seerrService.api.requestApi.requestPost(
|
||||
RequestPostRequest(
|
||||
is4k = is4k,
|
||||
mediaId = id,
|
||||
mediaType = RequestPostRequest.MediaType.TV,
|
||||
seasons = seasons.toList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fetchAndSetItem().await()
|
||||
updateSeasonStatus()
|
||||
updateCanCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ fun ExpandableDiscoverButtons(
|
|||
trailerOnClick: (Trailer) -> Unit,
|
||||
buttonOnFocusChanged: (FocusState) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
pendingOnClick: () -> Unit = {},
|
||||
) {
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
LazyRow(
|
||||
|
|
@ -89,7 +90,7 @@ fun ExpandableDiscoverButtons(
|
|||
SeerrAvailability.PENDING,
|
||||
SeerrAvailability.PROCESSING,
|
||||
-> {
|
||||
// TODO?
|
||||
pendingOnClick.invoke()
|
||||
}
|
||||
|
||||
SeerrAvailability.PARTIALLY_AVAILABLE,
|
||||
|
|
@ -109,6 +110,21 @@ fun ExpandableDiscoverButtons(
|
|||
.onFocusChanged(buttonOnFocusChanged),
|
||||
)
|
||||
}
|
||||
if (availability == SeerrAvailability.PARTIALLY_AVAILABLE) {
|
||||
item("request_partial") {
|
||||
ExpandableFaButton(
|
||||
title = R.string.request,
|
||||
iconStringRes = R.string.fa_download,
|
||||
onClick = {
|
||||
requestOnClick.invoke()
|
||||
},
|
||||
enabled = availability == SeerrAvailability.PARTIALLY_AVAILABLE,
|
||||
modifier =
|
||||
Modifier
|
||||
.onFocusChanged(buttonOnFocusChanged),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (canCancel) {
|
||||
item("cancel") {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,323 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.discover
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_TYPE_TELEVISION
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableStateSetOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||
import androidx.tv.material3.ListItem
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Surface
|
||||
import androidx.tv.material3.Switch
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.contentColorFor
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.api.seerr.model.Season
|
||||
import com.github.damontecres.wholphin.data.model.SeerrAvailability
|
||||
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.theme.WholphinTheme
|
||||
|
||||
data class RequestSeason(
|
||||
val season: Season,
|
||||
val availability: SeerrAvailability,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RequestSeasons(
|
||||
title: String,
|
||||
seasons: List<RequestSeason>,
|
||||
onSubmit: (Set<Int>, Boolean) -> Unit,
|
||||
request4kEnabled: Boolean,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val allSeasonNumbers = remember(seasons) { seasons.mapNotNull { it.season.seasonNumber }.toSet() }
|
||||
val selected =
|
||||
remember {
|
||||
mutableStateSetOf<Int>(
|
||||
*seasons
|
||||
.mapNotNull {
|
||||
if (it.availability > SeerrAvailability.UNKNOWN) {
|
||||
it.season.seasonNumber
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toTypedArray(),
|
||||
)
|
||||
}
|
||||
var is4k by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
LazyColumn {
|
||||
item {
|
||||
val isSelected = selected.containsAll(allSeasonNumbers)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ClickSwitch(
|
||||
label = stringResource(R.string.select_all),
|
||||
checked = isSelected,
|
||||
onClick = {
|
||||
if (isSelected) {
|
||||
selected.removeAll(allSeasonNumbers)
|
||||
} else {
|
||||
selected.addAll(allSeasonNumbers)
|
||||
}
|
||||
},
|
||||
)
|
||||
Button(
|
||||
onClick = { onSubmit.invoke(selected, is4k) },
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.submit),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (request4kEnabled) {
|
||||
item {
|
||||
ClickSwitch(
|
||||
label = stringResource(R.string.request_4k),
|
||||
checked = is4k,
|
||||
onClick = { is4k = !is4k },
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(seasons) { index, season ->
|
||||
val seasonNumber = season.season.seasonNumber
|
||||
val isSelected = seasonNumber in selected
|
||||
SeasonListItem(
|
||||
season = season,
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (isSelected) {
|
||||
selected.remove(seasonNumber)
|
||||
} else {
|
||||
seasonNumber?.let { selected.add(it) }
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
if (seasons.size > 7) {
|
||||
item {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
) {
|
||||
Button(
|
||||
onClick = { onSubmit.invoke(selected, is4k) },
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.submit),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SeasonListItem(
|
||||
season: RequestSeason,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ListItem(
|
||||
selected = false,
|
||||
headlineContent = {
|
||||
Text(
|
||||
text =
|
||||
season.season.name
|
||||
?: (stringResource(R.string.tv_season) + " ${season.season.seasonNumber}"),
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
season.season.episodeCount?.let {
|
||||
Text(
|
||||
// TODO should use plurals string
|
||||
text = "${season.season.episodeCount} " + stringResource(R.string.episodes),
|
||||
)
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
when (season.availability) {
|
||||
SeerrAvailability.UNKNOWN -> {}
|
||||
|
||||
SeerrAvailability.DELETED -> {}
|
||||
|
||||
SeerrAvailability.PENDING,
|
||||
SeerrAvailability.PROCESSING,
|
||||
-> {
|
||||
PendingIndicator()
|
||||
}
|
||||
|
||||
SeerrAvailability.PARTIALLY_AVAILABLE -> {
|
||||
PartiallyAvailableIndicator()
|
||||
}
|
||||
|
||||
SeerrAvailability.AVAILABLE -> {
|
||||
AvailableIndicator()
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Row {
|
||||
Switch(
|
||||
checked = selected,
|
||||
onCheckedChange = {
|
||||
onClick.invoke()
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClickSurface(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
colors =
|
||||
ClickableSurfaceDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,
|
||||
focusedContentColor = contentColorFor(MaterialTheme.colorScheme.inverseSurface),
|
||||
pressedContainerColor = MaterialTheme.colorScheme.inverseSurface,
|
||||
pressedContentColor = contentColorFor(MaterialTheme.colorScheme.inverseSurface),
|
||||
),
|
||||
onClick = onClick,
|
||||
content = content,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClickSwitch(
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ClickSurface(
|
||||
onClick = onClick,
|
||||
modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.height(54.dp),
|
||||
) {
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = {},
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RequestSeasonsDialog(
|
||||
title: String,
|
||||
seasons: List<RequestSeason>,
|
||||
request4kEnabled: Boolean,
|
||||
onSubmit: (Set<Int>, Boolean) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
BasicDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
RequestSeasons(
|
||||
title = title,
|
||||
seasons = seasons,
|
||||
request4kEnabled = request4kEnabled,
|
||||
onSubmit = onSubmit,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(
|
||||
device = "spec:parent=tv_1080p",
|
||||
backgroundColor = 0xFF383535,
|
||||
uiMode = UI_MODE_TYPE_TELEVISION,
|
||||
heightDp = 800,
|
||||
)
|
||||
@Composable
|
||||
fun RequestSeasonsPreview() {
|
||||
val seasons =
|
||||
List(10) {
|
||||
RequestSeason(
|
||||
season =
|
||||
Season(
|
||||
seasonNumber = it + 1,
|
||||
episodeCount = 10 + it,
|
||||
),
|
||||
availability =
|
||||
if (it < 3) {
|
||||
SeerrAvailability.AVAILABLE
|
||||
} else {
|
||||
SeerrAvailability.UNKNOWN
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
WholphinTheme {
|
||||
RequestSeasons(
|
||||
title = "Series title",
|
||||
seasons = seasons,
|
||||
request4kEnabled = true,
|
||||
onSubmit = { _, _ -> },
|
||||
modifier = Modifier.width(400.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.series
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -120,6 +123,7 @@ fun SeriesDetails(
|
|||
val people by viewModel.people.observeAsState(listOf())
|
||||
val similar by viewModel.similar.observeAsState(listOf())
|
||||
val discovered by viewModel.discovered.collectAsState()
|
||||
val discoverSeries by viewModel.discoverSeries.collectAsState()
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
|
||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||
|
|
@ -230,6 +234,12 @@ fun SeriesDetails(
|
|||
onClickExtra = { _, extra ->
|
||||
viewModel.navigateTo(extra.destination)
|
||||
},
|
||||
discoverSeries = discoverSeries,
|
||||
onClickDiscoverSeries = {
|
||||
discoverSeries?.let {
|
||||
viewModel.navigateTo(Destination.DiscoveredItem(it))
|
||||
}
|
||||
},
|
||||
discovered = discovered,
|
||||
onClickDiscover = { index, item ->
|
||||
viewModel.navigateTo(item.destination)
|
||||
|
|
@ -350,6 +360,8 @@ fun SeriesDetailsContent(
|
|||
onClickExtra: (Int, ExtrasItem) -> Unit,
|
||||
moreActions: MoreDialogActions,
|
||||
onClickDiscover: (Int, DiscoverItem) -> Unit,
|
||||
discoverSeries: DiscoverItem?,
|
||||
onClickDiscoverSeries: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -487,6 +499,25 @@ fun SeriesDetailsContent(
|
|||
},
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = discoverSeries != null,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExpandableFaButton(
|
||||
title = R.string.discover,
|
||||
iconStringRes = R.string.fa_magnifying_glass_plus,
|
||||
onClick = onClickDiscoverSeries,
|
||||
modifier =
|
||||
Modifier.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
scope.launch(ExceptionHandler()) {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import kotlinx.coroutines.async
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
|
@ -123,6 +124,7 @@ class SeriesViewModel
|
|||
|
||||
val peopleInEpisode = MutableLiveData<PeopleInItem>(PeopleInItem())
|
||||
val discovered = MutableStateFlow<List<DiscoverItem>>(listOf())
|
||||
val discoverSeries = MutableStateFlow<DiscoverItem?>(null)
|
||||
|
||||
val position = MutableStateFlow(SeriesOverviewPosition(0, 0))
|
||||
|
||||
|
|
@ -232,6 +234,22 @@ class SeriesViewModel
|
|||
val results = seerrService.similar(item).orEmpty()
|
||||
discovered.update { results }
|
||||
}
|
||||
viewModelScope.launchIO {
|
||||
seerrService.active.collectLatest { active ->
|
||||
val tv =
|
||||
if (active) {
|
||||
try {
|
||||
seerrService.getTvSeries(item)?.let { DiscoverItem(it) }
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex)
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
discoverSeries.update { tv }
|
||||
}
|
||||
}
|
||||
}
|
||||
mediaManagementService.deletedItemFlow
|
||||
.onEach { deletedItem ->
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ class SwitchSeerrViewModel
|
|||
)
|
||||
}
|
||||
}
|
||||
serverConnectionStatus.update { LoadingState.Success }
|
||||
} else {
|
||||
val message =
|
||||
results
|
||||
|
|
|
|||
|
|
@ -719,5 +719,6 @@
|
|||
<string name="creator">Creator</string>
|
||||
<string name="artist">Artist</string>
|
||||
<string name="search_for">Search %s</string>
|
||||
<string name="select_all">Select all</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1086,6 +1086,11 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Episode'
|
||||
status:
|
||||
type: integer
|
||||
example: 0
|
||||
description: Status of the request. 1 = PENDING APPROVAL, 2 = APPROVED, 3 = DECLINED
|
||||
readOnly: true
|
||||
TvDetails:
|
||||
type: object
|
||||
properties:
|
||||
|
|
@ -1268,6 +1273,10 @@ components:
|
|||
type: string
|
||||
type:
|
||||
type: string
|
||||
seasons:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Season'
|
||||
required:
|
||||
- id
|
||||
- status
|
||||
|
|
@ -6135,16 +6144,10 @@ paths:
|
|||
type: integer
|
||||
example: 123
|
||||
seasons:
|
||||
# oneOf:
|
||||
# - type: array
|
||||
# items:
|
||||
# type: integer
|
||||
# minimum: 0
|
||||
# - type: string
|
||||
# enum: [all]
|
||||
# TODO
|
||||
type: string
|
||||
enum: [all]
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
minimum: 0
|
||||
nullable: true
|
||||
is4k:
|
||||
type: boolean
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue