From 9daf679f7d455d099da068c14733bff054eefb8b Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:18:14 -0500 Subject: [PATCH] Fix some image issues (#926) ## Description Fixes/improves thumb image handling by falling back to backdrop images if available. Episodes without a parent thumb/backdrop will fall back to their primary image. Also fixes the nav drawer selected item not updating (bug introduced by #886). ### Related issues Fixes #642 Fixes #914 ### Testing Emulator in various situations with missing thumbs ## Screenshots N/A ## AI or LLM usage None --- .../wholphin/services/ImageUrlService.kt | 65 ++++++++++++++++++- .../wholphin/services/LatestNextUpService.kt | 7 ++ .../wholphin/ui/cards/BannerCard.kt | 2 +- .../ui/components/CollectionFolderGrid.kt | 7 +- .../damontecres/wholphin/ui/nav/NavDrawer.kt | 54 +++++++++++++++ 5 files changed, 132 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt index 39967e24..3dfc24c1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt @@ -30,6 +30,9 @@ class ImageUrlService useSeriesForPrimary: Boolean, imageTags: Map, imageType: ImageType, + parentThumbId: UUID? = null, + parentBackdropId: UUID? = null, + backdropTags: List = emptyList(), fillWidth: Int? = null, fillHeight: Int? = null, ): String? = @@ -54,8 +57,65 @@ class ImageUrlService } } + ImageType.THUMB -> { + if (useSeriesForPrimary && parentThumbId != null && + (itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON) + ) { + // Use parent's thumb + getItemImageUrl( + itemId = parentThumbId, + imageType = imageType, + fillWidth = fillWidth, + fillHeight = fillHeight, + ) + } else if (useSeriesForPrimary && parentBackdropId != null && + (itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON) + ) { + // No parent thumb, so use backdrop instead + getItemImageUrl( + itemId = parentBackdropId, + imageType = ImageType.BACKDROP, + fillWidth = fillWidth, + fillHeight = fillHeight, + ) + } else if (parentThumbId != null && itemType == BaseItemKind.SEASON && imageType !in imageTags) { + getItemImageUrl( + itemId = parentThumbId, + imageType = imageType, + fillWidth = fillWidth, + fillHeight = fillHeight, + ) + } else if (useSeriesForPrimary && + parentThumbId == null && + itemType == BaseItemKind.EPISODE && + imageType !in imageTags + ) { + // Workaround to fall back to episode image if no parent thumb + getItemImageUrl( + itemId = itemId, + imageType = ImageType.PRIMARY, + fillWidth = fillWidth, + fillHeight = fillHeight, + ) + } else if (imageType !in imageTags && backdropTags.isNotEmpty()) { + // If no thumb, use backdrop if available + getItemImageUrl( + itemId = itemId, + imageType = ImageType.BACKDROP, + fillWidth = fillWidth, + fillHeight = fillHeight, + ) + } else { + getItemImageUrl( + itemId = itemId, + imageType = imageType, + fillWidth = fillWidth, + fillHeight = fillHeight, + ) + } + } + ImageType.PRIMARY, - ImageType.THUMB, ImageType.BANNER, -> { if (useSeriesForPrimary && seriesId != null && @@ -108,6 +168,9 @@ class ImageUrlService useSeriesForPrimary = item.useSeriesForPrimary, imageTags = item.data.imageTags.orEmpty(), imageType = imageType, + parentThumbId = item.data.parentThumbItemId, + parentBackdropId = item.data.parentBackdropItemId, + backdropTags = item.data.backdropImageTags.orEmpty(), fillWidth = fillWidth, fillHeight = fillHeight, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt index 9342f63a..e30a39ca 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt @@ -20,6 +20,7 @@ import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userViewsApi import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType +import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.UserDto import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest import org.jellyfin.sdk.model.api.request.GetNextUpRequest @@ -60,6 +61,12 @@ class LatestNextUpService remove(BaseItemKind.EPISODE) } }, + enableImageTypes = + listOf( + ImageType.PRIMARY, + ImageType.THUMB, + ImageType.BACKDROP, + ), ) val items = api.itemsApi diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt index e5b57b79..294df8f6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt @@ -95,7 +95,7 @@ fun BannerCard( null } } - var imageError by remember { mutableStateOf(false) } + var imageError by remember(imageUrl) { mutableStateOf(false) } Card( modifier = modifier.size(cardHeight * aspectRatio, cardHeight), onClick = onClick, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index c44f3b3c..4c672998 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -346,7 +346,12 @@ class CollectionFolderViewModel filter.applyTo( GetItemsRequest( parentId = item?.id, - enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), + enableImageTypes = + listOf( + ImageType.PRIMARY, + ImageType.THUMB, + ImageType.BACKDROP, + ), includeItemTypes = includeItemTypes, recursive = recursive, excludeItemIds = item?.let { listOf(item.id) }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt index 3527a95e..f0276ffd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt @@ -27,6 +27,7 @@ import androidx.compose.material.icons.filled.KeyboardArrowLeft import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -54,6 +55,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.findViewTreeViewModelStoreOwner +import androidx.lifecycle.viewModelScope import androidx.tv.material3.DrawerState import androidx.tv.material3.DrawerValue import androidx.tv.material3.Icon @@ -77,7 +79,9 @@ import com.github.damontecres.wholphin.services.SetupNavigationManager import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.components.TimeDisplay import com.github.damontecres.wholphin.ui.ifElse +import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption +import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.setup.UserIconCardImage import com.github.damontecres.wholphin.ui.spacedByWithFooter import com.github.damontecres.wholphin.ui.theme.LocalTheme @@ -87,6 +91,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.imageApi import org.jellyfin.sdk.model.api.CollectionType +import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -145,6 +150,54 @@ class NavDrawerViewModel } fun getUserImage(user: JellyfinUser): String = api.imageApi.getUserImageUrl(user.id) + + fun updateSelectedIndex() { + viewModelScope.launchDefault { + val asDestinations = + ( + state.value.items + + listOf( + NavDrawerItem.More, + NavDrawerItem.Discover, + ) + state.value.moreItems + ).map { + if (it is ServerNavDrawerItem) { + it.destination + } else if (it is NavDrawerItem.Favorites) { + Destination.Favorites + } else if (it is NavDrawerItem.Discover) { + Destination.Discover + } else { + null + } + } + + val backstack = navigationManager.backStack.toList().reversed() + for (i in 0..= 0) { + idx + } else { + null + } + } + Timber.v("Found $index => $key") + if (index != null) { + selectedIndex.setValueOnMain(index) + break + } + } + } + } + } } sealed interface NavDrawerItem { @@ -208,6 +261,7 @@ fun NavDrawer( key = "${server.id}_${user.id}", // Keyed to the server & user to ensure its reset when switching either ), ) { + LaunchedEffect(Unit) { viewModel.updateSelectedIndex() } val scope = rememberCoroutineScope() val context = LocalContext.current val density = LocalDensity.current