Updates & fixes to discover pages (#1836)

## Description
Fixes several issues on the discover pages:
- Fixes sorting by release date for tv shows
- Fixes automatic go-to for movie & tv show grid
- Fixes tv show discover page's go-to using a movie detail page
- Updates the alignment & padding for the headers on different discover
pages to match the reset of the app
- Use media's logo image if available and the setting is enabled

### Related issues
N/A

### Testing
Emulator mostly

## Screenshots
N/A, headers and logos are unchanged, just moved slightly to match
regular content

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-08-16 10:29:03 -04:00 • committed by GitHub
parent 4e53cbaa7f
commit f89d752d2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 182 additions and 75 deletions

View file

@ -103,7 +103,7 @@ Requires Android 6+ (or Fire TV OS 6+) and Jellyfin server `10.10.x` or `10.11.x
The app is tested on a variety of Android TV/Fire TV OS devices, but if you encounter issues, please file an issue!
Seerr integration is tested with `v3.3.0`. Older versions may not work.
Seerr integration is generally tested with the latest stable version. Older versions may not work as expected. The Seerr server should configured with integration to your Jellyfin server for full compatibility.
## Contributions

View file

@ -17,10 +17,11 @@ private val dateFormatter = DateTimeFormatter.ofPattern("yyyy-M-d")
enum class DiscoverSort(
val key: String,
@StringRes val stringRes: Int,
@param:StringRes val stringRes: Int,
val tvKey: String = key,
) {
POPULARITY("popularity", R.string.popularity),
RELEASE_DATE("release_date", R.string.sort_by_date_released),
RELEASE_DATE("release_date", R.string.sort_by_date_released, "first_air_date"),
TMDB_VOTE("vote_average", R.string.community_rating),
ALPHABETICAL("original_title", R.string.sort_by_name),
}
@ -36,7 +37,7 @@ data class DiscoverSortAndDirection(
val sort: DiscoverSort = DiscoverSort.POPULARITY,
val direction: SortOrder = SortOrder.DESCENDING,
) {
val key = "${sort.key}.${direction.key}"
fun key(isTv: Boolean) = if (isTv) "${sort.tvKey}.${direction.key}" else "${sort.key}.${direction.key}"
fun flip() = copy(direction = direction.flip())
}
@ -103,7 +104,7 @@ data class DiscoverFilter(
network = null,
keywords = keywordIds?.joinToString(",") { it.toString() },
excludeKeywords = excludeKeywordIds?.joinToString(",") { it.toString() },
sortBy = sortBy?.key,
sortBy = sortBy?.key(true),
// firstAirDateGte = firstAirDateGte?.let { dateFormatter.format(it) },
// firstAirDateLte = firstAirDateLte?.let { dateFormatter.format(it) },
withRuntimeGte = withRuntimeGte?.inWholeMinutes?.toInt(),
@ -135,7 +136,7 @@ data class DiscoverFilter(
// network = null,
keywords = keywordIds?.joinToString(",") { it.toString() },
excludeKeywords = excludeKeywordIds?.joinToString(",") { it.toString() },
sortBy = sortBy?.key,
sortBy = sortBy?.key(false),
// firstAirDateGte = firstAirDateGte?.let { dateFormatter.format(it) },
// firstAirDateLte = firstAirDateLte?.let { dateFormatter.format(it) },
withRuntimeGte = withRuntimeGte?.inWholeMinutes?.toInt(),

View file

@ -103,6 +103,7 @@ data class DiscoverItem(
@Serializable(LocalDateSerializer::class) val releaseDate: LocalDate?,
val posterUrl: String?,
val backDropUrl: String?,
val logoUrl: String?,
val jellyfinItemId: UUID?,
) : CardGridItem {
override val gridId: String get() = id.toString()

View file

@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.firstOrNull
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@ -169,14 +170,7 @@ class SeerrService
backdropWidth: Int = 1920,
): String? {
if (mediaInfo != null) {
val itemId =
if (mediaInfo.jellyfinMediaId.isNotNullOrBlank()) {
mediaInfo.jellyfinMediaId.toUUIDOrNull()
} else if (mediaInfo.jellyfinMediaId4k.isNotNullOrBlank()) {
mediaInfo.jellyfinMediaId4k.toUUIDOrNull()
} else {
null
}
val itemId = mediaInfo.jellyfinId
if (itemId != null) {
return imageUrlService.getItemImageUrl(
itemId = itemId,
@ -348,7 +342,8 @@ class SeerrService
releaseDate = toLocalDate(movie.releaseDate),
posterUrl = createImageUrl(ImageType.PRIMARY, movie.posterPath, movie.mediaInfo),
backDropUrl = createImageUrl(ImageType.BACKDROP, movie.backdropPath, movie.mediaInfo),
jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
logoUrl = createImageUrl(ImageType.LOGO, null, movie.mediaInfo),
jellyfinItemId = movie.mediaInfo?.jellyfinId,
)
suspend fun createDiscoverItem(movie: MovieDetails): DiscoverItem =
@ -364,7 +359,8 @@ class SeerrService
releaseDate = toLocalDate(movie.releaseDate),
posterUrl = createImageUrl(ImageType.PRIMARY, movie.posterPath, movie.mediaInfo),
backDropUrl = createImageUrl(ImageType.BACKDROP, movie.backdropPath, movie.mediaInfo),
jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
logoUrl = createImageUrl(ImageType.LOGO, null, movie.mediaInfo),
jellyfinItemId = movie.mediaInfo?.jellyfinId,
)
suspend fun createDiscoverItem(tv: TvResult): DiscoverItem =
@ -380,7 +376,8 @@ class SeerrService
releaseDate = toLocalDate(tv.firstAirDate),
posterUrl = createImageUrl(ImageType.PRIMARY, tv.posterPath, tv.mediaInfo),
backDropUrl = createImageUrl(ImageType.BACKDROP, tv.backdropPath, tv.mediaInfo),
jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
logoUrl = createImageUrl(ImageType.LOGO, null, tv.mediaInfo),
jellyfinItemId = tv.mediaInfo?.jellyfinId,
)
suspend fun createDiscoverItem(tv: TvDetails): DiscoverItem =
@ -396,7 +393,8 @@ class SeerrService
releaseDate = toLocalDate(tv.firstAirDate),
posterUrl = createImageUrl(ImageType.PRIMARY, tv.posterPath, tv.mediaInfo),
backDropUrl = createImageUrl(ImageType.BACKDROP, tv.backdropPath, tv.mediaInfo),
jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
logoUrl = createImageUrl(ImageType.LOGO, null, tv.mediaInfo),
jellyfinItemId = tv.mediaInfo?.jellyfinId,
)
suspend fun createDiscoverItem(search: SeerrSearchResult): DiscoverItem =
@ -412,7 +410,8 @@ class SeerrService
releaseDate = toLocalDate(search.releaseDate ?: search.firstAirDate),
posterUrl = createImageUrl(ImageType.PRIMARY, search.posterPath, search.mediaInfo),
backDropUrl = createImageUrl(ImageType.BACKDROP, search.backdropPath, search.mediaInfo),
jellyfinItemId = search.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
logoUrl = createImageUrl(ImageType.LOGO, null, search.mediaInfo),
jellyfinItemId = search.mediaInfo?.jellyfinId,
)
suspend fun createDiscoverItem(credit: CreditCast): DiscoverItem =
@ -438,7 +437,8 @@ class SeerrService
credit.backdropPath,
credit.mediaInfo,
),
jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
logoUrl = createImageUrl(ImageType.LOGO, null, credit.mediaInfo),
jellyfinItemId = credit.mediaInfo?.jellyfinId,
)
suspend fun createDiscoverItem(credit: CreditCrew): DiscoverItem =
@ -464,6 +464,13 @@ class SeerrService
credit.backdropPath,
credit.mediaInfo,
),
jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(),
logoUrl = createImageUrl(ImageType.LOGO, null, credit.mediaInfo),
jellyfinItemId = credit.mediaInfo?.jellyfinId,
)
}
val MediaInfo.jellyfinId: UUID?
get() = jellyfinMediaId4k?.toUUIDOrNull() ?: jellyfinMediaId?.toUUIDOrNull()
val MediaInfo.jellyfinIdAsString: String?
get() = jellyfinMediaId4k ?: jellyfinMediaId

View file

@ -21,6 +21,7 @@ import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.logCoilError
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
@ -40,6 +41,10 @@ fun TitleOrLogo(
model = logoImageUrl,
contentDescription = title,
contentScale = ContentScale.Fit,
onError = {
logCoilError(logoImageUrl, it.result)
imageError = true
},
modifier =
Modifier
.height(HeaderUtils.logoHeight)
@ -113,6 +118,10 @@ fun SimpleTitleOrLogo(
model = logoImageUrl,
contentDescription = item?.title,
contentScale = ContentScale.Fit,
onError = {
logCoilError(logoImageUrl, it.result)
imageError = true
},
modifier = Modifier.fillMaxSize(),
)
} else {

View file

@ -54,6 +54,7 @@ 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.ErrorMessage
import com.github.damontecres.wholphin.ui.components.HeaderUtils
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
@ -64,7 +65,6 @@ import com.github.damontecres.wholphin.util.DataLoadingState
import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
@Composable
fun DiscoverMovieDetails(
@ -138,14 +138,7 @@ fun DiscoverMovieDetails(
)
},
goToOnClick = {
movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull()?.let {
viewModel.navigateTo(
Destination.MediaItem(
itemId = it,
type = BaseItemKind.MOVIE,
),
)
}
viewModel.goTo(movie.mediaInfo, BaseItemKind.MOVIE)
},
moreOnClick = {},
onLongClickPerson = { index, person -> },
@ -238,10 +231,11 @@ fun DiscoverMovieDetailsContent(
rating = rating,
bringIntoViewRequester = bringIntoViewRequester,
overviewOnClick = overviewOnClick,
showLogo = preferences.appPreferences.interfacePreferences.showLogos,
modifier =
Modifier
.fillMaxWidth()
.padding(top = 32.dp, bottom = 16.dp),
.padding(top = HeaderUtils.topPadding, bottom = 16.dp),
)
ExpandableDiscoverButtons(
availability =

View file

@ -14,7 +14,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
@ -23,9 +22,13 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
import com.github.damontecres.wholphin.data.model.DiscoverRating
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.jellyfinId
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.components.GenreText
import com.github.damontecres.wholphin.ui.components.HeaderUtils
import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.QuickDetailsText
import com.github.damontecres.wholphin.ui.components.TitleOrLogo
import com.github.damontecres.wholphin.ui.formatDuration
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.letNotEmpty
@ -33,6 +36,7 @@ import com.github.damontecres.wholphin.ui.listToDotString
import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.ImageType
import java.util.Locale
import kotlin.time.Duration.Companion.minutes
@ -43,29 +47,36 @@ fun DiscoverMovieDetailsHeader(
rating: DiscoverRating?,
bringIntoViewRequester: BringIntoViewRequester,
overviewOnClick: () -> Unit,
showLogo: Boolean,
modifier: Modifier = Modifier,
) {
val imageUrlService = LocalImageUrlService.current
val logoImageUrl =
remember(movie) {
movie.mediaInfo?.jellyfinId?.let {
imageUrlService.getItemImageUrl(it, ImageType.LOGO)
}
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier,
) {
// Title
Text(
text = movie.title ?: "",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.displaySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(.75f),
TitleOrLogo(
title = movie.title,
logoImageUrl = logoImageUrl,
showLogo = showLogo,
modifier =
Modifier
.fillMaxWidth(.75f)
.padding(start = HeaderUtils.startPadding),
)
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxWidth(.60f),
) {
val padding = 4.dp
val resources = LocalResources.current
val details =
remember(movie, rating, resources) {
@ -102,9 +113,9 @@ fun DiscoverMovieDetailsHeader(
}
}
QuickDetailsText(details)
QuickDetailsText(details, Modifier.padding(start = HeaderUtils.startPadding))
movie.genres?.mapNotNull { it.name }?.letNotEmpty {
GenreText(it, Modifier.padding(bottom = padding))
GenreText(it, Modifier.padding(start = HeaderUtils.startPadding))
}
val tagline = remember { movie.tagline?.takeIf { it.isNotNullOrBlank() } }
@ -113,7 +124,7 @@ fun DiscoverMovieDetailsHeader(
text = tagline,
style = MaterialTheme.typography.bodyLarge,
fontStyle = FontStyle.Italic,
modifier = Modifier,
modifier = Modifier.padding(start = HeaderUtils.startPadding),
)
}
@ -150,6 +161,7 @@ fun DiscoverMovieDetailsHeader(
text = stringResource(R.string.directed_by, it),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(start = HeaderUtils.startPadding),
)
}
}

View file

@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.detail.discover
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.api.seerr.model.MediaInfo
import com.github.damontecres.wholphin.api.seerr.model.MediaRequest
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
import com.github.damontecres.wholphin.api.seerr.model.RelatedVideo
@ -23,6 +24,7 @@ import com.github.damontecres.wholphin.services.SeerrService
import com.github.damontecres.wholphin.services.SeerrUserConfig
import com.github.damontecres.wholphin.ui.equalsNotNull
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.showToast
@ -44,6 +46,7 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.api.BaseItemKind
import timber.log.Timber
import kotlin.coroutines.cancellation.CancellationException
@ -172,6 +175,15 @@ class DiscoverMovieViewModel
navigationManager.navigateTo(destination)
}
fun goTo(
mediaInfo: MediaInfo?,
type: BaseItemKind,
) {
viewModelScope.launchDefault {
goToButtonDiscover(mediaInfo, type, context, navigationManager)
}
}
fun request(request: MovieRequest) {
viewModelScope.launchIO {
if (request.movieId == null) {

View file

@ -27,12 +27,9 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
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.TvDetails
import com.github.damontecres.wholphin.data.model.BaseItem
@ -45,6 +42,8 @@ import com.github.damontecres.wholphin.data.model.hasPermission
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.SeerrUserConfig
import com.github.damontecres.wholphin.services.TrailerService
import com.github.damontecres.wholphin.services.jellyfinId
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard
import com.github.damontecres.wholphin.ui.cards.DiscoverPersonRow
import com.github.damontecres.wholphin.ui.cards.ItemRow
@ -53,9 +52,11 @@ 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.GenreText
import com.github.damontecres.wholphin.ui.components.HeaderUtils
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.QuickDetailsText
import com.github.damontecres.wholphin.ui.components.TitleOrLogo
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.formatDuration
@ -70,7 +71,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.successValue
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import org.jellyfin.sdk.model.api.ImageType
import kotlin.time.Duration.Companion.minutes
@Composable
@ -125,14 +126,7 @@ fun DiscoverSeriesDetails(
viewModel.navigateTo(Destination.DiscoveredItem(it))
},
goToOnClick = {
item.mediaInfo?.jellyfinMediaId?.toUUIDOrNull()?.let {
viewModel.navigateTo(
Destination.MediaItem(
itemId = it,
type = BaseItemKind.MOVIE,
),
)
}
viewModel.goTo(item.mediaInfo, BaseItemKind.SERIES)
},
overviewOnClick = {
overviewDialog =
@ -254,7 +248,6 @@ fun DiscoverSeriesDetailsContent(
Column(
modifier =
Modifier
.padding(16.dp)
.fillMaxSize(),
) {
LazyColumn(
@ -274,10 +267,11 @@ fun DiscoverSeriesDetailsContent(
series = series,
rating = rating,
overviewOnClick = overviewOnClick,
showLogo = preferences.appPreferences.interfacePreferences.showLogos,
modifier =
Modifier
.fillMaxWidth()
.padding(top = 32.dp, bottom = 16.dp),
.padding(top = HeaderUtils.topPadding, bottom = 16.dp),
)
ExpandableDiscoverButtons(
availability =
@ -430,24 +424,33 @@ fun DiscoverSeriesDetailsHeader(
series: TvDetails,
rating: DiscoverRating?,
overviewOnClick: () -> Unit,
showLogo: Boolean,
modifier: Modifier = Modifier,
) {
val imageUrlService = LocalImageUrlService.current
val logoImageUrl =
remember(series) {
series.mediaInfo?.jellyfinId?.let {
imageUrlService.getItemImageUrl(it, ImageType.LOGO)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier,
) {
Text(
text = series.name ?: stringResource(R.string.unknown),
style = MaterialTheme.typography.displaySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(.75f),
TitleOrLogo(
title = series.name,
logoImageUrl = logoImageUrl,
showLogo = showLogo,
modifier =
Modifier
.fillMaxWidth(.75f)
.padding(start = HeaderUtils.startPadding),
)
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxWidth(.60f),
) {
val padding = 4.dp
val resources = LocalResources.current
val details =
remember(series, rating, resources) {
@ -469,9 +472,9 @@ fun DiscoverSeriesDetailsHeader(
}
}
QuickDetailsText(details)
QuickDetailsText(details, Modifier.padding(start = HeaderUtils.startPadding))
series.genres?.mapNotNull { it.name }?.letNotEmpty {
GenreText(it, Modifier.padding(bottom = padding))
GenreText(it, Modifier.padding(start = HeaderUtils.startPadding, bottom = 4.dp))
}
series.overview?.let { overview ->
OverviewText(

View file

@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.detail.discover
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.api.seerr.model.MediaInfo
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.RequestRequestIdPutRequest
@ -20,6 +21,7 @@ import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SeerrServerRepository
import com.github.damontecres.wholphin.services.SeerrService
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.showToast
@ -45,6 +47,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.api.BaseItemKind
import timber.log.Timber
@HiltViewModel(assistedFactory = DiscoverSeriesViewModel.Factory::class)
@ -162,6 +165,15 @@ class DiscoverSeriesViewModel
navigationManager.navigateTo(destination)
}
fun goTo(
mediaInfo: MediaInfo?,
type: BaseItemKind,
) {
viewModelScope.launchDefault {
goToButtonDiscover(mediaInfo, type, context, navigationManager)
}
}
private suspend fun updateSeasonStatus(tv: TvDetails) {
if (request4kEnabled.first()) {
updateSeasonStatus(tv, true)

View file

@ -1,8 +1,17 @@
package com.github.damontecres.wholphin.ui.detail.discover
import android.content.Context
import com.github.damontecres.wholphin.api.seerr.infrastructure.ClientException
import com.github.damontecres.wholphin.api.seerr.model.MediaInfo
import com.github.damontecres.wholphin.data.model.DiscoverRating
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.jellyfinId
import com.github.damontecres.wholphin.services.jellyfinIdAsString
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.showToast
import kotlinx.coroutines.CancellationException
import org.jellyfin.sdk.model.api.BaseItemKind
import timber.log.Timber
suspend fun getDiscoverRating(
@ -24,3 +33,40 @@ suspend fun getDiscoverRating(
Timber.e(ex, "Error fetching rating for %s", id)
null
}
/**
* Navigates to the Jellyfin media item if the [mediaInfo] has a valid [jellyfinId]
*
* If not, a error toast is shown
*/
suspend fun goToButtonDiscover(
mediaInfo: MediaInfo?,
type: BaseItemKind,
context: Context,
navigationManager: NavigationManager,
) {
val id = mediaInfo?.jellyfinId
if (id != null) {
navigationManager.navigateTo(
Destination.MediaItem(
itemId = id,
type = type,
),
)
} else {
val stringId = mediaInfo?.jellyfinIdAsString
val msg =
if (stringId.isNotNullOrBlank()) {
"Unknown Jellyfin ID: $stringId"
} else {
"No Jellyfin ID found"
}
Timber.w(
"Unknown jellyfinId for tmdb=%s: %s/%s",
mediaInfo?.tmdbId,
mediaInfo?.jellyfinMediaId,
mediaInfo?.jellyfinMediaId4k,
)
showToast(context, msg)
}
}

View file

@ -13,6 +13,7 @@ sealed interface DiscoverFocusedItem {
val subtitle: String?
val overview: String?
val backDropUrl: String?
val logoUrl: String?
data class Item(
val item: DiscoverItem,
@ -26,6 +27,8 @@ sealed interface DiscoverFocusedItem {
get() = item.overview
override val backDropUrl: String?
get() = item.backDropUrl
override val logoUrl: String?
get() = item.logoUrl
}
data class Genre(
@ -39,6 +42,8 @@ sealed interface DiscoverFocusedItem {
get() = null
override val overview: String?
get() = null
override val logoUrl: String?
get() = null
}
}

View file

@ -299,7 +299,7 @@ fun DiscoverRequestGrid(
initialPosition = state.startIndex,
pager = s.data,
onClickItem = { index, item ->
viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item))
viewModel.navigationManager.navigateTo(item.destination)
},
onLongClickItem = { index, item -> },
onClickPlay = { _, _ -> },

View file

@ -6,9 +6,9 @@ import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
@ -40,6 +40,7 @@ import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SeerrService
import com.github.damontecres.wholphin.ui.components.Genre
import com.github.damontecres.wholphin.ui.components.HeaderUtils
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.detail.discover.getDiscoverRating
import com.github.damontecres.wholphin.ui.launchIO
@ -438,16 +439,20 @@ fun SeerrDiscoverPage(
title = focusedItem?.title?.getString(),
subtitle = focusedItem?.subtitle,
overview = focusedItem?.overview,
overviewTwoLines = true,
overviewTwoLines = false,
quickDetails = details,
timeRemaining = null,
endsAt = null,
showLogo = preferences.appPreferences.interfacePreferences.showLogos,
logoImageUrl = null, // TODO
logoImageUrl = focusedItem?.logoUrl,
modifier =
Modifier
.padding(top = 24.dp, bottom = 16.dp, start = 32.dp)
.fillMaxHeight(.25f),
.padding(
top = HeaderUtils.topPadding,
// Intentionally no bottom padding because of tabs
bottom = 0.dp,
start = HeaderUtils.startPadding,
).height(HeaderUtils.height),
)
val density = LocalDensity.current