mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
Refactor home composables into subpackages
This commit is contained in:
@@ -6,7 +6,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import hu.bbara.purefin.app.home.ui.DownloadsContent
|
import hu.bbara.purefin.app.home.ui.downloads.DownloadsContent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadsScreen(
|
fun DownloadsScreen(
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import hu.bbara.purefin.app.home.ui.HomeContent
|
import hu.bbara.purefin.app.home.ui.HomeContent
|
||||||
import hu.bbara.purefin.app.home.ui.HomeSearchOverlay
|
|
||||||
import hu.bbara.purefin.app.home.ui.HomeTopBar
|
import hu.bbara.purefin.app.home.ui.HomeTopBar
|
||||||
import hu.bbara.purefin.app.home.ui.homePreviewContinueWatching
|
import hu.bbara.purefin.app.home.ui.homePreviewContinueWatching
|
||||||
import hu.bbara.purefin.app.home.ui.homePreviewLibraries
|
import hu.bbara.purefin.app.home.ui.homePreviewLibraries
|
||||||
import hu.bbara.purefin.app.home.ui.homePreviewLibraryContent
|
import hu.bbara.purefin.app.home.ui.homePreviewLibraryContent
|
||||||
import hu.bbara.purefin.app.home.ui.homePreviewNextUp
|
import hu.bbara.purefin.app.home.ui.homePreviewNextUp
|
||||||
|
import hu.bbara.purefin.app.home.ui.search.HomeSearchOverlay
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import hu.bbara.purefin.app.home.ui.HomeNavItem
|
|
||||||
import hu.bbara.purefin.app.home.ui.DefaultTopBar
|
import hu.bbara.purefin.app.home.ui.DefaultTopBar
|
||||||
import hu.bbara.purefin.app.home.ui.LibrariesContent
|
import hu.bbara.purefin.app.home.ui.HomeNavItem
|
||||||
|
import hu.bbara.purefin.app.home.ui.libraries.LibrariesContent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibrariesScreen(
|
fun LibrariesScreen(
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.app.home.ui.continuewatching.ContinueWatchingSection
|
||||||
|
import hu.bbara.purefin.app.home.ui.featured.HomeFeaturedSection
|
||||||
|
import hu.bbara.purefin.app.home.ui.library.LibraryPosterSection
|
||||||
|
import hu.bbara.purefin.app.home.ui.nextup.NextUpSection
|
||||||
|
import hu.bbara.purefin.app.home.ui.shared.HomeEmptyState
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
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
|
||||||
|
|||||||
@@ -1,724 +0,0 @@
|
|||||||
package hu.bbara.purefin.app.home.ui
|
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
|
||||||
import androidx.compose.material.icons.outlined.Collections
|
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
|
||||||
import androidx.compose.material3.FilledTonalButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
|
||||||
import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator
|
|
||||||
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
|
||||||
import org.jellyfin.sdk.model.UUID
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HomeFeaturedSection(
|
|
||||||
items: List<FeaturedHomeItem>,
|
|
||||||
onOpenFeaturedItem: (FeaturedHomeItem) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
if (items.isEmpty()) return
|
|
||||||
|
|
||||||
val pagerState = rememberPagerState(pageCount = { items.size })
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
HorizontalPager(
|
|
||||||
state = pagerState,
|
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 56.dp),
|
|
||||||
pageSpacing = 16.dp,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) { page ->
|
|
||||||
HomeFeaturedCard(
|
|
||||||
item = items[page],
|
|
||||||
onClick = { onOpenFeaturedItem(items[page]) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (items.size > 1) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
repeat(items.size) { index ->
|
|
||||||
val selected = pagerState.currentPage == index
|
|
||||||
val indicatorWidth = animateDpAsState(
|
|
||||||
targetValue = if (selected) 22.dp else 8.dp
|
|
||||||
)
|
|
||||||
val indicatorColor = animateColorAsState(
|
|
||||||
targetValue = if (selected) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.outlineVariant
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(indicatorWidth.value)
|
|
||||||
.height(8.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(indicatorColor.value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun HomeFeaturedCard(
|
|
||||||
item: FeaturedHomeItem,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
val description = item.description.trim()
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
color = scheme.surfaceContainerLow,
|
|
||||||
shape = RoundedCornerShape(30.dp),
|
|
||||||
tonalElevation = 4.dp,
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(30.dp))
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(16f / 11f)
|
|
||||||
) {
|
|
||||||
PurefinAsyncImage(
|
|
||||||
model = item.imageUrl,
|
|
||||||
contentDescription = item.title,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.background(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Black.copy(alpha = 0.08f),
|
|
||||||
Color.Black.copy(alpha = 0.18f),
|
|
||||||
Color.Black.copy(alpha = 0.72f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(24.dp)
|
|
||||||
) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
|
||||||
Text(
|
|
||||||
text = item.title
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = item.title,
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
color = Color.White,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
if (item.metadata.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = item.metadata.joinToString(" • "),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = Color.White.copy(alpha = 0.88f),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (description.isNotBlank()) {
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = Color.White.copy(alpha = 0.88f),
|
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.widthIn(max = 520.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (item.progress != null && item.progress > 0f) {
|
|
||||||
MediaProgressBar(
|
|
||||||
progress = item.progress.coerceIn(0f, 1f),
|
|
||||||
foregroundColor = scheme.primary,
|
|
||||||
backgroundColor = Color.White.copy(alpha = 0.26f),
|
|
||||||
modifier = Modifier.align(Alignment.BottomStart)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ContinueWatchingSection(
|
|
||||||
items: List<ContinueWatchingItem>,
|
|
||||||
onMovieSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
if (items.isEmpty()) return
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
SectionHeader(title = "Continue Watching")
|
|
||||||
LazyRow(
|
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
items(items = items, key = { item -> item.id }) { item ->
|
|
||||||
ContinueWatchingCard(
|
|
||||||
item = item,
|
|
||||||
onMovieSelected = onMovieSelected,
|
|
||||||
onEpisodeSelected = onEpisodeSelected
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ContinueWatchingCard(
|
|
||||||
item: ContinueWatchingItem,
|
|
||||||
onMovieSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
val supportingText = when (item.type) {
|
|
||||||
BaseItemKind.MOVIE -> listOf(
|
|
||||||
item.movie?.year,
|
|
||||||
item.movie?.runtime
|
|
||||||
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
|
||||||
|
|
||||||
BaseItemKind.EPISODE -> listOf(
|
|
||||||
"Episode ${item.episode?.index}",
|
|
||||||
item.episode?.runtime
|
|
||||||
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
|
||||||
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
val imageUrl = when (item.type) {
|
|
||||||
BaseItemKind.MOVIE -> item.movie?.heroImageUrl
|
|
||||||
BaseItemKind.EPISODE -> item.episode?.heroImageUrl
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(26.dp),
|
|
||||||
color = scheme.surfaceContainer,
|
|
||||||
tonalElevation = 3.dp,
|
|
||||||
modifier = modifier.width(320.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
when (item.type) {
|
|
||||||
BaseItemKind.MOVIE -> onMovieSelected(item.movie!!.id)
|
|
||||||
BaseItemKind.EPISODE -> {
|
|
||||||
val episode = item.episode!!
|
|
||||||
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(16f / 9f)
|
|
||||||
.background(scheme.surfaceContainer)
|
|
||||||
) {
|
|
||||||
if (imageUrl != null) {
|
|
||||||
PurefinAsyncImage(
|
|
||||||
model = imageUrl,
|
|
||||||
contentDescription = item.primaryText,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.background(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
Color.Black.copy(alpha = 0.08f),
|
|
||||||
Color.Black.copy(alpha = 0.38f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
MediaProgressBar(
|
|
||||||
progress = (item.progress.toFloat() / 100f).coerceIn(0f, 1f),
|
|
||||||
foregroundColor = scheme.primary,
|
|
||||||
backgroundColor = Color.White.copy(alpha = 0.24f),
|
|
||||||
modifier = Modifier.align(Alignment.BottomStart)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = item.primaryText,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
if (supportingText.isNotBlank()) {
|
|
||||||
Text(
|
|
||||||
text = supportingText,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = scheme.onSurfaceVariant,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NextUpSection(
|
|
||||||
items: List<NextUpItem>,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
if (items.isEmpty()) return
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
SectionHeader(title = "Next Up")
|
|
||||||
LazyRow(
|
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
items(items = items, key = { item -> item.id }) { item ->
|
|
||||||
NextUpCard(
|
|
||||||
item = item,
|
|
||||||
onEpisodeSelected = onEpisodeSelected
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NextUpCard(
|
|
||||||
item: NextUpItem,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(24.dp),
|
|
||||||
color = scheme.surfaceContainer,
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
modifier = modifier.width(256.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
onEpisodeSelected(
|
|
||||||
item.episode.seriesId, item.episode.seasonId, item.episode.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(16f / 10f)
|
|
||||||
.background(scheme.surfaceVariant)
|
|
||||||
) {
|
|
||||||
PurefinAsyncImage(
|
|
||||||
model = item.episode.heroImageUrl,
|
|
||||||
contentDescription = item.primaryText,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.background(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
Color.Transparent,
|
|
||||||
Color.Black.copy(alpha = 0.26f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = item.primaryText,
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = listOf("Episode ${item.episode.index}", item.episode.runtime, item.secondaryText)
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.joinToString(" • "),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = scheme.onSurfaceVariant,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LibraryPosterSection(
|
|
||||||
library: LibraryItem,
|
|
||||||
items: List<PosterItem>,
|
|
||||||
onLibrarySelected: (LibraryItem) -> Unit,
|
|
||||||
onMovieSelected: (UUID) -> Unit,
|
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
if (items.isEmpty()) return
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
SectionHeader(
|
|
||||||
title = library.name,
|
|
||||||
actionLabel = "See all",
|
|
||||||
onActionClick = { onLibrarySelected(library) }
|
|
||||||
)
|
|
||||||
LazyRow(
|
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
items(items = items, key = { item -> item.id }) { item ->
|
|
||||||
HomeBrowseCard(
|
|
||||||
item = item,
|
|
||||||
onMovieSelected = onMovieSelected,
|
|
||||||
onSeriesSelected = onSeriesSelected,
|
|
||||||
onEpisodeSelected = onEpisodeSelected
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun HomeBrowseCard(
|
|
||||||
item: PosterItem,
|
|
||||||
onMovieSelected: (UUID) -> Unit,
|
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
val supportingText = when (item.type) {
|
|
||||||
BaseItemKind.MOVIE -> listOf(
|
|
||||||
item.movie?.year,
|
|
||||||
item.movie?.runtime
|
|
||||||
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
|
||||||
|
|
||||||
BaseItemKind.SERIES -> item.series!!.let { series ->
|
|
||||||
if (series.seasonCount == 1) "1 season" else "${series.seasonCount} seasons"
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseItemKind.EPISODE -> listOf(
|
|
||||||
"Episode ${item.episode?.index}",
|
|
||||||
item.episode?.runtime
|
|
||||||
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
|
||||||
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = scheme.surfaceContainer,
|
|
||||||
modifier = modifier.width(188.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
when (item.type) {
|
|
||||||
BaseItemKind.MOVIE -> onMovieSelected(item.id)
|
|
||||||
BaseItemKind.SERIES -> onSeriesSelected(item.id)
|
|
||||||
BaseItemKind.EPISODE -> {
|
|
||||||
val episode = item.episode!!
|
|
||||||
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(16f / 10f)
|
|
||||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
|
||||||
.border(
|
|
||||||
1.dp, scheme.outlineVariant.copy(alpha = 0.35f), RoundedCornerShape(18.dp)
|
|
||||||
)
|
|
||||||
.background(scheme.surfaceVariant)
|
|
||||||
) {
|
|
||||||
PurefinAsyncImage(
|
|
||||||
model = item.imageUrl,
|
|
||||||
contentDescription = item.title,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
when (item.type) {
|
|
||||||
BaseItemKind.MOVIE -> {
|
|
||||||
val movie = item.movie!!
|
|
||||||
WatchStateIndicator(
|
|
||||||
size = 28,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.padding(8.dp),
|
|
||||||
watched = movie.watched,
|
|
||||||
started = (movie.progress ?: 0.0) > 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseItemKind.EPISODE -> {
|
|
||||||
val episode = item.episode!!
|
|
||||||
WatchStateIndicator(
|
|
||||||
size = 28,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.padding(8.dp),
|
|
||||||
watched = episode.watched,
|
|
||||||
started = (episode.progress ?: 0.0) > 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseItemKind.SERIES -> {
|
|
||||||
UnwatchedEpisodeIndicator(
|
|
||||||
size = 28,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.padding(8.dp),
|
|
||||||
unwatchedCount = item.series!!.unwatchedEpisodeCount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
|
||||||
Column(modifier = modifier.padding(12.dp)) {
|
|
||||||
Text(
|
|
||||||
text = item.title,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
if (supportingText.isNotBlank()) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = supportingText,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = scheme.onSurfaceVariant,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SectionHeader(
|
|
||||||
title: String,
|
|
||||||
actionLabel: String? = null,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onActionClick: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
if (actionLabel != null) {
|
|
||||||
TextButton(onClick = onActionClick) {
|
|
||||||
Text(text = actionLabel)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Outlined.ArrowForward,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HomeEmptyState(
|
|
||||||
onRefresh: () -> Unit,
|
|
||||||
onBrowseLibrariesClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(30.dp),
|
|
||||||
color = scheme.surfaceContainerLow,
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
modifier = modifier.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(18.dp),
|
|
||||||
modifier = Modifier.padding(24.dp)
|
|
||||||
) {
|
|
||||||
ContentBadge(
|
|
||||||
text = "Home is warming up",
|
|
||||||
containerColor = scheme.primaryContainer,
|
|
||||||
contentColor = scheme.onPrimaryContainer
|
|
||||||
)
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = "Nothing is on deck yet",
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Pull to refresh for recent activity or jump into your libraries to start browsing.",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = scheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
FilledTonalButton(onClick = onRefresh) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Refresh,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text("Refresh")
|
|
||||||
}
|
|
||||||
OutlinedButton(onClick = onBrowseLibrariesClick) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Collections,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text("Browse libraries")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ContentBadge(
|
|
||||||
text: String,
|
|
||||||
containerColor: Color,
|
|
||||||
contentColor: Color,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
color = containerColor,
|
|
||||||
shape = CircleShape,
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
color = contentColor,
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.continuewatching
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
||||||
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ContinueWatchingCard(
|
||||||
|
item: ContinueWatchingItem,
|
||||||
|
onMovieSelected: (UUID) -> Unit,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val supportingText = when (item.type) {
|
||||||
|
BaseItemKind.MOVIE -> listOf(
|
||||||
|
item.movie?.year,
|
||||||
|
item.movie?.runtime
|
||||||
|
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
||||||
|
|
||||||
|
BaseItemKind.EPISODE -> listOf(
|
||||||
|
"Episode ${item.episode?.index}",
|
||||||
|
item.episode?.runtime
|
||||||
|
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
||||||
|
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
val imageUrl = when (item.type) {
|
||||||
|
BaseItemKind.MOVIE -> item.movie?.heroImageUrl
|
||||||
|
BaseItemKind.EPISODE -> item.episode?.heroImageUrl
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(26.dp),
|
||||||
|
color = scheme.surfaceContainer,
|
||||||
|
tonalElevation = 3.dp,
|
||||||
|
modifier = modifier.width(320.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
when (item.type) {
|
||||||
|
BaseItemKind.MOVIE -> onMovieSelected(item.movie!!.id)
|
||||||
|
BaseItemKind.EPISODE -> {
|
||||||
|
val episode = item.episode!!
|
||||||
|
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(16f / 9f)
|
||||||
|
.background(scheme.surfaceContainer)
|
||||||
|
) {
|
||||||
|
if (imageUrl != null) {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = imageUrl,
|
||||||
|
contentDescription = item.primaryText,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.08f),
|
||||||
|
Color.Black.copy(alpha = 0.38f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
MediaProgressBar(
|
||||||
|
progress = (item.progress.toFloat() / 100f).coerceIn(0f, 1f),
|
||||||
|
foregroundColor = scheme.primary,
|
||||||
|
backgroundColor = Color.White.copy(alpha = 0.24f),
|
||||||
|
modifier = Modifier.align(Alignment.BottomStart)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = item.primaryText,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
if (supportingText.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = supportingText,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.continuewatching
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.app.home.ui.shared.SectionHeader
|
||||||
|
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContinueWatchingSection(
|
||||||
|
items: List<ContinueWatchingItem>,
|
||||||
|
onMovieSelected: (UUID) -> Unit,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (items.isEmpty()) return
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
SectionHeader(title = "Continue Watching")
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(items = items, key = { item -> item.id }) { item ->
|
||||||
|
ContinueWatchingCard(
|
||||||
|
item = item,
|
||||||
|
onMovieSelected = onMovieSelected,
|
||||||
|
onEpisodeSelected = onEpisodeSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,16 @@
|
|||||||
package hu.bbara.purefin.app.home.ui
|
package hu.bbara.purefin.app.home.ui.downloads
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Close
|
import androidx.compose.material.icons.outlined.Close
|
||||||
import androidx.compose.material.icons.outlined.Download
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -27,7 +19,6 @@ import androidx.compose.material3.LinearProgressIndicator
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.ui.Alignment
|
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.draw.clip
|
||||||
@@ -35,94 +26,11 @@ 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 androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import hu.bbara.purefin.common.ui.PosterCard
|
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.feature.shared.download.ActiveDownloadItem
|
import hu.bbara.purefin.feature.shared.download.ActiveDownloadItem
|
||||||
import hu.bbara.purefin.feature.shared.download.DownloadsViewModel
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadsContent(
|
internal fun DownloadingItemRow(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
viewModel: DownloadsViewModel = hiltViewModel(),
|
|
||||||
) {
|
|
||||||
val downloads = viewModel.downloads.collectAsState(emptyList())
|
|
||||||
val activeDownloads = viewModel.activeDownloads.collectAsState()
|
|
||||||
|
|
||||||
val isEmpty = downloads.value.isEmpty() && activeDownloads.value.isEmpty()
|
|
||||||
|
|
||||||
if (isEmpty) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Download,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(64.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "No downloads yet",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = GridCells.Adaptive(minSize = 120.dp),
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = modifier.background(MaterialTheme.colorScheme.background)
|
|
||||||
) {
|
|
||||||
if (activeDownloads.value.isNotEmpty()) {
|
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
||||||
Text(
|
|
||||||
text = "Downloading",
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
items(
|
|
||||||
items = activeDownloads.value,
|
|
||||||
key = { it.contentId },
|
|
||||||
span = { GridItemSpan(maxLineSpan) }
|
|
||||||
) { item ->
|
|
||||||
DownloadingItemRow(
|
|
||||||
item = item,
|
|
||||||
onCancel = { viewModel.cancelDownload(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (downloads.value.isNotEmpty()) {
|
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
||||||
Text(
|
|
||||||
text = "Downloaded",
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
modifier = Modifier.padding(top = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items(downloads.value) { item ->
|
|
||||||
PosterCard(
|
|
||||||
item = item,
|
|
||||||
onMovieSelected = viewModel::onMovieSelected,
|
|
||||||
onSeriesSelected = viewModel::onSeriesSelected,
|
|
||||||
onEpisodeSelected = { _, _, _ -> },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DownloadingItemRow(
|
|
||||||
item: ActiveDownloadItem,
|
item: ActiveDownloadItem,
|
||||||
onCancel: (String) -> Unit,
|
onCancel: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.downloads
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Download
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import hu.bbara.purefin.common.ui.PosterCard
|
||||||
|
import hu.bbara.purefin.feature.shared.download.DownloadsViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DownloadsContent(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: DownloadsViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val downloads = viewModel.downloads.collectAsState(emptyList())
|
||||||
|
val activeDownloads = viewModel.activeDownloads.collectAsState()
|
||||||
|
|
||||||
|
val isEmpty = downloads.value.isEmpty() && activeDownloads.value.isEmpty()
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Download,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(64.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "No downloads yet",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(minSize = 120.dp),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = modifier.background(MaterialTheme.colorScheme.background)
|
||||||
|
) {
|
||||||
|
if (activeDownloads.value.isNotEmpty()) {
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
Text(
|
||||||
|
text = "Downloading",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(
|
||||||
|
items = activeDownloads.value,
|
||||||
|
key = { it.contentId },
|
||||||
|
span = { GridItemSpan(maxLineSpan) }
|
||||||
|
) { item ->
|
||||||
|
DownloadingItemRow(
|
||||||
|
item = item,
|
||||||
|
onCancel = { viewModel.cancelDownload(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (downloads.value.isNotEmpty()) {
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
Text(
|
||||||
|
text = "Downloaded",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(downloads.value) { item ->
|
||||||
|
PosterCard(
|
||||||
|
item = item,
|
||||||
|
onMovieSelected = viewModel::onMovieSelected,
|
||||||
|
onSeriesSelected = viewModel::onSeriesSelected,
|
||||||
|
onEpisodeSelected = { _, _, _ -> },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.featured
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.app.home.ui.FeaturedHomeItem
|
||||||
|
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
||||||
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun HomeFeaturedCard(
|
||||||
|
item: FeaturedHomeItem,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val description = item.description.trim()
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
color = scheme.surfaceContainerLow,
|
||||||
|
shape = RoundedCornerShape(30.dp),
|
||||||
|
tonalElevation = 4.dp,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(30.dp))
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(16f / 11f)
|
||||||
|
) {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = item.imageUrl,
|
||||||
|
contentDescription = item.title,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Black.copy(alpha = 0.08f),
|
||||||
|
Color.Black.copy(alpha = 0.18f),
|
||||||
|
Color.Black.copy(alpha = 0.72f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp)
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
Text(
|
||||||
|
text = item.title
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = item.title,
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
if (item.metadata.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = item.metadata.joinToString(" • "),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = Color.White.copy(alpha = 0.88f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (description.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color.White.copy(alpha = 0.88f),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.widthIn(max = 520.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.progress != null && item.progress > 0f) {
|
||||||
|
MediaProgressBar(
|
||||||
|
progress = item.progress.coerceIn(0f, 1f),
|
||||||
|
foregroundColor = scheme.primary,
|
||||||
|
backgroundColor = Color.White.copy(alpha = 0.26f),
|
||||||
|
modifier = Modifier.align(Alignment.BottomStart)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.featured
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.app.home.ui.FeaturedHomeItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeFeaturedSection(
|
||||||
|
items: List<FeaturedHomeItem>,
|
||||||
|
onOpenFeaturedItem: (FeaturedHomeItem) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (items.isEmpty()) return
|
||||||
|
|
||||||
|
val pagerState = rememberPagerState(pageCount = { items.size })
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
contentPadding = PaddingValues(start = 16.dp, end = 56.dp),
|
||||||
|
pageSpacing = 16.dp,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) { page ->
|
||||||
|
HomeFeaturedCard(
|
||||||
|
item = items[page],
|
||||||
|
onClick = { onOpenFeaturedItem(items[page]) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (items.size > 1) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
repeat(items.size) { index ->
|
||||||
|
val selected = pagerState.currentPage == index
|
||||||
|
val indicatorWidth = animateDpAsState(
|
||||||
|
targetValue = if (selected) 22.dp else 8.dp
|
||||||
|
)
|
||||||
|
val indicatorColor = animateColorAsState(
|
||||||
|
targetValue = if (selected) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.outlineVariant
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(indicatorWidth.value)
|
||||||
|
.height(8.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(indicatorColor.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.libraries
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.app.home.ui.HomeNavItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibrariesContent(
|
||||||
|
items: List<HomeNavItem>,
|
||||||
|
onLibrarySelected: (HomeNavItem) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
modifier = modifier,
|
||||||
|
columns = GridCells.Adaptive(minSize = 160.dp),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
items(items, key = { it.id }) { item ->
|
||||||
|
LibraryListItem(
|
||||||
|
item = item,
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onLibrarySelected(item)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
package hu.bbara.purefin.app.home.ui
|
package hu.bbara.purefin.app.home.ui.libraries
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -17,32 +11,9 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.app.home.ui.HomeNavItem
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LibrariesContent(
|
|
||||||
items: List<HomeNavItem>,
|
|
||||||
onLibrarySelected: (HomeNavItem) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
LazyVerticalGrid(
|
|
||||||
modifier = modifier,
|
|
||||||
columns = GridCells.Adaptive(minSize = 160.dp),
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
) {
|
|
||||||
items(items, key = { it.id }) { item ->
|
|
||||||
LibraryListItem(
|
|
||||||
item = item,
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
onLibrarySelected(item)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryListItem(
|
fun LibraryListItem(
|
||||||
item: HomeNavItem,
|
item: HomeNavItem,
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.library
|
||||||
|
|
||||||
|
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.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
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.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator
|
||||||
|
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
||||||
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun HomeBrowseCard(
|
||||||
|
item: PosterItem,
|
||||||
|
onMovieSelected: (UUID) -> Unit,
|
||||||
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val supportingText = when (item.type) {
|
||||||
|
BaseItemKind.MOVIE -> listOf(
|
||||||
|
item.movie?.year,
|
||||||
|
item.movie?.runtime
|
||||||
|
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
||||||
|
|
||||||
|
BaseItemKind.SERIES -> item.series!!.let { series ->
|
||||||
|
if (series.seasonCount == 1) "1 season" else "${series.seasonCount} seasons"
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseItemKind.EPISODE -> listOf(
|
||||||
|
"Episode ${item.episode?.index}",
|
||||||
|
item.episode?.runtime
|
||||||
|
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
||||||
|
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = scheme.surfaceContainer,
|
||||||
|
modifier = modifier.width(188.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
when (item.type) {
|
||||||
|
BaseItemKind.MOVIE -> onMovieSelected(item.id)
|
||||||
|
BaseItemKind.SERIES -> onSeriesSelected(item.id)
|
||||||
|
BaseItemKind.EPISODE -> {
|
||||||
|
val episode = item.episode!!
|
||||||
|
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(16f / 10f)
|
||||||
|
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||||
|
.border(
|
||||||
|
1.dp, scheme.outlineVariant.copy(alpha = 0.35f), RoundedCornerShape(18.dp)
|
||||||
|
)
|
||||||
|
.background(scheme.surfaceVariant)
|
||||||
|
) {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = item.imageUrl,
|
||||||
|
contentDescription = item.title,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
when (item.type) {
|
||||||
|
BaseItemKind.MOVIE -> {
|
||||||
|
val movie = item.movie!!
|
||||||
|
WatchStateIndicator(
|
||||||
|
size = 28,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(8.dp),
|
||||||
|
watched = movie.watched,
|
||||||
|
started = (movie.progress ?: 0.0) > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseItemKind.EPISODE -> {
|
||||||
|
val episode = item.episode!!
|
||||||
|
WatchStateIndicator(
|
||||||
|
size = 28,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(8.dp),
|
||||||
|
watched = episode.watched,
|
||||||
|
started = (episode.progress ?: 0.0) > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseItemKind.SERIES -> {
|
||||||
|
UnwatchedEpisodeIndicator(
|
||||||
|
size = 28,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(8.dp),
|
||||||
|
unwatchedCount = item.series!!.unwatchedEpisodeCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
Column(modifier = modifier.padding(12.dp)) {
|
||||||
|
Text(
|
||||||
|
text = item.title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
if (supportingText.isNotBlank()) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = supportingText,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.library
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.app.home.ui.shared.SectionHeader
|
||||||
|
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||||
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryPosterSection(
|
||||||
|
library: LibraryItem,
|
||||||
|
items: List<PosterItem>,
|
||||||
|
onLibrarySelected: (LibraryItem) -> Unit,
|
||||||
|
onMovieSelected: (UUID) -> Unit,
|
||||||
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (items.isEmpty()) return
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
SectionHeader(
|
||||||
|
title = library.name,
|
||||||
|
actionLabel = "See all",
|
||||||
|
onActionClick = { onLibrarySelected(library) }
|
||||||
|
)
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(items = items, key = { item -> item.id }) { item ->
|
||||||
|
HomeBrowseCard(
|
||||||
|
item = item,
|
||||||
|
onMovieSelected = onMovieSelected,
|
||||||
|
onSeriesSelected = onSeriesSelected,
|
||||||
|
onEpisodeSelected = onEpisodeSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.nextup
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun NextUpCard(
|
||||||
|
item: NextUpItem,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
color = scheme.surfaceContainer,
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
modifier = modifier.width(256.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
onEpisodeSelected(
|
||||||
|
item.episode.seriesId, item.episode.seasonId, item.episode.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(16f / 10f)
|
||||||
|
.background(scheme.surfaceVariant)
|
||||||
|
) {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = item.episode.heroImageUrl,
|
||||||
|
contentDescription = item.primaryText,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.26f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = item.primaryText,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = listOf("Episode ${item.episode.index}", item.episode.runtime, item.secondaryText)
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.joinToString(" • "),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.nextup
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.app.home.ui.shared.SectionHeader
|
||||||
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NextUpSection(
|
||||||
|
items: List<NextUpItem>,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (items.isEmpty()) return
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
SectionHeader(title = "Next Up")
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(items = items, key = { item -> item.id }) { item ->
|
||||||
|
NextUpCard(
|
||||||
|
item = item,
|
||||||
|
onEpisodeSelected = onEpisodeSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.home.ui
|
package hu.bbara.purefin.app.home.ui.search
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -9,12 +9,10 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Close
|
import androidx.compose.material.icons.outlined.Close
|
||||||
import androidx.compose.material.icons.outlined.Search
|
import androidx.compose.material.icons.outlined.Search
|
||||||
@@ -24,7 +22,6 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SearchBar
|
import androidx.compose.material3.SearchBar
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -35,15 +32,9 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
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.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 androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
|
||||||
import hu.bbara.purefin.core.model.SearchResult
|
|
||||||
import hu.bbara.purefin.feature.shared.search.SearchViewModel
|
import hu.bbara.purefin.feature.shared.search.SearchViewModel
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
@@ -164,94 +155,3 @@ fun HomeSearchOverlay(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SearchMessage(
|
|
||||||
title: String,
|
|
||||||
body: String,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 24.dp, vertical = 32.dp)
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(24.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
tonalElevation = 1.dp,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(24.dp)
|
|
||||||
) {
|
|
||||||
androidx.compose.foundation.layout.Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = body,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun HomeSearchResultCard(
|
|
||||||
item: SearchResult,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(22.dp),
|
|
||||||
color = scheme.surfaceContainer,
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(22.dp))
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
) {
|
|
||||||
androidx.compose.foundation.layout.Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
modifier = Modifier.padding(12.dp)
|
|
||||||
) {
|
|
||||||
PurefinAsyncImage(
|
|
||||||
model = item.posterUrl,
|
|
||||||
contentDescription = item.title,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(180.dp)
|
|
||||||
.clip(RoundedCornerShape(18.dp))
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = item.title,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = when (item.type) {
|
|
||||||
BaseItemKind.MOVIE -> "Movie"
|
|
||||||
BaseItemKind.SERIES -> "Series"
|
|
||||||
else -> "Title"
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = scheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.search
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.core.model.SearchResult
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun HomeSearchResultCard(
|
||||||
|
item: SearchResult,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(22.dp),
|
||||||
|
color = scheme.surfaceContainer,
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(22.dp))
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
modifier = Modifier.padding(12.dp)
|
||||||
|
) {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = item.posterUrl,
|
||||||
|
contentDescription = item.title,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(180.dp)
|
||||||
|
.clip(RoundedCornerShape(18.dp))
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = item.title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = when (item.type) {
|
||||||
|
BaseItemKind.MOVIE -> "Movie"
|
||||||
|
BaseItemKind.SERIES -> "Series"
|
||||||
|
else -> "Title"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = scheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.search
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SearchMessage(
|
||||||
|
title: String,
|
||||||
|
body: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 32.dp)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = body,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.shared
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ContentBadge(
|
||||||
|
text: String,
|
||||||
|
containerColor: Color,
|
||||||
|
contentColor: Color,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
color = containerColor,
|
||||||
|
shape = CircleShape,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = contentColor,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.shared
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Collections
|
||||||
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeEmptyState(
|
||||||
|
onRefresh: () -> Unit,
|
||||||
|
onBrowseLibrariesClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(30.dp),
|
||||||
|
color = scheme.surfaceContainerLow,
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
modifier = modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(18.dp),
|
||||||
|
modifier = Modifier.padding(24.dp)
|
||||||
|
) {
|
||||||
|
ContentBadge(
|
||||||
|
text = "Home is warming up",
|
||||||
|
containerColor = scheme.primaryContainer,
|
||||||
|
contentColor = scheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "Nothing is on deck yet",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Pull to refresh for recent activity or jump into your libraries to start browsing.",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = scheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
FilledTonalButton(onClick = onRefresh) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Refresh,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Refresh")
|
||||||
|
}
|
||||||
|
OutlinedButton(onClick = onBrowseLibrariesClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Collections,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Browse libraries")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package hu.bbara.purefin.app.home.ui.shared
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SectionHeader(
|
||||||
|
title: String,
|
||||||
|
actionLabel: String? = null,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onActionClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
if (actionLabel != null) {
|
||||||
|
TextButton(onClick = onActionClick) {
|
||||||
|
Text(text = actionLabel)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Outlined.ArrowForward,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user