Compare commits

...

9 Commits

Author SHA1 Message Date
ce539a1a5a Refine SuggestionsSection layout for better scaling
Adjusts the `HorizontalPager` to use a fixed page size and consistent horizontal padding. This ensures the suggestion cards have a predictable width on wider screens.
2026-03-31 23:50:19 +02:00
b08b74bab3 Replace HomeAvatar with standard Icon and update top bar layout
- Remove HomeAvatar.kt and replace usages with standard Material Icons
- Update search and profile button sizes to 50dp and icon sizes to 30dp
- Add spacing between top bar actions in HomeTopBar
2026-03-31 23:38:07 +02:00
6852e0b13a Update app branding and home screen top bar
Replaces the previous application icon with a new version (`purefin_logo_2`) across all density buckets and updates the `AndroidManifest.xml` accordingly.

The `HomeTopBar` is redesigned to feature a new brand-aligned layout:
- Replaced the generic "Watch now" title and subtitle with the Purefin logo and stylized "PureFin" text.
- Added a `PurefinLogo` component to render the raw logo resource.
- Updated the search button styling to use theme-based secondary colors.
2026-03-31 23:31:48 +02:00
72f6a37203 refactor: update MediaResumeButton to use white color for button text 2026-03-31 22:29:45 +02:00
c85767cf25 refactor: remove MediaPlayButton component as it is unused 2026-03-31 22:28:51 +02:00
8563b41111 Refactor media screens to use a unified hero section with gradient overlays
Moved title, metadata chips, and hero images into new `MediaHeroSection`, `EpisodeHeroSection`, and `SeriesHeroSection` components across Movie, Episode, and Series screens. This change:
- Increases hero section height to 40% of the screen.
- Adds a vertical gradient overlay to the hero image to improve text legibility.
- Layers title and metadata directly over the hero section instead of placing them in the scrollable content body.
- Cleans up unused imports and experimental annotations in component files.
2026-03-31 22:27:52 +02:00
3daf45a421 refactor: remove unused initialization logic in AppViewModel 2026-03-31 20:53:29 +02:00
74c532140e feat: auto-scroll to first page on SuggestionsSection load 2026-03-31 19:41:34 +02:00
e8938a9290 refactor: remove redundant Text component in SuggestionCard 2026-03-31 19:41:27 +02:00
36 changed files with 350 additions and 265 deletions

View File

@@ -12,9 +12,9 @@
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/purefin_logo" android:icon="@mipmap/purefin_logo_2"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/purefin_logo_round" android:roundIcon="@mipmap/purefin_logo_2_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Purefin"> android:theme="@style/Theme.Purefin">
<activity <activity

View File

