Refactor media models to use MediaUiModel for improved consistency and type safety

This commit is contained in:
2026-04-17 21:13:45 +02:00
parent 5a377fc428
commit 632dda52d6
27 changed files with 310 additions and 494 deletions

View File

@@ -3,20 +3,30 @@ package hu.bbara.purefin.ui.common.card
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import hu.bbara.purefin.core.model.MediaKind
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.core.ui.model.MovieUiModel
import hu.bbara.purefin.core.ui.model.SeriesUiModel
import hu.bbara.purefin.feature.browse.home.PosterItem
import java.util.UUID
@Composable
fun PosterCard(
item: PosterItem,
item: MediaUiModel,
modifier: Modifier = Modifier,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) {
PosterCardContent(
model = item.toPosterCardModel(),
onClick = { item.open(onMovieSelected, onSeriesSelected, onEpisodeSelected) },
model = item,
onClick = {
//TODO same throw this shit out when to composite onSelect is finished
when (item) {
is MovieUiModel -> onMovieSelected(item.id)
is SeriesUiModel -> onSeriesSelected(item.id)
else -> Unit
}
},
modifier = modifier
)
}

View File

@@ -9,29 +9,26 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.browse.home.SuggestedItem
import hu.bbara.purefin.ui.screen.AppBottomBar
import hu.bbara.purefin.ui.screen.home.components.HomeContent
import hu.bbara.purefin.ui.screen.home.components.HomeTopBar
import hu.bbara.purefin.ui.screen.home.components.search.HomeSearchOverlay
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.feature.browse.home.SuggestedItem
import hu.bbara.purefin.ui.screen.AppBottomBar
import java.util.UUID
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
libraries: List<LibraryItem>,
libraryContent: Map<UUID, List<PosterItem>>,
libraryContent: Map<UUID, List<MediaUiModel>>,
suggestions: List<SuggestedItem>,
continueWatching: List<ContinueWatchingItem>,
nextUp: List<NextUpItem>,
continueWatching: List<MediaUiModel>,
nextUp: List<MediaUiModel>,
isRefreshing: Boolean,
onRefresh: () -> Unit,
onMovieSelected: (UUID) -> Unit,
@@ -46,14 +43,6 @@ fun HomeScreen(
modifier: Modifier = Modifier
) {
var isSearchVisible by rememberSaveable { mutableStateOf(false) }
val subtitle = remember(continueWatching, nextUp, libraries) {
when {
continueWatching.isNotEmpty() -> "Continue where you left off"
nextUp.isNotEmpty() -> "Fresh episodes waiting for you"
libraries.isNotEmpty() -> "Browse your latest additions"
else -> "Pull to refresh or explore your libraries"
}
}
Scaffold(
modifier = modifier

View File

@@ -22,26 +22,24 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.core.model.MediaKind
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.feature.browse.home.SuggestedItem
import hu.bbara.purefin.ui.screen.home.components.continuewatching.ContinueWatchingSection
import hu.bbara.purefin.ui.screen.home.components.featured.SuggestionsSection
import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection
import hu.bbara.purefin.ui.screen.home.components.nextup.NextUpSection
import java.util.UUID
import hu.bbara.purefin.core.model.MediaKind
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeContent(
libraries: List<LibraryItem>,
libraryContent: Map<UUID, List<PosterItem>>,
libraryContent: Map<UUID, List<MediaUiModel>>,
suggestions: List<SuggestedItem>,
continueWatching: List<ContinueWatchingItem>,
nextUp: List<NextUpItem>,
continueWatching: List<MediaUiModel>,
nextUp: List<MediaUiModel>,
isRefreshing: Boolean,
onRefresh: () -> Unit,
onMovieSelected: (UUID) -> Unit,

View File

@@ -23,46 +23,21 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.ui.model.EpisodeUiModel
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.core.ui.model.MovieUiModel
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import java.util.UUID
import hu.bbara.purefin.core.model.MediaKind
import hu.bbara.purefin.core.image.ArtworkKind
@Composable
internal fun ContinueWatchingCard(
item: ContinueWatchingItem,
item: MediaUiModel,
onMovieSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier
) {
val scheme = MaterialTheme.colorScheme
val supportingText = when (item.type) {
MediaKind.MOVIE -> listOf(
item.movie?.year,
item.movie?.runtime
).filterNotNull().filter { it.isNotBlank() }.joinToString("")
MediaKind.EPISODE -> listOf(
"Episode ${item.episode?.index}",
item.episode?.runtime
).filterNotNull().filter { it.isNotBlank() }.joinToString("")
else -> ""
}
val imageUrl = when (item.type) {
MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl(
prefixImageUrl = item.movie?.imageUrlPrefix,
artworkKind = ArtworkKind.PRIMARY
)
MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl(
prefixImageUrl = item.episode?.imageUrlPrefix,
artworkKind = ArtworkKind.PRIMARY
)
else -> null
}
Surface(
shape = RoundedCornerShape(26.dp),
@@ -73,13 +48,10 @@ internal fun ContinueWatchingCard(
modifier = Modifier
.fillMaxWidth()
.clickable {
when (item.type) {
MediaKind.MOVIE -> onMovieSelected(item.movie!!.id)
MediaKind.EPISODE -> {
val episode = item.episode!!
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
}
// TODO fix this shit as well
when (item) {
is MovieUiModel -> onMovieSelected(item.id)
is EpisodeUiModel -> onEpisodeSelected(item.seriesId, item.seasonId, item.id)
else -> Unit
}
}
@@ -90,14 +62,12 @@ internal fun ContinueWatchingCard(
.aspectRatio(16f / 9f)
.background(scheme.surfaceContainer)
) {
if (imageUrl != null) {
PurefinAsyncImage(
model = imageUrl,
contentDescription = item.primaryText,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
PurefinAsyncImage(
model = item.imageUrl,
contentDescription = item.primaryText,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
Box(
modifier = Modifier
.matchParentSize()
@@ -111,12 +81,14 @@ internal fun ContinueWatchingCard(
)
)
)
MediaProgressBar(
progress = (item.progress.toFloat() / 100f).coerceIn(0f, 1f),
foregroundColor = scheme.primary,
backgroundColor = Color.White.copy(alpha = 0.24f),
modifier = Modifier.align(Alignment.BottomStart)
)
item.progress?.let { progress ->
MediaProgressBar(
progress = progress,
foregroundColor = scheme.primary,
backgroundColor = Color.White.copy(alpha = 0.24f),
modifier = Modifier.align(Alignment.BottomStart)
)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
@@ -129,15 +101,13 @@ internal fun ContinueWatchingCard(
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (supportingText.isNotBlank()) {
Text(
text = supportingText,
style = MaterialTheme.typography.bodySmall,
color = scheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text(
text = item.secondaryText,
style = MaterialTheme.typography.bodySmall,
color = scheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}

View File

@@ -9,13 +9,13 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.ui.common.header.SectionHeader
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import java.util.UUID
@Composable
fun ContinueWatchingSection(
items: List<ContinueWatchingItem>,
items: List<MediaUiModel>,
onMovieSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier

View File

@@ -23,39 +23,23 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.model.MediaKind
import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeBadge
import hu.bbara.purefin.core.ui.model.EpisodeUiModel
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.core.ui.model.MovieUiModel
import hu.bbara.purefin.core.ui.model.SeriesUiModel
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import java.util.UUID
@Composable
internal fun HomeBrowseCard(
item: PosterItem,
uiModel: MediaUiModel,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier
) {
val scheme = MaterialTheme.colorScheme
val supportingText = when (item.type) {
MediaKind.MOVIE -> listOf(
item.movie?.year,
item.movie?.runtime
).filterNotNull().filter { it.isNotBlank() }.joinToString("")
MediaKind.SERIES -> item.series!!.let { series ->
if (series.seasonCount == 1) "1 season" else "${series.seasonCount} seasons"
}
MediaKind.EPISODE -> listOf(
"Episode ${item.episode?.index}",
item.episode?.runtime
).filterNotNull().filter { it.isNotBlank() }.joinToString("")
else -> ""
}
Surface(
shape = RoundedCornerShape(12.dp),
@@ -66,15 +50,11 @@ internal fun HomeBrowseCard(
modifier = Modifier
.fillMaxWidth()
.clickable {
when (item.type) {
MediaKind.MOVIE -> onMovieSelected(item.id)
MediaKind.SERIES -> onSeriesSelected(item.id)
MediaKind.EPISODE -> {
val episode = item.episode!!
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
}
else -> Unit
//TODO fix this shit
when (uiModel) {
is MovieUiModel -> onMovieSelected(uiModel.id)
is SeriesUiModel -> onSeriesSelected(uiModel.id)
is EpisodeUiModel -> onEpisodeSelected(uiModel.seriesId, uiModel.seasonId, uiModel.id)
}
}
) {
@@ -86,68 +66,42 @@ internal fun HomeBrowseCard(
.background(scheme.surface)
) {
PurefinAsyncImage(
model = item.imageUrl,
contentDescription = item.title,
model = uiModel.imageUrl,
contentDescription = uiModel.primaryText,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
when (item.type) {
MediaKind.MOVIE -> {
val movie = item.movie!!
when (uiModel) {
is MovieUiModel, is EpisodeUiModel -> {
WatchStateBadge(
size = 28,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp),
watched = movie.watched,
started = (movie.progress ?: 0.0) > 0
watched = uiModel.watched,
started = (uiModel.progress ?: 0f) > 0f
)
}
MediaKind.EPISODE -> {
val episode = item.episode!!
WatchStateBadge(
size = 28,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp),
watched = episode.watched,
started = (episode.progress ?: 0.0) > 0
)
}
MediaKind.SERIES -> {
UnwatchedEpisodeBadge(
size = 28,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp),
unwatchedCount = item.series!!.unwatchedEpisodeCount
)
}
else -> Unit
}
}
Spacer(modifier = Modifier.height(10.dp))
Column(modifier = modifier.padding(12.dp)) {
Text(
text = item.title,
text = uiModel.primaryText,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
if (supportingText.isNotBlank()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = supportingText,
style = MaterialTheme.typography.bodySmall,
color = scheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = uiModel.secondaryText,
style = MaterialTheme.typography.bodySmall,
color = scheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}

View File

@@ -9,15 +9,15 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.ui.common.header.SectionHeader
import java.util.UUID
@Composable
fun LibraryPosterSection(
library: LibraryItem,
items: List<PosterItem>,
items: List<MediaUiModel>,
onLibrarySelected: (LibraryItem) -> Unit,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
@@ -42,7 +42,7 @@ fun LibraryPosterSection(
) {
items(items = items, key = { item -> item.id }) { item ->
HomeBrowseCard(
item = item,
uiModel = item,
onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected

View File

@@ -22,15 +22,14 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.ui.model.EpisodeUiModel
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.feature.browse.home.NextUpItem
import java.util.UUID
import hu.bbara.purefin.core.image.ArtworkKind
@Composable
internal fun NextUpCard(
item: NextUpItem,
uiModel: MediaUiModel,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier
) {
@@ -45,9 +44,11 @@ internal fun NextUpCard(
modifier = Modifier
.fillMaxWidth()
.clickable {
onEpisodeSelected(
item.episode.seriesId, item.episode.seasonId, item.episode.id
)
// TODO fix this shit
when (uiModel) {
is EpisodeUiModel -> onEpisodeSelected(uiModel.seriesId, uiModel.seasonId, uiModel.id)
else -> Unit
}
}
) {
Box(
@@ -57,8 +58,8 @@ internal fun NextUpCard(
.background(scheme.surface)
) {
PurefinAsyncImage(
model = ImageUrlBuilder.finishImageUrl(item.episode.imageUrlPrefix, ArtworkKind.PRIMARY),
contentDescription = item.primaryText,
model = uiModel.imageUrl,
contentDescription = uiModel.primaryText,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
@@ -81,16 +82,14 @@ internal fun NextUpCard(
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
) {
Text(
text = item.primaryText,
text = uiModel.primaryText,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = listOf("Episode ${item.episode.index}", item.episode.runtime, item.secondaryText)
.filter { it.isNotBlank() }
.joinToString(""),
text = uiModel.secondaryText,
style = MaterialTheme.typography.bodySmall,
color = scheme.onSurface,
maxLines = 1,

View File

@@ -9,13 +9,13 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.ui.common.header.SectionHeader
import hu.bbara.purefin.feature.browse.home.NextUpItem
import java.util.UUID
@Composable
fun NextUpSection(
items: List<NextUpItem>,
items: List<MediaUiModel>,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier
) {
@@ -33,7 +33,7 @@ fun NextUpSection(
) {
items(items = items, key = { item -> item.id }) { item ->
NextUpCard(
item = item,
uiModel = item,
onEpisodeSelected = onEpisodeSelected
)
}

View File

@@ -21,11 +21,11 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.ui.common.card.PosterCard
import hu.bbara.purefin.ui.common.button.PurefinIconButton
import hu.bbara.purefin.core.navigation.LibraryDto
import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.feature.browse.library.LibraryViewModel
import hu.bbara.purefin.ui.common.button.PurefinIconButton
import hu.bbara.purefin.ui.common.card.PosterCard
@Composable
fun LibraryScreen(
@@ -72,7 +72,7 @@ internal fun LibraryTopBar(
@Composable
internal fun LibraryPosterGrid(
libraryItems: List<PosterItem>,
libraryItems: List<MediaUiModel>,
modifier: Modifier = Modifier,
viewModel: LibraryViewModel = hiltViewModel()
) {