mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Compare commits
5 Commits
920b0e0e4a
...
d0e5e978be
| Author | SHA1 | Date | |
|---|---|---|---|
| d0e5e978be | |||
| a39581682f | |||
| a9b0f771f8 | |||
| 18e352eb1e | |||
| 9545e4d619 |
@@ -1,15 +1,40 @@
|
|||||||
package hu.bbara.purefin.ui.common.card
|
package hu.bbara.purefin.ui.common.card
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import 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 androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.core.ui.model.EpisodeUiModel
|
import hu.bbara.purefin.core.ui.model.EpisodeUiModel
|
||||||
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.core.ui.model.MovieUiModel
|
import hu.bbara.purefin.core.ui.model.MovieUiModel
|
||||||
import hu.bbara.purefin.core.ui.model.SeriesUiModel
|
import hu.bbara.purefin.core.ui.model.SeriesUiModel
|
||||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
||||||
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -50,51 +75,102 @@ fun PosterCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun PosterItem.toPosterCardModel(): PosterCardModel {
|
@Composable
|
||||||
return PosterCardModel(
|
fun PosterCardContent(
|
||||||
title = title,
|
model: MediaUiModel,
|
||||||
secondaryText = secondaryText,
|
onClick: () -> Unit,
|
||||||
imageUrl = imageUrl,
|
modifier: Modifier = Modifier,
|
||||||
mediaKind = type,
|
imageModifier: Modifier = Modifier,
|
||||||
badge = when (type) {
|
posterWidth: Dp = 144.dp,
|
||||||
MediaKind.MOVIE -> {
|
contentScale: Float = 1f,
|
||||||
val movie = requireNotNull(movie)
|
showSecondaryText: Boolean = false,
|
||||||
PosterCardBadge.WatchState(
|
indicatorSize: Int = 28,
|
||||||
watched = movie.watched,
|
indicatorPadding: Dp = 8.dp,
|
||||||
started = (movie.progress ?: 0.0) > 0.0
|
onFocused: () -> Unit = {},
|
||||||
)
|
focusedScale: Float = 1f,
|
||||||
}
|
focusedBorderWidth: Dp = 1.dp,
|
||||||
|
focusedTransformOrigin: TransformOrigin = TransformOrigin(0.5f, 0f)
|
||||||
MediaKind.EPISODE -> {
|
|
||||||
val episode = requireNotNull(episode)
|
|
||||||
PosterCardBadge.WatchState(
|
|
||||||
watched = episode.watched,
|
|
||||||
started = (episode.progress ?: 0.0) > 0.0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaKind.SERIES -> PosterCardBadge.UnwatchedEpisodes(
|
|
||||||
count = requireNotNull(series).unwatchedEpisodeCount
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> PosterCardBadge.None
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun PosterItem.open(
|
|
||||||
onMovieSelected: (UUID) -> Unit,
|
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
when (type) {
|
val scheme = MaterialTheme.colorScheme
|
||||||
MediaKind.MOVIE -> onMovieSelected(id)
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
MediaKind.SERIES -> onSeriesSelected(id)
|
val scale by animateFloatAsState(
|
||||||
MediaKind.EPISODE -> {
|
targetValue = if (isFocused) focusedScale else 1f,
|
||||||
val ep = requireNotNull(episode)
|
label = "scale"
|
||||||
onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.width(posterWidth)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
transformOrigin = focusedTransformOrigin
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = model.primaryImageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = imageModifier
|
||||||
|
.aspectRatio(2f / 3f)
|
||||||
|
.clip(RoundedCornerShape(14.dp))
|
||||||
|
.border(
|
||||||
|
width = if (isFocused) focusedBorderWidth else 1.dp,
|
||||||
|
color = if (isFocused) scheme.primary else scheme.outlineVariant.copy(alpha = 0.3f),
|
||||||
|
shape = RoundedCornerShape(14.dp)
|
||||||
|
)
|
||||||
|
.background(scheme.surfaceVariant)
|
||||||
|
.onFocusChanged {
|
||||||
|
isFocused = it.isFocused
|
||||||
|
if (it.isFocused) {
|
||||||
|
onFocused()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
when (model) {
|
||||||
|
is MovieUiModel, is EpisodeUiModel -> {
|
||||||
|
WatchStateBadge(
|
||||||
|
size = indicatorSize,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(indicatorPadding),
|
||||||
|
watched = model.watched,
|
||||||
|
started = (model.progress ?: 0f) > 0f
|
||||||
|
)
|
||||||
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
top = 8.dp,
|
||||||
|
start = 4.dp,
|
||||||
|
end = 4.dp,
|
||||||
|
bottom = 8.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = model.primaryText,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
model.secondaryText
|
||||||
|
.takeIf { showSecondaryText }
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let { text ->
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ fun TvAppScreen(
|
|||||||
libraryContent = latestLibraryContent,
|
libraryContent = latestLibraryContent,
|
||||||
continueWatching = continueWatching,
|
continueWatching = continueWatching,
|
||||||
nextUp = nextUp,
|
nextUp = nextUp,
|
||||||
onMovieSelected = viewModel::onMovieSelected,
|
onMediaSelected = viewModel::onMediaSelected,
|
||||||
onSeriesSelected = viewModel::onSeriesSelected,
|
|
||||||
onEpisodeSelected = viewModel::onEpisodeSelected,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ fun TvHomeScreen(
|
|||||||
libraryContent: Map<UUID, List<MediaUiModel>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
continueWatching: List<MediaUiModel>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<MediaUiModel>,
|
nextUp: List<MediaUiModel>,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
@@ -53,9 +51,7 @@ fun TvHomeScreen(
|
|||||||
onMediaFocused = {
|
onMediaFocused = {
|
||||||
focusedMediaUiModel.value = it
|
focusedMediaUiModel.value = it
|
||||||
},
|
},
|
||||||
onMovieSelected = onMovieSelected,
|
onMediaSelected = onMediaSelected,
|
||||||
onSeriesSelected = onSeriesSelected,
|
|
||||||
onEpisodeSelected = onEpisodeSelected,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
@@ -37,9 +37,7 @@ fun TvHomeContent(
|
|||||||
continueWatching: List<MediaUiModel>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<MediaUiModel>,
|
nextUp: List<MediaUiModel>,
|
||||||
onMediaFocused: (MediaUiModel) -> Unit,
|
onMediaFocused: (MediaUiModel) -> Unit,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
contentPadding: PaddingValues = PaddingValues(bottom = 32.dp),
|
contentPadding: PaddingValues = PaddingValues(bottom = 32.dp),
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -81,8 +79,7 @@ fun TvHomeContent(
|
|||||||
TvContinueWatchingSection(
|
TvContinueWatchingSection(
|
||||||
items = continueWatching,
|
items = continueWatching,
|
||||||
onFocusedItem = onMediaFocused,
|
onFocusedItem = onMediaFocused,
|
||||||
onMovieSelected = onMovieSelected,
|
onMediaSelected = onMediaSelected,
|
||||||
onEpisodeSelected = onEpisodeSelected,
|
|
||||||
firstItemFocusRequester = initialFocusRequester,
|
firstItemFocusRequester = initialFocusRequester,
|
||||||
firstItemTestTag = TvHomeInitialFocusTag,
|
firstItemTestTag = TvHomeInitialFocusTag,
|
||||||
rowTestTag = TvHomeContinueWatchingRowTag
|
rowTestTag = TvHomeContinueWatchingRowTag
|
||||||
@@ -95,7 +92,7 @@ fun TvHomeContent(
|
|||||||
TvNextUpSection(
|
TvNextUpSection(
|
||||||
items = nextUp,
|
items = nextUp,
|
||||||
onFocusedItem = onMediaFocused,
|
onFocusedItem = onMediaFocused,
|
||||||
onEpisodeSelected = onEpisodeSelected,
|
onMediaSelected = onMediaSelected,
|
||||||
firstItemFocusRequester = initialFocusRequester.takeIf { !hasContinueWatching },
|
firstItemFocusRequester = initialFocusRequester.takeIf { !hasContinueWatching },
|
||||||
firstItemTestTag = TvHomeInitialFocusTag.takeIf { !hasContinueWatching },
|
firstItemTestTag = TvHomeInitialFocusTag.takeIf { !hasContinueWatching },
|
||||||
rowTestTag = TvHomeNextUpRowTag
|
rowTestTag = TvHomeNextUpRowTag
|
||||||
@@ -115,9 +112,7 @@ fun TvHomeContent(
|
|||||||
!hasContinueWatching && !hasNextUp && library.id == firstLibraryWithItemsId
|
!hasContinueWatching && !hasNextUp && library.id == firstLibraryWithItemsId
|
||||||
},
|
},
|
||||||
firstItemTestTag = tvHomeLibraryFirstItemTag(library.id),
|
firstItemTestTag = tvHomeLibraryFirstItemTag(library.id),
|
||||||
onMovieSelected = onMovieSelected,
|
onMediaSelected = onMediaSelected
|
||||||
onSeriesSelected = onSeriesSelected,
|
|
||||||
onEpisodeSelected = onEpisodeSelected
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,10 +44,9 @@ 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.core.ui.model.EpisodeUiModel
|
|
||||||
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
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.card.PosterCardContent
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -83,8 +82,7 @@ internal fun tvHomeLibraryFirstItemTag(libraryId: UUID): String =
|
|||||||
fun TvContinueWatchingSection(
|
fun TvContinueWatchingSection(
|
||||||
items: List<MediaUiModel>,
|
items: List<MediaUiModel>,
|
||||||
onFocusedItem: (MediaUiModel) -> Unit = {},
|
onFocusedItem: (MediaUiModel) -> Unit = {},
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
firstItemFocusRequester: FocusRequester? = null,
|
firstItemFocusRequester: FocusRequester? = null,
|
||||||
firstItemTestTag: String? = null,
|
firstItemTestTag: String? = null,
|
||||||
rowTestTag: String? = null,
|
rowTestTag: String? = null,
|
||||||
@@ -120,13 +118,7 @@ fun TvContinueWatchingSection(
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
onFocusedItem = { onFocusedItem(item) },
|
onFocusedItem = { onFocusedItem(item) },
|
||||||
onClick = {
|
onClick = { onMediaSelected(item) }
|
||||||
when (item) {
|
|
||||||
//TODO fix this shit
|
|
||||||
is EpisodeUiModel -> onEpisodeSelected(item.seriesId, item.seasonId, item.id)
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +128,7 @@ fun TvContinueWatchingSection(
|
|||||||
fun TvNextUpSection(
|
fun TvNextUpSection(
|
||||||
items: List<MediaUiModel>,
|
items: List<MediaUiModel>,
|
||||||
onFocusedItem: (MediaUiModel) -> Unit = {},
|
onFocusedItem: (MediaUiModel) -> Unit = {},
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
firstItemFocusRequester: FocusRequester? = null,
|
firstItemFocusRequester: FocusRequester? = null,
|
||||||
firstItemTestTag: String? = null,
|
firstItemTestTag: String? = null,
|
||||||
rowTestTag: String? = null,
|
rowTestTag: String? = null,
|
||||||
@@ -171,12 +163,7 @@ fun TvNextUpSection(
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
onFocusedItem = { onFocusedItem(item) },
|
onFocusedItem = { onFocusedItem(item) },
|
||||||
onClick = {
|
onClick = { onMediaSelected(item) }
|
||||||
//TODO FIX
|
|
||||||
(item as? EpisodeUiModel)?.let { episode ->
|
|
||||||
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,9 +178,7 @@ fun TvLibraryPosterSection(
|
|||||||
firstItemTestTag: String? = null,
|
firstItemTestTag: String? = null,
|
||||||
rowTestTag: String? = null,
|
rowTestTag: String? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
TvSectionHeader(
|
TvSectionHeader(
|
||||||
title = title,
|
title = title,
|
||||||
@@ -203,8 +188,9 @@ fun TvLibraryPosterSection(
|
|||||||
rowTestTag = rowTestTag
|
rowTestTag = rowTestTag
|
||||||
) {
|
) {
|
||||||
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||||
PosterCard(
|
PosterCardContent(
|
||||||
item = item,
|
model = item,
|
||||||
|
onClick = { onMediaSelected(item) },
|
||||||
posterWidth = TvHomePosterCardWidth,
|
posterWidth = TvHomePosterCardWidth,
|
||||||
contentScale = TvHomeMediaCardScale,
|
contentScale = TvHomeMediaCardScale,
|
||||||
showSecondaryText = true,
|
showSecondaryText = true,
|
||||||
@@ -225,10 +211,9 @@ fun TvLibraryPosterSection(
|
|||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
onFocusedItem = onFocusedItem,
|
onFocused = { onFocusedItem(item) },
|
||||||
onMovieSelected = onMovieSelected,
|
focusedScale = 1.07f,
|
||||||
onSeriesSelected = onSeriesSelected,
|
focusedBorderWidth = 2.dp
|
||||||
onEpisodeSelected = onEpisodeSelected
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,40 @@
|
|||||||
package hu.bbara.purefin.ui.common.card
|
package hu.bbara.purefin.ui.common.card
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import hu.bbara.purefin.core.ui.model.EpisodeUiModel
|
||||||
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.core.ui.model.MovieUiModel
|
import hu.bbara.purefin.core.ui.model.MovieUiModel
|
||||||
import hu.bbara.purefin.core.ui.model.SeriesUiModel
|
import hu.bbara.purefin.core.ui.model.SeriesUiModel
|
||||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
||||||
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -24,58 +52,105 @@ fun PosterCard(
|
|||||||
when (item) {
|
when (item) {
|
||||||
is MovieUiModel -> onMovieSelected(item.id)
|
is MovieUiModel -> onMovieSelected(item.id)
|
||||||
is SeriesUiModel -> onSeriesSelected(item.id)
|
is SeriesUiModel -> onSeriesSelected(item.id)
|
||||||
else -> Unit
|
is EpisodeUiModel -> onEpisodeSelected(item.seriesId, item.seasonId, item.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun PosterItem.toPosterCardModel(): PosterCardModel {
|
@Composable
|
||||||
return PosterCardModel(
|
fun PosterCardContent(
|
||||||
title = title,
|
model: MediaUiModel,
|
||||||
secondaryText = secondaryText,
|
onClick: () -> Unit,
|
||||||
imageUrl = imageUrl,
|
modifier: Modifier = Modifier,
|
||||||
mediaKind = type,
|
imageModifier: Modifier = Modifier,
|
||||||
badge = when (type) {
|
posterWidth: Dp = 144.dp,
|
||||||
MediaKind.MOVIE -> {
|
showSecondaryText: Boolean = false,
|
||||||
val movie = requireNotNull(movie)
|
indicatorSize: Int = 28,
|
||||||
PosterCardBadge.WatchState(
|
indicatorPadding: Dp = 8.dp,
|
||||||
watched = movie.watched,
|
onFocused: () -> Unit = {},
|
||||||
started = (movie.progress ?: 0.0) > 0.0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaKind.EPISODE -> {
|
|
||||||
val episode = requireNotNull(episode)
|
|
||||||
PosterCardBadge.WatchState(
|
|
||||||
watched = episode.watched,
|
|
||||||
started = (episode.progress ?: 0.0) > 0.0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaKind.SERIES -> PosterCardBadge.UnwatchedEpisodes(
|
|
||||||
count = requireNotNull(series).unwatchedEpisodeCount
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> PosterCardBadge.None
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun PosterItem.open(
|
|
||||||
onMovieSelected: (UUID) -> Unit,
|
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
when (type) {
|
val scheme = MaterialTheme.colorScheme
|
||||||
MediaKind.MOVIE -> onMovieSelected(id)
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
MediaKind.SERIES -> onSeriesSelected(id)
|
val scale by animateFloatAsState(
|
||||||
MediaKind.EPISODE -> {
|
targetValue = if (isFocused) 1.07f else 1f,
|
||||||
val ep = requireNotNull(episode)
|
label = "scale"
|
||||||
onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.width(posterWidth)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
transformOrigin = TransformOrigin(0.5f, 0f)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = model.primaryImageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = imageModifier
|
||||||
|
.aspectRatio(2f / 3f)
|
||||||
|
.clip(RoundedCornerShape(14.dp))
|
||||||
|
.border(
|
||||||
|
width = if (isFocused) 2.dp else 1.dp,
|
||||||
|
color = if (isFocused) scheme.primary else scheme.outlineVariant.copy(alpha = 0.3f),
|
||||||
|
shape = RoundedCornerShape(14.dp)
|
||||||
|
)
|
||||||
|
.background(scheme.surfaceVariant)
|
||||||
|
.onFocusChanged {
|
||||||
|
isFocused = it.isFocused
|
||||||
|
if (it.isFocused) {
|
||||||
|
onFocused()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
when (model) {
|
||||||
|
is MovieUiModel, is EpisodeUiModel -> {
|
||||||
|
WatchStateBadge(
|
||||||
|
size = indicatorSize,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(indicatorPadding),
|
||||||
|
watched = model.watched,
|
||||||
|
started = (model.progress ?: 0f) > 0f
|
||||||
|
)
|
||||||
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
top = 8.dp,
|
||||||
|
start = 4.dp,
|
||||||
|
end = 4.dp,
|
||||||
|
bottom = 8.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = model.primaryText,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
model.secondaryText
|
||||||
|
.takeIf { showSecondaryText }
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let { text ->
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,52 @@
|
|||||||
package hu.bbara.purefin.ui.screen
|
package hu.bbara.purefin.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
|
import androidx.compose.animation.ContentTransform
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
import androidx.navigation3.runtime.entryProvider
|
||||||
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
|
import androidx.navigation3.scene.Scene
|
||||||
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
||||||
|
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||||
import hu.bbara.purefin.ui.screen.download.DownloadsScreen
|
import hu.bbara.purefin.ui.screen.download.DownloadsScreen
|
||||||
import hu.bbara.purefin.ui.screen.home.HomeScreen
|
import hu.bbara.purefin.ui.screen.home.HomeScreen
|
||||||
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
||||||
import hu.bbara.purefin.ui.screen.libraries.LibrariesScreen
|
import hu.bbara.purefin.ui.screen.libraries.LibrariesScreen
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppScreen(
|
fun AppScreen(
|
||||||
viewModel: AppViewModel = hiltViewModel(),
|
viewModel: AppViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var selectedTab by remember { mutableIntStateOf(0) }
|
|
||||||
|
|
||||||
val libraries by viewModel.libraries.collectAsState()
|
val libraries by viewModel.libraries.collectAsState()
|
||||||
val libraryContent by viewModel.latestLibraryContent.collectAsState()
|
val libraryContent by viewModel.latestLibraryContent.collectAsState()
|
||||||
val suggestions by viewModel.suggestions.collectAsState()
|
val suggestions by viewModel.suggestions.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()
|
||||||
val isRefreshing by viewModel.isRefreshing.collectAsState()
|
val isRefreshing by viewModel.isRefreshing.collectAsState()
|
||||||
|
val navigationManager = LocalNavigationManager.current
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val backStack = rememberNavBackStack(AppTabRoute.Home) as NavBackStack<AppTabRoute>
|
||||||
|
val currentRoute = backStack.lastOrNull() ?: AppTabRoute.Home
|
||||||
|
val selectedTab = currentRoute.toTabIndex()
|
||||||
|
|
||||||
val libraryNavItems = libraries.map {
|
val libraryNavItems = libraries.map {
|
||||||
HomeNavItem(
|
HomeNavItem(
|
||||||
@@ -43,8 +61,18 @@ fun AppScreen(
|
|||||||
onPauseOrDispose { }
|
onPauseOrDispose { }
|
||||||
}
|
}
|
||||||
|
|
||||||
when (selectedTab) {
|
val onTabSelected = remember(backStack) {
|
||||||
0 -> HomeScreen(
|
{ selectedIndex: Int ->
|
||||||
|
val route = selectedIndex.toAppTabRoute()
|
||||||
|
if (backStack.lastOrNull() != route) {
|
||||||
|
backStack.add(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val tabEntryProvider = entryProvider {
|
||||||
|
entry<AppTabRoute.Home>(metadata = appTabMetadata(AppTabRoute.Home)) {
|
||||||
|
HomeScreen(
|
||||||
libraries = libraries,
|
libraries = libraries,
|
||||||
libraryContent = libraryContent,
|
libraryContent = libraryContent,
|
||||||
suggestions = suggestions,
|
suggestions = suggestions,
|
||||||
@@ -52,9 +80,7 @@ fun AppScreen(
|
|||||||
nextUp = nextUp,
|
nextUp = nextUp,
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
onRefresh = viewModel::onRefresh,
|
onRefresh = viewModel::onRefresh,
|
||||||
onMovieSelected = viewModel::onMovieSelected,
|
onMediaSelected = viewModel::onMediaSelected,
|
||||||
onSeriesSelected = viewModel::onSeriesSelected,
|
|
||||||
onEpisodeSelected = viewModel::onEpisodeSelected,
|
|
||||||
onLibrarySelected = { library ->
|
onLibrarySelected = { library ->
|
||||||
viewModel.onLibrarySelected(
|
viewModel.onLibrarySelected(
|
||||||
library.id,
|
library.id,
|
||||||
@@ -65,23 +91,101 @@ fun AppScreen(
|
|||||||
onSettingsClick = {},
|
onSettingsClick = {},
|
||||||
onLogoutClick = viewModel::logout,
|
onLogoutClick = viewModel::logout,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it },
|
onTabSelected = onTabSelected,
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
1 -> LibrariesScreen(
|
}
|
||||||
|
entry<AppTabRoute.Libraries>(metadata = appTabMetadata(AppTabRoute.Libraries)) {
|
||||||
|
LibrariesScreen(
|
||||||
items = libraryNavItems,
|
items = libraryNavItems,
|
||||||
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) },
|
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) },
|
||||||
onProfileClick = {},
|
onProfileClick = {},
|
||||||
onSettingsClick = {},
|
onSettingsClick = {},
|
||||||
onLogoutClick = viewModel::logout,
|
onLogoutClick = viewModel::logout,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it },
|
onTabSelected = onTabSelected,
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
|
||||||
2 -> DownloadsScreen(
|
|
||||||
selectedTab = selectedTab,
|
|
||||||
onTabSelected = { selectedTab = it },
|
|
||||||
modifier = modifier.fillMaxSize()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
entry<AppTabRoute.Downloads>(metadata = appTabMetadata(AppTabRoute.Downloads)) {
|
||||||
|
DownloadsScreen(
|
||||||
|
selectedTab = selectedTab,
|
||||||
|
onTabSelected = onTabSelected,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavDisplay(
|
||||||
|
backStack = backStack,
|
||||||
|
onBack = {
|
||||||
|
if (backStack.size > 1) {
|
||||||
|
backStack.removeLastOrNull()
|
||||||
|
} else {
|
||||||
|
navigationManager.pop()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
entryDecorators = listOf(
|
||||||
|
rememberSaveableStateHolderNavEntryDecorator()
|
||||||
|
),
|
||||||
|
transitionSpec = { appTabTransition() },
|
||||||
|
popTransitionSpec = { appTabTransition() },
|
||||||
|
predictivePopTransitionSpec = { _ -> appTabTransition() },
|
||||||
|
entryProvider = tabEntryProvider,
|
||||||
|
modifier = modifier.fillMaxSize()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private sealed interface AppTabRoute : NavKey {
|
||||||
|
@Serializable
|
||||||
|
data object Home : AppTabRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Libraries : AppTabRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Downloads : AppTabRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AppTabRoute.toTabIndex(): Int = when (this) {
|
||||||
|
AppTabRoute.Home -> 0
|
||||||
|
AppTabRoute.Libraries -> 1
|
||||||
|
AppTabRoute.Downloads -> 2
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.toAppTabRoute(): AppTabRoute = when (this) {
|
||||||
|
0 -> AppTabRoute.Home
|
||||||
|
1 -> AppTabRoute.Libraries
|
||||||
|
2 -> AppTabRoute.Downloads
|
||||||
|
else -> AppTabRoute.Home
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AnimatedContentTransitionScope<Scene<AppTabRoute>>.appTabTransition(): ContentTransform {
|
||||||
|
val initialIndex = initialState.metadata.appTabIndex()
|
||||||
|
val targetIndex = targetState.metadata.appTabIndex()
|
||||||
|
val animationSpec = tween<IntOffset>(durationMillis = 140)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
targetIndex > initialIndex -> {
|
||||||
|
slideInHorizontally(animationSpec = animationSpec) { fullWidth -> fullWidth } togetherWith
|
||||||
|
slideOutHorizontally(animationSpec = animationSpec) { fullWidth -> -fullWidth }
|
||||||
|
}
|
||||||
|
targetIndex < initialIndex -> {
|
||||||
|
slideInHorizontally(animationSpec = animationSpec) { fullWidth -> -fullWidth } togetherWith
|
||||||
|
slideOutHorizontally(animationSpec = animationSpec) { fullWidth -> fullWidth }
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
slideInHorizontally(animationSpec = animationSpec) { 0 } togetherWith
|
||||||
|
slideOutHorizontally(animationSpec = animationSpec) { 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appTabMetadata(route: AppTabRoute): Map<String, Any> =
|
||||||
|
mapOf(APP_TAB_INDEX_METADATA to route.toTabIndex())
|
||||||
|
|
||||||
|
private fun Map<String, Any>.appTabIndex(): Int =
|
||||||
|
this[APP_TAB_INDEX_METADATA] as? Int ?: 0
|
||||||
|
|
||||||
|
private const val APP_TAB_INDEX_METADATA = "app_tab_index"
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ 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.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.SuggestedItem
|
|
||||||
import hu.bbara.purefin.ui.screen.AppBottomBar
|
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
|
||||||
@@ -26,14 +25,12 @@ import java.util.UUID
|
|||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryItem>,
|
||||||
libraryContent: Map<UUID, List<MediaUiModel>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
suggestions: List<SuggestedItem>,
|
suggestions: List<MediaUiModel>,
|
||||||
continueWatching: List<MediaUiModel>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<MediaUiModel>,
|
nextUp: List<MediaUiModel>,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
onLibrarySelected: (LibraryItem) -> Unit,
|
onLibrarySelected: (LibraryItem) -> Unit,
|
||||||
onProfileClick: () -> Unit,
|
onProfileClick: () -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
@@ -73,9 +70,7 @@ fun HomeScreen(
|
|||||||
nextUp = nextUp,
|
nextUp = nextUp,
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
onMovieSelected = onMovieSelected,
|
onMediaSelected = onMediaSelected,
|
||||||
onSeriesSelected = onSeriesSelected,
|
|
||||||
onEpisodeSelected = onEpisodeSelected,
|
|
||||||
onLibrarySelected = onLibrarySelected,
|
onLibrarySelected = onLibrarySelected,
|
||||||
onBrowseLibrariesClick = { onTabSelected(1) },
|
onBrowseLibrariesClick = { onTabSelected(1) },
|
||||||
modifier = Modifier.padding(innerPadding)
|
modifier = Modifier.padding(innerPadding)
|
||||||
@@ -86,11 +81,12 @@ fun HomeScreen(
|
|||||||
onDismiss = { isSearchVisible = false },
|
onDismiss = { isSearchVisible = false },
|
||||||
onMovieSelected = {
|
onMovieSelected = {
|
||||||
isSearchVisible = false
|
isSearchVisible = false
|
||||||
onMovieSelected(it)
|
//TODO use MediaUiModel as well
|
||||||
|
//onMovieSelected(it)
|
||||||
},
|
},
|
||||||
onSeriesSelected = {
|
onSeriesSelected = {
|
||||||
isSearchVisible = false
|
isSearchVisible = false
|
||||||
onSeriesSelected(it)
|
//onSeriesSelected(it)
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package hu.bbara.purefin.ui.screen.home.components
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -22,10 +21,8 @@ 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.core.model.MediaKind
|
|
||||||
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
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.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
|
||||||
@@ -37,14 +34,12 @@ import java.util.UUID
|
|||||||
fun HomeContent(
|
fun HomeContent(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryItem>,
|
||||||
libraryContent: Map<UUID, List<MediaUiModel>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
suggestions: List<SuggestedItem>,
|
suggestions: List<MediaUiModel>,
|
||||||
continueWatching: List<MediaUiModel>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<MediaUiModel>,
|
nextUp: List<MediaUiModel>,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
onLibrarySelected: (LibraryItem) -> Unit,
|
onLibrarySelected: (LibraryItem) -> Unit,
|
||||||
onBrowseLibrariesClick: () -> Unit,
|
onBrowseLibrariesClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -101,16 +96,7 @@ fun HomeContent(
|
|||||||
item(key = "featured") {
|
item(key = "featured") {
|
||||||
SuggestionsSection(
|
SuggestionsSection(
|
||||||
items = suggestions,
|
items = suggestions,
|
||||||
onItemOpen = { item ->
|
onItemOpen = { item -> onMediaSelected(item) }
|
||||||
when (item.type) {
|
|
||||||
MediaKind.MOVIE -> onMovieSelected(item.id)
|
|
||||||
MediaKind.SERIES -> onSeriesSelected(item.id)
|
|
||||||
MediaKind.EPISODE -> onEpisodeSelected(item.id, item.id, item.id)
|
|
||||||
else -> {
|
|
||||||
Log.e("HomeContent", "Unsupported item type: ${item.type}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,8 +105,7 @@ fun HomeContent(
|
|||||||
item(key = "continue-watching") {
|
item(key = "continue-watching") {
|
||||||
ContinueWatchingSection(
|
ContinueWatchingSection(
|
||||||
items = continueWatching,
|
items = continueWatching,
|
||||||
onMovieSelected = onMovieSelected,
|
onMediaSelected = onMediaSelected
|
||||||
onEpisodeSelected = onEpisodeSelected
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +114,7 @@ fun HomeContent(
|
|||||||
item(key = "next-up") {
|
item(key = "next-up") {
|
||||||
NextUpSection(
|
NextUpSection(
|
||||||
items = nextUp,
|
items = nextUp,
|
||||||
onEpisodeSelected = onEpisodeSelected
|
onMediaSelected = onMediaSelected
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,9 +127,7 @@ fun HomeContent(
|
|||||||
library = library,
|
library = library,
|
||||||
items = libraryContent[library.id].orEmpty(),
|
items = libraryContent[library.id].orEmpty(),
|
||||||
onLibrarySelected = onLibrarySelected,
|
onLibrarySelected = onLibrarySelected,
|
||||||
onMovieSelected = onMovieSelected,
|
onMediaSelected = onMediaSelected
|
||||||
onSeriesSelected = onSeriesSelected,
|
|
||||||
onEpisodeSelected = onEpisodeSelected
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,18 +23,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.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 java.util.UUID
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ContinueWatchingCard(
|
internal fun ContinueWatchingCard(
|
||||||
item: MediaUiModel,
|
item: MediaUiModel,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
@@ -47,14 +43,7 @@ internal fun ContinueWatchingCard(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable { onMediaSelected(item) }
|
||||||
// TODO fix this shit as well
|
|
||||||
when (item) {
|
|
||||||
is MovieUiModel -> onMovieSelected(item.id)
|
|
||||||
is EpisodeUiModel -> onEpisodeSelected(item.seriesId, item.seasonId, item.id)
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ 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.core.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContinueWatchingSection(
|
fun ContinueWatchingSection(
|
||||||
items: List<MediaUiModel>,
|
items: List<MediaUiModel>,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
@@ -35,8 +33,7 @@ fun ContinueWatchingSection(
|
|||||||
items(items = items, key = { item -> item.id }) { item ->
|
items(items = items, key = { item -> item.id }) { item ->
|
||||||
ContinueWatchingCard(
|
ContinueWatchingCard(
|
||||||
item = item,
|
item = item,
|
||||||
onMovieSelected = onMovieSelected,
|
onMediaSelected = onMediaSelected
|
||||||
onEpisodeSelected = onEpisodeSelected
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ 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.MediaUiModel
|
||||||
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.feature.browse.home.SuggestedItem
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SuggestionCard(
|
internal fun SuggestionCard(
|
||||||
item: SuggestedItem,
|
item: MediaUiModel,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -51,8 +51,8 @@ internal fun SuggestionCard(
|
|||||||
.aspectRatio(16f / 11f)
|
.aspectRatio(16f / 11f)
|
||||||
) {
|
) {
|
||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = item.imageUrl,
|
model = item.primaryImageUrl,
|
||||||
contentDescription = item.title,
|
contentDescription = item.primaryText,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
@@ -77,23 +77,20 @@ internal fun SuggestionCard(
|
|||||||
) {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = item.title,
|
text = item.primaryText,
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
if (item.metadata.isNotEmpty()) {
|
|
||||||
Text(
|
Text(
|
||||||
text = item.metadata.joinToString(" • "),
|
text = item.secondaryText,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = Color.White.copy(alpha = 0.88f),
|
color = Color.White.copy(alpha = 0.88f),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
|
||||||
if (description.isNotBlank()) {
|
|
||||||
Text(
|
Text(
|
||||||
text = description,
|
text = description,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
@@ -104,7 +101,6 @@ internal fun SuggestionCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (item.progress != null && item.progress!! > 0f) {
|
if (item.progress != null && item.progress!! > 0f) {
|
||||||
MediaProgressBar(
|
MediaProgressBar(
|
||||||
progress = item.progress!!.coerceIn(0f, 1f),
|
progress = item.progress!!.coerceIn(0f, 1f),
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.feature.browse.home.SuggestedItem
|
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SuggestionsSection(
|
fun SuggestionsSection(
|
||||||
items: List<SuggestedItem>,
|
items: List<MediaUiModel>,
|
||||||
onItemOpen: (SuggestedItem) -> Unit,
|
onItemOpen: (MediaUiModel) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
|
|||||||
@@ -26,17 +26,13 @@ import androidx.compose.ui.unit.dp
|
|||||||
import hu.bbara.purefin.core.ui.model.EpisodeUiModel
|
import hu.bbara.purefin.core.ui.model.EpisodeUiModel
|
||||||
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.core.ui.model.MovieUiModel
|
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
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun HomeBrowseCard(
|
internal fun HomeBrowseCard(
|
||||||
uiModel: MediaUiModel,
|
uiModel: MediaUiModel,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
@@ -49,14 +45,7 @@ internal fun HomeBrowseCard(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable { onMediaSelected(uiModel) }
|
||||||
//TODO fix this shit
|
|
||||||
when (uiModel) {
|
|
||||||
is MovieUiModel -> onMovieSelected(uiModel.id)
|
|
||||||
is SeriesUiModel -> onSeriesSelected(uiModel.id)
|
|
||||||
is EpisodeUiModel -> onEpisodeSelected(uiModel.seriesId, uiModel.seasonId, uiModel.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -12,16 +12,13 @@ import androidx.compose.ui.unit.dp
|
|||||||
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
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.ui.common.header.SectionHeader
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryPosterSection(
|
fun LibraryPosterSection(
|
||||||
library: LibraryItem,
|
library: LibraryItem,
|
||||||
items: List<MediaUiModel>,
|
items: List<MediaUiModel>,
|
||||||
onLibrarySelected: (LibraryItem) -> Unit,
|
onLibrarySelected: (LibraryItem) -> Unit,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
@@ -43,9 +40,7 @@ fun LibraryPosterSection(
|
|||||||
items(items = items, key = { item -> item.id }) { item ->
|
items(items = items, key = { item -> item.id }) { item ->
|
||||||
HomeBrowseCard(
|
HomeBrowseCard(
|
||||||
uiModel = item,
|
uiModel = item,
|
||||||
onMovieSelected = onMovieSelected,
|
onMediaSelected = onMediaSelected
|
||||||
onSeriesSelected = onSeriesSelected,
|
|
||||||
onEpisodeSelected = onEpisodeSelected
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,15 +22,13 @@ 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.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun NextUpCard(
|
internal fun NextUpCard(
|
||||||
uiModel: MediaUiModel,
|
uiModel: MediaUiModel,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
@@ -43,13 +41,7 @@ internal fun NextUpCard(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable { onMediaSelected(uiModel) }
|
||||||
// TODO fix this shit
|
|
||||||
when (uiModel) {
|
|
||||||
is EpisodeUiModel -> onEpisodeSelected(uiModel.seriesId, uiModel.seasonId, uiModel.id)
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -11,12 +11,11 @@ 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.core.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NextUpSection(
|
fun NextUpSection(
|
||||||
items: List<MediaUiModel>,
|
items: List<MediaUiModel>,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
@@ -34,7 +33,7 @@ fun NextUpSection(
|
|||||||
items(items = items, key = { item -> item.id }) { item ->
|
items(items = items, key = { item -> item.id }) { item ->
|
||||||
NextUpCard(
|
NextUpCard(
|
||||||
uiModel = item,
|
uiModel = item,
|
||||||
onEpisodeSelected = onEpisodeSelected
|
onMediaSelected = onMediaSelected
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ fun PlayerScreen(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(Color.Black)
|
||||||
) {
|
) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { ctx ->
|
factory = { ctx ->
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
package hu.bbara.purefin.ui.common.card
|
|
||||||
|
|
||||||
import hu.bbara.purefin.core.model.CastMember
|
|
||||||
import hu.bbara.purefin.core.model.Episode
|
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
|
||||||
import hu.bbara.purefin.core.model.Movie
|
|
||||||
import hu.bbara.purefin.core.model.Season
|
|
||||||
import hu.bbara.purefin.core.model.Series
|
|
||||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
|
||||||
import java.util.UUID
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class PosterCardMappingTest {
|
|
||||||
@Test
|
|
||||||
fun `maps movie poster item to watch-state card model`() {
|
|
||||||
val movie = Movie(
|
|
||||||
id = UUID.fromString("11111111-1111-1111-1111-111111111111"),
|
|
||||||
libraryId = UUID.fromString("22222222-2222-2222-2222-222222222222"),
|
|
||||||
title = "Blade Runner 2049",
|
|
||||||
progress = 18.0,
|
|
||||||
watched = false,
|
|
||||||
year = "2017",
|
|
||||||
rating = "16+",
|
|
||||||
runtime = "2h 44m",
|
|
||||||
format = "Dolby Vision",
|
|
||||||
synopsis = "Synthetic future noir.",
|
|
||||||
imageUrlPrefix = "https://example.test/movie/",
|
|
||||||
audioTrack = "English 5.1",
|
|
||||||
subtitles = "English CC",
|
|
||||||
cast = emptyList()
|
|
||||||
)
|
|
||||||
|
|
||||||
val model = PosterItem(type = MediaKind.MOVIE, movie = movie).toPosterCardModel()
|
|
||||||
|
|
||||||
assertEquals("Blade Runner 2049", model.title)
|
|
||||||
assertEquals("2017", model.secondaryText)
|
|
||||||
assertEquals("https://example.test/movie/Primary", model.imageUrl)
|
|
||||||
assertEquals(MediaKind.MOVIE, model.mediaKind)
|
|
||||||
assertTrue(model.badge is PosterCardBadge.WatchState)
|
|
||||||
val badge = model.badge as PosterCardBadge.WatchState
|
|
||||||
assertEquals(false, badge.watched)
|
|
||||||
assertEquals(true, badge.started)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `maps series poster item to unwatched badge card model`() {
|
|
||||||
val series = Series(
|
|
||||||
id = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
|
||||||
libraryId = UUID.fromString("44444444-4444-4444-4444-444444444444"),
|
|
||||||
name = "Severance",
|
|
||||||
synopsis = "Corporate sci-fi.",
|
|
||||||
year = "2025",
|
|
||||||
imageUrlPrefix = "https://example.test/series/",
|
|
||||||
unwatchedEpisodeCount = 12,
|
|
||||||
seasonCount = 2,
|
|
||||||
seasons = listOf(
|
|
||||||
Season(
|
|
||||||
id = UUID.fromString("55555555-5555-5555-5555-555555555555"),
|
|
||||||
seriesId = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
|
||||||
name = "Season 1",
|
|
||||||
index = 1,
|
|
||||||
unwatchedEpisodeCount = 9,
|
|
||||||
episodeCount = 9,
|
|
||||||
episodes = emptyList()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
cast = listOf(CastMember("Adam Scott", "Mark", null))
|
|
||||||
)
|
|
||||||
|
|
||||||
val model = PosterItem(type = MediaKind.SERIES, series = series).toPosterCardModel()
|
|
||||||
|
|
||||||
assertEquals("Severance", model.title)
|
|
||||||
assertEquals("2025", model.secondaryText)
|
|
||||||
assertEquals(MediaKind.SERIES, model.mediaKind)
|
|
||||||
assertTrue(model.badge is PosterCardBadge.UnwatchedEpisodes)
|
|
||||||
val badge = model.badge as PosterCardBadge.UnwatchedEpisodes
|
|
||||||
assertEquals(12, badge.count)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `maps episode poster item to watch-state card model`() {
|
|
||||||
val episode = Episode(
|
|
||||||
id = UUID.fromString("66666666-6666-6666-6666-666666666666"),
|
|
||||||
seriesId = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
|
||||||
seasonId = UUID.fromString("55555555-5555-5555-5555-555555555555"),
|
|
||||||
index = 4,
|
|
||||||
title = "The You You Are",
|
|
||||||
synopsis = "Office mystery.",
|
|
||||||
releaseDate = "2025",
|
|
||||||
rating = "16+",
|
|
||||||
runtime = "53m",
|
|
||||||
progress = 0.0,
|
|
||||||
watched = true,
|
|
||||||
format = "4K",
|
|
||||||
imageUrlPrefix = "https://example.test/episode/",
|
|
||||||
cast = emptyList()
|
|
||||||
)
|
|
||||||
|
|
||||||
val model = PosterItem(type = MediaKind.EPISODE, episode = episode).toPosterCardModel()
|
|
||||||
|
|
||||||
assertEquals("The You You Are", model.title)
|
|
||||||
assertEquals("2025", model.secondaryText)
|
|
||||||
assertEquals(MediaKind.EPISODE, model.mediaKind)
|
|
||||||
assertTrue(model.badge is PosterCardBadge.WatchState)
|
|
||||||
val badge = model.badge as PosterCardBadge.WatchState
|
|
||||||
assertEquals(true, badge.watched)
|
|
||||||
assertEquals(false, badge.started)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
package hu.bbara.purefin.ui.common.card
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
|
||||||
import hu.bbara.purefin.core.ui.model.EpisodeUiModel
|
|
||||||
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
|
||||||
import hu.bbara.purefin.core.ui.model.MovieUiModel
|
|
||||||
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
|
||||||
|
|
||||||
data class PosterCardModel(
|
|
||||||
val title: String,
|
|
||||||
val secondaryText: String = "",
|
|
||||||
val imageUrl: String?,
|
|
||||||
val mediaKind: MediaKind,
|
|
||||||
val badge: PosterCardBadge = PosterCardBadge.None
|
|
||||||
)
|
|
||||||
|
|
||||||
sealed interface PosterCardBadge {
|
|
||||||
data object None : PosterCardBadge
|
|
||||||
|
|
||||||
data class WatchState(
|
|
||||||
val watched: Boolean,
|
|
||||||
val started: Boolean
|
|
||||||
) : PosterCardBadge
|
|
||||||
|
|
||||||
data class UnwatchedEpisodes(
|
|
||||||
val count: Int
|
|
||||||
) : PosterCardBadge
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun PosterCardContent(
|
|
||||||
model: MediaUiModel,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
imageModifier: Modifier = Modifier,
|
|
||||||
posterWidth: Dp = 144.dp,
|
|
||||||
contentScale: Float = 1f,
|
|
||||||
showSecondaryText: Boolean = false,
|
|
||||||
indicatorSize: Int = 28,
|
|
||||||
indicatorPadding: Dp = 8.dp,
|
|
||||||
onFocused: () -> Unit = {},
|
|
||||||
focusedScale: Float = 1f,
|
|
||||||
focusedBorderWidth: Dp = 1.dp,
|
|
||||||
focusedTransformOrigin: TransformOrigin = TransformOrigin(0.5f, 0f)
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
|
||||||
val shape = RoundedCornerShape((14f * contentScale).dp)
|
|
||||||
val unfocusedBorderWidth = (1f * contentScale).dp
|
|
||||||
val scaledFocusedBorderWidth = focusedBorderWidth * contentScale
|
|
||||||
val contentTopPadding = (8f * contentScale).dp
|
|
||||||
val contentHorizontalPadding = (4f * contentScale).dp
|
|
||||||
val contentBottomPadding = (8f * contentScale).dp
|
|
||||||
val titleFontSize = (13f * contentScale).sp
|
|
||||||
val secondaryFontSize = (11f * contentScale).sp
|
|
||||||
val scale by animateFloatAsState(
|
|
||||||
targetValue = if (isFocused) focusedScale else 1f,
|
|
||||||
label = "scale"
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.width(posterWidth)
|
|
||||||
.graphicsLayer {
|
|
||||||
scaleX = scale
|
|
||||||
scaleY = scale
|
|
||||||
transformOrigin = focusedTransformOrigin
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Box {
|
|
||||||
PurefinAsyncImage(
|
|
||||||
model = model.primaryImageUrl,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = imageModifier
|
|
||||||
.aspectRatio(2f / 3f)
|
|
||||||
.clip(shape)
|
|
||||||
.border(
|
|
||||||
width = if (isFocused) scaledFocusedBorderWidth else unfocusedBorderWidth,
|
|
||||||
color = if (isFocused) scheme.primary else scheme.outlineVariant.copy(alpha = 0.3f),
|
|
||||||
shape = shape
|
|
||||||
)
|
|
||||||
.background(scheme.surfaceVariant)
|
|
||||||
.onFocusChanged {
|
|
||||||
isFocused = it.isFocused
|
|
||||||
if (it.isFocused) {
|
|
||||||
onFocused()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.clickable(onClick = onClick),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
when (model) {
|
|
||||||
is MovieUiModel, is EpisodeUiModel -> {
|
|
||||||
WatchStateBadge(
|
|
||||||
size = indicatorSize,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.padding(indicatorPadding),
|
|
||||||
watched = model.watched,
|
|
||||||
started = (model.progress ?: 0f) > 0f
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(
|
|
||||||
top = contentTopPadding,
|
|
||||||
start = contentHorizontalPadding,
|
|
||||||
end = contentHorizontalPadding,
|
|
||||||
bottom = contentBottomPadding
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = model.primaryText,
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = titleFontSize,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
model.secondaryText
|
|
||||||
.takeIf { showSecondaryText }
|
|
||||||
?.takeIf { it.isNotBlank() }
|
|
||||||
?.let { text ->
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
color = scheme.onSurfaceVariant,
|
|
||||||
fontSize = secondaryFontSize,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,17 +7,18 @@ 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.data.session.UserSessionRepository
|
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||||
import hu.bbara.purefin.core.download.MediaDownloadController
|
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.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 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 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
|
||||||
@@ -72,13 +73,13 @@ 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 {
|
||||||
SuggestedMovie(movie = it)
|
MovieUiModel(movie = it)
|
||||||
}
|
}
|
||||||
is Media.SeriesMedia -> seriesMap[media.seriesId]?.let {
|
is Media.SeriesMedia -> seriesMap[media.seriesId]?.let {
|
||||||
SuggestedSeries(series = it)
|
SeriesUiModel(series = it)
|
||||||
}
|
}
|
||||||
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
||||||
SuggestedEpisode(episode = it)
|
EpisodeUiModel(episode = it)
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -165,7 +166,15 @@ class AppViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMovieSelected(movieId: UUID) {
|
fun onMediaSelected(mediaUiModel : MediaUiModel) {
|
||||||
|
when (mediaUiModel) {
|
||||||
|
is MovieUiModel -> onMovieSelected(mediaUiModel.id)
|
||||||
|
is SeriesUiModel -> onSeriesSelected(mediaUiModel.id)
|
||||||
|
is EpisodeUiModel -> onEpisodeSelected(mediaUiModel.seriesId, mediaUiModel.seasonId, mediaUiModel.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onMovieSelected(movieId: UUID) {
|
||||||
navigationManager.navigate(Route.MovieRoute(
|
navigationManager.navigate(Route.MovieRoute(
|
||||||
MovieDto(
|
MovieDto(
|
||||||
id = movieId,
|
id = movieId,
|
||||||
@@ -173,7 +182,7 @@ class AppViewModel @Inject constructor(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSeriesSelected(seriesId: UUID) {
|
private fun onSeriesSelected(seriesId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
navigationManager.navigate(Route.SeriesRoute(
|
navigationManager.navigate(Route.SeriesRoute(
|
||||||
SeriesDto(
|
SeriesDto(
|
||||||
@@ -183,7 +192,7 @@ class AppViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEpisodeSelected(seriesId: UUID, seasonId: UUID, episodeId: UUID) {
|
private fun onEpisodeSelected(seriesId: UUID, seasonId: UUID, episodeId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
navigationManager.navigate(Route.EpisodeRoute(
|
navigationManager.navigate(Route.EpisodeRoute(
|
||||||
EpisodeDto(
|
EpisodeDto(
|
||||||
|
|||||||
@@ -9,85 +9,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
sealed interface SuggestedItem {
|
|
||||||
val id: UUID
|
|
||||||
val badge: String
|
|
||||||
val title: String
|
|
||||||
val supportingText: String
|
|
||||||
val description: String
|
|
||||||
val metadata: List<String>
|
|
||||||
val imageUrl: String
|
|
||||||
val ctaLabel: String
|
|
||||||
val progress: Float?
|
|
||||||
val type: MediaKind
|
|
||||||
}
|
|
||||||
|
|
||||||
data class SuggestedEpisode (
|
|
||||||
val episode: Episode,
|
|
||||||
override val badge: String = "",
|
|
||||||
override val supportingText: String = listOf("Episode ${episode.index}", episode.runtime)
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.joinToString(" • "),
|
|
||||||
override val metadata: List<String> =
|
|
||||||
listOf(episode.releaseDate, episode.runtime, episode.rating, episode.format)
|
|
||||||
.filter { it.isNotBlank() },
|
|
||||||
override val ctaLabel: String = "Open",
|
|
||||||
override val progress: Float? = episode.progress?.toFloat(),
|
|
||||||
override val type: MediaKind = MediaKind.EPISODE,
|
|
||||||
override val id: UUID = episode.id,
|
|
||||||
override val title: String = episode.title,
|
|
||||||
override val description: String = episode.synopsis,
|
|
||||||
override val imageUrl: String = ImageUrlBuilder.finishImageUrl(
|
|
||||||
prefixImageUrl = episode.imageUrlPrefix,
|
|
||||||
artworkKind = ArtworkKind.PRIMARY
|
|
||||||
)
|
|
||||||
) : SuggestedItem
|
|
||||||
|
|
||||||
data class SuggestedSeries (
|
|
||||||
val series: Series,
|
|
||||||
override val badge: String = "",
|
|
||||||
override val supportingText: String =
|
|
||||||
if (series.unwatchedEpisodeCount > 0) {
|
|
||||||
"${series.unwatchedEpisodeCount} unwatched episodes"
|
|
||||||
} else {
|
|
||||||
"${series.seasonCount} seasons"
|
|
||||||
},
|
|
||||||
override val metadata: List<String> =
|
|
||||||
listOf(series.year, "${series.seasonCount} seasons").filter { it.isNotBlank() },
|
|
||||||
override val ctaLabel: String = "Open",
|
|
||||||
override val progress: Float? = null,
|
|
||||||
override val type: MediaKind = MediaKind.SERIES,
|
|
||||||
override val id: UUID = series.id,
|
|
||||||
override val title: String = series.name,
|
|
||||||
override val description: String = series.synopsis,
|
|
||||||
override val imageUrl: String = ImageUrlBuilder.finishImageUrl(
|
|
||||||
prefixImageUrl = series.imageUrlPrefix,
|
|
||||||
artworkKind = ArtworkKind.PRIMARY
|
|
||||||
)
|
|
||||||
) : SuggestedItem
|
|
||||||
|
|
||||||
data class SuggestedMovie (
|
|
||||||
val movie: Movie,
|
|
||||||
override val badge: String = "",
|
|
||||||
override val supportingText: String = listOf(movie.year, movie.runtime)
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.joinToString(" • "),
|
|
||||||
override val metadata: List<String> =
|
|
||||||
listOf(movie.year, movie.runtime, movie.rating, movie.format)
|
|
||||||
.filter { it.isNotBlank() },
|
|
||||||
override val ctaLabel: String = "Open",
|
|
||||||
override val progress: Float? = movie.progress?.toFloat(),
|
|
||||||
override val type: MediaKind = MediaKind.MOVIE,
|
|
||||||
override val id: UUID = movie.id,
|
|
||||||
override val title: String = movie.title,
|
|
||||||
override val description: String = movie.synopsis,
|
|
||||||
override val imageUrl: String = ImageUrlBuilder.finishImageUrl(
|
|
||||||
prefixImageUrl = movie.imageUrlPrefix,
|
|
||||||
artworkKind = ArtworkKind.PRIMARY
|
|
||||||
)
|
|
||||||
) : SuggestedItem
|
|
||||||
|
|
||||||
sealed interface FocusableItem {
|
sealed interface FocusableItem {
|
||||||
val imageUrl: String
|
val imageUrl: String
|
||||||
val primaryText: String
|
val primaryText: String
|
||||||
|
|||||||
Reference in New Issue
Block a user