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
This commit is contained in:
Damontecres 2026-04-26 17:52:57 -04:00 committed by GitHub
parent 8cf99900e5
commit 5571654a22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 69 additions and 42 deletions

View file

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

View file

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

View file

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