From 5571654a2250b10cbe43319fad9126d633aacd2d Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:52:57 -0400 Subject: [PATCH] Show overlay for extra items (#1320) ## Description Shows the overlay (watched, progress bar) for extras appearing as single items. Grouped extras individually show the overlay when clicking into. If all of the grouped are played, the group is show with the watched indicator. Also very slightly optimizations the UI by offloading the title/subtitle/imageUrl to object creation time instead of when they're shown on screen. ### Related issues Closes #1311 ### Testing Shield testing with different extras ## Screenshots N/A ## AI or LLM usage None --- .../damontecres/wholphin/data/ExtrasItem.kt | 21 +++++++-- .../wholphin/services/ExtrasService.kt | 47 ++++++++++++++++++- .../wholphin/ui/cards/ExtrasRow.kt | 43 +++-------------- 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt index ede44e08..ce73e6eb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt @@ -15,7 +15,11 @@ sealed interface ExtrasItem { val parentId: UUID val type: ExtraType val destination: Destination - val title: String? + val imageUrl: String? + val title: String + val subtitle: String? + val isPlayed: Boolean + val playedPercentage: Double /** * Represents multiple extras of the same type @@ -24,11 +28,16 @@ sealed interface ExtrasItem { override val parentId: UUID, override val type: ExtraType, val items: List, + override val imageUrl: String?, + override val title: String, + override val subtitle: String, + override val isPlayed: Boolean, ) : ExtrasItem { override val destination: Destination = Destination.ItemGrid(null, type.stringRes, items.map { it.id }) - override val title: String? = null + override val playedPercentage + get() = -1.0 } /** @@ -38,12 +47,18 @@ sealed interface ExtrasItem { override val parentId: UUID, override val type: ExtraType, val item: BaseItem, + override val imageUrl: String?, + override val title: String, + override val subtitle: String?, ) : ExtrasItem { override val destination: Destination = Destination.Playback( item = item, ) - override val title: String? get() = item.title + override val isPlayed: Boolean + get() = item.played + override val playedPercentage + get() = item.data.userData?.playedPercentage ?: 0.0 } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt index 65b74083..e7770ff5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt @@ -1,10 +1,15 @@ package com.github.damontecres.wholphin.services +import android.content.Context +import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ExtrasItem import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.pluralRes +import dagger.hilt.android.qualifiers.ApplicationContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.ExtraType +import org.jellyfin.sdk.model.api.ImageType import java.util.UUID import javax.inject.Inject import javax.inject.Singleton @@ -19,6 +24,8 @@ class ExtrasService @Inject constructor( private val api: ApiClient, + @param:ApplicationContext private val context: Context, + private val imageUrlService: ImageUrlService, ) { /** * Get the [ExtrasItem]s for the given item @@ -39,9 +46,45 @@ class ExtrasService extrasMap .mapNotNull { (type, items) -> if (items.size == 1) { - ExtrasItem.Single(itemId, type, items.first()) + val item = items.first() + val title = + item.title ?: context.resources.getQuantityString(type.pluralRes, 1) + val subtitle = + if (item.title == null) { + context.resources.getQuantityString(type.pluralRes, 1) + } else { + null + } + ExtrasItem.Single( + parentId = itemId, + type = type, + item = item, + title = title, + subtitle = subtitle, + imageUrl = imageUrlService.getItemImageUrl(item, ImageType.PRIMARY), + ) } else if (items.size > 1) { - ExtrasItem.Group(itemId, type, items) + val title = + context.resources.getQuantityString(type.pluralRes, items.size) + val subtitle = + context.resources.getQuantityString( + R.plurals.items, + items.size, + items.size, + ) + ExtrasItem.Group( + parentId = itemId, + type = type, + items = items, + title = title, + subtitle = subtitle, + isPlayed = items.all { it.played }, + imageUrl = + imageUrlService.getItemImageUrl( + items.random(), + ImageType.PRIMARY, + ), + ) } else { null } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ExtrasRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ExtrasRow.kt index adfff582..1d9927fe 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ExtrasRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ExtrasRow.kt @@ -1,19 +1,13 @@ package com.github.damontecres.wholphin.ui.cards import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ExtrasItem -import com.github.damontecres.wholphin.data.pluralRes import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.Cards -import com.github.damontecres.wholphin.ui.LocalImageUrlService -import org.jellyfin.sdk.model.api.ImageType @Composable fun ExtrasRow( @@ -22,52 +16,27 @@ fun ExtrasRow( onLongClickItem: (Int, ExtrasItem) -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current - val resources = LocalResources.current ItemRow( title = stringResource(R.string.extras), items = extras, onClickItem = onClickItem, onLongClickItem = onLongClickItem, cardContent = { index, item, mod, onClick, onLongClick -> - val imageUrlService = LocalImageUrlService.current - val imageUrl = - remember { - val item = - when (item) { - is ExtrasItem.Group -> item.items.random() - is ExtrasItem.Single -> item.item - null -> null - } - imageUrlService.getItemImageUrl(item, ImageType.PRIMARY) - } SeasonCard( - title = - when (item) { - is ExtrasItem.Group -> item.type.pluralRes.let { resources.getQuantityString(it, item.items.size) } - is ExtrasItem.Single -> item.title ?: item.type.pluralRes.let { resources.getQuantityString(it, 1) } - null -> null - }, + title = item?.title, name = null, - subtitle = - if (item is ExtrasItem.Single && item.title != null) { - item.type.pluralRes.let { resources.getQuantityString(it, 1) } - } else if (item is ExtrasItem.Group) { - resources.getQuantityString(R.plurals.items, item.items.size, item.items.size) - } else { - null - }, + subtitle = item?.subtitle, onClick = onClick, onLongClick = onLongClick, modifier = mod, showImageOverlay = true, - imageHeight = Cards.height2x3 * .75f, + imageHeight = Cards.heightEpisode, imageWidth = Dp.Unspecified, - imageUrl = imageUrl, + imageUrl = item?.imageUrl, isFavorite = false, - isPlayed = false, + isPlayed = item?.isPlayed == true, unplayedItemCount = -1, - playedPercentage = -1.0, + playedPercentage = item?.playedPercentage ?: 0.0, numberOfVersions = -1, aspectRatio = AspectRatios.FOUR_THREE, // TODO )