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
This commit is contained in:
Ray 2026-02-19 19:18:14 -05:00 committed by GitHub
parent f57cba85f2
commit 9daf679f7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 132 additions and 3 deletions

View file

@ -30,6 +30,9 @@ class ImageUrlService
useSeriesForPrimary: Boolean,
imageTags: Map<ImageType, String?>,
imageType: ImageType,
parentThumbId: UUID? = null,
parentBackdropId: UUID? = null,
backdropTags: List<String> = 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,
)

View file

@ -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

View file

@ -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,

View file

@ -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) },

View file

@ -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..<backstack.size) {
val key = backstack[i]
if (key is Destination) {
val index =
if (key is Destination.Home) {
-1
} else if (key is Destination.Search) {
-2
} else {
val idx = asDestinations.indexOf(key)
if (idx >= 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