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

This commit is contained in:
2026-04-01 22:30:32 +02:00
parent d6d05be93f
commit ce5266f17c
16 changed files with 46 additions and 297 deletions

View File

@@ -65,7 +65,7 @@ class MovieScreenContentTest {
runtime = "164m",
format = "4K",
synopsis = "Officer K uncovers a secret that sends him searching for Rick Deckard.",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
audioTrack = "ENG 5.1",
subtitles = "ENG",
cast = listOf(

View File

@@ -19,9 +19,11 @@ import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection
import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.MovieDto
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
import org.jellyfin.sdk.model.api.ImageType
@Composable
fun MovieScreen(
@@ -60,7 +62,7 @@ internal fun MovieScreenContent(
}
TvMediaDetailScaffold(
heroImageUrl = movie.heroImageUrl,
heroImageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
resetScrollKey = movie.id,
modifier = modifier,
topBar = {

View File

@@ -39,11 +39,13 @@ import androidx.compose.ui.unit.sp
import hu.bbara.purefin.common.ui.PosterCard
import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
import kotlin.math.nextUp
@Composable
@@ -67,8 +69,6 @@ fun TvContinueWatchingSection(
itemsIndexed(items = items) { index, item ->
TvContinueWatchingCard(
item = item,
isFirstItem = index == 0,
isLastItem = index == items.lastIndex,
onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected
)
@@ -80,8 +80,6 @@ fun TvContinueWatchingSection(
fun TvContinueWatchingCard(
item: ContinueWatchingItem,
modifier: Modifier = Modifier,
isFirstItem: Boolean = false,
isLastItem: Boolean = false,
onMovieSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) {
@@ -91,7 +89,10 @@ fun TvContinueWatchingCard(
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
val imageUrl = when (item.type) {
BaseItemKind.MOVIE -> item.movie?.heroImageUrl
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
prefixImageUrl = item.movie?.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
BaseItemKind.EPISODE -> item.episode?.heroImageUrl
else -> null
}

View File

@@ -35,12 +35,14 @@ import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.MovieDto
import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.feature.download.DownloadState
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.api.ImageType
import java.util.UUID
@Composable
@@ -135,7 +137,7 @@ fun MediaHeroSection(
.height(sectionHeight)
) {
MediaHero(
imageUrl = movie.heroImageUrl,
imageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
backgroundColor = MaterialTheme.colorScheme.background,
modifier = Modifier.fillMaxSize()
)
@@ -209,7 +211,7 @@ private fun previewMovie(): Movie =
runtime = "2h 44m",
format = "Dolby Vision",
synopsis = "A new blade runner uncovers a buried secret that forces him to trace the vanished footsteps of Rick Deckard.",
heroImageUrl = "https://images.unsplash.com/photo-1519608487953-e999c86e7455",
imageUrlPrefix = "https://images.unsplash.com/photo-1519608487953-e999c86e7455",
audioTrack = "English 5.1",
subtitles = "English CC",
cast = listOf(

View File

@@ -21,26 +21,19 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.app.home.ui.continuewatching.ContinueWatchingSection
import hu.bbara.purefin.app.home.ui.featured.SuggestionsSection
import hu.bbara.purefin.app.home.ui.library.LibraryPosterSection
import hu.bbara.purefin.app.home.ui.nextup.NextUpSection
import hu.bbara.purefin.app.home.ui.shared.HomeEmptyState
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.core.model.Series
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem
import hu.bbara.purefin.feature.shared.home.SuggestedItem
import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import java.util.UUID as JavaUuid
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -170,265 +163,3 @@ fun HomeContent(
}
}
}
@Preview(name = "Home Full", showBackground = true, widthDp = 412, heightDp = 915)
@Composable
private fun HomeContentPreview() {
AppTheme(darkTheme = true) {
HomeContent(
libraries = homePreviewLibraries(),
libraryContent = homePreviewLibraryContent(),
suggestions = emptyList(),
continueWatching = homePreviewContinueWatching(),
nextUp = homePreviewNextUp(),
isRefreshing = false,
onRefresh = {},
onMovieSelected = {},
onSeriesSelected = {},
onEpisodeSelected = { _, _, _ -> },
onLibrarySelected = {},
onBrowseLibrariesClick = {}
)
}
}
@Preview(name = "Home Libraries Only", showBackground = true, widthDp = 412, heightDp = 915)
@Composable
private fun HomeLibrariesOnlyPreview() {
AppTheme(darkTheme = true) {
HomeContent(
libraries = homePreviewLibraries(),
libraryContent = homePreviewLibraryContent(),
suggestions = emptyList(),
continueWatching = emptyList(),
nextUp = emptyList(),
isRefreshing = false,
onRefresh = {},
onMovieSelected = {},
onSeriesSelected = {},
onEpisodeSelected = { _, _, _ -> },
onLibrarySelected = {},
onBrowseLibrariesClick = {}
)
}
}
@Preview(name = "Home Empty", showBackground = true, widthDp = 412, heightDp = 915)
@Composable
private fun HomeEmptyPreview() {
AppTheme(darkTheme = false) {
HomeContent(
libraries = emptyList(),
libraryContent = emptyMap(),
suggestions = emptyList(),
continueWatching = emptyList(),
nextUp = emptyList(),
isRefreshing = false,
onRefresh = {},
onMovieSelected = {},
onSeriesSelected = {},
onEpisodeSelected = { _, _, _ -> },
onLibrarySelected = {},
onBrowseLibrariesClick = {}
)
}
}
internal fun homePreviewLibraries(): List<LibraryItem> {
return listOf(
LibraryItem(
id = JavaUuid.fromString("11111111-1111-1111-1111-111111111111"),
name = "Movies",
type = CollectionType.MOVIES,
posterUrl = "https://images.unsplash.com/photo-1517604931442-7e0c8ed2963c",
isEmpty = false
),
LibraryItem(
id = JavaUuid.fromString("22222222-2222-2222-2222-222222222222"),
name = "Series",
type = CollectionType.TVSHOWS,
posterUrl = "https://images.unsplash.com/photo-1489599849927-2ee91cede3ba",
isEmpty = false
)
)
}
internal fun homePreviewLibraryContent(): Map<UUID, List<PosterItem>> {
val movie = homePreviewMovie(
id = "33333333-3333-3333-3333-333333333333",
title = "Blade Runner 2049",
year = "2017",
runtime = "2h 44m",
rating = "16+",
format = "Dolby Vision",
synopsis = "A young blade runner uncovers a buried secret that pulls him toward a vanished legend.",
heroImageUrl = "https://images.unsplash.com/photo-1519608487953-e999c86e7455",
progress = 42.0,
watched = false
)
val secondMovie = homePreviewMovie(
id = "44444444-4444-4444-4444-444444444444",
title = "Arrival",
year = "2016",
runtime = "1h 56m",
rating = "12+",
format = "4K",
synopsis = "A linguist is recruited when mysterious spacecraft touch down around the world.",
heroImageUrl = "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429",
progress = null,
watched = false
)
val series = homePreviewSeries()
val episode = homePreviewEpisode(
id = "66666666-6666-6666-6666-666666666666",
title = "Signals",
index = 2,
releaseDate = "2025",
runtime = "48m",
rating = "16+",
progress = 18.0,
watched = false,
heroImageUrl = "https://images.unsplash.com/photo-1520034475321-cbe63696469a",
synopsis = "Anomalies around the station point to a cover-up."
)
return mapOf(
homePreviewLibraries()[0].id to listOf(
PosterItem(type = BaseItemKind.MOVIE, movie = movie),
PosterItem(type = BaseItemKind.MOVIE, movie = secondMovie)
),
homePreviewLibraries()[1].id to listOf(
PosterItem(type = BaseItemKind.SERIES, series = series),
PosterItem(type = BaseItemKind.EPISODE, episode = episode)
)
)
}
internal fun homePreviewContinueWatching(): List<ContinueWatchingItem> {
return listOf(
ContinueWatchingItem(
type = BaseItemKind.MOVIE,
movie = homePreviewMovie(
id = "77777777-7777-7777-7777-777777777777",
title = "Dune: Part Two",
year = "2024",
runtime = "2h 46m",
rating = "13+",
format = "IMAX",
synopsis = "Paul Atreides unites with the Fremen while seeking justice and revenge.",
heroImageUrl = "https://images.unsplash.com/photo-1446776811953-b23d57bd21aa",
progress = 58.0,
watched = false
)
),
ContinueWatchingItem(
type = BaseItemKind.EPISODE,
episode = homePreviewEpisode(
id = "88888888-8888-8888-8888-888888888888",
title = "A Fresh Start",
index = 1,
releaseDate = "2025",
runtime = "51m",
rating = "16+",
progress = 23.0,
watched = false,
heroImageUrl = "https://images.unsplash.com/photo-1497032205916-ac775f0649ae",
synopsis = "A fractured crew tries to reassemble after a year apart."
)
)
)
}
internal fun homePreviewNextUp(): List<NextUpItem> {
return listOf(
NextUpItem(
episode = homePreviewEpisode(
id = "99999999-9999-9999-9999-999999999999",
title = "Return Window",
index = 3,
releaseDate = "2025",
runtime = "54m",
rating = "16+",
progress = null,
watched = false,
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
synopsis = "A high-risk jump changes the rules of the mission."
)
)
)
}
internal fun homePreviewMovie(
id: String,
title: String,
year: String,
runtime: String,
rating: String,
format: String,
synopsis: String,
heroImageUrl: String,
progress: Double?,
watched: Boolean
): Movie {
return Movie(
id = JavaUuid.fromString(id),
libraryId = JavaUuid.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
title = title,
progress = progress,
watched = watched,
year = year,
rating = rating,
runtime = runtime,
format = format,
synopsis = synopsis,
heroImageUrl = heroImageUrl,
audioTrack = "English 5.1",
subtitles = "English CC",
cast = emptyList()
)
}
internal fun homePreviewSeries(): Series {
return Series(
id = JavaUuid.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
libraryId = JavaUuid.fromString("cccccccc-cccc-cccc-cccc-cccccccccccc"),
name = "Orbital",
synopsis = "A reluctant crew returns to a damaged station as political pressure mounts on Earth.",
year = "2025",
heroImageUrl = "https://images.unsplash.com/photo-1520034475321-cbe63696469a",
unwatchedEpisodeCount = 4,
seasonCount = 2,
seasons = emptyList(),
cast = emptyList()
)
}
internal fun homePreviewEpisode(
id: String,
title: String,
index: Int,
releaseDate: String,
runtime: String,
rating: String,
progress: Double?,
watched: Boolean,
heroImageUrl: String,
synopsis: String
): Episode {
return Episode(
id = JavaUuid.fromString(id),
seriesId = JavaUuid.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd"),
seasonId = JavaUuid.fromString("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"),
index = index,
title = title,
synopsis = synopsis,
releaseDate = releaseDate,
rating = rating,
runtime = runtime,
progress = progress,
watched = watched,
format = "4K",
heroImageUrl = heroImageUrl,
cast = emptyList()
)
}

View File

@@ -25,9 +25,11 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
@Composable
internal fun ContinueWatchingCard(
@@ -51,7 +53,10 @@ internal fun ContinueWatchingCard(
else -> ""
}
val imageUrl = when (item.type) {
BaseItemKind.MOVIE -> item.movie?.heroImageUrl
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
prefixImageUrl = item.movie?.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
BaseItemKind.EPISODE -> item.episode?.heroImageUrl
else -> null
}

View File

@@ -433,10 +433,9 @@ class InMemoryAppContentRepository @Inject constructor(
runtime = formatRuntime(this.runTimeTicks),
synopsis = this.overview ?: "No synopsis available",
format = container?.uppercase() ?: "VIDEO",
heroImageUrl = JellyfinImageHelper.toImageUrl(
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl,
itemId = this.id,
type = ImageType.PRIMARY
),
subtitles = "ENG",
audioTrack = "ENG",

View File

@@ -31,7 +31,10 @@ class JellyfinImageHelper {
.toString()
}
fun finishImageUrl(prefixImageUrl: String, imageType: ImageType): String {
fun finishImageUrl(prefixImageUrl: String?, imageType: ImageType): String {
if (prefixImageUrl.isNullOrEmpty()) {
return ""
}
return StringBuilder()
.append(prefixImageUrl)
.append(imageType.serialName)

View File

@@ -1,7 +1,6 @@
package hu.bbara.purefin.core.data.room.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import java.util.UUID
@@ -21,7 +20,7 @@ data class MovieEntity(
val runtime: String,
val format: String,
val synopsis: String,
val heroImageUrl: String,
val imageUrlPrefix: String,
val audioTrack: String,
val subtitles: String
)

View File

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

View File

@@ -208,7 +208,7 @@ class OfflineRoomMediaLocalDataSource(
runtime = runtime,
format = format,
synopsis = synopsis,
heroImageUrl = heroImageUrl,
imageUrlPrefix = imageUrlPrefix,
audioTrack = audioTrack,
subtitles = subtitles
)
@@ -260,7 +260,7 @@ class OfflineRoomMediaLocalDataSource(
runtime = runtime,
format = format,
synopsis = synopsis,
heroImageUrl = heroImageUrl,
imageUrlPrefix = imageUrlPrefix,
audioTrack = audioTrack,
subtitles = subtitles,
cast = emptyList()

View File

@@ -13,7 +13,7 @@ data class Movie(
val runtime: String,
val format: String,
val synopsis: String,
val heroImageUrl: String,
val imageUrlPrefix: String,
val audioTrack: String,
val subtitles: String,
val cast: List<CastMember>

View File

@@ -124,6 +124,7 @@ class PlayerMediaRepository @Inject constructor(
}
}
@OptIn(UnstableApi::class)
private suspend fun buildOfflineMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? {
val download = downloadManager.downloadIndex.getDownload(mediaId.toString())
?.takeIf { it.state == Download.STATE_COMPLETED }
@@ -138,7 +139,7 @@ class PlayerMediaRepository @Inject constructor(
}
val subtitle = episode?.let { episodeSubtitle(null, it.index) }
val artworkUrl = when {
movie != null -> movie.heroImageUrl
movie != null -> JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY)
episode != null -> episode.heroImageUrl
else -> JellyfinImageHelper.toImageUrl(serverUrl, mediaId, ImageType.PRIMARY)
}

View File

@@ -23,7 +23,6 @@ import hu.bbara.purefin.core.model.Season
import hu.bbara.purefin.core.model.Series
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -32,6 +31,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.ImageType
@@ -156,10 +156,9 @@ class MediaDownloadManager @Inject constructor(
runtime = formatRuntime(itemInfo.runTimeTicks),
synopsis = itemInfo.overview ?: "No synopsis available",
format = itemInfo.container?.uppercase() ?: "VIDEO",
heroImageUrl = JellyfinImageHelper.toImageUrl(
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl,
itemId = itemInfo.id,
type = ImageType.PRIMARY
itemId = itemInfo.id
),
subtitles = "ENG",
audioTrack = "ENG",

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.core.data.OfflineMediaRepository
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.MovieDto
import hu.bbara.purefin.core.data.navigation.NavigationManager
import hu.bbara.purefin.core.data.navigation.Route
@@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
import javax.inject.Inject
@HiltViewModel
@@ -70,7 +72,7 @@ class DownloadsViewModel @Inject constructor(
contentId = contentId,
title = movie.title,
subtitle = "",
imageUrl = movie.heroImageUrl,
imageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
progress = progress
)
} else {

View File

@@ -1,11 +1,13 @@
package hu.bbara.purefin.feature.shared.home
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.core.model.Series
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.ImageType
sealed interface SuggestedItem {
@@ -74,7 +76,10 @@ data class SuggestedMovie (
override val id: UUID = movie.id,
override val title: String = movie.title,
override val description: String = movie.synopsis,
override val imageUrl: String = movie.heroImageUrl
override val imageUrl: String = JellyfinImageHelper.finishImageUrl(
prefixImageUrl = movie.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
) : SuggestedItem
data class ContinueWatchingItem(
@@ -139,7 +144,7 @@ data class PosterItem(
else -> throw IllegalArgumentException("Invalid type: $type")
}
val imageUrl: String = when (type) {
BaseItemKind.MOVIE -> movie!!.heroImageUrl
BaseItemKind.MOVIE -> movie!!.imageUrlPrefix
BaseItemKind.EPISODE -> episode!!.heroImageUrl
BaseItemKind.SERIES -> series!!.heroImageUrl
else -> throw IllegalArgumentException("Invalid type: $type")