refactor: update image handling in Episode dto to use imageUrlPrefix and JellyfinImageHelper

This commit is contained in:
2026-04-01 22:49:37 +02:00
parent 98850087ff
commit 680df51ce7
20 changed files with 52 additions and 35 deletions

View File

@@ -101,7 +101,7 @@ class EpisodeScreenContentTest {
progress = progress, progress = progress,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
cast = listOf( cast = listOf(
CastMember("Adam Scott", "Mark Scout", null) CastMember("Adam Scott", "Mark Scout", null)
) )

View File

@@ -117,7 +117,7 @@ class SeriesScreenContentTest {
progress = 18.0, progress = 18.0,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
cast = emptyList() cast = emptyList()
), ),
Episode( Episode(
@@ -133,7 +133,7 @@ class SeriesScreenContentTest {
progress = null, progress = null,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
cast = emptyList() cast = emptyList()
) )
) )

View File

@@ -19,12 +19,14 @@ import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection
import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.EpisodeDto import hu.bbara.purefin.core.data.navigation.EpisodeDto
import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.navigation.Route
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
import org.jellyfin.sdk.model.api.ImageType
@Composable @Composable
fun EpisodeScreen( fun EpisodeScreen(
@@ -91,7 +93,7 @@ internal fun EpisodeScreenContent(
} }
TvMediaDetailScaffold( TvMediaDetailScaffold(
heroImageUrl = episode.heroImageUrl, heroImageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
resetScrollKey = episode.id, resetScrollKey = episode.id,
modifier = modifier, modifier = modifier,
topBar = { topBar = {

View File

@@ -60,11 +60,13 @@ import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.MediaResumeButton import hu.bbara.purefin.common.ui.components.MediaResumeButton
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.common.ui.components.WatchStateIndicator import hu.bbara.purefin.common.ui.components.WatchStateIndicator
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Season import hu.bbara.purefin.core.model.Season
import hu.bbara.purefin.core.model.Series import hu.bbara.purefin.core.model.Series
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
import org.jellyfin.sdk.model.api.ImageType
internal const val SeriesPlayButtonTag = "series-play-button" internal const val SeriesPlayButtonTag = "series-play-button"
internal const val SeriesFirstSeasonTabTag = "series-first-season-tab" internal const val SeriesFirstSeasonTabTag = "series-first-season-tab"
@@ -310,7 +312,7 @@ private fun EpisodeCard(
) )
) { ) {
PurefinAsyncImage( PurefinAsyncImage(
model = episode.heroImageUrl, model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop

View File

@@ -93,7 +93,10 @@ fun TvContinueWatchingCard(
prefixImageUrl = item.movie?.imageUrlPrefix, prefixImageUrl = item.movie?.imageUrlPrefix,
imageType = ImageType.PRIMARY imageType = ImageType.PRIMARY
) )
BaseItemKind.EPISODE -> item.episode?.heroImageUrl BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
prefixImageUrl = item.episode?.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
else -> null else -> null
} }
@@ -216,7 +219,7 @@ fun TvNextUpCard(
var isFocused by remember { mutableStateOf(false) } var isFocused by remember { mutableStateOf(false) }
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale") val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
val imageUrl = item.episode.heroImageUrl val imageUrl = JellyfinImageHelper.finishImageUrl(item.episode.imageUrlPrefix, ImageType.PRIMARY)
val cardWidth = 280.dp val cardWidth = 280.dp

View File

@@ -37,6 +37,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaMetaChip import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaHero import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.EpisodeDto import hu.bbara.purefin.core.data.navigation.EpisodeDto
import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack
import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.navigation.Route
@@ -45,6 +46,7 @@ import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.feature.download.DownloadState import hu.bbara.purefin.feature.download.DownloadState
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.api.ImageType
import java.util.UUID import java.util.UUID
@Composable @Composable
@@ -162,7 +164,7 @@ private fun EpisodeHeroSection(
.height(sectionHeight) .height(sectionHeight)
) { ) {
MediaHero( MediaHero(
imageUrl = episode.heroImageUrl, imageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
backgroundColor = scheme.background, backgroundColor = scheme.background,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
@@ -258,7 +260,7 @@ private fun previewEpisode(): Episode {
progress = 63.0, progress = 63.0,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
cast = listOf( cast = listOf(
CastMember("Adam Scott", "Mark Scout", null), CastMember("Adam Scott", "Mark Scout", null),
CastMember("Britt Lower", "Helly R.", null), CastMember("Britt Lower", "Helly R.", null),

View File

@@ -65,6 +65,7 @@ import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.MediaResumeButton import hu.bbara.purefin.common.ui.components.MediaResumeButton
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.common.ui.components.WatchStateIndicator import hu.bbara.purefin.common.ui.components.WatchStateIndicator
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.EpisodeDto import hu.bbara.purefin.core.data.navigation.EpisodeDto
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.navigation.Route
@@ -75,6 +76,7 @@ import hu.bbara.purefin.core.model.Series
import hu.bbara.purefin.feature.download.DownloadState import hu.bbara.purefin.feature.download.DownloadState
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
import hu.bbara.purefin.player.PlayerActivity import hu.bbara.purefin.player.PlayerActivity
import org.jellyfin.sdk.model.api.ImageType
@Composable @Composable
internal fun SeriesTopBar( internal fun SeriesTopBar(
@@ -347,7 +349,7 @@ private fun EpisodeCard(
.border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp)) .border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp))
) { ) {
PurefinAsyncImage( PurefinAsyncImage(
model = episode.heroImageUrl, model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop

View File

@@ -272,7 +272,7 @@ private fun previewSeries(): Series {
progress = 100.0, progress = 100.0,
watched = true, watched = true,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1497032205916-ac775f0649ae", imageUrlPrefix = "https://images.unsplash.com/photo-1497032205916-ac775f0649ae",
cast = emptyList() cast = emptyList()
), ),
Episode( Episode(
@@ -288,7 +288,7 @@ private fun previewSeries(): Series {
progress = 34.0, progress = 34.0,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1520034475321-cbe63696469a", imageUrlPrefix = "https://images.unsplash.com/photo-1520034475321-cbe63696469a",
cast = emptyList() cast = emptyList()
) )
) )
@@ -306,7 +306,7 @@ private fun previewSeries(): Series {
progress = null, progress = null,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429", imageUrlPrefix = "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429",
cast = emptyList() cast = emptyList()
) )
) )

View File

@@ -57,7 +57,10 @@ internal fun ContinueWatchingCard(
prefixImageUrl = item.movie?.imageUrlPrefix, prefixImageUrl = item.movie?.imageUrlPrefix,
imageType = ImageType.PRIMARY imageType = ImageType.PRIMARY
) )
BaseItemKind.EPISODE -> item.episode?.heroImageUrl BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
prefixImageUrl = item.episode?.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
else -> null else -> null
} }

View File

@@ -23,8 +23,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.NextUpItem
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.ImageType
@Composable @Composable
internal fun NextUpCard( internal fun NextUpCard(
@@ -55,7 +57,7 @@ internal fun NextUpCard(
.background(scheme.surface) .background(scheme.surface)
) { ) {
PurefinAsyncImage( PurefinAsyncImage(
model = item.episode.heroImageUrl, model = JellyfinImageHelper.finishImageUrl(item.episode.imageUrlPrefix, ImageType.PRIMARY),
contentDescription = item.primaryText, contentDescription = item.primaryText,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop

View File

@@ -476,11 +476,10 @@ class InMemoryAppContentRepository @Inject constructor(
private fun BaseItemDto.toEpisode(serverUrl: String): Episode { private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
val releaseDate = formatReleaseDate(premiereDate, productionYear) val releaseDate = formatReleaseDate(premiereDate, productionYear)
val heroImageUrl = id?.let { itemId -> val imageUrlPrefix = id?.let { itemId ->
JellyfinImageHelper.toImageUrl( JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl, url = serverUrl,
itemId = itemId, itemId = itemId
type = ImageType.PRIMARY
) )
} ?: "" } ?: ""
return Episode( return Episode(
@@ -496,7 +495,7 @@ class InMemoryAppContentRepository @Inject constructor(
watched = userData!!.played, watched = userData!!.played,
format = container?.uppercase() ?: "VIDEO", format = container?.uppercase() ?: "VIDEO",
synopsis = overview ?: "No synopsis available.", synopsis = overview ?: "No synopsis available.",
heroImageUrl = heroImageUrl, imageUrlPrefix = imageUrlPrefix,
cast = emptyList() cast = emptyList()
) )
} }

View File

@@ -125,8 +125,8 @@ class InMemoryMediaRepository @Inject constructor(
private fun BaseItemDto.toEpisode(serverUrl: String): Episode { private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
val releaseDate = formatReleaseDate(premiereDate, productionYear) val releaseDate = formatReleaseDate(premiereDate, productionYear)
val heroImageUrl = id?.let { itemId -> val imageUrlPrefix = id?.let { itemId ->
JellyfinImageHelper.toImageUrl(url = serverUrl, itemId = itemId, type = ImageType.PRIMARY) JellyfinImageHelper.toPrefixImageUrl(url = serverUrl, itemId = itemId)
} ?: "" } ?: ""
return Episode( return Episode(
id = id, id = id,
@@ -141,7 +141,7 @@ class InMemoryMediaRepository @Inject constructor(
watched = userData!!.played, watched = userData!!.played,
format = container?.uppercase() ?: "VIDEO", format = container?.uppercase() ?: "VIDEO",
synopsis = overview ?: "No synopsis available.", synopsis = overview ?: "No synopsis available.",
heroImageUrl = heroImageUrl, imageUrlPrefix = imageUrlPrefix,
cast = emptyList() cast = emptyList()
) )
} }

View File

@@ -31,5 +31,5 @@ data class EpisodeEntity(
val progress: Double?, val progress: Double?,
val watched: Boolean, val watched: Boolean,
val format: String, val format: String,
val heroImageUrl: String val imageUrlPrefix: String
) )

View File

@@ -23,7 +23,7 @@ import hu.bbara.purefin.core.data.room.entity.SmartDownloadEntity
EpisodeEntity::class, EpisodeEntity::class,
SmartDownloadEntity::class, SmartDownloadEntity::class,
], ],
version = 8, version = 9,
exportSchema = false exportSchema = false
) )
@TypeConverters(UuidConverters::class) @TypeConverters(UuidConverters::class)

View File

@@ -246,7 +246,7 @@ class OfflineRoomMediaLocalDataSource(
progress = progress, progress = progress,
watched = watched, watched = watched,
format = format, format = format,
heroImageUrl = heroImageUrl imageUrlPrefix = imageUrlPrefix
) )
private fun MovieEntity.toDomain() = Movie( private fun MovieEntity.toDomain() = Movie(
@@ -302,7 +302,7 @@ class OfflineRoomMediaLocalDataSource(
progress = progress, progress = progress,
watched = watched, watched = watched,
format = format, format = format,
heroImageUrl = heroImageUrl, imageUrlPrefix = imageUrlPrefix,
cast = emptyList() cast = emptyList()
) )
} }

View File

@@ -15,6 +15,6 @@ data class Episode(
val progress: Double?, val progress: Double?,
val watched: Boolean, val watched: Boolean,
val format: String, val format: String,
val heroImageUrl: String, val imageUrlPrefix: String,
val cast: List<CastMember> val cast: List<CastMember>
) )

View File

@@ -140,7 +140,7 @@ class PlayerMediaRepository @Inject constructor(
val subtitle = episode?.let { episodeSubtitle(null, it.index) } val subtitle = episode?.let { episodeSubtitle(null, it.index) }
val artworkUrl = when { val artworkUrl = when {
movie != null -> JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY) movie != null -> JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY)
episode != null -> episode.heroImageUrl episode != null -> JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY)
else -> JellyfinImageHelper.toImageUrl(serverUrl, mediaId, ImageType.PRIMARY) else -> JellyfinImageHelper.toImageUrl(serverUrl, mediaId, ImageType.PRIMARY)
} }
val resumePositionMs = resumePositionFor(movie, episode) val resumePositionMs = resumePositionFor(movie, episode)

