From 2cd34692e914b8fd4c6fd9601e6059a33d00f4c5 Mon Sep 17 00:00:00 2001
From: Ray <154766448+damontecres@users.noreply.github.com>
Date: Fri, 9 Jan 2026 14:01:37 -0500
Subject: [PATCH] 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
---
.../damontecres/wholphin/ui/UiConstants.kt | 2 +
.../wholphin/ui/cards/EpisodeCard.kt | 1 +
.../wholphin/ui/cards/ExtrasRow.kt | 1 +
.../damontecres/wholphin/ui/cards/GridCard.kt | 1 +
.../wholphin/ui/cards/ItemCardImage.kt | 51 +++++++++++++++----
.../wholphin/ui/cards/PersonCard.kt | 1 +
.../wholphin/ui/cards/SeasonCard.kt | 3 ++
.../ui/components/VideoStreamDetails.kt | 18 ++++---
.../wholphin/ui/detail/PlaylistDetails.kt | 1 +
.../ui/detail/episode/EpisodeDetailsHeader.kt | 1 +
.../ui/detail/movie/MovieDetailsHeader.kt | 1 +
.../ui/detail/series/FocusedEpisodeHeader.kt | 1 +
.../wholphin/ui/theme/ThemePreview.kt | 4 +-
13 files changed, 67 insertions(+), 19 deletions(-)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt
index beafccb2..6b33ad49 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt
@@ -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 {
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt
index 8cc3ad12..e4ffa5ee 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt
@@ -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
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 39750da3..adfff582 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
@@ -68,6 +68,7 @@ fun ExtrasRow(
isPlayed = false,
unplayedItemCount = -1,
playedPercentage = -1.0,
+ numberOfVersions = -1,
aspectRatio = AspectRatios.FOUR_THREE, // TODO
)
},
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt
index 1bc35a08..7b653865 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt
@@ -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 =
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt
index 9254fbd3..1a2bbacf 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt
@@ -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,20 +202,45 @@ fun ItemCardImageOverlay(
watched: Boolean,
unwatchedCount: Int,
watchedPercent: Double?,
+ numberOfVersions: Int,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.fillMaxSize()) {
- if (favorite) {
- Text(
- modifier =
- Modifier
- .align(Alignment.TopStart)
- .padding(8.dp),
- color = colorResource(android.R.color.holo_red_light),
- text = stringResource(R.string.fa_heart),
- fontSize = 20.sp,
- fontFamily = FontAwesome,
- )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier =
+ Modifier
+ .align(Alignment.TopStart)
+ .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),
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt
index 83d72010..4d0c0fe5 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt
@@ -77,6 +77,7 @@ fun PersonCard(
favorite = item.favorite,
watched = false,
unwatchedCount = -1,
+ numberOfVersions = -1,
watchedPercent = null,
useFallbackText = false,
modifier =
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt
index 243bb6d6..e3e6059e 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt
@@ -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
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt
index a557d7f0..e9a11d54 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt
@@ -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)
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt
index 4907d71e..5bf996a3 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt
@@ -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,
)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetailsHeader.kt
index d3c77b67..09c8ed0f 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetailsHeader.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetailsHeader.kt
@@ -59,6 +59,7 @@ fun EpisodeDetailsHeader(
VideoStreamDetails(
chosenStreams = chosenStreams,
+ numberOfVersions = dto.mediaSourceCount ?: 0,
modifier = Modifier.padding(bottom = padding),
)
dto.taglines?.firstOrNull()?.let { tagline ->
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt
index c976e79b..df30078c 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt
@@ -73,6 +73,7 @@ fun MovieDetailsHeader(
VideoStreamDetails(
chosenStreams = chosenStreams,
+ numberOfVersions = movie.data.mediaSourceCount ?: 0,
modifier = Modifier.padding(bottom = padding),
)
dto.taglines?.firstOrNull()?.let { tagline ->
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt
index 5b25783b..97791749 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt
@@ -38,6 +38,7 @@ fun FocusedEpisodeHeader(
if (dto != null) {
VideoStreamDetails(
chosenStreams = chosenStreams,
+ numberOfVersions = dto.mediaSourceCount ?: 0,
modifier = Modifier,
)
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/theme/ThemePreview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/theme/ThemePreview.kt
index dfae73c5..8d3735e9 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/theme/ThemePreview.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/theme/ThemePreview.kt
@@ -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,