@@ -6,8 +6,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
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.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -40,7 +38,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import hu.bbara.purefin.common.ui.MediaCastRow import hu.bbara.purefin.common.ui.MediaCastRow
import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.MediaSynopsis import hu.bbara.purefin.common.ui.MediaSynopsis
import hu.bbara.purefin.common.ui.components.GhostIconButton import hu.bbara.purefin.common.ui.components.GhostIconButton
import hu.bbara.purefin.common.ui.components.MediaActionButton import hu.bbara.purefin.common.ui.components.MediaActionButton
@@ -114,7 +111,6 @@ internal fun EpisodeTopBar(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
internal fun EpisodeDetails( internal fun EpisodeDetails(
episode: Episode, episode: Episode,
@@ -134,37 +130,6 @@ internal fun EpisodeDetails(
} }
Column(modifier = modifier) { Column(modifier = modifier) {
Text(
text = episode.title,
color = scheme.onBackground,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
lineHeight = 38.sp
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Episode ${episode.index}",
color = scheme.onBackground,
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(16.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
MediaMetaChip(text = episode.releaseDate)
MediaMetaChip(text = episode.rating)
MediaMetaChip(text = episode.runtime)
MediaMetaChip(
text = episode.format,
background = scheme.primary.copy(alpha = 0.2f),
border = scheme.primary.copy(alpha = 0.3f),
textColor = scheme.primary
)
}
Spacer(modifier = Modifier.height(24.dp))
MediaSynopsis( MediaSynopsis(
synopsis = episode.synopsis synopsis = episode.synopsis
) )

View File

@@ -4,30 +4,44 @@ import android.Manifest
import android.os.Build import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.ui.tooling.preview.Preview 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.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
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.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaHero import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.core.data.navigation.EpisodeDto import hu.bbara.purefin.core.data.navigation.EpisodeDto
import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.navigation.Route
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.feature.download.DownloadState import hu.bbara.purefin.feature.download.DownloadState
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
@@ -39,7 +53,6 @@ fun EpisodeScreen(
viewModel: EpisodeScreenViewModel = hiltViewModel(), viewModel: EpisodeScreenViewModel = hiltViewModel(),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val navigationManager = LocalNavigationManager.current
val backStack = LocalNavigationBackStack.current val backStack = LocalNavigationBackStack.current
val previousRoute = remember(backStack) { backStack.getOrNull(backStack.lastIndex - 1) } val previousRoute = remember(backStack) { backStack.getOrNull(backStack.lastIndex - 1) }
@@ -120,12 +133,7 @@ private fun EpisodeScreenInternal(
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
MediaHero( EpisodeHeroSection(episode = episode)
imageUrl = episode.heroImageUrl,
backgroundColor = MaterialTheme.colorScheme.background,
heightFraction = 0.30f,
modifier = Modifier.fillMaxWidth()
)
EpisodeDetails( EpisodeDetails(
episode = episode, episode = episode,
downloadState = downloadState, downloadState = downloadState,
@@ -139,6 +147,86 @@ private fun EpisodeScreenInternal(
} }
} }
@Composable
private fun EpisodeHeroSection(
episode: Episode,
modifier: Modifier = Modifier,
) {
val scheme = MaterialTheme.colorScheme
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val sectionHeight = screenHeight * 0.4f
Box(
modifier = modifier
.fillMaxWidth()
.height(sectionHeight)
) {
MediaHero(
imageUrl = episode.heroImageUrl,
backgroundColor = scheme.background,
modifier = Modifier.fillMaxSize()
)
Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Transparent,
scheme.background.copy(alpha = 0.5f),
scheme.background
)
)
)
)
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomStart)
.padding(16.dp)
) {
Text(
text = episode.title,
color = scheme.onBackground,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
lineHeight = 38.sp
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Episode ${episode.index}",
color = scheme.onBackground,
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(16.dp))
EpisodeMetaChips(episode = episode)
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun EpisodeMetaChips(episode: Episode) {
val scheme = MaterialTheme.colorScheme
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
MediaMetaChip(text = episode.releaseDate)
MediaMetaChip(text = episode.rating)
MediaMetaChip(text = episode.runtime)
MediaMetaChip(
text = episode.format,
background = scheme.primary.copy(alpha = 0.2f),
border = scheme.primary.copy(alpha = 0.3f),
textColor = scheme.primary
)
}
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun EpisodeScreenPreview() { private fun EpisodeScreenPreview() {

View File

@@ -4,7 +4,6 @@ import android.content.Intent
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.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
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.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -12,9 +11,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Cast import androidx.compose.material.icons.outlined.Cast
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
@@ -33,7 +30,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import hu.bbara.purefin.common.ui.MediaCastRow import hu.bbara.purefin.common.ui.MediaCastRow
import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.MediaSynopsis import hu.bbara.purefin.common.ui.MediaSynopsis
import hu.bbara.purefin.common.ui.components.GhostIconButton import hu.bbara.purefin.common.ui.components.GhostIconButton
import hu.bbara.purefin.common.ui.components.MediaActionButton import hu.bbara.purefin.common.ui.components.MediaActionButton
@@ -89,30 +85,6 @@ internal fun MovieDetails(
} }
Column(modifier = modifier) { Column(modifier = modifier) {
Text(
text = movie.title,
color = scheme.onBackground,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
lineHeight = 38.sp
)
Spacer(modifier = Modifier.height(16.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
MediaMetaChip(text = movie.year)
MediaMetaChip(text = movie.rating)
MediaMetaChip(text = movie.runtime)
MediaMetaChip(
text = movie.format,
background = scheme.primary.copy(alpha = 0.2f),
border = scheme.primary.copy(alpha = 0.3f),
textColor = scheme.primary
)
}
Spacer(modifier = Modifier.height(24.dp))
MediaSynopsis( MediaSynopsis(
synopsis = movie.synopsis synopsis = movie.synopsis
) )

View File

@@ -4,21 +4,35 @@ import android.Manifest
import android.os.Build import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.ui.tooling.preview.Preview 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.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
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.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaHero import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.core.data.navigation.MovieDto import hu.bbara.purefin.core.data.navigation.MovieDto
@@ -93,12 +107,7 @@ private fun MovieScreenInternal(
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
MediaHero( MediaHeroSection(movie = movie)
imageUrl = movie.heroImageUrl,
backgroundColor = MaterialTheme.colorScheme.background,
heightFraction = 0.30f,
modifier = Modifier.fillMaxWidth()
)
MovieDetails( MovieDetails(
movie = movie, movie = movie,
downloadState = downloadState, downloadState = downloadState,
@@ -112,6 +121,69 @@ private fun MovieScreenInternal(
} }
} }
@Composable
fun MediaHeroSection(
movie: Movie,
modifier: Modifier = Modifier) {
val scheme = MaterialTheme.colorScheme
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val sectionHeight = screenHeight * 0.4f
Box (
modifier = modifier
.height(sectionHeight)
) {
MediaHero(
imageUrl = movie.heroImageUrl,
backgroundColor = MaterialTheme.colorScheme.background,
modifier = Modifier.fillMaxSize()
)
Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Transparent,
scheme.background.copy(alpha = 0.5f),
scheme.background
)
)
)
)
Column (
modifier = Modifier.fillMaxWidth()
.align(Alignment.BottomStart)
.padding(16.dp)
) {
Text(
text = movie.title,
color = scheme.onBackground,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
lineHeight = 38.sp
)
Spacer(modifier = Modifier.height(16.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
MediaMetaChip(text = movie.year)
MediaMetaChip(text = movie.rating)
MediaMetaChip(text = movie.runtime)
MediaMetaChip(
text = movie.format,
background = scheme.primary.copy(alpha = 0.2f),
border = scheme.primary.copy(alpha = 0.3f),
textColor = scheme.primary
)
}
}
}
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun MovieScreenPreview() { private fun MovieScreenPreview() {

View File

@@ -19,7 +19,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -128,28 +134,13 @@ private fun SeriesScreenInternal(
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
MediaHero( SeriesHeroSection(series = series)
imageUrl = series.heroImageUrl,
heightFraction = 0.30f,
backgroundColor = MaterialTheme.colorScheme.background,
modifier = Modifier.fillMaxWidth()
)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(bottom = innerPadding.calculateBottomPadding()) .padding(bottom = innerPadding.calculateBottomPadding())
) { ) {
Text(
text = series.name,
color = scheme.onBackground,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
lineHeight = 36.sp
)
Spacer(modifier = Modifier.height(16.dp))
SeriesMetaChips(series = series)
Spacer(modifier = Modifier.height(24.dp))
SeriesActionButtons( SeriesActionButtons(
nextUpEpisode = nextUpEpisode, nextUpEpisode = nextUpEpisode,
seriesDownloadState = seriesDownloadState, seriesDownloadState = seriesDownloadState,
@@ -192,6 +183,58 @@ private fun SeriesScreenInternal(
} }
} }
@Composable
private fun SeriesHeroSection(
series: Series,
modifier: Modifier = Modifier,
) {
val scheme = MaterialTheme.colorScheme
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val sectionHeight = screenHeight * 0.4f
Box(
modifier = modifier
.fillMaxWidth()
.height(sectionHeight)
) {
MediaHero(
imageUrl = series.heroImageUrl,
backgroundColor = scheme.background,
modifier = Modifier.fillMaxSize()
)
Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Transparent,
scheme.background.copy(alpha = 0.5f),
scheme.background
)
)
)
)
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomStart)
.padding(16.dp)
) {
Text(
text = series.name,
color = scheme.onBackground,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
lineHeight = 36.sp
)
Spacer(modifier = Modifier.height(16.dp))
SeriesMetaChips(series = series)
}
}
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun SeriesScreenPreview() { private fun SeriesScreenPreview() {

View File

@@ -22,10 +22,10 @@ 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.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.SuggestedItem
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
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.shared.home.PosterItem
import hu.bbara.purefin.feature.shared.home.SuggestedItem
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
@@ -67,8 +67,6 @@ fun HomeScreen(
contentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
topBar = { topBar = {
HomeTopBar( HomeTopBar(
title = "Watch now",
subtitle = subtitle,
onSearchClick = { isSearchVisible = true }, onSearchClick = { isSearchVisible = true },
onProfileClick = onProfileClick, onProfileClick = onProfileClick,
onSettingsClick = onSettingsClick, onSettingsClick = onSettingsClick,

View File

@@ -11,6 +11,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -81,13 +82,10 @@ fun DefaultTopBar(
.size(56.dp) .size(56.dp)
.clip(CircleShape), .clip(CircleShape),
) { ) {
HomeAvatar( Icon(
size = 56.dp, imageVector = Icons.Outlined.Person,
borderWidth = 1.dp, contentDescription = "Profile",
borderColor = scheme.outlineVariant, modifier = Modifier.size(30.dp),
backgroundColor = scheme.secondaryContainer,
icon = Icons.Outlined.Person,
iconTint = scheme.onSecondaryContainer
) )
} }
DropdownMenu( DropdownMenu(

View File

@@ -1,41 +0,0 @@
package hu.bbara.purefin.app.home.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
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.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
@Composable
fun HomeAvatar(
size: Dp,
borderWidth: Dp,
borderColor: Color,
backgroundColor: Color,
icon: ImageVector,
iconTint: Color,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.size(size)
.clip(CircleShape)
.border(borderWidth, borderColor, CircleShape)
.background(backgroundColor),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = iconTint
)
}
}

View File

@@ -1,6 +1,8 @@
package hu.bbara.purefin.app.home.ui package hu.bbara.purefin.app.home.ui
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -11,6 +13,7 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -20,16 +23,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import 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
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import hu.bbara.purefin.common.ui.components.PurefinLogo
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomeTopBar( fun HomeTopBar(
title: String,
subtitle: String,
onSearchClick: () -> Unit, onSearchClick: () -> Unit,
onProfileClick: () -> Unit, onProfileClick: () -> Unit,
onSettingsClick: () -> Unit, onSettingsClick: () -> Unit,
@@ -41,42 +47,57 @@ fun HomeTopBar(
TopAppBar( TopAppBar(
title = { title = {
Column { Row(
Text( modifier = Modifier,
text = title, horizontalArrangement = Arrangement.spacedBy(12.dp),
style = MaterialTheme.typography.headlineSmall, verticalAlignment = Alignment.CenterVertically
maxLines = 1, ) {
overflow = TextOverflow.Ellipsis PurefinLogo(
contentDescription = "Purefin",
modifier = Modifier.size(48.dp),
contentScale = ContentScale.Fit,
) )
Text( Text(
text = subtitle, text = "PureFin",
style = MaterialTheme.typography.bodySmall, fontSize = 32.sp,
color = scheme.onSurfaceVariant, fontWeight = FontWeight.Bold,
maxLines = 1, fontStyle = FontStyle.Italic,
overflow = TextOverflow.Ellipsis color = scheme.onSecondary
) )
} }
}, },
actions = { actions = {
IconButton(onClick = onSearchClick) { IconButton(
onClick = onSearchClick,
colors = IconButtonColors(
containerColor = scheme.secondary,
contentColor = scheme.onSecondary,
disabledContainerColor = scheme.secondary,
disabledContentColor = scheme.onSecondary),
modifier = Modifier.size(50.dp),
) {
Icon( Icon(
imageVector = Icons.Outlined.Search, imageVector = Icons.Outlined.Search,
contentDescription = "Search" contentDescription = "Search",
modifier = Modifier.size(30.dp),
) )
} }
Spacer(modifier = Modifier.size(12.dp))
IconButton( IconButton(
onClick = { isProfileMenuExpanded = true }, onClick = { isProfileMenuExpanded = true },
colors = IconButtonColors(
containerColor = scheme.secondary,
contentColor = scheme.onSecondary,
disabledContainerColor = scheme.secondary,
disabledContentColor = scheme.onSecondary),
modifier = Modifier modifier = Modifier
.size(48.dp) .size(50.dp)
.clip(CircleShape), .clip(CircleShape),
) { ) {
HomeAvatar( Icon(
size = 40.dp, imageVector = Icons.Outlined.Person,
borderWidth = 1.dp, contentDescription = "Profile",
borderColor = scheme.outlineVariant, modifier = Modifier.size(30.dp),
backgroundColor = scheme.secondaryContainer,
icon = Icons.Outlined.Person,
iconTint = scheme.onSecondaryContainer
) )
} }
DropdownMenu( DropdownMenu(

View File

@@ -76,9 +76,6 @@ internal fun SuggestionCard(
.padding(24.dp) .padding(24.dp)
) { ) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(
text = item.title
)
Text( Text(
text = item.title, text = item.title,
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,

View File

@@ -13,10 +13,12 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -32,13 +34,18 @@ fun SuggestionsSection(
val pagerState = rememberPagerState(pageCount = { items.size }) val pagerState = rememberPagerState(pageCount = { items.size })
LaunchedEffect(Unit) {
pagerState.scrollToPage(0)
}
Column( Column(
verticalArrangement = Arrangement.spacedBy(14.dp), verticalArrangement = Arrangement.spacedBy(14.dp),
modifier = modifier modifier = modifier.fillMaxWidth()
) { ) {
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
contentPadding = PaddingValues(start = 16.dp, end = 56.dp), pageSize = PageSize.Fixed(360.dp),
contentPadding = PaddingValues(horizontal = 16.dp),
pageSpacing = 16.dp, pageSpacing = 16.dp,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { page -> ) { page ->

View File

@@ -1,73 +0,0 @@
package hu.bbara.purefin.common.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
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.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
@Composable
fun MediaPlayButton(
backgroundColor: Color,
foregroundColor: Color,
text: String = "Play",
subText: String? = null,
size: Dp,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.height(size)
.clip(CircleShape)
.background(backgroundColor)
.padding(start = 16.dp, end = 32.dp)
.clickable { onClick() },
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = foregroundColor,
modifier = Modifier.size(42.dp)
)
Column() {
Text(
text = text,
color = foregroundColor,
fontSize = TextUnit(
value = 16f,
type = TextUnitType.Sp
)
)
subText?.let {
Text(
text = subText,
color = foregroundColor.copy(alpha = 0.7f),
fontSize = TextUnit(
value = 14f,
type = TextUnitType.Sp
)
)
}
}
}
}

View File

@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.BoxWithConstraints
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -22,6 +21,7 @@ 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
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -50,7 +50,7 @@ fun MediaResumeButton(
.background(onPrimaryColor), .background(onPrimaryColor),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
ButtonContent(text = text, color = primaryColor) ButtonContent(text = text, color = Color.White)
} }
// Top layer: primary colors, clipped to the progress % // Top layer: primary colors, clipped to the progress %
@@ -71,7 +71,7 @@ fun MediaResumeButton(
.background(primaryColor), .background(primaryColor),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
ButtonContent(text = text, color = onPrimaryColor) ButtonContent(text = text, color = Color.White)
} }
} }
} }

View File

@@ -0,0 +1,32 @@
package hu.bbara.purefin.common.ui.components
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import hu.bbara.purefin.R
@Composable
fun PurefinLogo(
modifier: Modifier = Modifier,
contentDescription: String? = null,
contentScale: ContentScale = ContentScale.Fit,
) {
val resources = LocalContext.current.resources
val logoBitmap = remember(resources) {
BitmapFactory.decodeResource(resources, R.raw.logo_raw)?.asImageBitmap()
}
logoBitmap?.let { bitmap ->
Image(
bitmap = bitmap,
contentDescription = contentDescription,
contentScale = contentScale,
modifier = modifier,
)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/purefin_logo_2_background"/>
<foreground android:drawable="@mipmap/purefin_logo_2_foreground"/>
<monochrome android:drawable="@mipmap/purefin_logo_2_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/purefin_logo_2_background"/>
<foreground android:drawable="@mipmap/purefin_logo_2_foreground"/>
<monochrome android:drawable="@mipmap/purefin_logo_2_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purefin_logo_2_background">#201337</color>
</resources>

View File

@@ -152,16 +152,6 @@ class AppViewModel @Inject constructor(
initialValue = emptyMap() initialValue = emptyMap()
) )
init {
viewModelScope.launch {
try {
appContentRepository.ensureReady()
} catch (e: Exception) {
// State is already set to Error by ensureReady; don't crash the app
}
}
}
fun onLibrarySelected(id: UUID, name: String) { fun onLibrarySelected(id: UUID, name: String) {
viewModelScope.launch { viewModelScope.launch {
navigationManager.navigate(Route.LibraryRoute(library = LibraryDto(id = id, name = name))) navigationManager.navigate(Route.LibraryRoute(library = LibraryDto(id = id, name = name)))