View File

@@ -369,10 +369,9 @@ class MediaDownloadManager @Inject constructor(
watched = userData?.played ?: false, watched = userData?.played ?: false,
format = container?.uppercase() ?: "VIDEO", format = container?.uppercase() ?: "VIDEO",
synopsis = overview ?: "No synopsis available.", synopsis = overview ?: "No synopsis available.",
heroImageUrl = JellyfinImageHelper.toImageUrl( imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl, url = serverUrl,
itemId = id, itemId = id
type = ImageType.PRIMARY
), ),
cast = emptyList() cast = emptyList()
) )

View File

@@ -82,7 +82,7 @@ class DownloadsViewModel @Inject constructor(
contentId = contentId, contentId = contentId,
title = it.title, title = it.title,
subtitle = seriesMap[it.seriesId]?.name ?: "", subtitle = seriesMap[it.seriesId]?.name ?: "",
imageUrl = it.heroImageUrl, imageUrl = JellyfinImageHelper.finishImageUrl(it.imageUrlPrefix, ImageType.PRIMARY),
progress = progress progress = progress
) )
} }

View File

@@ -38,7 +38,10 @@ data class SuggestedEpisode (
override val id: UUID = episode.id, override val id: UUID = episode.id,
override val title: String = episode.title, override val title: String = episode.title,
override val description: String = episode.synopsis, override val description: String = episode.synopsis,
override val imageUrl: String = episode.heroImageUrl override val imageUrl: String = JellyfinImageHelper.finishImageUrl(
prefixImageUrl = episode.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
) : SuggestedItem ) : SuggestedItem
data class SuggestedSeries ( data class SuggestedSeries (
@@ -148,7 +151,7 @@ data class PosterItem(
} }
val imageUrl: String = when (type) { val imageUrl: String = when (type) {
BaseItemKind.MOVIE -> movie!!.imageUrlPrefix BaseItemKind.MOVIE -> movie!!.imageUrlPrefix
BaseItemKind.EPISODE -> episode!!.heroImageUrl BaseItemKind.EPISODE -> episode!!.imageUrlPrefix
BaseItemKind.SERIES -> series!!.imageUrlPrefix BaseItemKind.SERIES -> series!!.imageUrlPrefix
else -> throw IllegalArgumentException("Invalid type: $type") else -> throw IllegalArgumentException("Invalid type: $type")
} }