mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +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 androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
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 hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PosterCard(
|
fun PosterCard(
|
||||||
item: PosterItem,
|
item: MediaUiModel,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
imageModifier: Modifier = Modifier,
|
imageModifier: Modifier = Modifier,
|
||||||
posterWidth: Dp = 144.dp,
|
posterWidth: Dp = 144.dp,
|
||||||
showSecondaryText: Boolean = false,
|
showSecondaryText: Boolean = false,
|
||||||
indicatorSize: Int = 28,
|
indicatorSize: Int = 28,
|
||||||
indicatorPadding: Dp = 8.dp,
|
indicatorPadding: Dp = 8.dp,
|
||||||
onFocusedItem: (FocusableItem) -> Unit = {},
|
onFocusedItem: (MediaUiModel) -> Unit = {},
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
) {
|
) {
|
||||||
PosterCardContent(
|
PosterCardContent(
|
||||||
model = item.toPosterCardModel(),
|
model = item,
|
||||||
onClick = { item.open(onMovieSelected, onSeriesSelected, onEpisodeSelected) },
|
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,
|
modifier = modifier,
|
||||||
imageModifier = imageModifier,
|
imageModifier = imageModifier,
|
||||||
posterWidth = posterWidth,
|
posterWidth = posterWidth,
|
||||||
|
|||||||
@@ -15,20 +15,19 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
|
import hu.bbara.purefin.core.model.LibraryKind
|
||||||
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
||||||
import hu.bbara.purefin.ui.screen.home.TvHomeScreen
|
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.TvDrawerDestination
|
||||||
import hu.bbara.purefin.ui.screen.home.components.TvDrawerDestinationItem
|
import hu.bbara.purefin.ui.screen.home.components.TvDrawerDestinationItem
|
||||||
import hu.bbara.purefin.ui.screen.home.components.TvNavigationDrawer
|
import hu.bbara.purefin.ui.screen.home.components.TvNavigationDrawer
|
||||||
import hu.bbara.purefin.ui.screen.libraries.TvLibrariesOverviewScreen
|
import hu.bbara.purefin.ui.screen.libraries.TvLibrariesOverviewScreen
|
||||||
import hu.bbara.purefin.core.model.LibraryKind
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvAppScreen(
|
fun TvAppScreen(
|
||||||
viewModel: AppViewModel = hiltViewModel(),
|
viewModel: AppViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val serverUrl by viewModel.serverUrl.collectAsState()
|
|
||||||
val libraries by viewModel.libraries.collectAsState()
|
val libraries by viewModel.libraries.collectAsState()
|
||||||
val continueWatching by viewModel.continueWatching.collectAsState()
|
val continueWatching by viewModel.continueWatching.collectAsState()
|
||||||
val nextUp by viewModel.nextUp.collectAsState()
|
val nextUp by viewModel.nextUp.collectAsState()
|
||||||
@@ -77,7 +76,6 @@ fun TvAppScreen(
|
|||||||
libraryContent = latestLibraryContent,
|
libraryContent = latestLibraryContent,
|
||||||
continueWatching = continueWatching,
|
continueWatching = continueWatching,
|
||||||
nextUp = nextUp,
|
nextUp = nextUp,
|
||||||
serverUrl = serverUrl,
|
|
||||||
onMovieSelected = viewModel::onMovieSelected,
|
onMovieSelected = viewModel::onMovieSelected,
|
||||||
onSeriesSelected = viewModel::onSeriesSelected,
|
onSeriesSelected = viewModel::onSeriesSelected,
|
||||||
onEpisodeSelected = viewModel::onEpisodeSelected,
|
onEpisodeSelected = viewModel::onEpisodeSelected,
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ internal fun TvEpisodeScreenContent(
|
|||||||
resetScrollKey = episode.id,
|
resetScrollKey = episode.id,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
item {
|
item(key = "episode-hero") {
|
||||||
TvMediaDetailBodyBox(
|
TvMediaDetailBodyBox(
|
||||||
backgroundImageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
backgroundImageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
modifier = it
|
modifier = it
|
||||||
@@ -101,7 +101,7 @@ internal fun TvEpisodeScreenContent(
|
|||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item(key = "episode-overview") {
|
||||||
Column(modifier = it.fillMaxWidth()) {
|
Column(modifier = it.fillMaxWidth()) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
MediaDetailOverviewSection(
|
MediaDetailOverviewSection(
|
||||||
@@ -111,7 +111,7 @@ internal fun TvEpisodeScreenContent(
|
|||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item(key = "episode-playback") {
|
||||||
Column(modifier = it.fillMaxWidth()) {
|
Column(modifier = it.fillMaxWidth()) {
|
||||||
MediaDetailPlaybackSection(
|
MediaDetailPlaybackSection(
|
||||||
audioTrack = "ENG",
|
audioTrack = "ENG",
|
||||||
@@ -122,7 +122,7 @@ internal fun TvEpisodeScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (episode.cast.isNotEmpty()) {
|
if (episode.cast.isNotEmpty()) {
|
||||||
item {
|
item(key = "episode-cast") {
|
||||||
Column(modifier = it.fillMaxWidth()) {
|
Column(modifier = it.fillMaxWidth()) {
|
||||||
MediaDetailSectionTitle(text = "Cast")
|
MediaDetailSectionTitle(text = "Cast")
|
||||||
Spacer(modifier = Modifier.height(14.dp))
|
Spacer(modifier = Modifier.height(14.dp))
|
||||||
|
|||||||
@@ -9,10 +9,8 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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.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.TvFocusedItemHero
|
||||||
import hu.bbara.purefin.ui.screen.home.components.TvHomeContent
|
import hu.bbara.purefin.ui.screen.home.components.TvHomeContent
|
||||||
import hu.bbara.purefin.ui.screen.home.components.rememberTvHomeHeroState
|
import hu.bbara.purefin.ui.screen.home.components.rememberTvHomeHeroState
|
||||||
@@ -25,10 +23,9 @@ private val TvHomeMaxHeroHeight = 200.dp
|
|||||||
@Composable
|
@Composable
|
||||||
fun TvHomeScreen(
|
fun TvHomeScreen(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryItem>,
|
||||||
libraryContent: Map<UUID, List<PosterItem>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
continueWatching: List<ContinueWatchingItem>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<NextUpItem>,
|
nextUp: List<MediaUiModel>,
|
||||||
serverUrl: String,
|
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
@@ -66,7 +63,8 @@ fun TvHomeScreen(
|
|||||||
libraryContent = libraryContent,
|
libraryContent = libraryContent,
|
||||||
continueWatching = continueWatching,
|
continueWatching = continueWatching,
|
||||||
nextUp = nextUp,
|
nextUp = nextUp,
|
||||||
onMediaFocused = heroState.onMediaFocused,
|
//TODO implement it by throwing out complex TvHomeHeroState shit ass code. Fuck ai btw
|
||||||
|
onMediaFocused = {},
|
||||||
onMovieSelected = onMovieSelected,
|
onMovieSelected = onMovieSelected,
|
||||||
onSeriesSelected = onSeriesSelected,
|
onSeriesSelected = onSeriesSelected,
|
||||||
onEpisodeSelected = onEpisodeSelected,
|
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.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|
||||||
internal const val TvHomeHeroTitleTag = "tv-home-hero-title"
|
internal const val TvHomeHeroTitleTag = "tv-home-hero-title"
|
||||||
@@ -32,7 +33,7 @@ private const val TvHomeHeroAnimationMillis = 180
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TvFocusedItemHero(
|
internal fun TvFocusedItemHero(
|
||||||
item: TvFocusedHeroModel,
|
item: MediaUiModel,
|
||||||
height: Dp,
|
height: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -44,7 +45,7 @@ internal fun TvFocusedItemHero(
|
|||||||
.background(scheme.background)
|
.background(scheme.background)
|
||||||
) {
|
) {
|
||||||
Crossfade(
|
Crossfade(
|
||||||
targetState = item.backdropImageUrl,
|
targetState = item.imageUrl,
|
||||||
animationSpec = tween(durationMillis = TvHomeHeroAnimationMillis),
|
animationSpec = tween(durationMillis = TvHomeHeroAnimationMillis),
|
||||||
label = "tv-home-hero-background"
|
label = "tv-home-hero-background"
|
||||||
) { imageUrl ->
|
) { imageUrl ->
|
||||||
@@ -98,7 +99,7 @@ internal fun TvFocusedItemHero(
|
|||||||
modifier = Modifier.widthIn(max = 720.dp)
|
modifier = Modifier.widthIn(max = 720.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = hero.title,
|
text = hero.primaryText,
|
||||||
color = scheme.onBackground,
|
color = scheme.onBackground,
|
||||||
fontSize = 34.sp,
|
fontSize = 34.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -107,16 +108,14 @@ internal fun TvFocusedItemHero(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.testTag(TvHomeHeroTitleTag)
|
modifier = Modifier.testTag(TvHomeHeroTitleTag)
|
||||||
)
|
)
|
||||||
hero.metadataText?.let { metadataText ->
|
Text(
|
||||||
Text(
|
text = hero.description,
|
||||||
text = metadataText,
|
color = scheme.onSurfaceVariant,
|
||||||
color = scheme.onSurfaceVariant,
|
fontSize = 14.sp,
|
||||||
fontSize = 14.sp,
|
lineHeight = 18.sp,
|
||||||
lineHeight = 18.sp,
|
maxLines = 1,
|
||||||
maxLines = 1,
|
overflow = TextOverflow.Ellipsis
|
||||||
overflow = TextOverflow.Ellipsis
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,8 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.unit.dp
|
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.FocusableItem
|
|
||||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
||||||
@@ -37,10 +34,10 @@ internal const val TvHomeContentViewportTag = "tv-home-content-viewport"
|
|||||||
@Composable
|
@Composable
|
||||||
fun TvHomeContent(
|
fun TvHomeContent(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryItem>,
|
||||||
libraryContent: Map<UUID, List<PosterItem>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
continueWatching: List<ContinueWatchingItem>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<NextUpItem>,
|
nextUp: List<MediaUiModel>,
|
||||||
onMediaFocused: (FocusableItem) -> Unit,
|
onMediaFocused: (MediaUiModel) -> Unit,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
@@ -84,11 +81,11 @@ fun TvHomeContent(
|
|||||||
.testTag(TvHomeContentViewportTag),
|
.testTag(TvHomeContentViewportTag),
|
||||||
contentPadding = contentPadding
|
contentPadding = contentPadding
|
||||||
) {
|
) {
|
||||||
item {
|
item(key = "tv-home-top-spacer") {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
if (hasContinueWatching) {
|
if (hasContinueWatching) {
|
||||||
item {
|
item(key = "tv-home-continue-watching") {
|
||||||
TvContinueWatchingSection(
|
TvContinueWatchingSection(
|
||||||
items = continueWatching,
|
items = continueWatching,
|
||||||
onFocusedItem = onMediaFocused,
|
onFocusedItem = onMediaFocused,
|
||||||
@@ -102,13 +99,13 @@ fun TvHomeContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasContinueWatching && (hasNextUp || hasLibraries)) {
|
if (hasContinueWatching && (hasNextUp || hasLibraries)) {
|
||||||
item {
|
item(key = "tv-home-post-continue-gap") {
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasNextUp) {
|
if (hasNextUp) {
|
||||||
item {
|
item(key = "tv-home-next-up") {
|
||||||
TvNextUpSection(
|
TvNextUpSection(
|
||||||
items = nextUp,
|
items = nextUp,
|
||||||
onFocusedItem = onMediaFocused,
|
onFocusedItem = onMediaFocused,
|
||||||
@@ -121,7 +118,7 @@ fun TvHomeContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasLibraries && (hasContinueWatching || hasNextUp)) {
|
if (hasLibraries && (hasContinueWatching || hasNextUp)) {
|
||||||
item {
|
item(key = "tv-home-post-next-up-gap") {
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +153,7 @@ fun TvHomeContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasVisibleContent) {
|
if (hasVisibleContent) {
|
||||||
item {
|
item(key = "tv-home-trailing-space") {
|
||||||
Spacer(modifier = Modifier.height(TvHomeBringIntoViewTrailingSpace))
|
Spacer(modifier = Modifier.height(TvHomeBringIntoViewTrailingSpace))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,24 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
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.FocusableItem
|
||||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
|
//TODO throw this out and simplify it.
|
||||||
|
|
||||||
internal data class TvHomeItemRegistry(
|
internal data class TvHomeItemRegistry(
|
||||||
val visibleLibraries: List<LibraryItem>,
|
val visibleLibraries: List<LibraryItem>,
|
||||||
private val libraryContent: Map<UUID, List<PosterItem>>,
|
private val libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
private val continueWatching: List<ContinueWatchingItem>,
|
private val continueWatching: List<MediaUiModel>,
|
||||||
private val nextUp: List<NextUpItem>,
|
private val nextUp: List<MediaUiModel>,
|
||||||
val firstAvailableItemId: UUID?,
|
val firstAvailableItemId: UUID?,
|
||||||
) {
|
) {
|
||||||
val firstAvailableItem: FocusableItem?
|
val firstAvailableItem: MediaUiModel?
|
||||||
get() = firstAvailableItemId?.let(::itemById)
|
get() = firstAvailableItemId?.let(::itemById)
|
||||||
|
|
||||||
fun itemById(id: UUID): FocusableItem? {
|
fun itemById(id: UUID): MediaUiModel? {
|
||||||
return continueWatching.firstOrNull { it.id == id }
|
return continueWatching.firstOrNull { it.id == id }
|
||||||
?: nextUp.firstOrNull { it.id == id }
|
?: nextUp.firstOrNull { it.id == id }
|
||||||
?: visibleLibraries.asSequence()
|
?: visibleLibraries.asSequence()
|
||||||
@@ -33,16 +33,16 @@ internal data class TvHomeItemRegistry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal class TvHomeHeroState(
|
internal class TvHomeHeroState(
|
||||||
val focusedHero: TvFocusedHeroModel?,
|
val focusedHero: MediaUiModel?,
|
||||||
val onMediaFocused: (FocusableItem) -> Unit,
|
val onMediaFocused: (FocusableItem) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun rememberTvHomeItemRegistry(
|
internal fun rememberTvHomeItemRegistry(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryItem>,
|
||||||
libraryContent: Map<UUID, List<PosterItem>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
continueWatching: List<ContinueWatchingItem>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<NextUpItem>,
|
nextUp: List<MediaUiModel>,
|
||||||
): TvHomeItemRegistry {
|
): TvHomeItemRegistry {
|
||||||
return remember(libraries, libraryContent, continueWatching, nextUp) {
|
return remember(libraries, libraryContent, continueWatching, nextUp) {
|
||||||
createTvHomeItemRegistry(
|
createTvHomeItemRegistry(
|
||||||
@@ -56,9 +56,9 @@ internal fun rememberTvHomeItemRegistry(
|
|||||||
|
|
||||||
internal fun createTvHomeItemRegistry(
|
internal fun createTvHomeItemRegistry(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryItem>,
|
||||||
libraryContent: Map<UUID, List<PosterItem>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
continueWatching: List<ContinueWatchingItem>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<NextUpItem>,
|
nextUp: List<MediaUiModel>,
|
||||||
): TvHomeItemRegistry {
|
): TvHomeItemRegistry {
|
||||||
val visibleLibraries = libraries.filter { libraryContent[it.id]?.isEmpty() != true }
|
val visibleLibraries = libraries.filter { libraryContent[it.id]?.isEmpty() != true }
|
||||||
val firstAvailableItemId = continueWatching.firstOrNull()?.id
|
val firstAvailableItemId = continueWatching.firstOrNull()?.id
|
||||||
@@ -79,9 +79,9 @@ internal fun createTvHomeItemRegistry(
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun rememberTvHomeHeroState(
|
internal fun rememberTvHomeHeroState(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryItem>,
|
||||||
libraryContent: Map<UUID, List<PosterItem>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
continueWatching: List<ContinueWatchingItem>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<NextUpItem>,
|
nextUp: List<MediaUiModel>,
|
||||||
): TvHomeHeroState {
|
): TvHomeHeroState {
|
||||||
val itemRegistry = rememberTvHomeItemRegistry(
|
val itemRegistry = rememberTvHomeItemRegistry(
|
||||||
libraries = libraries,
|
libraries = libraries,
|
||||||
@@ -94,7 +94,7 @@ internal fun rememberTvHomeHeroState(
|
|||||||
focusedItemId?.let(itemRegistry::itemById)
|
focusedItemId?.let(itemRegistry::itemById)
|
||||||
}
|
}
|
||||||
val focusedHero = remember(focusedItem, itemRegistry) {
|
val focusedHero = remember(focusedItem, itemRegistry) {
|
||||||
(focusedItem ?: itemRegistry.firstAvailableItem)?.toTvFocusedHeroModel()
|
(focusedItem ?: itemRegistry.firstAvailableItem)
|
||||||
}
|
}
|
||||||
val onMediaFocused: (FocusableItem) -> Unit = remember {
|
val onMediaFocused: (FocusableItem) -> Unit = remember {
|
||||||
{ item ->
|
{ item ->
|
||||||
|
|||||||
@@ -44,17 +44,12 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
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 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.bar.MediaProgressBar
|
||||||
|
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
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 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 TvHomeSectionsThumbShape = RoundedCornerShape(20.dp)
|
||||||
private val TvHomeSectionsHorizontalPadding = 32.dp
|
private val TvHomeSectionsHorizontalPadding = 32.dp
|
||||||
@@ -70,8 +65,8 @@ internal fun tvHomeLibraryRowTag(libraryId: UUID): String =
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvContinueWatchingSection(
|
fun TvContinueWatchingSection(
|
||||||
items: List<ContinueWatchingItem>,
|
items: List<MediaUiModel>,
|
||||||
onFocusedItem: (FocusableItem) -> Unit = {},
|
onFocusedItem: (MediaUiModel) -> Unit = {},
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
firstItemFocusRequester: FocusRequester? = null,
|
firstItemFocusRequester: FocusRequester? = null,
|
||||||
@@ -88,24 +83,11 @@ fun TvContinueWatchingSection(
|
|||||||
rowTestTag = rowTestTag
|
rowTestTag = rowTestTag
|
||||||
) {
|
) {
|
||||||
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||||
val progressFraction = (item.progress / 100.0).toFloat().coerceIn(0f, 1f)
|
|
||||||
TvHomeLandscapeCard(
|
TvHomeLandscapeCard(
|
||||||
title = item.primaryText,
|
title = item.primaryText,
|
||||||
supporting = item.secondaryText,
|
supporting = item.secondaryText,
|
||||||
imageUrl = when (item.type) {
|
imageUrl = item.imageUrl,
|
||||||
MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl(
|
progress = item.progress ?: 0f,
|
||||||
prefixImageUrl = item.movie?.imageUrlPrefix,
|
|
||||||
artworkKind = ArtworkKind.PRIMARY
|
|
||||||
)
|
|
||||||
|
|
||||||
MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl(
|
|
||||||
prefixImageUrl = item.episode?.imageUrlPrefix,
|
|
||||||
artworkKind = ArtworkKind.PRIMARY
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
progress = progressFraction,
|
|
||||||
imageModifier = Modifier
|
imageModifier = Modifier
|
||||||
.then(
|
.then(
|
||||||
if (index == 0 && firstItemFocusRequester != null) {
|
if (index == 0 && firstItemFocusRequester != null) {
|
||||||
@@ -123,13 +105,9 @@ fun TvContinueWatchingSection(
|
|||||||
),
|
),
|
||||||
onFocusedItem = { onFocusedItem(item) },
|
onFocusedItem = { onFocusedItem(item) },
|
||||||
onClick = {
|
onClick = {
|
||||||
when (item.type) {
|
when (item) {
|
||||||
MediaKind.MOVIE -> onMovieSelected(item.movie!!.id)
|
//TODO fix this shit
|
||||||
MediaKind.EPISODE -> {
|
is EpisodeUiModel -> onEpisodeSelected(item.seriesId, item.seasonId, item.id)
|
||||||
val episode = item.episode!!
|
|
||||||
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,8 +118,8 @@ fun TvContinueWatchingSection(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvNextUpSection(
|
fun TvNextUpSection(
|
||||||
items: List<NextUpItem>,
|
items: List<MediaUiModel>,
|
||||||
onFocusedItem: (FocusableItem) -> Unit = {},
|
onFocusedItem: (MediaUiModel) -> Unit = {},
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
firstItemFocusRequester: FocusRequester? = null,
|
firstItemFocusRequester: FocusRequester? = null,
|
||||||
firstItemTestTag: String? = null,
|
firstItemTestTag: String? = null,
|
||||||
@@ -178,8 +156,10 @@ fun TvNextUpSection(
|
|||||||
),
|
),
|
||||||
onFocusedItem = { onFocusedItem(item) },
|
onFocusedItem = { onFocusedItem(item) },
|
||||||
onClick = {
|
onClick = {
|
||||||
val episode = item.episode
|
//TODO FIX
|
||||||
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
(item as? EpisodeUiModel)?.let { episode ->
|
||||||
|
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -189,8 +169,8 @@ fun TvNextUpSection(
|
|||||||
@Composable
|
@Composable
|
||||||
fun TvLibraryPosterSection(
|
fun TvLibraryPosterSection(
|
||||||
title: String,
|
title: String,
|
||||||
items: List<PosterItem>,
|
items: List<MediaUiModel>,
|
||||||
onFocusedItem: (FocusableItem) -> Unit = {},
|
onFocusedItem: (MediaUiModel) -> Unit = {},
|
||||||
firstItemFocusRequester: FocusRequester? = null,
|
firstItemFocusRequester: FocusRequester? = null,
|
||||||
firstItemTestTag: String? = null,
|
firstItemTestTag: String? = null,
|
||||||
rowTestTag: String? = null,
|
rowTestTag: String? = null,
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
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.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.feature.browse.library.LibraryViewModel
|
||||||
|
import hu.bbara.purefin.ui.common.button.PurefinIconButton
|
||||||
|
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -92,7 +92,7 @@ private fun TvLibraryTopBar(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvLibraryContent(
|
fun TvLibraryContent(
|
||||||
libraryItems: List<PosterItem>,
|
libraryItems: List<MediaUiModel>,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -105,7 +105,7 @@ fun TvLibraryContent(
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.background)
|
modifier = Modifier.background(MaterialTheme.colorScheme.background)
|
||||||
) {
|
) {
|
||||||
items(libraryItems) { item ->
|
items(libraryItems, key = { item -> item.id }) { item ->
|
||||||
PosterCard(
|
PosterCard(
|
||||||
item = item,
|
item = item,
|
||||||
onMovieSelected = onMovieSelected,
|
onMovieSelected = onMovieSelected,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ internal fun TvMovieScreenContent(
|
|||||||
resetScrollKey = movie.id,
|
resetScrollKey = movie.id,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
item {
|
item(key = "movie-hero") {
|
||||||
TvMediaDetailBodyBox(
|
TvMediaDetailBodyBox(
|
||||||
backgroundImageUrl = tvMediaDetailBackgroundImageUrl(movie.imageUrlPrefix),
|
backgroundImageUrl = tvMediaDetailBackgroundImageUrl(movie.imageUrlPrefix),
|
||||||
modifier = it
|
modifier = it
|
||||||
@@ -78,7 +78,7 @@ internal fun TvMovieScreenContent(
|
|||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item(key = "movie-overview") {
|
||||||
Column(modifier = it.fillMaxWidth()) {
|
Column(modifier = it.fillMaxWidth()) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
MediaDetailOverviewSection(
|
MediaDetailOverviewSection(
|
||||||
@@ -88,7 +88,7 @@ internal fun TvMovieScreenContent(
|
|||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item(key = "movie-playback") {
|
||||||
Column(modifier = it.fillMaxWidth()) {
|
Column(modifier = it.fillMaxWidth()) {
|
||||||
MediaDetailPlaybackSection(
|
MediaDetailPlaybackSection(
|
||||||
audioTrack = movie.audioTrack,
|
audioTrack = movie.audioTrack,
|
||||||
@@ -99,7 +99,7 @@ internal fun TvMovieScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (movie.cast.isNotEmpty()) {
|
if (movie.cast.isNotEmpty()) {
|
||||||
item {
|
item(key = "movie-cast") {
|
||||||
Column(modifier = it.fillMaxWidth()) {
|
Column(modifier = it.fillMaxWidth()) {
|
||||||
MediaDetailSectionTitle(text = "Cast")
|
MediaDetailSectionTitle(text = "Cast")
|
||||||
Spacer(modifier = Modifier.height(14.dp))
|
Spacer(modifier = Modifier.height(14.dp))
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ internal fun TvSeriesScreenContent(
|
|||||||
resetScrollKey = series.id,
|
resetScrollKey = series.id,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
item {
|
item(key = "series-hero") {
|
||||||
TvMediaDetailBodyBox(
|
TvMediaDetailBodyBox(
|
||||||
backgroundImageUrl = tvMediaDetailBackgroundImageUrl(series.imageUrlPrefix),
|
backgroundImageUrl = tvMediaDetailBackgroundImageUrl(series.imageUrlPrefix),
|
||||||
modifier = it
|
modifier = it
|
||||||
@@ -97,7 +97,7 @@ internal fun TvSeriesScreenContent(
|
|||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item(key = "series-overview") {
|
||||||
Column(modifier = it.fillMaxWidth()) {
|
Column(modifier = it.fillMaxWidth()) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
MediaDetailOverviewSection(
|
MediaDetailOverviewSection(
|
||||||
@@ -107,7 +107,7 @@ internal fun TvSeriesScreenContent(
|
|||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item(key = "series-season-tabs") {
|
||||||
TvSeasonTabs(
|
TvSeasonTabs(
|
||||||
seasons = series.seasons,
|
seasons = series.seasons,
|
||||||
selectedSeason = selectedSeason,
|
selectedSeason = selectedSeason,
|
||||||
@@ -117,7 +117,7 @@ internal fun TvSeriesScreenContent(
|
|||||||
modifier = it
|
modifier = it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item(key = "series-episodes") {
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
TvEpisodeCarousel(
|
TvEpisodeCarousel(
|
||||||
episodes = selectedSeason.episodes,
|
episodes = selectedSeason.episodes,
|
||||||
@@ -125,7 +125,7 @@ internal fun TvSeriesScreenContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (series.cast.isNotEmpty()) {
|
if (series.cast.isNotEmpty()) {
|
||||||
item {
|
item(key = "series-cast") {
|
||||||
Column(modifier = it) {
|
Column(modifier = it) {
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
MediaDetailSectionTitle(text = "Cast")
|
MediaDetailSectionTitle(text = "Cast")
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ internal fun TvEpisodeCarousel(episodes: List<Episode>, modifier: Modifier = Mod
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
items(episodes) { episode ->
|
items(episodes, key = { episode -> episode.id }) { episode ->
|
||||||
TvEpisodeCard(episode = episode)
|
TvEpisodeCard(episode = episode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,30 @@ package hu.bbara.purefin.ui.common.card
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
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 hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PosterCard(
|
fun PosterCard(
|
||||||
item: PosterItem,
|
item: MediaUiModel,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
) {
|
) {
|
||||||
PosterCardContent(
|
PosterCardContent(
|
||||||
model = item.toPosterCardModel(),
|
model = item,
|
||||||
onClick = { item.open(onMovieSelected, onSeriesSelected, onEpisodeSelected) },
|
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
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ fun DownloadsContent(
|
|||||||
modifier = modifier.background(MaterialTheme.colorScheme.background)
|
modifier = modifier.background(MaterialTheme.colorScheme.background)
|
||||||
) {
|
) {
|
||||||
if (activeDownloads.value.isNotEmpty()) {
|
if (activeDownloads.value.isNotEmpty()) {
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
item(key = "downloading-header", span = { GridItemSpan(maxLineSpan) }) {
|
||||||
Text(
|
Text(
|
||||||
text = "Downloading",
|
text = "Downloading",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
@@ -84,7 +84,7 @@ fun DownloadsContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (downloads.value.isNotEmpty()) {
|
if (downloads.value.isNotEmpty()) {
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
item(key = "downloaded-header", span = { GridItemSpan(maxLineSpan) }) {
|
||||||
Text(
|
Text(
|
||||||
text = "Downloaded",
|
text = "Downloaded",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
@@ -95,7 +95,7 @@ fun DownloadsContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items(downloads.value) { item ->
|
items(downloads.value, key = { item -> item.id }) { item ->
|
||||||
PosterCard(
|
PosterCard(
|
||||||
item = item,
|
item = item,
|
||||||
onMovieSelected = viewModel::onMovieSelected,
|
onMovieSelected = viewModel::onMovieSelected,
|
||||||
|
|||||||
@@ -9,29 +9,26 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
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.HomeContent
|
||||||
import hu.bbara.purefin.ui.screen.home.components.HomeTopBar
|
import hu.bbara.purefin.ui.screen.home.components.HomeTopBar
|
||||||
import hu.bbara.purefin.ui.screen.home.components.search.HomeSearchOverlay
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryItem>,
|
||||||
libraryContent: Map<UUID, List<PosterItem>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
suggestions: List<SuggestedItem>,
|
suggestions: List<SuggestedItem>,
|
||||||
continueWatching: List<ContinueWatchingItem>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<NextUpItem>,
|
nextUp: List<MediaUiModel>,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
@@ -46,14 +43,6 @@ fun HomeScreen(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var isSearchVisible by rememberSaveable { mutableStateOf(false) }
|
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(
|
Scaffold(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|||||||
@@ -22,26 +22,24 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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.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.feature.browse.home.SuggestedItem
|
||||||
import hu.bbara.purefin.ui.screen.home.components.continuewatching.ContinueWatchingSection
|
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.featured.SuggestionsSection
|
||||||
import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection
|
import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection
|
||||||
import hu.bbara.purefin.ui.screen.home.components.nextup.NextUpSection
|
import hu.bbara.purefin.ui.screen.home.components.nextup.NextUpSection
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeContent(
|
fun HomeContent(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryItem>,
|
||||||
libraryContent: Map<UUID, List<PosterItem>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
suggestions: List<SuggestedItem>,
|
suggestions: List<SuggestedItem>,
|
||||||
continueWatching: List<ContinueWatchingItem>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<NextUpItem>,
|
nextUp: List<MediaUiModel>,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onMovieSelected: (UUID) -> 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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
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 java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
|
||||||
import hu.bbara.purefin.core.image.ArtworkKind
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ContinueWatchingCard(
|
internal fun ContinueWatchingCard(
|
||||||
item: ContinueWatchingItem,
|
item: MediaUiModel,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
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(
|
Surface(
|
||||||
shape = RoundedCornerShape(26.dp),
|
shape = RoundedCornerShape(26.dp),
|
||||||
@@ -73,13 +48,10 @@ internal fun ContinueWatchingCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
when (item.type) {
|
// TODO fix this shit as well
|
||||||
MediaKind.MOVIE -> onMovieSelected(item.movie!!.id)
|
when (item) {
|
||||||
MediaKind.EPISODE -> {
|
is MovieUiModel -> onMovieSelected(item.id)
|
||||||
val episode = item.episode!!
|
is EpisodeUiModel -> onEpisodeSelected(item.seriesId, item.seasonId, item.id)
|
||||||
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,14 +62,12 @@ internal fun ContinueWatchingCard(
|
|||||||
.aspectRatio(16f / 9f)
|
.aspectRatio(16f / 9f)
|
||||||
.background(scheme.surfaceContainer)
|
.background(scheme.surfaceContainer)
|
||||||
) {
|
) {
|
||||||
if (imageUrl != null) {
|
PurefinAsyncImage(
|
||||||
PurefinAsyncImage(
|
model = item.imageUrl,
|
||||||
model = imageUrl,
|
contentDescription = item.primaryText,
|
||||||
contentDescription = item.primaryText,
|
modifier = Modifier.fillMaxSize(),
|
||||||
modifier = Modifier.fillMaxSize(),
|
contentScale = ContentScale.Crop
|
||||||
contentScale = ContentScale.Crop
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.matchParentSize()
|
.matchParentSize()
|
||||||
@@ -111,12 +81,14 @@ internal fun ContinueWatchingCard(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
MediaProgressBar(
|
item.progress?.let { progress ->
|
||||||
progress = (item.progress.toFloat() / 100f).coerceIn(0f, 1f),
|
MediaProgressBar(
|
||||||
foregroundColor = scheme.primary,
|
progress = progress,
|
||||||
backgroundColor = Color.White.copy(alpha = 0.24f),
|
foregroundColor = scheme.primary,
|
||||||
modifier = Modifier.align(Alignment.BottomStart)
|
backgroundColor = Color.White.copy(alpha = 0.24f),
|
||||||
)
|
modifier = Modifier.align(Alignment.BottomStart)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
@@ -129,15 +101,13 @@ internal fun ContinueWatchingCard(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
if (supportingText.isNotBlank()) {
|
Text(
|
||||||
Text(
|
text = item.secondaryText,
|
||||||
text = supportingText,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
color = scheme.onSurface,
|
||||||
color = scheme.onSurface,
|
maxLines = 1,
|
||||||
maxLines = 1,
|
overflow = TextOverflow.Ellipsis
|
||||||
overflow = TextOverflow.Ellipsis
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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.ui.common.header.SectionHeader
|
||||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContinueWatchingSection(
|
fun ContinueWatchingSection(
|
||||||
items: List<ContinueWatchingItem>,
|
items: List<MediaUiModel>,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
import hu.bbara.purefin.core.ui.model.EpisodeUiModel
|
||||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeBadge
|
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.badge.WatchStateBadge
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun HomeBrowseCard(
|
internal fun HomeBrowseCard(
|
||||||
item: PosterItem,
|
uiModel: MediaUiModel,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
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(
|
Surface(
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
@@ -66,15 +50,11 @@ internal fun HomeBrowseCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
when (item.type) {
|
//TODO fix this shit
|
||||||
MediaKind.MOVIE -> onMovieSelected(item.id)
|
when (uiModel) {
|
||||||
MediaKind.SERIES -> onSeriesSelected(item.id)
|
is MovieUiModel -> onMovieSelected(uiModel.id)
|
||||||
MediaKind.EPISODE -> {
|
is SeriesUiModel -> onSeriesSelected(uiModel.id)
|
||||||
val episode = item.episode!!
|
is EpisodeUiModel -> onEpisodeSelected(uiModel.seriesId, uiModel.seasonId, uiModel.id)
|
||||||
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -86,68 +66,42 @@ internal fun HomeBrowseCard(
|
|||||||
.background(scheme.surface)
|
.background(scheme.surface)
|
||||||
) {
|
) {
|
||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = item.imageUrl,
|
model = uiModel.imageUrl,
|
||||||
contentDescription = item.title,
|
contentDescription = uiModel.primaryText,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
when (item.type) {
|
when (uiModel) {
|
||||||
MediaKind.MOVIE -> {
|
is MovieUiModel, is EpisodeUiModel -> {
|
||||||
val movie = item.movie!!
|
|
||||||
WatchStateBadge(
|
WatchStateBadge(
|
||||||
size = 28,
|
size = 28,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
watched = movie.watched,
|
watched = uiModel.watched,
|
||||||
started = (movie.progress ?: 0.0) > 0
|
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
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
Column(modifier = modifier.padding(12.dp)) {
|
Column(modifier = modifier.padding(12.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = item.title,
|
text = uiModel.primaryText,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
if (supportingText.isNotBlank()) {
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Text(
|
||||||
Text(
|
text = uiModel.secondaryText,
|
||||||
text = supportingText,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
color = scheme.onSurface,
|
||||||
color = scheme.onSurface,
|
maxLines = 1,
|
||||||
maxLines = 1,
|
overflow = TextOverflow.Ellipsis
|
||||||
overflow = TextOverflow.Ellipsis
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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.LibraryItem
|
||||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
|
||||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryPosterSection(
|
fun LibraryPosterSection(
|
||||||
library: LibraryItem,
|
library: LibraryItem,
|
||||||
items: List<PosterItem>,
|
items: List<MediaUiModel>,
|
||||||
onLibrarySelected: (LibraryItem) -> Unit,
|
onLibrarySelected: (LibraryItem) -> Unit,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
@@ -42,7 +42,7 @@ fun LibraryPosterSection(
|
|||||||
) {
|
) {
|
||||||
items(items = items, key = { item -> item.id }) { item ->
|
items(items = items, key = { item -> item.id }) { item ->
|
||||||
HomeBrowseCard(
|
HomeBrowseCard(
|
||||||
item = item,
|
uiModel = item,
|
||||||
onMovieSelected = onMovieSelected,
|
onMovieSelected = onMovieSelected,
|
||||||
onSeriesSelected = onSeriesSelected,
|
onSeriesSelected = onSeriesSelected,
|
||||||
onEpisodeSelected = onEpisodeSelected
|
onEpisodeSelected = onEpisodeSelected
|
||||||
|
|||||||
@@ -22,15 +22,14 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
|
||||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import hu.bbara.purefin.core.image.ArtworkKind
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun NextUpCard(
|
internal fun NextUpCard(
|
||||||
item: NextUpItem,
|
uiModel: MediaUiModel,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -45,9 +44,11 @@ internal fun NextUpCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
onEpisodeSelected(
|
// TODO fix this shit
|
||||||
item.episode.seriesId, item.episode.seasonId, item.episode.id
|
when (uiModel) {
|
||||||
)
|
is EpisodeUiModel -> onEpisodeSelected(uiModel.seriesId, uiModel.seasonId, uiModel.id)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
@@ -57,8 +58,8 @@ internal fun NextUpCard(
|
|||||||
.background(scheme.surface)
|
.background(scheme.surface)
|
||||||
) {
|
) {
|
||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = ImageUrlBuilder.finishImageUrl(item.episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
model = uiModel.imageUrl,
|
||||||
contentDescription = item.primaryText,
|
contentDescription = uiModel.primaryText,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
@@ -81,16 +82,14 @@ internal fun NextUpCard(
|
|||||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = item.primaryText,
|
text = uiModel.primaryText,
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = listOf("Episode ${item.episode.index}", item.episode.runtime, item.secondaryText)
|
text = uiModel.secondaryText,
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.joinToString(" • "),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = scheme.onSurface,
|
color = scheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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.ui.common.header.SectionHeader
|
||||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NextUpSection(
|
fun NextUpSection(
|
||||||
items: List<NextUpItem>,
|
items: List<MediaUiModel>,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -33,7 +33,7 @@ fun NextUpSection(
|
|||||||
) {
|
) {
|
||||||
items(items = items, key = { item -> item.id }) { item ->
|
items(items = items, key = { item -> item.id }) { item ->
|
||||||
NextUpCard(
|
NextUpCard(
|
||||||
item = item,
|
uiModel = item,
|
||||||
onEpisodeSelected = onEpisodeSelected
|
onEpisodeSelected = onEpisodeSelected
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
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.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.feature.browse.library.LibraryViewModel
|
||||||
|
import hu.bbara.purefin.ui.common.button.PurefinIconButton
|
||||||
|
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryScreen(
|
fun LibraryScreen(
|
||||||
@@ -72,7 +72,7 @@ internal fun LibraryTopBar(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LibraryPosterGrid(
|
internal fun LibraryPosterGrid(
|
||||||
libraryItems: List<PosterItem>,
|
libraryItems: List<MediaUiModel>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: LibraryViewModel = hiltViewModel()
|
viewModel: LibraryViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
@@ -83,7 +83,7 @@ internal fun LibraryPosterGrid(
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier = modifier.background(MaterialTheme.colorScheme.background)
|
modifier = modifier.background(MaterialTheme.colorScheme.background)
|
||||||
) {
|
) {
|
||||||
items(libraryItems) { item ->
|
items(libraryItems, key = { item -> item.id }) { item ->
|
||||||
PosterCard(
|
PosterCard(
|
||||||
item = item,
|
item = item,
|
||||||
onMovieSelected = viewModel::onMovieSelected,
|
onMovieSelected = viewModel::onMovieSelected,
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ fun PlayerQueuePanel(
|
|||||||
exit = fadeOut()
|
exit = fadeOut()
|
||||||
) {
|
) {
|
||||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
items(uiState.queue) { item ->
|
items(uiState.queue, key = { item -> item.id }) { item ->
|
||||||
QueueRow(
|
QueueRow(
|
||||||
title = item.title,
|
title = item.title,
|
||||||
subtitle = item.subtitle,
|
subtitle = item.subtitle,
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ internal fun EpisodeCarousel(episodes: List<Episode>, modifier: Modifier = Modif
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
items(episodes) { episode ->
|
items(episodes, key = { episode -> episode.id }) { episode ->
|
||||||
EpisodeCard(episode = episode)
|
EpisodeCard(episode = episode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,3 @@ kotlin {
|
|||||||
}
|
}
|
||||||
jvmToolchain(11)
|
jvmToolchain(11)
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":core:model"))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,3 +10,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
jvmToolchain(11)
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
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.badge.WatchStateBadge
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ sealed interface PosterCardBadge {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PosterCardContent(
|
fun PosterCardContent(
|
||||||
model: PosterCardModel,
|
model: MediaUiModel,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
imageModifier: Modifier = Modifier,
|
imageModifier: Modifier = Modifier,
|
||||||
@@ -108,36 +110,25 @@ fun PosterCardContent(
|
|||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
when (val badge = model.badge) {
|
when (model) {
|
||||||
is PosterCardBadge.WatchState -> {
|
is MovieUiModel, is EpisodeUiModel -> {
|
||||||
WatchStateBadge(
|
WatchStateBadge(
|
||||||
size = indicatorSize,
|
size = indicatorSize,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(indicatorPadding),
|
.padding(indicatorPadding),
|
||||||
watched = badge.watched,
|
watched = model.watched,
|
||||||
started = badge.started
|
started = (model.progress ?: 0f) > 0f
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
else -> Unit
|
||||||
is PosterCardBadge.UnwatchedEpisodes -> {
|
|
||||||
UnwatchedEpisodeBadge(
|
|
||||||
size = indicatorSize,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.padding(indicatorPadding),
|
|
||||||
unwatchedCount = badge.count
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
PosterCardBadge.None -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(top = 8.dp, start = 4.dp, end = 4.dp, bottom = 8.dp)
|
modifier = Modifier.padding(top = 8.dp, start = 4.dp, end = 4.dp, bottom = 8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = model.title,
|
text = model.primaryText,
|
||||||
color = scheme.onBackground,
|
color = scheme.onBackground,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
|
|||||||
@@ -79,7 +79,10 @@ fun MediaCastRow(
|
|||||||
contentPadding = PaddingValues(horizontal = 4.dp),
|
contentPadding = PaddingValues(horizontal = 4.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.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)) {
|
Column(modifier = Modifier.width(cardWidth)) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -5,18 +5,19 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import hu.bbara.purefin.core.data.HomeRepository
|
import hu.bbara.purefin.core.data.HomeRepository
|
||||||
import hu.bbara.purefin.core.data.MediaCatalogReader
|
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.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.LibraryKind
|
||||||
import hu.bbara.purefin.core.model.Media
|
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.EpisodeDto
|
||||||
import hu.bbara.purefin.core.navigation.LibraryDto
|
import hu.bbara.purefin.core.navigation.LibraryDto
|
||||||
import hu.bbara.purefin.core.navigation.MovieDto
|
import hu.bbara.purefin.core.navigation.MovieDto
|
||||||
import hu.bbara.purefin.core.navigation.NavigationManager
|
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||||
import hu.bbara.purefin.core.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.core.navigation.SeriesDto
|
import hu.bbara.purefin.core.navigation.SeriesDto
|
||||||
import java.util.UUID
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -25,6 +26,7 @@ import kotlinx.coroutines.flow.combine
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -95,10 +97,10 @@ class AppViewModel @Inject constructor(
|
|||||||
list.mapNotNull { media ->
|
list.mapNotNull { media ->
|
||||||
when (media) {
|
when (media) {
|
||||||
is Media.MovieMedia -> moviesMap[media.movieId]?.let {
|
is Media.MovieMedia -> moviesMap[media.movieId]?.let {
|
||||||
ContinueWatchingItem(type = MediaKind.MOVIE, movie = it)
|
MovieUiModel(movie = it)
|
||||||
}
|
}
|
||||||
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
||||||
ContinueWatchingItem(type = MediaKind.EPISODE, episode = it)
|
EpisodeUiModel(episode = it)
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -116,7 +118,7 @@ class AppViewModel @Inject constructor(
|
|||||||
list.mapNotNull { media ->
|
list.mapNotNull { media ->
|
||||||
when (media) {
|
when (media) {
|
||||||
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
||||||
NextUpItem(episode = it)
|
EpisodeUiModel(episode = it)
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -137,16 +139,16 @@ class AppViewModel @Inject constructor(
|
|||||||
items.mapNotNull { media ->
|
items.mapNotNull { media ->
|
||||||
when (media) {
|
when (media) {
|
||||||
is Media.MovieMedia -> moviesMap[media.movieId]?.let {
|
is Media.MovieMedia -> moviesMap[media.movieId]?.let {
|
||||||
PosterItem(type = MediaKind.MOVIE, movie = it)
|
MovieUiModel(movie = it)
|
||||||
}
|
}
|
||||||
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
||||||
PosterItem(type = MediaKind.EPISODE, episode = it)
|
EpisodeUiModel(episode = it)
|
||||||
}
|
}
|
||||||
is Media.SeriesMedia -> seriesMap[media.seriesId]?.let {
|
is Media.SeriesMedia -> seriesMap[media.seriesId]?.let {
|
||||||
PosterItem(type = MediaKind.SERIES, series = it)
|
SeriesUiModel(series = it)
|
||||||
}
|
}
|
||||||
is Media.SeasonMedia -> seriesMap[media.seriesId]?.let {
|
is Media.SeasonMedia -> seriesMap[media.seriesId]?.let {
|
||||||
PosterItem(type = MediaKind.SERIES, series = it)
|
SeriesUiModel(series = it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.distinctBy { it.id }
|
}.distinctBy { it.id }
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package hu.bbara.purefin.feature.browse.home
|
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.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.core.model.Episode
|
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.Movie
|
||||||
import hu.bbara.purefin.core.model.Series
|
import hu.bbara.purefin.core.model.Series
|
||||||
import java.util.UUID
|
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 {
|
sealed interface SuggestedItem {
|
||||||
@@ -97,49 +97,6 @@ sealed interface FocusableItem {
|
|||||||
val type: MediaKind
|
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(
|
data class NextUpItem(
|
||||||
val episode: Episode
|
val episode: Episode
|
||||||
) : FocusableItem {
|
) : FocusableItem {
|
||||||
|
|||||||
@@ -5,19 +5,20 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import hu.bbara.purefin.core.data.HomeRepository
|
import hu.bbara.purefin.core.data.HomeRepository
|
||||||
import hu.bbara.purefin.core.model.LibraryKind
|
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.MovieDto
|
||||||
import hu.bbara.purefin.core.navigation.NavigationManager
|
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||||
import hu.bbara.purefin.core.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.core.navigation.SeriesDto
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -28,7 +29,7 @@ class LibraryViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val selectedLibrary = MutableStateFlow<UUID?>(null)
|
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 ->
|
libraryId, libraries ->
|
||||||
if (libraryId == null) {
|
if (libraryId == null) {
|
||||||
return@combine emptyList()
|
return@combine emptyList()
|
||||||
@@ -36,10 +37,10 @@ class LibraryViewModel @Inject constructor(
|
|||||||
val library = libraries.find { it.id == libraryId } ?: return@combine emptyList()
|
val library = libraries.find { it.id == libraryId } ?: return@combine emptyList()
|
||||||
when (library.type) {
|
when (library.type) {
|
||||||
LibraryKind.SERIES -> library.series!!.map { series ->
|
LibraryKind.SERIES -> library.series!!.map { series ->
|
||||||
PosterItem(type = MediaKind.SERIES, series = series)
|
SeriesUiModel(series)
|
||||||
}
|
}
|
||||||
LibraryKind.MOVIES -> library.movies!!.map { movie ->
|
LibraryKind.MOVIES -> library.movies!!.map { movie ->
|
||||||
PosterItem(type = MediaKind.MOVIE, movie = movie)
|
MovieUiModel(movie)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||||
|
|||||||
@@ -5,19 +5,19 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import hu.bbara.purefin.core.data.OfflineCatalogReader
|
import hu.bbara.purefin.core.data.OfflineCatalogReader
|
||||||
import hu.bbara.purefin.core.download.MediaDownloadController
|
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.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
|
||||||
import hu.bbara.purefin.core.navigation.MovieDto
|
import hu.bbara.purefin.core.navigation.MovieDto
|
||||||
import hu.bbara.purefin.core.navigation.NavigationManager
|
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||||
import hu.bbara.purefin.core.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.core.navigation.SeriesDto
|
import hu.bbara.purefin.core.navigation.SeriesDto
|
||||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
import hu.bbara.purefin.core.ui.model.MovieUiModel
|
||||||
import java.util.UUID
|
import hu.bbara.purefin.core.ui.model.SeriesUiModel
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import hu.bbara.purefin.core.image.ArtworkKind
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -53,8 +53,8 @@ class DownloadsViewModel @Inject constructor(
|
|||||||
) { movies, series, inProgress ->
|
) { movies, series, inProgress ->
|
||||||
movies.values
|
movies.values
|
||||||
.filter { it.id.toString() !in inProgress }
|
.filter { it.id.toString() !in inProgress }
|
||||||
.map { PosterItem(type = MediaKind.MOVIE, movie = it) } +
|
.map { MovieUiModel(it) } +
|
||||||
series.values.map { PosterItem(type = MediaKind.SERIES, series = it) }
|
series.values.map { SeriesUiModel(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Items currently being downloaded with their progress. */
|
/** Items currently being downloaded with their progress. */
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "9.1.0"
|
agp = "9.1.1"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user