mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Compare commits
3 Commits
18844fcd38
...
632dda52d6
| Author | SHA1 | Date | |
|---|---|---|---|
| 632dda52d6 | |||
| 5a377fc428 | |||
| 858f599dbd |
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,3 @@ kotlin {
|
||||
}
|
||||
jvmToolchain(11)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
}
|
||||
|
||||
@@ -10,3 +10,7 @@ kotlin {
|
||||
}
|
||||
jvmToolchain(11)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:image"))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user