Show indicator for multiple versions of media file (#652)

## Description
Adds an indicator to cards on grid pages if there are more than a single
version of the item. Also adds the count to the video stream label, much
like for multiple audio/subtitle tracks.

No behavior changes, just visual changes. You still need to select the
version from More->Choose Version.

### Related issues
Closes #612

### Screenshots
<img width="535" height="476" alt="image"
src="https://github.com/user-attachments/assets/5bf80d36-7abd-4a0e-a14d-062439f024d9"
/>
<img width="958" height="581" alt="image"
src="https://github.com/user-attachments/assets/e3a0ac4e-a283-4bcf-bc16-e61d81401207"
/>
This commit is contained in:
Ray 2026-01-09 14:01:37 -05:00 committed by GitHub
parent d2e0d527dd
commit 2cd34692e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 67 additions and 19 deletions

View file

@ -51,6 +51,7 @@ val DefaultItemFields =
ItemFields.SORT_NAME,
ItemFields.CHAPTERS,
ItemFields.MEDIA_SOURCES,
ItemFields.MEDIA_SOURCE_COUNT,
)
/**
@ -63,6 +64,7 @@ val SlimItemFields =
ItemFields.CHILD_COUNT,
ItemFields.OVERVIEW,
ItemFields.SORT_NAME,
ItemFields.MEDIA_SOURCE_COUNT,
)
object Cards {

View file

@ -97,6 +97,7 @@ fun EpisodeCard(
watched = dto?.userData?.played ?: false,
unwatchedCount = dto?.userData?.unplayedItemCount ?: -1,
watchedPercent = dto?.userData?.playedPercentage,
numberOfVersions = dto?.mediaSourceCount ?: 0,
useFallbackText = false,
modifier =
Modifier

View file

@ -68,6 +68,7 @@ fun ExtrasRow(
isPlayed = false,
unplayedItemCount = -1,
playedPercentage = -1.0,
numberOfVersions = -1,
aspectRatio = AspectRatios.FOUR_THREE, // TODO
)
},

View file

@ -91,6 +91,7 @@ fun GridCard(
watched = dto?.userData?.played ?: false,
unwatchedCount = dto?.userData?.unplayedItemCount ?: -1,
watchedPercent = dto?.userData?.playedPercentage,
numberOfVersions = dto?.mediaSourceCount ?: 0,
useFallbackText = false,
contentScale = imageContentScale,
modifier =

View file

@ -58,6 +58,7 @@ fun ItemCardImage(
watched: Boolean,
unwatchedCount: Int,
watchedPercent: Double?,
numberOfVersions: Int,
modifier: Modifier = Modifier,
imageType: ImageType = ImageType.PRIMARY,
useFallbackText: Boolean = true,
@ -86,6 +87,7 @@ fun ItemCardImage(
watched = watched,
unwatchedCount = unwatchedCount,
watchedPercent = watchedPercent,
numberOfVersions = numberOfVersions,
modifier =
modifier.onLayoutRectChanged(
throttleMillis = 100,
@ -107,6 +109,7 @@ fun ItemCardImage(
watched: Boolean,
unwatchedCount: Int,
watchedPercent: Double?,
numberOfVersions: Int,
modifier: Modifier = Modifier,
useFallbackText: Boolean = true,
contentScale: ContentScale = ContentScale.Fit,
@ -143,6 +146,7 @@ fun ItemCardImage(
watched = watched,
unwatchedCount = unwatchedCount,
watchedPercent = watchedPercent,
numberOfVersions = numberOfVersions,
modifier = Modifier,
)
}
@ -198,21 +202,46 @@ fun ItemCardImageOverlay(
watched: Boolean,
unwatchedCount: Int,
watchedPercent: Double?,
numberOfVersions: Int,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.fillMaxSize()) {
if (favorite) {
Text(
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.align(Alignment.TopStart)
.padding(8.dp),
.padding(4.dp),
) {
if (numberOfVersions > 1) {
Box(
modifier =
Modifier
.background(
AppColors.TransparentBlack50,
shape = RoundedCornerShape(25),
),
) {
Text(
text = numberOfVersions.toString(),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
// fontSize = 16.sp,
modifier = Modifier.padding(4.dp),
)
}
}
if (favorite) {
Text(
color = colorResource(android.R.color.holo_red_light),
text = stringResource(R.string.fa_heart),
fontSize = 20.sp,
fontFamily = FontAwesome,
modifier = Modifier,
)
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,

View file

@ -77,6 +77,7 @@ fun PersonCard(
favorite = item.favorite,
watched = false,
unwatchedCount = -1,
numberOfVersions = -1,
watchedPercent = null,
useFallbackText = false,
modifier =

View file

@ -88,6 +88,7 @@ fun SeasonCard(
isPlayed = item?.data?.userData?.played ?: false,
unplayedItemCount = item?.data?.userData?.unplayedItemCount ?: 0,
playedPercentage = item?.data?.userData?.playedPercentage ?: 0.0,
numberOfVersions = item?.data?.mediaSourceCount ?: 0,
onClick = onClick,
onLongClick = onLongClick,
modifier = modifier,
@ -112,6 +113,7 @@ fun SeasonCard(
isPlayed: Boolean,
unplayedItemCount: Int,
playedPercentage: Double,
numberOfVersions: Int,
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
@ -172,6 +174,7 @@ fun SeasonCard(
watched = isPlayed,
unwatchedCount = unplayedItemCount,
watchedPercent = playedPercentage,
numberOfVersions = numberOfVersions,
useFallbackText = false,
modifier =
Modifier

View file

@ -47,12 +47,14 @@ import org.jellyfin.sdk.model.api.VideoRangeType
@NonRestartableComposable
fun VideoStreamDetails(
chosenStreams: ChosenStreams?,
numberOfVersions: Int,
modifier: Modifier = Modifier,
) = VideoStreamDetails(
chosenStreams?.source,
chosenStreams?.videoStream,
chosenStreams?.audioStream,
chosenStreams?.subtitleStream,
numberOfVersions,
modifier,
)
@ -62,6 +64,7 @@ fun VideoStreamDetails(
videoStream: MediaStream?,
audioStream: MediaStream?,
subtitleStream: MediaStream?,
numberOfVersions: Int = 0,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
@ -86,13 +89,16 @@ fun VideoStreamDetails(
null
}
val range = formatVideoRange(context, it.videoRange, it.videoRangeType, it.videoDoViTitle)
listOfNotNull(
resName.concatWithSpace(range),
it.codec?.uppercase(),
)
}.orEmpty()
resName.concatWithSpace(range)
}
video.forEach {
}
video?.let {
StreamLabel(
text = it,
count = numberOfVersions,
)
}
videoStream?.codec?.uppercase()?.let {
StreamLabel(it)
}

View file

@ -444,6 +444,7 @@ fun PlaylistItem(
watched = item?.data?.userData?.played ?: false,
unwatchedCount = item?.data?.userData?.unplayedItemCount ?: -1,
watchedPercent = 0.0,
numberOfVersions = item?.data?.mediaSourceCount ?: 0,
modifier = Modifier.width(160.dp),
useFallbackText = false,
)

View file

@ -59,6 +59,7 @@ fun EpisodeDetailsHeader(
VideoStreamDetails(
chosenStreams = chosenStreams,
numberOfVersions = dto.mediaSourceCount ?: 0,
modifier = Modifier.padding(bottom = padding),
)
dto.taglines?.firstOrNull()?.let { tagline ->

View file

@ -73,6 +73,7 @@ fun MovieDetailsHeader(
VideoStreamDetails(
chosenStreams = chosenStreams,
numberOfVersions = movie.data.mediaSourceCount ?: 0,
modifier = Modifier.padding(bottom = padding),
)
dto.taglines?.firstOrNull()?.let { tagline ->

View file

@ -38,6 +38,7 @@ fun FocusedEpisodeHeader(
if (dto != null) {
VideoStreamDetails(
chosenStreams = chosenStreams,
numberOfVersions = dto.mediaSourceCount ?: 0,
modifier = Modifier,
)
}

View file

@ -30,10 +30,8 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppThemeColors
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.cards.BannerCard
import com.github.damontecres.wholphin.ui.cards.SeasonCard
import com.github.damontecres.wholphin.ui.cards.WatchedIcon
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
import com.github.damontecres.wholphin.ui.nav.NavItem
import com.github.damontecres.wholphin.ui.playback.PlaybackButton
@ -109,6 +107,7 @@ private fun ThemeExample(theme: AppThemeColors) {
isPlayed = true,
unplayedItemCount = 2,
playedPercentage = 50.0,
numberOfVersions = 2,
onClick = { },
onLongClick = {},
imageHeight = 120.dp,
@ -125,6 +124,7 @@ private fun ThemeExample(theme: AppThemeColors) {
isPlayed = true,
unplayedItemCount = 2,
playedPercentage = 0.0,
numberOfVersions = 0,
onClick = { },
onLongClick = {},
imageHeight = 120.dp,