From 44556401428930d58aeaeaf0d4bc2d2d2f151a25 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Tue, 31 Mar 2026 11:58:57 +0200 Subject: [PATCH] Refactor home composables into subpackages --- .../bbara/purefin/app/home/DownloadsScreen.kt | 2 +- .../hu/bbara/purefin/app/home/HomeScreen.kt | 2 +- .../bbara/purefin/app/home/LibrariesScreen.kt | 4 +- .../bbara/purefin/app/home/ui/HomeContent.kt | 5 + .../bbara/purefin/app/home/ui/HomeSections.kt | 724 ------------------ .../continuewatching/ContinueWatchingCard.kt | 137 ++++ .../ContinueWatchingSection.kt | 44 ++ .../DownloadingItemRow.kt} | 96 +-- .../app/home/ui/downloads/DownloadsContent.kt | 107 +++ .../app/home/ui/featured/HomeFeaturedCard.kt | 122 +++ .../home/ui/featured/HomeFeaturedSection.kt | 80 ++ .../app/home/ui/libraries/LibrariesContent.kt | 36 + .../LibraryListItem.kt} | 33 +- .../app/home/ui/library/HomeBrowseCard.kt | 158 ++++ .../home/ui/library/LibraryPosterSection.kt | 53 ++ .../purefin/app/home/ui/nextup/NextUpCard.kt | 101 +++ .../app/home/ui/nextup/NextUpSection.kt | 42 + .../home/ui/{ => search}/HomeSearchOverlay.kt | 102 +-- .../home/ui/search/HomeSearchResultCard.kt | 72 ++ .../app/home/ui/search/SearchMessage.kt | 56 ++ .../app/home/ui/shared/ContentBadge.kt | 34 + .../app/home/ui/shared/HomeEmptyState.kt | 83 ++ .../app/home/ui/shared/SectionHeader.kt | 53 ++ 23 files changed, 1192 insertions(+), 954 deletions(-) delete mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingCard.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingSection.kt rename app/src/main/java/hu/bbara/purefin/app/home/ui/{DownloadsContent.kt => downloads/DownloadingItemRow.kt} (53%) create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadsContent.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/featured/HomeFeaturedCard.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/featured/HomeFeaturedSection.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibrariesContent.kt rename app/src/main/java/hu/bbara/purefin/app/home/ui/{LibrariesContent.kt => libraries/LibraryListItem.kt} (52%) create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/library/HomeBrowseCard.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/library/LibraryPosterSection.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpCard.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpSection.kt rename app/src/main/java/hu/bbara/purefin/app/home/ui/{ => search}/HomeSearchOverlay.kt (64%) create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchResultCard.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/search/SearchMessage.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/shared/ContentBadge.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/shared/HomeEmptyState.kt create mode 100644 app/src/main/java/hu/bbara/purefin/app/home/ui/shared/SectionHeader.kt diff --git a/app/src/main/java/hu/bbara/purefin/app/home/DownloadsScreen.kt b/app/src/main/java/hu/bbara/purefin/app/home/DownloadsScreen.kt index da3d6f6..65b8298 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/DownloadsScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/DownloadsScreen.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import hu.bbara.purefin.app.home.ui.DownloadsContent +import hu.bbara.purefin.app.home.ui.downloads.DownloadsContent @Composable fun DownloadsScreen( diff --git a/app/src/main/java/hu/bbara/purefin/app/home/HomeScreen.kt b/app/src/main/java/hu/bbara/purefin/app/home/HomeScreen.kt index 643b1ec..4ab7559 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/HomeScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/HomeScreen.kt @@ -15,12 +15,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview 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.homePreviewContinueWatching import hu.bbara.purefin.app.home.ui.homePreviewLibraries import hu.bbara.purefin.app.home.ui.homePreviewLibraryContent 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.LibraryItem import hu.bbara.purefin.feature.shared.home.NextUpItem diff --git a/app/src/main/java/hu/bbara/purefin/app/home/LibrariesScreen.kt b/app/src/main/java/hu/bbara/purefin/app/home/LibrariesScreen.kt index bedfeaf..3872d74 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/LibrariesScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/LibrariesScreen.kt @@ -6,9 +6,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable 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.LibrariesContent +import hu.bbara.purefin.app.home.ui.HomeNavItem +import hu.bbara.purefin.app.home.ui.libraries.LibrariesContent @Composable fun LibrariesScreen( diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt index b85ffc8..fc031e3 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt @@ -15,6 +15,11 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview 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.Movie import hu.bbara.purefin.core.model.Series diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt deleted file mode 100644 index e6031bd..0000000 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt +++ /dev/null @@ -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, - 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, - 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, - 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, - 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) - ) - } -} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingCard.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingCard.kt new file mode 100644 index 0000000..08120e1 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingCard.kt @@ -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 + ) + } + } + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingSection.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingSection.kt new file mode 100644 index 0000000..c185a55 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingSection.kt @@ -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, + 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 + ) + } + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/DownloadsContent.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadingItemRow.kt similarity index 53% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/DownloadsContent.kt rename to app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadingItemRow.kt index 1096084..99237cb 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/DownloadsContent.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadingItemRow.kt @@ -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.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -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.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.material.icons.Icons import androidx.compose.material.icons.outlined.Close -import androidx.compose.material.icons.outlined.Download import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon @@ -27,7 +19,6 @@ import androidx.compose.material3.LinearProgressIndicator 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.draw.clip @@ -35,94 +26,11 @@ 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.hilt.navigation.compose.hiltViewModel -import hu.bbara.purefin.common.ui.PosterCard import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.feature.shared.download.ActiveDownloadItem -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 = { _, _, _ -> }, - ) - } - } -} - -@Composable -private fun DownloadingItemRow( +internal fun DownloadingItemRow( item: ActiveDownloadItem, onCancel: (String) -> Unit, ) { diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadsContent.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadsContent.kt new file mode 100644 index 0000000..af4fbc9 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadsContent.kt @@ -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 = { _, _, _ -> }, + ) + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/featured/HomeFeaturedCard.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/featured/HomeFeaturedCard.kt new file mode 100644 index 0000000..20a04e2 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/featured/HomeFeaturedCard.kt @@ -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) + ) + } + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/featured/HomeFeaturedSection.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/featured/HomeFeaturedSection.kt new file mode 100644 index 0000000..20b545a --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/featured/HomeFeaturedSection.kt @@ -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, + 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) + ) + } + } + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibrariesContent.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibrariesContent.kt new file mode 100644 index 0000000..9655fc6 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibrariesContent.kt @@ -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, + 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) + } + ) + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/LibrariesContent.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibraryListItem.kt similarity index 52% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/LibrariesContent.kt rename to app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibraryListItem.kt index d5d7fc6..1fb9934 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/LibrariesContent.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibraryListItem.kt @@ -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.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth 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.material3.MaterialTheme import androidx.compose.material3.Text @@ -17,32 +11,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp +import hu.bbara.purefin.app.home.ui.HomeNavItem import hu.bbara.purefin.common.ui.components.PurefinAsyncImage -@Composable -fun LibrariesContent( - items: List, - 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 fun LibraryListItem( item: HomeNavItem, diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/library/HomeBrowseCard.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/library/HomeBrowseCard.kt new file mode 100644 index 0000000..abe99ed --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/library/HomeBrowseCard.kt @@ -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 + ) + } + } + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/library/LibraryPosterSection.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/library/LibraryPosterSection.kt new file mode 100644 index 0000000..cd2ff9d --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/library/LibraryPosterSection.kt @@ -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, + 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 + ) + } + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpCard.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpCard.kt new file mode 100644 index 0000000..ffb1596 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpCard.kt @@ -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 + ) + } + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpSection.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpSection.kt new file mode 100644 index 0000000..fdbe305 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpSection.kt @@ -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, + 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 + ) + } + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSearchOverlay.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchOverlay.kt similarity index 64% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSearchOverlay.kt rename to app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchOverlay.kt index 6a9f895..79e67bb 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSearchOverlay.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchOverlay.kt @@ -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.compose.foundation.background @@ -9,12 +9,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues 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.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.material.icons.Icons import androidx.compose.material.icons.outlined.Close 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.SearchBar import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -35,15 +32,9 @@ import androidx.compose.runtime.saveable.rememberSaveable 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.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.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 org.jellyfin.sdk.model.UUID 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 - ) - } - } -} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchResultCard.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchResultCard.kt new file mode 100644 index 0000000..2c955d9 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchResultCard.kt @@ -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 + ) + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/search/SearchMessage.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/search/SearchMessage.kt new file mode 100644 index 0000000..f4f39c9 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/search/SearchMessage.kt @@ -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 + ) + } + } + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/ContentBadge.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/ContentBadge.kt new file mode 100644 index 0000000..ee7dec4 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/ContentBadge.kt @@ -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) + ) + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/HomeEmptyState.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/HomeEmptyState.kt new file mode 100644 index 0000000..36581a0 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/HomeEmptyState.kt @@ -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") + } + } + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/SectionHeader.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/SectionHeader.kt new file mode 100644 index 0000000..80919ee --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/SectionHeader.kt @@ -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) + ) + } + } + } +}