mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-07 03:04:50 +01:00
Discover movies & tv shows by genre (#1610)
## Description Adds genre rows for movies & TV shows to the discover page. These work the same as on the Seerr web UI, clicking one will show media in the genre. ### Related issues Related to https://github.com/damontecres/Wholphin/issues/702 ### Testing Just on the emulator so far ## Screenshots <img width="1280" height="720" alt="discover_genres Large" src="https://github.com/user-attachments/assets/93e5ef25-e253-49bc-9009-89f814889c98" /> ## AI or LLM usage None
This commit is contained in:
parent
c945e1a472
commit
f21b29fe1b
9 changed files with 564 additions and 130 deletions
|
|
@ -92,13 +92,7 @@ data class DiscoverItem(
|
||||||
|
|
||||||
val destination: Destination
|
val destination: Destination
|
||||||
get() {
|
get() {
|
||||||
val jfType =
|
val jfType = type.baseItemKind
|
||||||
when (type) {
|
|
||||||
SeerrItemType.MOVIE -> BaseItemKind.MOVIE
|
|
||||||
SeerrItemType.TV -> BaseItemKind.SERIES
|
|
||||||
SeerrItemType.PERSON -> BaseItemKind.PERSON
|
|
||||||
SeerrItemType.UNKNOWN -> null
|
|
||||||
}
|
|
||||||
return if (jellyfinItemId != null && jfType != null) {
|
return if (jellyfinItemId != null && jfType != null) {
|
||||||
Destination.MediaItem(
|
Destination.MediaItem(
|
||||||
itemId = jellyfinItemId,
|
itemId = jellyfinItemId,
|
||||||
|
|
|
||||||
|
|
@ -161,10 +161,12 @@ class SeerrService
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createImageUrl(
|
suspend fun createImageUrl(
|
||||||
imageType: ImageType,
|
imageType: ImageType,
|
||||||
path: String?,
|
path: String?,
|
||||||
mediaInfo: MediaInfo?,
|
mediaInfo: MediaInfo?,
|
||||||
|
backdropHeight: Int = 1080,
|
||||||
|
backdropWidth: Int = 1920,
|
||||||
): String? {
|
): String? {
|
||||||
if (mediaInfo != null) {
|
if (mediaInfo != null) {
|
||||||
val itemId =
|
val itemId =
|
||||||
|
|
@ -193,7 +195,7 @@ class SeerrService
|
||||||
val prefix =
|
val prefix =
|
||||||
when (imageType) {
|
when (imageType) {
|
||||||
ImageType.PRIMARY -> "/t/p/w500"
|
ImageType.PRIMARY -> "/t/p/w500"
|
||||||
ImageType.BACKDROP -> "/t/p/w1920_and_h1080_multi_faces"
|
ImageType.BACKDROP -> "/t/p/w${backdropWidth}_and_h${backdropHeight}_multi_faces"
|
||||||
else -> throw IllegalArgumentException("Image type not supported: $imageType")
|
else -> throw IllegalArgumentException("Image type not supported: $imageType")
|
||||||
}
|
}
|
||||||
return "${base}${prefix}$path"
|
return "${base}${prefix}$path"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.discover
|
||||||
|
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrItemType
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResArgStringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringStringProvider
|
||||||
|
|
||||||
|
sealed interface DiscoverFocusedItem {
|
||||||
|
val id: Int
|
||||||
|
val title: StringProvider?
|
||||||
|
val subtitle: String?
|
||||||
|
val overview: String?
|
||||||
|
val backDropUrl: String?
|
||||||
|
|
||||||
|
data class Item(
|
||||||
|
val item: DiscoverItem,
|
||||||
|
) : DiscoverFocusedItem {
|
||||||
|
override val id: Int
|
||||||
|
get() = item.id
|
||||||
|
override val title: StringProvider? = item.title?.let { StringStringProvider(it) }
|
||||||
|
override val subtitle: String?
|
||||||
|
get() = item.subtitle
|
||||||
|
override val overview: String?
|
||||||
|
get() = item.overview
|
||||||
|
override val backDropUrl: String?
|
||||||
|
get() = item.backDropUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Genre(
|
||||||
|
override val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val type: SeerrItemType,
|
||||||
|
override val backDropUrl: String?,
|
||||||
|
) : DiscoverFocusedItem {
|
||||||
|
override val title: StringProvider = discoverGenreTitle(name, type)
|
||||||
|
override val subtitle: String?
|
||||||
|
get() = null
|
||||||
|
override val overview: String?
|
||||||
|
get() = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun discoverGenreTitle(
|
||||||
|
name: String,
|
||||||
|
type: SeerrItemType,
|
||||||
|
): ResArgStringProvider =
|
||||||
|
when (type) {
|
||||||
|
SeerrItemType.MOVIE -> ResArgStringProvider(R.string.genre_movies, name)
|
||||||
|
SeerrItemType.TV -> ResArgStringProvider(R.string.genre_tv_shows, name)
|
||||||
|
else -> throw UnsupportedOperationException("$type not supported")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.discover
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
|
import com.github.damontecres.wholphin.ui.cards.GenreCard
|
||||||
|
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.Genre
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringProvider
|
||||||
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DiscoverGenreRow(
|
||||||
|
title: StringProvider,
|
||||||
|
items: DataLoadingState<List<Genre>>,
|
||||||
|
onClickItem: (Int, Genre) -> Unit,
|
||||||
|
onCardFocus: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onLongClickItem: (Int, Genre) -> Unit = { _, _ -> },
|
||||||
|
) {
|
||||||
|
when (items) {
|
||||||
|
is DataLoadingState.Error -> {
|
||||||
|
ErrorMessage(items.message, items.exception, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
DataLoadingState.Loading,
|
||||||
|
DataLoadingState.Pending,
|
||||||
|
-> {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title.getString(),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.loading),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataLoadingState.Success<List<Genre>> -> {
|
||||||
|
ItemRow(
|
||||||
|
title = title.getString(),
|
||||||
|
items = items.data,
|
||||||
|
onClickItem = onClickItem,
|
||||||
|
onLongClickItem = onLongClickItem,
|
||||||
|
cardContent = { index, item, mod, onClick, onLongClick ->
|
||||||
|
GenreCard(
|
||||||
|
genre = item,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
modifier =
|
||||||
|
mod
|
||||||
|
.height(Cards.heightEpisode)
|
||||||
|
.onFocusChanged {
|
||||||
|
if (it.isFocused) onCardFocus.invoke(index)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalPadding = 16.dp,
|
||||||
|
showViewMore = false,
|
||||||
|
viewMoreCardContent = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,12 +9,12 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
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.compose.ui.unit.Dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrItemType
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.SeerrApi
|
import com.github.damontecres.wholphin.services.SeerrApi
|
||||||
import com.github.damontecres.wholphin.services.SeerrService
|
import com.github.damontecres.wholphin.services.SeerrService
|
||||||
|
|
@ -28,6 +28,7 @@ import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
import com.github.damontecres.wholphin.util.DiscoverMovieRequestHandler
|
import com.github.damontecres.wholphin.util.DiscoverMovieRequestHandler
|
||||||
|
import com.github.damontecres.wholphin.util.DiscoverPagerType
|
||||||
import com.github.damontecres.wholphin.util.DiscoverRequestPager
|
import com.github.damontecres.wholphin.util.DiscoverRequestPager
|
||||||
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
||||||
import com.github.damontecres.wholphin.util.DiscoverTvRequestHandler
|
import com.github.damontecres.wholphin.util.DiscoverTvRequestHandler
|
||||||
|
|
@ -52,13 +53,13 @@ class DiscoverRequestViewModel
|
||||||
private val seerrService: SeerrService,
|
private val seerrService: SeerrService,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
private val api: SeerrApi,
|
private val api: SeerrApi,
|
||||||
@Assisted val type: DiscoverRequestType,
|
@Assisted val type: DiscoverPagerType,
|
||||||
@Assisted startIndex: Int,
|
@Assisted startIndex: Int,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
type: DiscoverRequestType,
|
type: DiscoverPagerType,
|
||||||
startIndex: Int,
|
startIndex: Int,
|
||||||
): DiscoverRequestViewModel
|
): DiscoverRequestViewModel
|
||||||
}
|
}
|
||||||
|
|
@ -71,53 +72,83 @@ class DiscoverRequestViewModel
|
||||||
try {
|
try {
|
||||||
val pager =
|
val pager =
|
||||||
when (type) {
|
when (type) {
|
||||||
DiscoverRequestType.DISCOVER_TV -> {
|
is DiscoverPagerType.RequestType -> {
|
||||||
DiscoverRequestPager(
|
when (type.type) {
|
||||||
api,
|
DiscoverRequestType.DISCOVER_TV -> {
|
||||||
DiscoverTvRequestHandler,
|
DiscoverRequestPager(
|
||||||
seerrService::createDiscoverItem,
|
api,
|
||||||
viewModelScope,
|
DiscoverTvRequestHandler(),
|
||||||
)
|
seerrService::createDiscoverItem,
|
||||||
|
viewModelScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscoverRequestType.DISCOVER_MOVIES -> {
|
||||||
|
DiscoverRequestPager(
|
||||||
|
api,
|
||||||
|
DiscoverMovieRequestHandler(),
|
||||||
|
seerrService::createDiscoverItem,
|
||||||
|
viewModelScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscoverRequestType.TRENDING -> {
|
||||||
|
DiscoverRequestPager(
|
||||||
|
api,
|
||||||
|
TrendingRequestHandler,
|
||||||
|
seerrService::createDiscoverItem,
|
||||||
|
viewModelScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscoverRequestType.UPCOMING_TV -> {
|
||||||
|
DiscoverRequestPager(
|
||||||
|
api,
|
||||||
|
UpcomingTvRequestHandler,
|
||||||
|
seerrService::createDiscoverItem,
|
||||||
|
viewModelScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscoverRequestType.UPCOMING_MOVIES -> {
|
||||||
|
DiscoverRequestPager(
|
||||||
|
api,
|
||||||
|
UpcomingMovieRequestHandler,
|
||||||
|
seerrService::createDiscoverItem,
|
||||||
|
viewModelScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscoverRequestType.UNKNOWN -> {
|
||||||
|
throw IllegalArgumentException("Cannot display grid for DiscoverRequestType.UNKNOWN")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscoverRequestType.DISCOVER_MOVIES -> {
|
is DiscoverPagerType.Genre -> {
|
||||||
DiscoverRequestPager(
|
when (type.type) {
|
||||||
api,
|
SeerrItemType.MOVIE -> {
|
||||||
DiscoverMovieRequestHandler,
|
DiscoverRequestPager(
|
||||||
seerrService::createDiscoverItem,
|
api,
|
||||||
viewModelScope,
|
DiscoverMovieRequestHandler(genre = type.genreId),
|
||||||
)
|
seerrService::createDiscoverItem,
|
||||||
}
|
viewModelScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
DiscoverRequestType.TRENDING -> {
|
SeerrItemType.TV -> {
|
||||||
DiscoverRequestPager(
|
DiscoverRequestPager(
|
||||||
api,
|
api,
|
||||||
TrendingRequestHandler,
|
DiscoverTvRequestHandler(genre = type.genreId),
|
||||||
seerrService::createDiscoverItem,
|
seerrService::createDiscoverItem,
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscoverRequestType.UPCOMING_TV -> {
|
else -> {
|
||||||
DiscoverRequestPager(
|
throw UnsupportedOperationException("Type not supported: ${type.type}")
|
||||||
api,
|
}
|
||||||
UpcomingTvRequestHandler,
|
}
|
||||||
seerrService::createDiscoverItem,
|
|
||||||
viewModelScope,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DiscoverRequestType.UPCOMING_MOVIES -> {
|
|
||||||
DiscoverRequestPager(
|
|
||||||
api,
|
|
||||||
UpcomingMovieRequestHandler,
|
|
||||||
seerrService::createDiscoverItem,
|
|
||||||
viewModelScope,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DiscoverRequestType.UNKNOWN -> {
|
|
||||||
throw IllegalArgumentException("Cannot display grid for DiscoverRequestType.UNKNOWN")
|
|
||||||
}
|
}
|
||||||
}.init(startIndex)
|
}.init(startIndex)
|
||||||
_state.update {
|
_state.update {
|
||||||
|
|
@ -168,7 +199,7 @@ fun DiscoverRequestGrid(
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
GridTitle(stringResource(destination.type.stringRes))
|
GridTitle(destination.type.title.getString())
|
||||||
|
|
||||||
CardGrid(
|
CardGrid(
|
||||||
initialPosition = destination.startIndex,
|
initialPosition = destination.startIndex,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -23,6 +22,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -39,6 +39,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.SeerrService
|
import com.github.damontecres.wholphin.services.SeerrService
|
||||||
|
import com.github.damontecres.wholphin.ui.components.Genre
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.listToDotString
|
import com.github.damontecres.wholphin.ui.listToDotString
|
||||||
|
|
@ -47,16 +48,21 @@ import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberPosition
|
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.ui.util.EmptyStringProvider
|
import com.github.damontecres.wholphin.ui.util.EmptyStringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResProviderStringProvider
|
||||||
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
||||||
import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec
|
import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec
|
||||||
import com.github.damontecres.wholphin.ui.util.StringProvider
|
import com.github.damontecres.wholphin.ui.util.StringProvider
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
|
import com.github.damontecres.wholphin.util.DiscoverPagerType
|
||||||
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
||||||
|
import com.github.damontecres.wholphin.util.successValue
|
||||||
import com.google.common.cache.CacheBuilder
|
import com.google.common.cache.CacheBuilder
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -126,6 +132,83 @@ class SeerrDiscoverViewModel
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val movieGenres =
|
||||||
|
try {
|
||||||
|
val result = seerrService.api.searchApi.discoverGenresliderMovieGet()
|
||||||
|
val genres =
|
||||||
|
result
|
||||||
|
.filter { it.id != null && it.name != null }
|
||||||
|
.map {
|
||||||
|
val id = it.id!!.toUUID()
|
||||||
|
val imageUrl =
|
||||||
|
it.backdrops?.randomOrNull()?.let { path ->
|
||||||
|
seerrService.createImageUrl(
|
||||||
|
imageType = ImageType.BACKDROP,
|
||||||
|
path = path,
|
||||||
|
mediaInfo = null,
|
||||||
|
// Don't need high resolution
|
||||||
|
backdropWidth = 1280,
|
||||||
|
backdropHeight = 720,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Genre(
|
||||||
|
id = id,
|
||||||
|
name = it.name!!,
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Timber.v("Got %s movie genres", genres.size)
|
||||||
|
DataLoadingState.Success(genres)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error getting movie genres")
|
||||||
|
DataLoadingState.Error(ex)
|
||||||
|
}
|
||||||
|
state.update {
|
||||||
|
it.copy(
|
||||||
|
movieGenres = movieGenres,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val tvGenres =
|
||||||
|
try {
|
||||||
|
val result = seerrService.api.searchApi.discoverGenresliderTvGet()
|
||||||
|
val genres =
|
||||||
|
result
|
||||||
|
.filter { it.id != null && it.name != null }
|
||||||
|
.map {
|
||||||
|
val id = it.id!!.toUUID()
|
||||||
|
val imageUrl =
|
||||||
|
it.backdrops?.randomOrNull()?.let { path ->
|
||||||
|
seerrService.createImageUrl(
|
||||||
|
imageType = ImageType.BACKDROP,
|
||||||
|
path = path,
|
||||||
|
mediaInfo = null,
|
||||||
|
// Don't need high resolution
|
||||||
|
backdropWidth = 1280,
|
||||||
|
backdropHeight = 720,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Genre(
|
||||||
|
id = id,
|
||||||
|
name = it.name!!,
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Timber.v("Got %s tv genres", genres.size)
|
||||||
|
DataLoadingState.Success(genres)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error getting tv genres")
|
||||||
|
DataLoadingState.Error(ex)
|
||||||
|
}
|
||||||
|
state.update {
|
||||||
|
it.copy(
|
||||||
|
tvGenres = tvGenres,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchAndUpdateState(
|
private fun fetchAndUpdateState(
|
||||||
|
|
@ -149,11 +232,13 @@ class SeerrDiscoverViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateBackdrop(item: DiscoverItem?) {
|
fun updateBackdrop(item: DiscoverFocusedItem?) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
backdropService.submit("discover_${item.id}", item.backDropUrl)
|
backdropService.submit("discover_${item.id}", item.backDropUrl)
|
||||||
fetchRating(item)
|
if (item is DiscoverFocusedItem.Item) {
|
||||||
|
fetchRating(item.id, item.item.type)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
backdropService.clearBackdrop()
|
backdropService.clearBackdrop()
|
||||||
}
|
}
|
||||||
|
|
@ -167,25 +252,28 @@ class SeerrDiscoverViewModel
|
||||||
.build<Int, DiscoverRating>()
|
.build<Int, DiscoverRating>()
|
||||||
|
|
||||||
// TODO this is not very efficient
|
// TODO this is not very efficient
|
||||||
fun fetchRating(item: DiscoverItem) {
|
fun fetchRating(
|
||||||
|
id: Int,
|
||||||
|
type: SeerrItemType,
|
||||||
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val cachedResult = ratingCache.getIfPresent(item.id)
|
val cachedResult = ratingCache.getIfPresent(id)
|
||||||
if (cachedResult != null) {
|
if (cachedResult != null) {
|
||||||
return@launchIO
|
return@launchIO
|
||||||
}
|
}
|
||||||
val result =
|
val result =
|
||||||
try {
|
try {
|
||||||
when (item.type) {
|
when (type) {
|
||||||
SeerrItemType.MOVIE -> {
|
SeerrItemType.MOVIE -> {
|
||||||
DiscoverRating(
|
DiscoverRating(
|
||||||
seerrService.api.moviesApi.movieMovieIdRatingsGet(
|
seerrService.api.moviesApi.movieMovieIdRatingsGet(
|
||||||
movieId = item.id,
|
movieId = id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SeerrItemType.TV -> {
|
SeerrItemType.TV -> {
|
||||||
DiscoverRating(seerrService.api.tvApi.tvTvIdRatingsGet(tvId = item.id))
|
DiscoverRating(seerrService.api.tvApi.tvTvIdRatingsGet(tvId = id))
|
||||||
}
|
}
|
||||||
|
|
||||||
SeerrItemType.PERSON -> {
|
SeerrItemType.PERSON -> {
|
||||||
|
|
@ -198,17 +286,17 @@ class SeerrDiscoverViewModel
|
||||||
}
|
}
|
||||||
} catch (ex: ClientException) {
|
} catch (ex: ClientException) {
|
||||||
if (ex.statusCode == 404) {
|
if (ex.statusCode == 404) {
|
||||||
Timber.w("No rating found for %s", item.id)
|
Timber.w("No rating found for %s", id)
|
||||||
DiscoverRating(null, null)
|
DiscoverRating(null, null)
|
||||||
} else {
|
} else {
|
||||||
Timber.e(ex, "Error getting rating for %s", item.id)
|
Timber.e(ex, "Error getting rating for %s", id)
|
||||||
return@launchIO
|
return@launchIO
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Error getting rating for %s", item.id)
|
Timber.e(ex, "Error getting rating for %s", id)
|
||||||
return@launchIO
|
return@launchIO
|
||||||
}
|
}
|
||||||
ratingCache.put(item.id, result)
|
ratingCache.put(id, result)
|
||||||
rating.update {
|
rating.update {
|
||||||
ratingCache.asMap().toMap()
|
ratingCache.asMap().toMap()
|
||||||
}
|
}
|
||||||
|
|
@ -216,6 +304,10 @@ class SeerrDiscoverViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Int.toUUID() = UUID(0L, toLong())
|
||||||
|
|
||||||
|
fun UUID.toInt() = leastSignificantBits.toInt()
|
||||||
|
|
||||||
data class DiscoverRowData(
|
data class DiscoverRowData(
|
||||||
val title: StringProvider,
|
val title: StringProvider,
|
||||||
val items: DataLoadingState<List<DiscoverItem>>,
|
val items: DataLoadingState<List<DiscoverItem>>,
|
||||||
|
|
@ -237,8 +329,20 @@ data class DiscoverState(
|
||||||
val trending: DiscoverRowData = DiscoverRowData.EMPTY,
|
val trending: DiscoverRowData = DiscoverRowData.EMPTY,
|
||||||
val upcomingMovies: DiscoverRowData = DiscoverRowData.EMPTY,
|
val upcomingMovies: DiscoverRowData = DiscoverRowData.EMPTY,
|
||||||
val upcomingTv: DiscoverRowData = DiscoverRowData.EMPTY,
|
val upcomingTv: DiscoverRowData = DiscoverRowData.EMPTY,
|
||||||
|
val movieGenres: DataLoadingState<List<Genre>> = DataLoadingState.Pending,
|
||||||
|
val tvGenres: DataLoadingState<List<Genre>> = DataLoadingState.Pending,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private const val ROW_TRENDING = 0
|
||||||
|
private const val ROW_MOVIES = ROW_TRENDING + 1
|
||||||
|
private const val ROW_GENRES_MOVIE = ROW_MOVIES + 1
|
||||||
|
private const val ROW_UPCOMING_MOVIES = ROW_GENRES_MOVIE + 1
|
||||||
|
private const val ROW_TV = ROW_UPCOMING_MOVIES + 1
|
||||||
|
private const val ROW_GENRES_TV = ROW_TV + 1
|
||||||
|
private const val ROW_UPCOMING_TV = ROW_GENRES_TV + 1
|
||||||
|
|
||||||
|
private const val LAST_ROW = ROW_UPCOMING_TV
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SeerrDiscoverPage(
|
fun SeerrDiscoverPage(
|
||||||
|
|
@ -248,16 +352,62 @@ fun SeerrDiscoverPage(
|
||||||
viewModel: SeerrDiscoverViewModel = hiltViewModel(),
|
viewModel: SeerrDiscoverViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val rows =
|
|
||||||
listOf(state.trending, state.movies, state.tv, state.upcomingMovies, state.upcomingTv)
|
|
||||||
val ratingMap by viewModel.rating.collectAsState()
|
val ratingMap by viewModel.rating.collectAsState()
|
||||||
|
|
||||||
val focusRequesters = remember(rows) { List(rows.size) { FocusRequester() } }
|
val focusRequesters = remember { List(LAST_ROW + 1) { FocusRequester() } }
|
||||||
var position by rememberPosition(0, -1)
|
var position by rememberPosition(0, -1)
|
||||||
val focusedItem =
|
val focusedItem: DiscoverFocusedItem? =
|
||||||
remember(position) {
|
remember(position) {
|
||||||
position.let {
|
position.let {
|
||||||
(rows.getOrNull(it.row)?.items as? DataLoadingState.Success)?.data?.getOrNull(it.column)
|
val discoverRow =
|
||||||
|
when (position.row) {
|
||||||
|
ROW_TRENDING -> state.trending
|
||||||
|
ROW_MOVIES -> state.movies
|
||||||
|
ROW_UPCOMING_MOVIES -> state.upcomingMovies
|
||||||
|
ROW_TV -> state.tv
|
||||||
|
ROW_UPCOMING_TV -> state.upcomingTv
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (discoverRow != null) {
|
||||||
|
(discoverRow.items as? DataLoadingState.Success)
|
||||||
|
?.data
|
||||||
|
?.getOrNull(it.column)
|
||||||
|
?.let {
|
||||||
|
DiscoverFocusedItem.Item(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (position.row) {
|
||||||
|
ROW_GENRES_MOVIE -> {
|
||||||
|
state.movieGenres.successValue
|
||||||
|
?.getOrNull(position.column)
|
||||||
|
?.let {
|
||||||
|
DiscoverFocusedItem.Genre(
|
||||||
|
id = it.id.toInt(),
|
||||||
|
name = it.name,
|
||||||
|
type = SeerrItemType.MOVIE,
|
||||||
|
backDropUrl = it.imageUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ROW_GENRES_TV -> {
|
||||||
|
state.movieGenres.successValue
|
||||||
|
?.getOrNull(position.column)
|
||||||
|
?.let {
|
||||||
|
DiscoverFocusedItem.Genre(
|
||||||
|
id = it.id.toInt(),
|
||||||
|
name = it.name,
|
||||||
|
type = SeerrItemType.TV,
|
||||||
|
backDropUrl = it.imageUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(focusedItem) {
|
LaunchedEffect(focusedItem) {
|
||||||
|
|
@ -277,25 +427,27 @@ fun SeerrDiscoverPage(
|
||||||
) {
|
) {
|
||||||
val details =
|
val details =
|
||||||
remember(focusedItem, ratingMap) {
|
remember(focusedItem, ratingMap) {
|
||||||
buildList {
|
(focusedItem as? DiscoverFocusedItem.Item)?.let { focusedItem ->
|
||||||
focusedItem
|
buildList {
|
||||||
?.releaseDate
|
focusedItem.item
|
||||||
?.year
|
.releaseDate
|
||||||
?.toString()
|
?.year
|
||||||
?.let(::add)
|
?.toString()
|
||||||
}.let {
|
?.let(::add)
|
||||||
val rating = focusedItem?.id?.let { ratingMap[it] }
|
}.let {
|
||||||
val str =
|
val rating = focusedItem.id.let { ratingMap[it] }
|
||||||
listToDotString(
|
val str =
|
||||||
it,
|
listToDotString(
|
||||||
rating?.audienceRating,
|
it,
|
||||||
rating?.criticRating?.toFloat(),
|
rating?.audienceRating,
|
||||||
)
|
rating?.criticRating?.toFloat(),
|
||||||
QuickDetailsData(str)
|
)
|
||||||
|
QuickDetailsData(str)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HomePageHeader(
|
HomePageHeader(
|
||||||
title = focusedItem?.title,
|
title = focusedItem?.title?.getString(),
|
||||||
subtitle = focusedItem?.subtitle,
|
subtitle = focusedItem?.subtitle,
|
||||||
overview = focusedItem?.overview,
|
overview = focusedItem?.overview,
|
||||||
overviewTwoLines = true,
|
overviewTwoLines = true,
|
||||||
|
|
@ -309,6 +461,7 @@ fun SeerrDiscoverPage(
|
||||||
.padding(top = 24.dp, bottom = 16.dp, start = 32.dp)
|
.padding(top = 24.dp, bottom = 16.dp, start = 32.dp)
|
||||||
.fillMaxHeight(.25f),
|
.fillMaxHeight(.25f),
|
||||||
)
|
)
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val spaceAbovePx =
|
val spaceAbovePx =
|
||||||
with(density) {
|
with(density) {
|
||||||
|
|
@ -316,6 +469,41 @@ fun SeerrDiscoverPage(
|
||||||
50.dp.toPx()
|
50.dp.toPx()
|
||||||
}
|
}
|
||||||
val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
|
val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun discoverItemRow(
|
||||||
|
rowIndex: Int,
|
||||||
|
row: DiscoverRowData,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(LocalBringIntoViewSpec provides defaultBringIntoViewSpec) {
|
||||||
|
DiscoverRow(
|
||||||
|
row = row,
|
||||||
|
onClickItem = { index, item ->
|
||||||
|
position = RowColumn(rowIndex, index)
|
||||||
|
viewModel.navigationManager.navigateTo(item.destination)
|
||||||
|
},
|
||||||
|
onLongClickItem = { index, item -> },
|
||||||
|
onCardFocus = { index ->
|
||||||
|
position = RowColumn(rowIndex, index)
|
||||||
|
positionCallback.invoke(position)
|
||||||
|
},
|
||||||
|
focusRequester = focusRequesters[rowIndex],
|
||||||
|
enableViewMore = row.type != DiscoverRequestType.UNKNOWN,
|
||||||
|
onClickViewMore = {
|
||||||
|
(row.items as? DataLoadingState.Success<List<DiscoverItem>>)?.data?.size?.let {
|
||||||
|
position = RowColumn(rowIndex, it)
|
||||||
|
positionCallback.invoke(position)
|
||||||
|
}
|
||||||
|
viewModel.navigationManager.navigateTo(
|
||||||
|
Destination.DiscoverMoreResult(DiscoverPagerType.RequestType(row.type)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalBringIntoViewSpec provides ScrollToTopBringIntoViewSpec(spaceAbovePx),
|
LocalBringIntoViewSpec provides ScrollToTopBringIntoViewSpec(spaceAbovePx),
|
||||||
) {
|
) {
|
||||||
|
|
@ -327,36 +515,81 @@ fun SeerrDiscoverPage(
|
||||||
.focusRestorer()
|
.focusRestorer()
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(rows) { rowIndex, row ->
|
item { discoverItemRow(ROW_TRENDING, state.trending) }
|
||||||
|
|
||||||
|
// Movies
|
||||||
|
item { discoverItemRow(ROW_MOVIES, state.movies) }
|
||||||
|
item {
|
||||||
CompositionLocalProvider(LocalBringIntoViewSpec provides defaultBringIntoViewSpec) {
|
CompositionLocalProvider(LocalBringIntoViewSpec provides defaultBringIntoViewSpec) {
|
||||||
DiscoverRow(
|
DiscoverGenreRow(
|
||||||
row = row,
|
title =
|
||||||
onClickItem = { index, item ->
|
remember {
|
||||||
position = RowColumn(rowIndex, index)
|
ResProviderStringProvider(
|
||||||
viewModel.navigationManager.navigateTo(item.destination)
|
R.string.genres_in,
|
||||||
},
|
ResStringProvider(R.string.movies_title),
|
||||||
onLongClickItem = { index, item -> },
|
)
|
||||||
onCardFocus = { index ->
|
},
|
||||||
position = RowColumn(rowIndex, index)
|
items = state.movieGenres,
|
||||||
positionCallback.invoke(position)
|
onClickItem = { index, genre ->
|
||||||
},
|
position = RowColumn(ROW_GENRES_MOVIE, index)
|
||||||
focusRequester = focusRequesters[rowIndex],
|
|
||||||
enableViewMore = row.type != DiscoverRequestType.UNKNOWN,
|
|
||||||
onClickViewMore = {
|
|
||||||
(row.items as? DataLoadingState.Success<List<DiscoverItem>>)?.data?.size?.let {
|
|
||||||
position = RowColumn(rowIndex, it)
|
|
||||||
positionCallback.invoke(position)
|
|
||||||
}
|
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigationManager.navigateTo(
|
||||||
Destination.DiscoverMoreResult(row.type),
|
Destination.DiscoverMoreResult(
|
||||||
|
type =
|
||||||
|
DiscoverPagerType.Genre(
|
||||||
|
genreId = genre.id.toInt(),
|
||||||
|
name = genre.name,
|
||||||
|
type = SeerrItemType.MOVIE,
|
||||||
|
),
|
||||||
|
startIndex = 0,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
onCardFocus = { index ->
|
||||||
Modifier
|
position = RowColumn(ROW_GENRES_MOVIE, index)
|
||||||
.fillMaxWidth(),
|
positionCallback.invoke(position)
|
||||||
|
},
|
||||||
|
modifier = Modifier.focusRequester(focusRequesters[ROW_GENRES_MOVIE]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
item { discoverItemRow(ROW_UPCOMING_MOVIES, state.upcomingMovies) }
|
||||||
|
|
||||||
|
// TV
|
||||||
|
item { discoverItemRow(ROW_TV, state.tv) }
|
||||||
|
item {
|
||||||
|
CompositionLocalProvider(LocalBringIntoViewSpec provides defaultBringIntoViewSpec) {
|
||||||
|
DiscoverGenreRow(
|
||||||
|
title =
|
||||||
|
remember {
|
||||||
|
ResProviderStringProvider(
|
||||||
|
R.string.genres_in,
|
||||||
|
ResStringProvider(R.string.tv_shows_title),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
items = state.tvGenres,
|
||||||
|
onClickItem = { index, genre ->
|
||||||
|
position = RowColumn(ROW_GENRES_TV, index)
|
||||||
|
viewModel.navigationManager.navigateTo(
|
||||||
|
Destination.DiscoverMoreResult(
|
||||||
|
type =
|
||||||
|
DiscoverPagerType.Genre(
|
||||||
|
genreId = genre.id.toInt(),
|
||||||
|
name = genre.name,
|
||||||
|
type = SeerrItemType.TV,
|
||||||
|
),
|
||||||
|
startIndex = 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onCardFocus = { index ->
|
||||||
|
position = RowColumn(ROW_GENRES_TV, index)
|
||||||
|
positionCallback.invoke(position)
|
||||||
|
},
|
||||||
|
modifier = Modifier.focusRequester(focusRequesters[ROW_GENRES_TV]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item { discoverItemRow(ROW_UPCOMING_TV, state.upcomingTv) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
||||||
import com.github.damontecres.wholphin.ui.util.StringProvider
|
import com.github.damontecres.wholphin.ui.util.StringProvider
|
||||||
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
import com.github.damontecres.wholphin.util.DiscoverPagerType
|
||||||
import com.github.damontecres.wholphin.util.RequestHandler
|
import com.github.damontecres.wholphin.util.RequestHandler
|
||||||
import com.github.damontecres.wholphin.util.SEERR_PAGE_SIZE
|
import com.github.damontecres.wholphin.util.SEERR_PAGE_SIZE
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
|
|
@ -146,8 +146,9 @@ sealed class Destination(
|
||||||
val item: DiscoverItem,
|
val item: DiscoverItem,
|
||||||
) : Destination(false)
|
) : Destination(false)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class DiscoverMoreResult(
|
data class DiscoverMoreResult(
|
||||||
val type: DiscoverRequestType,
|
val type: DiscoverPagerType,
|
||||||
val startIndex: Int = SEERR_PAGE_SIZE,
|
val startIndex: Int = SEERR_PAGE_SIZE,
|
||||||
) : Destination(false)
|
) : Destination(false)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,14 @@ import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.MovieResult
|
import com.github.damontecres.wholphin.api.seerr.model.MovieResult
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.TvResult
|
import com.github.damontecres.wholphin.api.seerr.model.TvResult
|
||||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrItemType
|
||||||
import com.github.damontecres.wholphin.services.SeerrApi
|
import com.github.damontecres.wholphin.services.SeerrApi
|
||||||
import com.github.damontecres.wholphin.services.SeerrSearchResult
|
import com.github.damontecres.wholphin.services.SeerrSearchResult
|
||||||
|
import com.github.damontecres.wholphin.ui.discover.discoverGenreTitle
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [RequestPager] for Seerr server queries
|
* A [RequestPager] for Seerr server queries
|
||||||
|
|
@ -45,6 +50,27 @@ enum class DiscoverRequestType(
|
||||||
UNKNOWN(R.string.unknown),
|
UNKNOWN(R.string.unknown),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface DiscoverPagerType {
|
||||||
|
val title: StringProvider
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RequestType(
|
||||||
|
val type: DiscoverRequestType,
|
||||||
|
) : DiscoverPagerType {
|
||||||
|
override val title: StringProvider = ResStringProvider(type.stringRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Genre(
|
||||||
|
val genreId: Int,
|
||||||
|
val name: String,
|
||||||
|
val type: SeerrItemType,
|
||||||
|
) : DiscoverPagerType {
|
||||||
|
override val title: StringProvider = discoverGenreTitle(name, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies how a [RequestPager] should prepare and execute API calls
|
* Specifies how a [RequestPager] should prepare and execute API calls
|
||||||
*/
|
*/
|
||||||
|
|
@ -55,27 +81,37 @@ interface DiscoverRequestHandler<T> {
|
||||||
): QueryResult<T>
|
): QueryResult<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
val DiscoverTvRequestHandler =
|
class DiscoverTvRequestHandler(
|
||||||
object : DiscoverRequestHandler<TvResult> {
|
private val genre: Int? = null,
|
||||||
override suspend fun execute(
|
) : DiscoverRequestHandler<TvResult> {
|
||||||
api: SeerrApi,
|
override suspend fun execute(
|
||||||
pageNumber: Int,
|
api: SeerrApi,
|
||||||
): QueryResult<TvResult> =
|
pageNumber: Int,
|
||||||
api.api.searchApi.discoverTvGet(page = pageNumber).let {
|
): QueryResult<TvResult> =
|
||||||
|
api.api.searchApi
|
||||||
|
.discoverTvGet(
|
||||||
|
page = pageNumber,
|
||||||
|
genre = genre?.toString(),
|
||||||
|
).let {
|
||||||
QueryResult(it.results.orEmpty(), it.totalResults ?: 0)
|
QueryResult(it.results.orEmpty(), it.totalResults ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val DiscoverMovieRequestHandler =
|
class DiscoverMovieRequestHandler(
|
||||||
object : DiscoverRequestHandler<MovieResult> {
|
private val genre: Int? = null,
|
||||||
override suspend fun execute(
|
) : DiscoverRequestHandler<MovieResult> {
|
||||||
api: SeerrApi,
|
override suspend fun execute(
|
||||||
pageNumber: Int,
|
api: SeerrApi,
|
||||||
): QueryResult<MovieResult> =
|
pageNumber: Int,
|
||||||
api.api.searchApi.discoverMoviesGet(page = pageNumber).let {
|
): QueryResult<MovieResult> =
|
||||||
|
api.api.searchApi
|
||||||
|
.discoverMoviesGet(
|
||||||
|
page = pageNumber,
|
||||||
|
genre = genre?.toString(),
|
||||||
|
).let {
|
||||||
QueryResult(it.results.orEmpty(), it.totalResults ?: 0)
|
QueryResult(it.results.orEmpty(), it.totalResults ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val TrendingRequestHandler =
|
val TrendingRequestHandler =
|
||||||
object : DiscoverRequestHandler<SeerrSearchResult> {
|
object : DiscoverRequestHandler<SeerrSearchResult> {
|
||||||
|
|
|
||||||
|
|
@ -821,6 +821,8 @@
|
||||||
<string name="tomatoes">Tomatoes</string>
|
<string name="tomatoes">Tomatoes</string>
|
||||||
<string name="sort">Sort</string>
|
<string name="sort">Sort</string>
|
||||||
<string name="split_into_separate_rows">Split into separate rows</string>
|
<string name="split_into_separate_rows">Split into separate rows</string>
|
||||||
|
<string name="genre_movies">%1$s Movies</string>
|
||||||
|
<string name="genre_tv_shows">%1$s TV Shows</string>
|
||||||
<string name="quality_profile">Quality profile</string>
|
<string name="quality_profile">Quality profile</string>
|
||||||
<string name="is_default">Default</string>
|
<string name="is_default">Default</string>
|
||||||
<string name="root_folder">Root folder</string>
|
<string name="root_folder">Root folder</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue