mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-05 18:30:59 +01:00
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:
parent
4e53cbaa7f
commit
f89d752d2c
14 changed files with 182 additions and 75 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = { _, _ -> },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue