From 632dda52d65e4f5643343c9a430082f65bbde264 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Fri, 17 Apr 2026 21:13:45 +0200 Subject: [PATCH] Refactor media models to use MediaUiModel for improved consistency and type safety --- .../purefin/ui/common/card/PosterCard.kt | 20 ++- .../hu/bbara/purefin/ui/screen/TvAppScreen.kt | 4 +- .../purefin/ui/screen/home/TvHomeScreen.kt | 14 +- .../home/components/TvFocusedHeroModel.kt | 122 ------------------ .../home/components/TvFocusedItemHero.kt | 25 ++-- .../screen/home/components/TvHomeContent.kt | 13 +- .../screen/home/components/TvHomeHeroState.kt | 38 +++--- .../screen/home/components/TvHomeSections.kt | 56 +++----- .../ui/screen/library/TvLibraryScreen.kt | 8 +- .../purefin/ui/common/card/PosterCard.kt | 16 ++- .../purefin/ui/screen/home/HomeScreen.kt | 25 +--- .../ui/screen/home/components/HomeContent.kt | 12 +- .../continuewatching/ContinueWatchingCard.kt | 88 +++++-------- .../ContinueWatchingSection.kt | 4 +- .../home/components/library/HomeBrowseCard.kt | 96 ++++---------- .../library/LibraryPosterSection.kt | 6 +- .../home/components/nextup/NextUpCard.kt | 25 ++-- .../home/components/nextup/NextUpSection.kt | 6 +- .../ui/screen/library/LibraryScreen.kt | 8 +- core/image/build.gradle.kts | 4 - core/model/build.gradle.kts | 4 + .../purefin/core/ui/model/MediaUiModel.kt | 85 ++++++++++++ .../ui/common/card/PosterCardContent.kt | 29 ++--- .../feature/browse/home/AppViewModel.kt | 22 ++-- .../purefin/feature/browse/home/HomeModels.kt | 49 +------ .../browse/library/LibraryViewModel.kt | 13 +- .../feature/downloads/DownloadsViewModel.kt | 12 +- 27 files changed, 310 insertions(+), 494 deletions(-) delete mode 100644 app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedHeroModel.kt create mode 100644 core/model/src/main/java/hu/bbara/purefin/core/ui/model/MediaUiModel.kt diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt index 8a08bd99..d73f70a7 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt @@ -5,27 +5,37 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import hu.bbara.purefin.core.model.MediaKind -import hu.bbara.purefin.feature.browse.home.FocusableItem +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.feature.browse.home.PosterItem import java.util.UUID @Composable fun PosterCard( - item: PosterItem, + item: MediaUiModel, modifier: Modifier = Modifier, imageModifier: Modifier = Modifier, posterWidth: Dp = 144.dp, showSecondaryText: Boolean = false, indicatorSize: Int = 28, indicatorPadding: Dp = 8.dp, - onFocusedItem: (FocusableItem) -> Unit = {}, + onFocusedItem: (MediaUiModel) -> Unit = {}, 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 Fix it by addig a handler into the viewModel like onMediaUiModelSelected + when (item) { + is SeriesUiModel -> onSeriesSelected(item.id) + is MovieUiModel -> onMovieSelected(item.id) + is EpisodeUiModel -> onEpisodeSelected(item.seriesId, item.seasonId, item.id) + } + }, modifier = modifier, imageModifier = imageModifier, posterWidth = posterWidth, diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt index 92a57f76..ecff928d 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt @@ -15,20 +15,19 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LifecycleResumeEffect +import hu.bbara.purefin.core.model.LibraryKind import hu.bbara.purefin.feature.browse.home.AppViewModel import hu.bbara.purefin.ui.screen.home.TvHomeScreen import hu.bbara.purefin.ui.screen.home.components.TvDrawerDestination import hu.bbara.purefin.ui.screen.home.components.TvDrawerDestinationItem import hu.bbara.purefin.ui.screen.home.components.TvNavigationDrawer import hu.bbara.purefin.ui.screen.libraries.TvLibrariesOverviewScreen -import hu.bbara.purefin.core.model.LibraryKind @Composable fun TvAppScreen( viewModel: AppViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { - val serverUrl by viewModel.serverUrl.collectAsState() val libraries by viewModel.libraries.collectAsState() val continueWatching by viewModel.continueWatching.collectAsState() val nextUp by viewModel.nextUp.collectAsState() @@ -77,7 +76,6 @@ fun TvAppScreen( libraryContent = latestLibraryContent, continueWatching = continueWatching, nextUp = nextUp, - serverUrl = serverUrl, onMovieSelected = viewModel::onMovieSelected, onSeriesSelected = viewModel::onSeriesSelected, onEpisodeSelected = viewModel::onEpisodeSelected, diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt index 4cc1c51c..2c99f84f 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt @@ -9,10 +9,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem +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.ui.screen.home.components.TvFocusedItemHero import hu.bbara.purefin.ui.screen.home.components.TvHomeContent import hu.bbara.purefin.ui.screen.home.components.rememberTvHomeHeroState @@ -25,10 +23,9 @@ private val TvHomeMaxHeroHeight = 200.dp @Composable fun TvHomeScreen( libraries: List, - libraryContent: Map>, - continueWatching: List, - nextUp: List, - serverUrl: String, + libraryContent: Map>, + continueWatching: List, + nextUp: List, onMovieSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, @@ -66,7 +63,8 @@ fun TvHomeScreen( libraryContent = libraryContent, continueWatching = continueWatching, nextUp = nextUp, - onMediaFocused = heroState.onMediaFocused, + //TODO implement it by throwing out complex TvHomeHeroState shit ass code. Fuck ai btw + onMediaFocused = {}, onMovieSelected = onMovieSelected, onSeriesSelected = onSeriesSelected, onEpisodeSelected = onEpisodeSelected, diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedHeroModel.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedHeroModel.kt deleted file mode 100644 index edae8fcb..00000000 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedHeroModel.kt +++ /dev/null @@ -1,122 +0,0 @@ -package hu.bbara.purefin.ui.screen.home.components - -import hu.bbara.purefin.core.image.ImageUrlBuilder -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.browse.home.ContinueWatchingItem -import hu.bbara.purefin.feature.browse.home.FocusableItem -import hu.bbara.purefin.feature.browse.home.NextUpItem -import hu.bbara.purefin.feature.browse.home.PosterItem -import java.util.UUID -import hu.bbara.purefin.core.model.MediaKind -import hu.bbara.purefin.core.image.ArtworkKind -import kotlin.math.roundToInt - -internal data class TvFocusedHeroModel( - val id: UUID, - val backdropImageUrl: String, - val title: String, - val metadata: List, -) { - val metadataText: String? - get() = metadata.takeIf { it.isNotEmpty() }?.joinToString(" • ") -} - -internal fun FocusableItem.toTvFocusedHeroModel(): TvFocusedHeroModel { - return when (this) { - is ContinueWatchingItem -> when (type) { - MediaKind.MOVIE -> movie!!.toTvFocusedHeroModel() - MediaKind.EPISODE -> episode!!.toTvFocusedHeroModel() - else -> unsupportedType(type) - } - - is NextUpItem -> episode.toTvFocusedHeroModel() - is PosterItem -> when (type) { - MediaKind.MOVIE -> movie!!.toTvFocusedHeroModel() - MediaKind.EPISODE -> episode!!.toTvFocusedHeroModel() - MediaKind.SERIES -> series!!.toTvFocusedHeroModel() - else -> unsupportedType(type) - } - } -} - -internal fun backdropImageUrl(imageUrlPrefix: String?, fallbackImageUrl: String): String { - val backdropImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.BACKDROP) - return backdropImageUrl.ifBlank { fallbackImageUrl } -} - -private fun Movie.toTvFocusedHeroModel(): TvFocusedHeroModel { - return TvFocusedHeroModel( - id = id, - backdropImageUrl = backdropImageUrl( - imageUrlPrefix = imageUrlPrefix, - fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY) - ), - title = title, - metadata = listOf(year, rating, runtime, format).compactMetadata(), - ) -} - -private fun Episode.toTvFocusedHeroModel(): TvFocusedHeroModel { - return TvFocusedHeroModel( - id = id, - backdropImageUrl = backdropImageUrl( - imageUrlPrefix = imageUrlPrefix, - fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY) - ), - title = title, - metadata = listOf( - "Episode $index", - releaseDate, - runtime, - rating, - format - ).compactMetadata(), - ) -} - -private fun Series.toTvFocusedHeroModel(): TvFocusedHeroModel { - val unwatchedText = if (unwatchedEpisodeCount > 0) { - "$unwatchedEpisodeCount unwatched" - } else { - null - } - - return TvFocusedHeroModel( - id = id, - backdropImageUrl = backdropImageUrl( - imageUrlPrefix = imageUrlPrefix, - fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY) - ), - title = name, - metadata = listOf(year, seasonLabel(seasonCount), unwatchedText).compactMetadata(), - ) -} - -private fun progressFraction(progress: Double?): Float? { - val fraction = progress - ?.div(100.0) - ?.toFloat() - ?.coerceIn(0f, 1f) - - return fraction?.takeIf { it > 0f } -} - -private fun progressLabel(progressFraction: Float?): String? { - return progressFraction?.let { "${(it * 100).roundToInt()}%" } -} - -private fun List.compactMetadata(): List { - return map(String?::orEmpty) - .map(String::trim) - .filter(String::isNotBlank) -} - -private fun seasonLabel(seasonCount: Int): String { - return if (seasonCount == 1) "1 season" else "$seasonCount seasons" -} - -private fun unsupportedType(type: MediaKind): Nothing { - throw UnsupportedOperationException("Unsupported item type: $type") -} diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedItemHero.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedItemHero.kt index ed0f3ea0..0737b95d 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedItemHero.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedItemHero.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import hu.bbara.purefin.core.ui.model.MediaUiModel import hu.bbara.purefin.ui.common.image.PurefinAsyncImage internal const val TvHomeHeroTitleTag = "tv-home-hero-title" @@ -32,7 +33,7 @@ private const val TvHomeHeroAnimationMillis = 180 @Composable internal fun TvFocusedItemHero( - item: TvFocusedHeroModel, + item: MediaUiModel, height: Dp, modifier: Modifier = Modifier, ) { @@ -44,7 +45,7 @@ internal fun TvFocusedItemHero( .background(scheme.background) ) { Crossfade( - targetState = item.backdropImageUrl, + targetState = item.imageUrl, animationSpec = tween(durationMillis = TvHomeHeroAnimationMillis), label = "tv-home-hero-background" ) { imageUrl -> @@ -98,7 +99,7 @@ internal fun TvFocusedItemHero( modifier = Modifier.widthIn(max = 720.dp) ) { Text( - text = hero.title, + text = hero.primaryText, color = scheme.onBackground, fontSize = 34.sp, fontWeight = FontWeight.Bold, @@ -107,16 +108,14 @@ internal fun TvFocusedItemHero( overflow = TextOverflow.Ellipsis, modifier = Modifier.testTag(TvHomeHeroTitleTag) ) - hero.metadataText?.let { metadataText -> - Text( - text = metadataText, - color = scheme.onSurfaceVariant, - fontSize = 14.sp, - lineHeight = 18.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } + Text( + text = hero.description, + color = scheme.onSurfaceVariant, + fontSize = 14.sp, + lineHeight = 18.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) } } } diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt index b1a57467..8df0134b 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt @@ -24,11 +24,8 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp -import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem -import hu.bbara.purefin.feature.browse.home.FocusableItem +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 java.util.UUID internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item" @@ -37,10 +34,10 @@ internal const val TvHomeContentViewportTag = "tv-home-content-viewport" @Composable fun TvHomeContent( libraries: List, - libraryContent: Map>, - continueWatching: List, - nextUp: List, - onMediaFocused: (FocusableItem) -> Unit, + libraryContent: Map>, + continueWatching: List, + nextUp: List, + onMediaFocused: (MediaUiModel) -> Unit, onMovieSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeHeroState.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeHeroState.kt index 51b0cff5..1c70d8f5 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeHeroState.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeHeroState.kt @@ -5,24 +5,24 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem +import hu.bbara.purefin.core.ui.model.MediaUiModel import hu.bbara.purefin.feature.browse.home.FocusableItem 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 java.util.UUID +//TODO throw this out and simplify it. + internal data class TvHomeItemRegistry( val visibleLibraries: List, - private val libraryContent: Map>, - private val continueWatching: List, - private val nextUp: List, + private val libraryContent: Map>, + private val continueWatching: List, + private val nextUp: List, val firstAvailableItemId: UUID?, ) { - val firstAvailableItem: FocusableItem? + val firstAvailableItem: MediaUiModel? get() = firstAvailableItemId?.let(::itemById) - fun itemById(id: UUID): FocusableItem? { + fun itemById(id: UUID): MediaUiModel? { return continueWatching.firstOrNull { it.id == id } ?: nextUp.firstOrNull { it.id == id } ?: visibleLibraries.asSequence() @@ -33,16 +33,16 @@ internal data class TvHomeItemRegistry( } internal class TvHomeHeroState( - val focusedHero: TvFocusedHeroModel?, + val focusedHero: MediaUiModel?, val onMediaFocused: (FocusableItem) -> Unit, ) @Composable internal fun rememberTvHomeItemRegistry( libraries: List, - libraryContent: Map>, - continueWatching: List, - nextUp: List, + libraryContent: Map>, + continueWatching: List, + nextUp: List, ): TvHomeItemRegistry { return remember(libraries, libraryContent, continueWatching, nextUp) { createTvHomeItemRegistry( @@ -56,9 +56,9 @@ internal fun rememberTvHomeItemRegistry( internal fun createTvHomeItemRegistry( libraries: List, - libraryContent: Map>, - continueWatching: List, - nextUp: List, + libraryContent: Map>, + continueWatching: List, + nextUp: List, ): TvHomeItemRegistry { val visibleLibraries = libraries.filter { libraryContent[it.id]?.isEmpty() != true } val firstAvailableItemId = continueWatching.firstOrNull()?.id @@ -79,9 +79,9 @@ internal fun createTvHomeItemRegistry( @Composable internal fun rememberTvHomeHeroState( libraries: List, - libraryContent: Map>, - continueWatching: List, - nextUp: List, + libraryContent: Map>, + continueWatching: List, + nextUp: List, ): TvHomeHeroState { val itemRegistry = rememberTvHomeItemRegistry( libraries = libraries, @@ -94,7 +94,7 @@ internal fun rememberTvHomeHeroState( focusedItemId?.let(itemRegistry::itemById) } val focusedHero = remember(focusedItem, itemRegistry) { - (focusedItem ?: itemRegistry.firstAvailableItem)?.toTvFocusedHeroModel() + (focusedItem ?: itemRegistry.firstAvailableItem) } val onMediaFocused: (FocusableItem) -> Unit = remember { { item -> diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeSections.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeSections.kt index 25677d7e..1971e85e 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeSections.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeSections.kt @@ -44,17 +44,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import hu.bbara.purefin.ui.common.card.PosterCard +import hu.bbara.purefin.core.ui.model.EpisodeUiModel +import hu.bbara.purefin.core.ui.model.MediaUiModel import hu.bbara.purefin.ui.common.bar.MediaProgressBar +import hu.bbara.purefin.ui.common.card.PosterCard import hu.bbara.purefin.ui.common.image.PurefinAsyncImage -import hu.bbara.purefin.core.image.ImageUrlBuilder -import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem -import hu.bbara.purefin.feature.browse.home.FocusableItem -import hu.bbara.purefin.feature.browse.home.NextUpItem -import hu.bbara.purefin.feature.browse.home.PosterItem import java.util.UUID -import hu.bbara.purefin.core.model.MediaKind -import hu.bbara.purefin.core.image.ArtworkKind private val TvHomeSectionsThumbShape = RoundedCornerShape(20.dp) private val TvHomeSectionsHorizontalPadding = 32.dp @@ -70,8 +65,8 @@ internal fun tvHomeLibraryRowTag(libraryId: UUID): String = @Composable fun TvContinueWatchingSection( - items: List, - onFocusedItem: (FocusableItem) -> Unit = {}, + items: List, + onFocusedItem: (MediaUiModel) -> Unit = {}, onMovieSelected: (UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, firstItemFocusRequester: FocusRequester? = null, @@ -88,24 +83,11 @@ fun TvContinueWatchingSection( rowTestTag = rowTestTag ) { itemsIndexed(items = items, key = { _, item -> item.id }) { index, item -> - val progressFraction = (item.progress / 100.0).toFloat().coerceIn(0f, 1f) TvHomeLandscapeCard( title = item.primaryText, supporting = item.secondaryText, - 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 - }, - progress = progressFraction, + imageUrl = item.imageUrl, + progress = item.progress ?: 0f, imageModifier = Modifier .then( if (index == 0 && firstItemFocusRequester != null) { @@ -123,13 +105,9 @@ fun TvContinueWatchingSection( ), onFocusedItem = { onFocusedItem(item) }, onClick = { - when (item.type) { - MediaKind.MOVIE -> onMovieSelected(item.movie!!.id) - MediaKind.EPISODE -> { - val episode = item.episode!! - onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id) - } - + when (item) { + //TODO fix this shit + is EpisodeUiModel -> onEpisodeSelected(item.seriesId, item.seasonId, item.id) else -> Unit } } @@ -140,8 +118,8 @@ fun TvContinueWatchingSection( @Composable fun TvNextUpSection( - items: List, - onFocusedItem: (FocusableItem) -> Unit = {}, + items: List, + onFocusedItem: (MediaUiModel) -> Unit = {}, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, firstItemFocusRequester: FocusRequester? = null, firstItemTestTag: String? = null, @@ -178,8 +156,10 @@ fun TvNextUpSection( ), onFocusedItem = { onFocusedItem(item) }, onClick = { - val episode = item.episode - onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id) + //TODO FIX + (item as? EpisodeUiModel)?.let { episode -> + onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id) + } } ) } @@ -189,8 +169,8 @@ fun TvNextUpSection( @Composable fun TvLibraryPosterSection( title: String, - items: List, - onFocusedItem: (FocusableItem) -> Unit = {}, + items: List, + onFocusedItem: (MediaUiModel) -> Unit = {}, firstItemFocusRequester: FocusRequester? = null, firstItemTestTag: String? = null, rowTestTag: String? = null, diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/library/TvLibraryScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/library/TvLibraryScreen.kt index 73bb12ac..e45fbfa0 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/library/TvLibraryScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/library/TvLibraryScreen.kt @@ -22,11 +22,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 import java.util.UUID @Composable @@ -92,7 +92,7 @@ private fun TvLibraryTopBar( @Composable fun TvLibraryContent( - libraryItems: List, + libraryItems: List, onMovieSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit, modifier: Modifier = Modifier diff --git a/app/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt b/app/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt index 6d9dc303..cba46fe6 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt @@ -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 ) } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/HomeScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/HomeScreen.kt index 75fa4733..56b14bfe 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/HomeScreen.kt @@ -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, - libraryContent: Map>, + libraryContent: Map>, suggestions: List, - continueWatching: List, - nextUp: List, + continueWatching: List, + nextUp: List, 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 diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt index 844a27af..1ab920aa 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt @@ -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, - libraryContent: Map>, + libraryContent: Map>, suggestions: List, - continueWatching: List, - nextUp: List, + continueWatching: List, + nextUp: List, isRefreshing: Boolean, onRefresh: () -> Unit, onMovieSelected: (UUID) -> Unit, diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingCard.kt index 1a869dcc..399966a3 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingCard.kt @@ -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 + ) } } } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingSection.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingSection.kt index ca8c8f93..2f898778 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingSection.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingSection.kt @@ -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, + items: List, onMovieSelected: (UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, modifier: Modifier = Modifier diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/HomeBrowseCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/HomeBrowseCard.kt index 5ca77e8b..b0bd3fc2 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/HomeBrowseCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/HomeBrowseCard.kt @@ -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 + ) } } } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/LibraryPosterSection.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/LibraryPosterSection.kt index 6c28fa03..3bdd56ac 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/LibraryPosterSection.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/LibraryPosterSection.kt @@ -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, + items: List, 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 diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpCard.kt index 589637b0..fbac8798 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpCard.kt @@ -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, diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpSection.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpSection.kt index 42a3665f..fa2aaef5 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpSection.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpSection.kt @@ -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, + items: List, 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 ) } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt index 0b2d9bad..5968bc97 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt @@ -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, + libraryItems: List, modifier: Modifier = Modifier, viewModel: LibraryViewModel = hiltViewModel() ) { diff --git a/core/image/build.gradle.kts b/core/image/build.gradle.kts index ee574787..3c4e61fa 100644 --- a/core/image/build.gradle.kts +++ b/core/image/build.gradle.kts @@ -10,7 +10,3 @@ kotlin { } jvmToolchain(11) } - -dependencies { - implementation(project(":core:model")) -} diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index 3c4e61fa..ffa8bee8 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -10,3 +10,7 @@ kotlin { } jvmToolchain(11) } + +dependencies { + implementation(project(":core:image")) +} \ No newline at end of file diff --git a/core/model/src/main/java/hu/bbara/purefin/core/ui/model/MediaUiModel.kt b/core/model/src/main/java/hu/bbara/purefin/core/ui/model/MediaUiModel.kt new file mode 100644 index 00000000..8e38023c --- /dev/null +++ b/core/model/src/main/java/hu/bbara/purefin/core/ui/model/MediaUiModel.kt @@ -0,0 +1,85 @@ +package hu.bbara.purefin.core.ui.model + +import hu.bbara.purefin.core.image.ArtworkKind +import hu.bbara.purefin.core.image.ImageUrlBuilder +import hu.bbara.purefin.core.model.Episode +import hu.bbara.purefin.core.model.Movie +import hu.bbara.purefin.core.model.Series +import java.util.UUID + +sealed interface MediaUiModel { + val id: UUID + val primaryText: String + val secondaryText: String + val description: String + val imageUrl: String + val progress: Float? + get() = null + val watched: Boolean + get() = false +} + +class MovieUiModel: MediaUiModel { + override val id: UUID + override val primaryText: String + override val secondaryText: String + override val description: String + override val imageUrl: String + override val progress: Float? + + constructor(movie: Movie) { + id = movie.id + primaryText = movie.title + secondaryText = movie.year + description = movie.synopsis + imageUrl = ImageUrlBuilder.finishImageUrl( + prefixImageUrl = movie.imageUrlPrefix, + artworkKind = ArtworkKind.PRIMARY + ) + progress = (movie.progress?.toFloat() ?: 0f) / 100f + } +} + +class SeriesUiModel : MediaUiModel { + override val id: UUID + override val primaryText: String + override val secondaryText: String + override val description: String + override val imageUrl: String + + constructor(series: Series) { + id = series.id + primaryText = series.name + secondaryText = "${series.seasonCount} seasons" + description = series.synopsis + imageUrl = ImageUrlBuilder.finishImageUrl( + prefixImageUrl = series.imageUrlPrefix, + artworkKind = ArtworkKind.PRIMARY + ) + } +} + +class EpisodeUiModel : MediaUiModel { + override val id: UUID + override val primaryText: String + override val secondaryText: String + override val description: String + override val imageUrl: String + override val progress: Float? + val seriesId: UUID + val seasonId: UUID + + constructor(episode: Episode) { + id = episode.id + primaryText = episode.title + secondaryText = episode.releaseDate + description = episode.synopsis + imageUrl = ImageUrlBuilder.finishImageUrl( + prefixImageUrl = episode.imageUrlPrefix, + artworkKind = ArtworkKind.PRIMARY + ) + progress = (episode.progress?.toFloat() ?: 0f) / 100f + seriesId = episode.seriesId + seasonId = episode.seasonId + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/hu/bbara/purefin/ui/common/card/PosterCardContent.kt b/core/ui/src/main/java/hu/bbara/purefin/ui/common/card/PosterCardContent.kt index 3baad735..e0f96e2a 100644 --- a/core/ui/src/main/java/hu/bbara/purefin/ui/common/card/PosterCardContent.kt +++ b/core/ui/src/main/java/hu/bbara/purefin/ui/common/card/PosterCardContent.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import hu.bbara.purefin.core.model.MediaKind -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.ui.common.badge.WatchStateBadge import hu.bbara.purefin.ui.common.image.PurefinAsyncImage @@ -57,7 +59,7 @@ sealed interface PosterCardBadge { @Composable fun PosterCardContent( - model: PosterCardModel, + model: MediaUiModel, onClick: () -> Unit, modifier: Modifier = Modifier, imageModifier: Modifier = Modifier, @@ -108,36 +110,25 @@ fun PosterCardContent( .clickable(onClick = onClick), contentScale = ContentScale.Crop ) - when (val badge = model.badge) { - is PosterCardBadge.WatchState -> { + when (model) { + is MovieUiModel, is EpisodeUiModel -> { WatchStateBadge( size = indicatorSize, modifier = Modifier .align(Alignment.TopEnd) .padding(indicatorPadding), - watched = badge.watched, - started = badge.started + watched = model.watched, + started = (model.progress ?: 0f) > 0f ) } - - is PosterCardBadge.UnwatchedEpisodes -> { - UnwatchedEpisodeBadge( - size = indicatorSize, - modifier = Modifier - .align(Alignment.TopEnd) - .padding(indicatorPadding), - unwatchedCount = badge.count - ) - } - - PosterCardBadge.None -> Unit + else -> Unit } } Column( modifier = Modifier.padding(top = 8.dp, start = 4.dp, end = 4.dp, bottom = 8.dp) ) { Text( - text = model.title, + text = model.primaryText, color = scheme.onBackground, fontSize = 13.sp, fontWeight = FontWeight.Medium, diff --git a/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt b/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt index 43e53834..d5e52535 100644 --- a/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt +++ b/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt @@ -5,18 +5,19 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import hu.bbara.purefin.core.data.HomeRepository import hu.bbara.purefin.core.data.MediaCatalogReader -import hu.bbara.purefin.core.download.MediaDownloadController import hu.bbara.purefin.core.data.session.UserSessionRepository +import hu.bbara.purefin.core.download.MediaDownloadController +import hu.bbara.purefin.core.ui.model.EpisodeUiModel import hu.bbara.purefin.core.model.LibraryKind import hu.bbara.purefin.core.model.Media -import hu.bbara.purefin.core.model.MediaKind +import hu.bbara.purefin.core.ui.model.MovieUiModel +import hu.bbara.purefin.core.ui.model.SeriesUiModel import hu.bbara.purefin.core.navigation.EpisodeDto import hu.bbara.purefin.core.navigation.LibraryDto import hu.bbara.purefin.core.navigation.MovieDto import hu.bbara.purefin.core.navigation.NavigationManager import hu.bbara.purefin.core.navigation.Route import hu.bbara.purefin.core.navigation.SeriesDto -import java.util.UUID import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -25,6 +26,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -95,10 +97,10 @@ class AppViewModel @Inject constructor( list.mapNotNull { media -> when (media) { is Media.MovieMedia -> moviesMap[media.movieId]?.let { - ContinueWatchingItem(type = MediaKind.MOVIE, movie = it) + MovieUiModel(movie = it) } is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let { - ContinueWatchingItem(type = MediaKind.EPISODE, episode = it) + EpisodeUiModel(episode = it) } else -> null } @@ -116,7 +118,7 @@ class AppViewModel @Inject constructor( list.mapNotNull { media -> when (media) { is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let { - NextUpItem(episode = it) + EpisodeUiModel(episode = it) } else -> null } @@ -137,16 +139,16 @@ class AppViewModel @Inject constructor( items.mapNotNull { media -> when (media) { is Media.MovieMedia -> moviesMap[media.movieId]?.let { - PosterItem(type = MediaKind.MOVIE, movie = it) + MovieUiModel(movie = it) } is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let { - PosterItem(type = MediaKind.EPISODE, episode = it) + EpisodeUiModel(episode = it) } is Media.SeriesMedia -> seriesMap[media.seriesId]?.let { - PosterItem(type = MediaKind.SERIES, series = it) + SeriesUiModel(series = it) } is Media.SeasonMedia -> seriesMap[media.seriesId]?.let { - PosterItem(type = MediaKind.SERIES, series = it) + SeriesUiModel(series = it) } } }.distinctBy { it.id } diff --git a/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/home/HomeModels.kt b/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/home/HomeModels.kt index c861aad6..452ef499 100644 --- a/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/home/HomeModels.kt +++ b/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/home/HomeModels.kt @@ -1,13 +1,13 @@ package hu.bbara.purefin.feature.browse.home +import hu.bbara.purefin.core.image.ArtworkKind import hu.bbara.purefin.core.image.ImageUrlBuilder import hu.bbara.purefin.core.model.Episode +import hu.bbara.purefin.core.model.LibraryKind +import hu.bbara.purefin.core.model.MediaKind import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Series import java.util.UUID -import hu.bbara.purefin.core.model.MediaKind -import hu.bbara.purefin.core.model.LibraryKind -import hu.bbara.purefin.core.image.ArtworkKind sealed interface SuggestedItem { @@ -97,49 +97,6 @@ sealed interface FocusableItem { val type: MediaKind } -data class ContinueWatchingItem( - override val type: MediaKind, - val movie: Movie? = null, - val episode: Episode? = null, -) : FocusableItem { - override val id: UUID = when (type) { - MediaKind.MOVIE -> movie!!.id - MediaKind.EPISODE -> episode!!.id - else -> throw UnsupportedOperationException("Unsupported item type: $type") - } - override val primaryText: String = when (type) { - MediaKind.MOVIE -> movie!!.title - MediaKind.EPISODE -> episode!!.title - else -> throw UnsupportedOperationException("Unsupported item type: $type") - } - override val secondaryText: String = when (type) { - MediaKind.MOVIE -> movie!!.year - MediaKind.EPISODE -> episode!!.releaseDate - else -> throw UnsupportedOperationException("Unsupported item type: $type") - } - override val imageUrl: String = when (type) { - MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl( - prefixImageUrl = movie!!.imageUrlPrefix, - artworkKind = ArtworkKind.PRIMARY - ) - MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl( - prefixImageUrl = episode!!.imageUrlPrefix, - artworkKind = ArtworkKind.PRIMARY - ) - else -> throw IllegalArgumentException("Invalid type: $type") - } - override val description: String = when (type) { - MediaKind.MOVIE -> movie!!.synopsis - MediaKind.EPISODE -> episode!!.synopsis - else -> throw UnsupportedOperationException("Unsupported item type: $type") - } - val progress: Double = when (type) { - MediaKind.MOVIE -> movie!!.progress ?: 0.0 - MediaKind.EPISODE -> episode!!.progress ?: 0.0 - else -> throw UnsupportedOperationException("Unsupported item type: $type") - } -} - data class NextUpItem( val episode: Episode ) : FocusableItem { diff --git a/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/library/LibraryViewModel.kt b/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/library/LibraryViewModel.kt index 9d4e782a..db30f79f 100644 --- a/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/library/LibraryViewModel.kt +++ b/feature/browse/src/main/java/hu/bbara/purefin/feature/browse/library/LibraryViewModel.kt @@ -5,19 +5,20 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import hu.bbara.purefin.core.data.HomeRepository import hu.bbara.purefin.core.model.LibraryKind -import hu.bbara.purefin.core.model.MediaKind -import hu.bbara.purefin.feature.browse.home.PosterItem import hu.bbara.purefin.core.navigation.MovieDto import hu.bbara.purefin.core.navigation.NavigationManager import hu.bbara.purefin.core.navigation.Route import hu.bbara.purefin.core.navigation.SeriesDto -import java.util.UUID +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 kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -28,7 +29,7 @@ class LibraryViewModel @Inject constructor( private val selectedLibrary = MutableStateFlow(null) - val contents: StateFlow> = combine(selectedLibrary, homeRepository.libraries) { + val contents: StateFlow> = combine(selectedLibrary, homeRepository.libraries) { libraryId, libraries -> if (libraryId == null) { return@combine emptyList() @@ -36,10 +37,10 @@ class LibraryViewModel @Inject constructor( val library = libraries.find { it.id == libraryId } ?: return@combine emptyList() when (library.type) { LibraryKind.SERIES -> library.series!!.map { series -> - PosterItem(type = MediaKind.SERIES, series = series) + SeriesUiModel(series) } LibraryKind.MOVIES -> library.movies!!.map { movie -> - PosterItem(type = MediaKind.MOVIE, movie = movie) + MovieUiModel(movie) } } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) diff --git a/feature/downloads/src/main/java/hu/bbara/purefin/feature/downloads/DownloadsViewModel.kt b/feature/downloads/src/main/java/hu/bbara/purefin/feature/downloads/DownloadsViewModel.kt index 78aa6156..9c443a66 100644 --- a/feature/downloads/src/main/java/hu/bbara/purefin/feature/downloads/DownloadsViewModel.kt +++ b/feature/downloads/src/main/java/hu/bbara/purefin/feature/downloads/DownloadsViewModel.kt @@ -5,19 +5,19 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import hu.bbara.purefin.core.data.OfflineCatalogReader import hu.bbara.purefin.core.download.MediaDownloadController +import hu.bbara.purefin.core.image.ArtworkKind import hu.bbara.purefin.core.image.ImageUrlBuilder -import hu.bbara.purefin.core.model.MediaKind import hu.bbara.purefin.core.navigation.MovieDto import hu.bbara.purefin.core.navigation.NavigationManager import hu.bbara.purefin.core.navigation.Route import hu.bbara.purefin.core.navigation.SeriesDto -import hu.bbara.purefin.feature.browse.home.PosterItem -import java.util.UUID +import hu.bbara.purefin.core.ui.model.MovieUiModel +import hu.bbara.purefin.core.ui.model.SeriesUiModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import hu.bbara.purefin.core.image.ArtworkKind +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -53,8 +53,8 @@ class DownloadsViewModel @Inject constructor( ) { movies, series, inProgress -> movies.values .filter { it.id.toString() !in inProgress } - .map { PosterItem(type = MediaKind.MOVIE, movie = it) } + - series.values.map { PosterItem(type = MediaKind.SERIES, series = it) } + .map { MovieUiModel(it) } + + series.values.map { SeriesUiModel(it) } } /** Items currently being downloaded with their progress. */