Compare commits

..

3 Commits

36 changed files with 342 additions and 523 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -86,7 +86,7 @@ internal fun TvEpisodeScreenContent(
resetScrollKey = episode.id,
modifier = modifier
) {
item {
item(key = "episode-hero") {
TvMediaDetailBodyBox(
backgroundImageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
modifier = it
@@ -101,7 +101,7 @@ internal fun TvEpisodeScreenContent(
Spacer(modifier = Modifier.height(12.dp))
}
}
item {
item(key = "episode-overview") {
Column(modifier = it.fillMaxWidth()) {
Spacer(modifier = Modifier.height(16.dp))
MediaDetailOverviewSection(
@@ -111,7 +111,7 @@ internal fun TvEpisodeScreenContent(
Spacer(modifier = Modifier.height(20.dp))
}
}
item {
item(key = "episode-playback") {
Column(modifier = it.fillMaxWidth()) {
MediaDetailPlaybackSection(
audioTrack = "ENG",
@@ -122,7 +122,7 @@ internal fun TvEpisodeScreenContent(
}
}
if (episode.cast.isNotEmpty()) {
item {
item(key = "episode-cast") {
Column(modifier = it.fillMaxWidth()) {
MediaDetailSectionTitle(text = "Cast")
Spacer(modifier = Modifier.height(14.dp))

View File

@@ -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<LibraryItem>,
libraryContent: Map<UUID, List<PosterItem>>,
continueWatching: List<ContinueWatchingItem>,
nextUp: List<NextUpItem>,
serverUrl: String,
libraryContent: Map<UUID, List<MediaUiModel>>,
continueWatching: List<MediaUiModel>,
nextUp: List<MediaUiModel>,
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,

View File

@@ -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<String>,
) {
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<String?>.compactMetadata(): List<String> {
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")
}

View File

@@ -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
)
}
}
}

View File

@@ -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<LibraryItem>,
libraryContent: Map<UUID, List<PosterItem>>,
continueWatching: List<ContinueWatchingItem>,
nextUp: List<NextUpItem>,
onMediaFocused: (FocusableItem) -> Unit,
libraryContent: Map<UUID, List<MediaUiModel>>,
continueWatching: List<MediaUiModel>,
nextUp: List<MediaUiModel>,
onMediaFocused: (MediaUiModel) -> Unit,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
@@ -84,11 +81,11 @@ fun TvHomeContent(
.testTag(TvHomeContentViewportTag),
contentPadding = contentPadding
) {
item {
item(key = "tv-home-top-spacer") {
Spacer(modifier = Modifier.height(8.dp))
}
if (hasContinueWatching) {
item {
item(key = "tv-home-continue-watching") {
TvContinueWatchingSection(
items = continueWatching,
onFocusedItem = onMediaFocused,
@@ -102,13 +99,13 @@ fun TvHomeContent(
}
if (hasContinueWatching && (hasNextUp || hasLibraries)) {
item {
item(key = "tv-home-post-continue-gap") {
Spacer(modifier = Modifier.height(20.dp))
}
}
if (hasNextUp) {
item {
item(key = "tv-home-next-up") {
TvNextUpSection(
items = nextUp,
onFocusedItem = onMediaFocused,
@@ -121,7 +118,7 @@ fun TvHomeContent(
}
if (hasLibraries && (hasContinueWatching || hasNextUp)) {
item {
item(key = "tv-home-post-next-up-gap") {
Spacer(modifier = Modifier.height(20.dp))
}
}
@@ -156,7 +153,7 @@ fun TvHomeContent(
}
if (hasVisibleContent) {
item {
item(key = "tv-home-trailing-space") {
Spacer(modifier = Modifier.height(TvHomeBringIntoViewTrailingSpace))
}
}

View File

@@ -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<LibraryItem>,
private val libraryContent: Map<UUID, List<PosterItem>>,
private val continueWatching: List<ContinueWatchingItem>,
private val nextUp: List<NextUpItem>,
private val libraryContent: Map<UUID, List<MediaUiModel>>,
private val continueWatching: List<MediaUiModel>,
private val nextUp: List<MediaUiModel>,
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<LibraryItem>,
libraryContent: Map<UUID, List<PosterItem>>,
continueWatching: List<ContinueWatchingItem>,
nextUp: List<NextUpItem>,
libraryContent: Map<UUID, List<MediaUiModel>>,
continueWatching: List<MediaUiModel>,
nextUp: List<MediaUiModel>,
): TvHomeItemRegistry {
return remember(libraries, libraryContent, continueWatching, nextUp) {
createTvHomeItemRegistry(
@@ -56,9 +56,9 @@ internal fun rememberTvHomeItemRegistry(
internal fun createTvHomeItemRegistry(
libraries: List<LibraryItem>,
libraryContent: Map<UUID, List<PosterItem>>,
continueWatching: List<ContinueWatchingItem>,
nextUp: List<NextUpItem>,
libraryContent: Map<UUID, List<MediaUiModel>>,
continueWatching: List<MediaUiModel>,
nextUp: List<MediaUiModel>,
): 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<LibraryItem>,
libraryContent: Map<UUID, List<PosterItem>>,
continueWatching: List<ContinueWatchingItem>,
nextUp: List<NextUpItem>,
libraryContent: Map<UUID, List<MediaUiModel>>,
continueWatching: List<MediaUiModel>,
nextUp: List<MediaUiModel>,
): 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 ->

View File

@@ -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<ContinueWatchingItem>,
onFocusedItem: (FocusableItem) -> Unit = {},
items: List<MediaUiModel>,
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<NextUpItem>,
onFocusedItem: (FocusableItem) -> Unit = {},
items: List<MediaUiModel>,
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<PosterItem>,
onFocusedItem: (FocusableItem) -> Unit = {},
items: List<MediaUiModel>,
onFocusedItem: (MediaUiModel) -> Unit = {},
firstItemFocusRequester: FocusRequester? = null,
firstItemTestTag: String? = null,
rowTestTag: String? = null,

View File

@@ -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<PosterItem>,
libraryItems: List<MediaUiModel>,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
modifier: Modifier = Modifier
@@ -105,7 +105,7 @@ fun TvLibraryContent(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.background(MaterialTheme.colorScheme.background)
) {
items(libraryItems) { item ->
items(libraryItems, key = { item -> item.id }) { item ->
PosterCard(
item = item,
onMovieSelected = onMovieSelected,

View File

@@ -64,7 +64,7 @@ internal fun TvMovieScreenContent(
resetScrollKey = movie.id,
modifier = modifier
) {
item {
item(key = "movie-hero") {
TvMediaDetailBodyBox(
backgroundImageUrl = tvMediaDetailBackgroundImageUrl(movie.imageUrlPrefix),
modifier = it
@@ -78,7 +78,7 @@ internal fun TvMovieScreenContent(
Spacer(modifier = Modifier.height(12.dp))
}
}
item {
item(key = "movie-overview") {
Column(modifier = it.fillMaxWidth()) {
Spacer(modifier = Modifier.height(16.dp))
MediaDetailOverviewSection(
@@ -88,7 +88,7 @@ internal fun TvMovieScreenContent(
Spacer(modifier = Modifier.height(20.dp))
}
}
item {
item(key = "movie-playback") {
Column(modifier = it.fillMaxWidth()) {
MediaDetailPlaybackSection(
audioTrack = movie.audioTrack,
@@ -99,7 +99,7 @@ internal fun TvMovieScreenContent(
}
}
if (movie.cast.isNotEmpty()) {
item {
item(key = "movie-cast") {
Column(modifier = it.fillMaxWidth()) {
MediaDetailSectionTitle(text = "Cast")
Spacer(modifier = Modifier.height(14.dp))

View File

@@ -81,7 +81,7 @@ internal fun TvSeriesScreenContent(
resetScrollKey = series.id,
modifier = modifier
) {
item {
item(key = "series-hero") {
TvMediaDetailBodyBox(
backgroundImageUrl = tvMediaDetailBackgroundImageUrl(series.imageUrlPrefix),
modifier = it
@@ -97,7 +97,7 @@ internal fun TvSeriesScreenContent(
Spacer(modifier = Modifier.height(12.dp))
}
}
item {
item(key = "series-overview") {
Column(modifier = it.fillMaxWidth()) {
Spacer(modifier = Modifier.height(16.dp))
MediaDetailOverviewSection(
@@ -107,7 +107,7 @@ internal fun TvSeriesScreenContent(
Spacer(modifier = Modifier.height(24.dp))
}
}
item {
item(key = "series-season-tabs") {
TvSeasonTabs(
seasons = series.seasons,
selectedSeason = selectedSeason,
@@ -117,7 +117,7 @@ internal fun TvSeriesScreenContent(
modifier = it
)
}
item {
item(key = "series-episodes") {
Spacer(modifier = Modifier.height(20.dp))
TvEpisodeCarousel(
episodes = selectedSeason.episodes,
@@ -125,7 +125,7 @@ internal fun TvSeriesScreenContent(
)
}
if (series.cast.isNotEmpty()) {
item {
item(key = "series-cast") {
Column(modifier = it) {
Spacer(modifier = Modifier.height(20.dp))
MediaDetailSectionTitle(text = "Cast")

View File

@@ -171,7 +171,7 @@ internal fun TvEpisodeCarousel(episodes: List<Episode>, modifier: Modifier = Mod
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(episodes) { episode ->
items(episodes, key = { episode -> episode.id }) { episode ->
TvEpisodeCard(episode = episode)
}
}

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

@@ -65,7 +65,7 @@ fun DownloadsContent(
modifier = modifier.background(MaterialTheme.colorScheme.background)
) {
if (activeDownloads.value.isNotEmpty()) {
item(span = { GridItemSpan(maxLineSpan) }) {
item(key = "downloading-header", span = { GridItemSpan(maxLineSpan) }) {
Text(
text = "Downloading",
style = MaterialTheme.typography.titleSmall,
@@ -84,7 +84,7 @@ fun DownloadsContent(
)
}
if (downloads.value.isNotEmpty()) {
item(span = { GridItemSpan(maxLineSpan) }) {
item(key = "downloaded-header", span = { GridItemSpan(maxLineSpan) }) {
Text(
text = "Downloaded",
style = MaterialTheme.typography.titleSmall,
@@ -95,7 +95,7 @@ fun DownloadsContent(
}
}
}
items(downloads.value) { item ->
items(downloads.value, key = { item -> item.id }) { item ->
PosterCard(
item = item,
onMovieSelected = viewModel::onMovieSelected,

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()
) {
@@ -83,7 +83,7 @@ internal fun LibraryPosterGrid(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier.background(MaterialTheme.colorScheme.background)
) {
items(libraryItems) { item ->
items(libraryItems, key = { item -> item.id }) { item ->
PosterCard(
item = item,
onMovieSelected = viewModel::onMovieSelected,

View File

@@ -80,7 +80,7 @@ fun PlayerQueuePanel(
exit = fadeOut()
) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
items(uiState.queue) { item ->
items(uiState.queue, key = { item -> item.id }) { item ->
QueueRow(
title = item.title,
subtitle = item.subtitle,

View File

@@ -304,7 +304,7 @@ internal fun EpisodeCarousel(episodes: List<Episode>, modifier: Modifier = Modif
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(episodes) { episode ->
items(episodes, key = { episode -> episode.id }) { episode ->
EpisodeCard(episode = episode)
}
}

View File

@@ -10,7 +10,3 @@ kotlin {
}
jvmToolchain(11)
}
dependencies {
implementation(project(":core:model"))
}

View File

@@ -10,3 +10,7 @@ kotlin {
}
jvmToolchain(11)
}
dependencies {
implementation(project(":core:image"))
}

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -79,7 +79,10 @@ fun MediaCastRow(
contentPadding = PaddingValues(horizontal = 4.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(cast) { member ->
items(
items = cast,
key = { member -> "${member.name}:${member.role}:${member.imageUrl.orEmpty()}" }
) { member ->
Column(modifier = Modifier.width(cardWidth)) {
Box(
modifier = Modifier

View File

@@ -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 }

View File

@@ -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 {

View File

@@ -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<UUID?>(null)
val contents: StateFlow<List<PosterItem>> = combine(selectedLibrary, homeRepository.libraries) {
val contents: StateFlow<List<MediaUiModel>> = 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())

View File

@@ -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. */

View File

@@ -1,5 +1,5 @@
[versions]
agp = "9.1.0"
agp = "9.1.1"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"