diff --git a/README.md b/README.md index b37ac85a..ed82692f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/filter/DiscoverFilter.kt b/app/src/main/java/com/github/damontecres/wholphin/data/filter/DiscoverFilter.kt index bef5c87a..446a64c0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/filter/DiscoverFilter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/filter/DiscoverFilter.kt @@ -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(), diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt index 5c5c7c40..24d7d340 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt @@ -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() diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt index df9488ba..8a283e4e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt index ff001d0b..99288b9a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt @@ -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 { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt index 4dea6d41..e91dfb82 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt @@ -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 = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetailsHeader.kt index 8b6f3e98..1c0915ec 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetailsHeader.kt @@ -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), ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt index 86c55d38..20cecd12 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt @@ -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) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt index d02b17eb..049f7610 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt @@ -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( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt index b473d510..ec500351 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt @@ -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) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverUtils.kt index 8dfc4290..3e625d60 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverUtils.kt @@ -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) + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverFocusedItem.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverFocusedItem.kt index c9464528..b0d49985 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverFocusedItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverFocusedItem.kt @@ -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 } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverRequestGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverRequestGrid.kt index 3d6d808e..fe6dd475 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverRequestGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverRequestGrid.kt @@ -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 = { _, _ -> }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt index 9702f9fa..cd7c6a27 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt @@ -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