refactor UI styling and image handling

- Remove custom color token classes (`EpisodeColors`, `MovieColors`, `SeriesColors`, `HomeColors`) in favor of standard `MaterialTheme.colorScheme`.
- Introduce `PurefinAsyncImage` component to provide consistent theme-synced placeholders for asynchronous images.
- Refactor various UI components (`MediaGhostIconButton`, `MediaMetaChip`, `MediaCastRow`, `PosterCard`) to use `MaterialTheme` directly instead of custom color objects.
- Simplify `MediaDetailColors` mapping logic by removing redundant conversion functions.
- Update `PurefinTheme` to disable dynamic color by default.
- Force light mode in `PlayerActivity` temporarily.
- Replace standard Coil `AsyncImage` with `PurefinAsyncImage` across the application.
This commit is contained in:
2026-01-24 13:40:39 +01:00
parent 48a773fded
commit dac4ee42dc
19 changed files with 193 additions and 379 deletions

View File

@@ -1,38 +0,0 @@
package hu.bbara.purefin.app.content.episode
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
internal data class EpisodeColors(
val primary: Color,
val onPrimary: Color,
val background: Color,
val surface: Color,
val surfaceAlt: Color,
val surfaceBorder: Color,
val textPrimary: Color,
val textSecondary: Color,
val textMuted: Color,
val textMutedStrong: Color
)
@Composable
internal fun rememberEpisodeColors(): EpisodeColors {
val scheme = MaterialTheme.colorScheme
return remember(scheme) {
EpisodeColors(
primary = scheme.primary,
onPrimary = scheme.onPrimary,
background = scheme.background,
surface = scheme.surface,
surfaceAlt = scheme.surfaceVariant,
surfaceBorder = scheme.outlineVariant,
textPrimary = scheme.onBackground,
textSecondary = scheme.onSurface,
textMuted = scheme.onSurfaceVariant,
textMutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
)
}
}

View File

@@ -36,7 +36,6 @@ import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.components.MediaActionButton
import hu.bbara.purefin.common.ui.components.MediaPlayButton
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
import hu.bbara.purefin.common.ui.toMediaDetailColors
import hu.bbara.purefin.player.PlayerActivity
@Composable
@@ -44,7 +43,6 @@ internal fun EpisodeTopBar(
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val colors = rememberEpisodeColors().toMediaDetailColors()
Row(
modifier = modifier
.fillMaxWidth()
@@ -54,14 +52,13 @@ internal fun EpisodeTopBar(
verticalAlignment = Alignment.CenterVertically
) {
MediaGhostIconButton(
colors = colors,
icon = Icons.Outlined.ArrowBack,
contentDescription = "Back",
onClick = onBack
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
MediaGhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
MediaGhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
}
}
}
@@ -72,6 +69,8 @@ internal fun EpisodeDetails(
episode: EpisodeUiModel,
modifier: Modifier = Modifier
) {
val scheme = MaterialTheme.colorScheme
val context = LocalContext.current
val playAction = remember(episode.id) {
{
@@ -81,11 +80,10 @@ internal fun EpisodeDetails(
}
}
val colors = rememberEpisodeColors().toMediaDetailColors()
Column(modifier = modifier) {
Text(
text = episode.title,
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
lineHeight = 38.sp
@@ -95,29 +93,28 @@ internal fun EpisodeDetails(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
MediaMetaChip(colors = colors, text = episode.releaseDate)
MediaMetaChip(colors = colors, text = episode.rating)
MediaMetaChip(colors = colors, text = episode.runtime)
MediaMetaChip(text = episode.releaseDate)
MediaMetaChip(text = episode.rating)
MediaMetaChip(text = episode.runtime)
MediaMetaChip(
colors = colors,
text = episode.format,
background = colors.primary.copy(alpha = 0.2f),
border = colors.primary.copy(alpha = 0.3f),
textColor = colors.primary
background = scheme.primary.copy(alpha = 0.2f),
border = scheme.primary.copy(alpha = 0.3f),
textColor = scheme.primary
)
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Synopsis",
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = episode.synopsis,
color = colors.textMuted,
color = scheme.onSurfaceVariant,
fontSize = 15.sp,
lineHeight = 22.sp
)
@@ -165,13 +162,12 @@ internal fun EpisodeDetails(
Text(
text = "Cast",
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
MediaCastRow(
colors = colors,
cast = episode.cast.map { it.toMediaCastMember() }
)
}

View File

@@ -1,38 +0,0 @@
package hu.bbara.purefin.app.content.movie
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
internal data class MovieColors(
val primary: Color,
val onPrimary: Color,
val background: Color,
val surface: Color,
val surfaceAlt: Color,
val surfaceBorder: Color,
val textPrimary: Color,
val textSecondary: Color,
val textMuted: Color,
val textMutedStrong: Color
)
@Composable
internal fun rememberMovieColors(): MovieColors {
val scheme = MaterialTheme.colorScheme
return remember(scheme) {
MovieColors(
primary = scheme.primary,
onPrimary = scheme.onPrimary,
background = scheme.background,
surface = scheme.surface,
surfaceAlt = scheme.surfaceVariant,
surfaceBorder = scheme.outlineVariant,
textPrimary = scheme.onBackground,
textSecondary = scheme.onSurface,
textMuted = scheme.onSurfaceVariant,
textMutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
)
}
}

View File

@@ -36,7 +36,6 @@ import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.components.MediaActionButton
import hu.bbara.purefin.common.ui.components.MediaPlayButton
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
import hu.bbara.purefin.common.ui.toMediaDetailColors
import hu.bbara.purefin.player.PlayerActivity
@Composable
@@ -44,7 +43,7 @@ internal fun MovieTopBar(
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val colors = rememberMovieColors().toMediaDetailColors()
val scheme = MaterialTheme.colorScheme
Row(
modifier = modifier
.fillMaxWidth()
@@ -54,14 +53,13 @@ internal fun MovieTopBar(
verticalAlignment = Alignment.CenterVertically
) {
MediaGhostIconButton(
colors = colors,
icon = Icons.Outlined.ArrowBack,
contentDescription = "Back",
onClick = onBack
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
MediaGhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
MediaGhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
}
}
}
@@ -72,6 +70,8 @@ internal fun MovieDetails(
movie: MovieUiModel,
modifier: Modifier = Modifier
) {
val scheme = MaterialTheme.colorScheme
val context = LocalContext.current
val playAction = remember(movie.id) {
{
@@ -81,11 +81,10 @@ internal fun MovieDetails(
}
}
val colors = rememberMovieColors().toMediaDetailColors()
Column(modifier = modifier) {
Text(
text = movie.title,
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
lineHeight = 38.sp
@@ -95,29 +94,28 @@ internal fun MovieDetails(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
MediaMetaChip(colors = colors, text = movie.year)
MediaMetaChip(colors = colors, text = movie.rating)
MediaMetaChip(colors = colors, text = movie.runtime)
MediaMetaChip(text = movie.year)
MediaMetaChip(text = movie.rating)
MediaMetaChip(text = movie.runtime)
MediaMetaChip(
colors = colors,
text = movie.format,
background = colors.primary.copy(alpha = 0.2f),
border = colors.primary.copy(alpha = 0.3f),
textColor = colors.primary
background = scheme.primary.copy(alpha = 0.2f),
border = scheme.primary.copy(alpha = 0.3f),
textColor = scheme.primary
)
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Synopsis",
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = movie.synopsis,
color = colors.textMuted,
color = scheme.onSurfaceVariant,
fontSize = 15.sp,
lineHeight = 22.sp
)
@@ -165,13 +163,12 @@ internal fun MovieDetails(
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Cast",
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
MediaCastRow(
colors = colors,
cast = movie.cast.map { it.toMediaCastMember() }
)
}

View File

@@ -1,38 +0,0 @@
package hu.bbara.purefin.app.content.series
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
internal data class SeriesColors(
val primary: Color,
val onPrimary: Color,
val background: Color,
val surface: Color,
val surfaceAlt: Color,
val surfaceBorder: Color,
val textPrimary: Color,
val textSecondary: Color,
val textMuted: Color,
val textMutedStrong: Color
)
@Composable
internal fun rememberSeriesColors(): SeriesColors {
val scheme = MaterialTheme.colorScheme
return remember(scheme) {
SeriesColors(
primary = scheme.primary,
onPrimary = scheme.onPrimary,
background = scheme.background,
surface = scheme.surface,
surfaceAlt = scheme.surfaceVariant,
surfaceBorder = scheme.outlineVariant,
textPrimary = scheme.onBackground,
textSecondary = scheme.onSurface,
textMuted = scheme.onSurfaceVariant,
textMutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
)
}
}

View File

@@ -46,21 +46,19 @@ 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 coil3.compose.AsyncImage
import hu.bbara.purefin.common.ui.MediaCastMember
import hu.bbara.purefin.common.ui.MediaCastRow
import hu.bbara.purefin.common.ui.MediaGhostIconButton
import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.components.MediaActionButton
import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.common.ui.toMediaDetailColors
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
@Composable
internal fun SeriesTopBar(
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val colors = rememberSeriesColors().toMediaDetailColors()
Row(
modifier = modifier
.fillMaxWidth()
@@ -70,13 +68,12 @@ internal fun SeriesTopBar(
verticalAlignment = Alignment.CenterVertically
) {
MediaGhostIconButton(
colors = colors,
onClick = onBack,
icon = Icons.Outlined.ArrowBack,
contentDescription = "Back")
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
MediaGhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
MediaGhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
}
}
}
@@ -87,7 +84,6 @@ internal fun SeriesHero(
height: Dp,
modifier: Modifier = Modifier
) {
val colors = rememberSeriesColors().toMediaDetailColors()
MediaHero(
imageUrl = imageUrl,
backgroundColor = MaterialTheme.colorScheme.background,
@@ -99,27 +95,25 @@ internal fun SeriesHero(
@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun SeriesMetaChips(series: SeriesUiModel) {
val colors = rememberSeriesColors().toMediaDetailColors()
val scheme = MaterialTheme.colorScheme
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
MediaMetaChip(colors = colors, text = series.year)
MediaMetaChip(colors = colors, text = series.rating)
MediaMetaChip(colors = colors, text = series.seasons)
MediaMetaChip(text = series.year)
MediaMetaChip(text = series.rating)
MediaMetaChip(text = series.seasons)
MediaMetaChip(
colors = colors,
text = series.format,
background = colors.primary.copy(alpha = 0.2f),
border = colors.primary.copy(alpha = 0.3f),
textColor = colors.primary
background = scheme.primary.copy(alpha = 0.2f),
border = scheme.primary.copy(alpha = 0.3f),
textColor = scheme.primary
)
}
}
@Composable
internal fun SeriesActionButtons(modifier: Modifier = Modifier) {
val colors = rememberSeriesColors().toMediaDetailColors()
Row() {
MediaActionButton(
backgroundColor = MaterialTheme.colorScheme.secondary,
@@ -153,9 +147,10 @@ internal fun SeasonTabs(seasons: List<SeriesSeasonUiModel>, modifier: Modifier =
@Composable
private fun SeasonTab(name: String, isSelected: Boolean) {
val colors = rememberSeriesColors()
val color = if (isSelected) colors.primary else colors.textMutedStrong
val borderColor = if (isSelected) colors.primary else Color.Transparent
val scheme = MaterialTheme.colorScheme
val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
val color = if (isSelected) scheme.primary else mutedStrong
val borderColor = if (isSelected) scheme.primary else Color.Transparent
Column(
modifier = Modifier
.padding(bottom = 8.dp)
@@ -195,13 +190,14 @@ private fun EpisodeCard(
viewModel: SeriesViewModel = hiltViewModel(),
episode: SeriesEpisodeUiModel
) {
val colors = rememberSeriesColors()
val scheme = MaterialTheme.colorScheme
val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
Column(
modifier = Modifier
.width(260.dp)
.clip(RoundedCornerShape(16.dp))
.background(colors.surfaceAlt.copy(alpha = 0.6f))
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(16.dp))
.background(scheme.surfaceVariant.copy(alpha = 0.6f))
.border(1.dp, scheme.outlineVariant, RoundedCornerShape(16.dp))
.padding(12.dp)
.clickable { viewModel.onSelectEpisode(episode.id) },
verticalArrangement = Arrangement.spacedBy(12.dp)
@@ -211,10 +207,10 @@ private fun EpisodeCard(
.fillMaxWidth()
.aspectRatio(16f / 9f)
.clip(RoundedCornerShape(12.dp))
.background(colors.surface)
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
.background(scheme.surface)
.border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp))
) {
AsyncImage(
PurefinAsyncImage(
model = episode.imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
@@ -223,12 +219,12 @@ private fun EpisodeCard(
Box(
modifier = Modifier
.matchParentSize()
.background(colors.background.copy(alpha = 0.2f))
.background(scheme.background.copy(alpha = 0.2f))
)
Icon(
imageVector = Icons.Outlined.PlayCircle,
contentDescription = null,
tint = colors.textPrimary,
tint = scheme.onBackground,
modifier = Modifier
.align(Alignment.Center)
.size(32.dp)
@@ -237,12 +233,12 @@ private fun EpisodeCard(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(6.dp)
.background(colors.background.copy(alpha = 0.8f), RoundedCornerShape(6.dp))
.background(scheme.background.copy(alpha = 0.8f), RoundedCornerShape(6.dp))
.padding(horizontal = 6.dp, vertical = 2.dp)
) {
Text(
text = episode.duration,
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 10.sp,
fontWeight = FontWeight.Bold
)
@@ -253,7 +249,7 @@ private fun EpisodeCard(
) {
Text(
text = episode.title,
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
@@ -261,7 +257,7 @@ private fun EpisodeCard(
)
Text(
text = episode.description,
color = colors.textMutedStrong,
color = mutedStrong,
fontSize = 11.sp,
lineHeight = 16.sp,
maxLines = 2,
@@ -273,9 +269,7 @@ private fun EpisodeCard(
@Composable
internal fun CastRow(cast: List<SeriesCastMemberUiModel>, modifier: Modifier = Modifier) {
val colors = rememberSeriesColors().toMediaDetailColors()
MediaCastRow(
colors = colors,
cast = cast.map { it.toMediaCastMember() },
modifier = modifier,
cardWidth = 84.dp,

View File

@@ -53,7 +53,8 @@ private fun SeriesScreenInternal(
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
val colors = rememberSeriesColors()
val scheme = MaterialTheme.colorScheme
val textMutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
Scaffold(
modifier = modifier,
@@ -83,7 +84,7 @@ private fun SeriesScreenInternal(
) {
Text(
text = series.title,
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
lineHeight = 36.sp
@@ -95,20 +96,20 @@ private fun SeriesScreenInternal(
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Synopsis",
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = series.synopsis,
color = colors.textMutedStrong,
color = textMutedStrong,
fontSize = 13.sp,
)
Spacer(modifier = Modifier.height(28.dp))
Text(
text = "Episodes",
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
@@ -123,7 +124,7 @@ private fun SeriesScreenInternal(
Spacer(modifier = Modifier.height(32.dp))
Text(
text = "Cast",
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)

View File

@@ -8,6 +8,7 @@ import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.Movie
import androidx.compose.material.icons.outlined.Tv
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold
@@ -23,7 +24,6 @@ import hu.bbara.purefin.app.home.ui.HomeDrawerContent
import hu.bbara.purefin.app.home.ui.HomeMockData
import hu.bbara.purefin.app.home.ui.HomeNavItem
import hu.bbara.purefin.app.home.ui.HomeTopBar
import hu.bbara.purefin.app.home.ui.rememberHomeColors
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.CollectionType
@@ -32,7 +32,6 @@ fun HomePage(
viewModel: HomePageViewModel = hiltViewModel(),
modifier: Modifier = Modifier
) {
val colors = rememberHomeColors()
val drawerState = rememberDrawerState(DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope()
@@ -56,13 +55,12 @@ fun HomePage(
modifier = Modifier
.width(280.dp)
.fillMaxSize(),
drawerContainerColor = colors.drawerBackground,
drawerContentColor = colors.textPrimary
drawerContainerColor = MaterialTheme.colorScheme.surface,
drawerContentColor = MaterialTheme.colorScheme.onBackground
) {
HomeDrawerContent(
title = "Jellyfin",
subtitle = "Library Dashboard",
colors = colors,
primaryNavItems = libraries,
secondaryNavItems = HomeMockData.secondaryNavItems,
user = HomeMockData.user,
@@ -72,18 +70,16 @@ fun HomePage(
) {
Scaffold(
modifier = modifier.fillMaxSize(),
containerColor = colors.background,
contentColor = colors.textPrimary,
containerColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
topBar = {
HomeTopBar(
title = "Home",
colors = colors,
onMenuClick = { coroutineScope.launch { drawerState.open() } }
)
}
) { innerPadding ->
HomeContent(
colors = colors,
continueWatching = continueWatching.value,
modifier = Modifier.padding(innerPadding)
)

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -17,7 +18,6 @@ import hu.bbara.purefin.app.home.HomePageViewModel
@Composable
fun HomeContent(
viewModel: HomePageViewModel = hiltViewModel(),
colors: HomeColors,
continueWatching: List<ContinueWatchingItem>,
modifier: Modifier = Modifier
) {
@@ -28,15 +28,14 @@ fun HomeContent(
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(colors.background)
.background(MaterialTheme.colorScheme.background)
) {
item {
Spacer(modifier = Modifier.height(8.dp))
}
item {
ContinueWatchingSection(
items = continueWatching,
colors = colors
items = continueWatching
)
}
items(
@@ -47,7 +46,6 @@ fun HomeContent(
title = item.name,
items = libraryContent[item.id] ?: emptyList(),
action = "See All",
colors = colors
)
}
item {

View File

@@ -16,6 +16,7 @@ import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -32,7 +33,6 @@ import hu.bbara.purefin.app.home.HomePageViewModel
fun HomeDrawerContent(
title: String,
subtitle: String,
colors: HomeColors,
primaryNavItems: List<HomeNavItem>,
secondaryNavItems: List<HomeNavItem>,
user: HomeUser,
@@ -41,16 +41,14 @@ fun HomeDrawerContent(
Column(modifier = modifier.fillMaxSize()) {
HomeDrawerHeader(
title = title,
subtitle = subtitle,
colors = colors
subtitle = subtitle
)
HomeDrawerNav(
primaryItems = primaryNavItems,
secondaryItems = secondaryNavItems,
colors = colors,
secondaryItems = secondaryNavItems
)
Spacer(modifier = Modifier.weight(1f))
HomeDrawerFooter(user = user, colors = colors)
HomeDrawerFooter(user = user)
}
}
@@ -58,9 +56,10 @@ fun HomeDrawerContent(
fun HomeDrawerHeader(
title: String,
subtitle: String,
colors: HomeColors,
modifier: Modifier = Modifier
) {
val scheme = MaterialTheme.colorScheme
Row(
modifier = modifier
.fillMaxWidth()
@@ -70,38 +69,37 @@ fun HomeDrawerHeader(
Row(
modifier = Modifier
.size(40.dp)
.background(colors.primary, RoundedCornerShape(12.dp)),
.background(scheme.primary, RoundedCornerShape(12.dp)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = colors.onPrimary
tint = scheme.onPrimary
)
}
Column(modifier = Modifier.padding(start = 12.dp)) {
Text(
text = title,
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
Text(
text = subtitle,
color = colors.textSecondary,
color = scheme.onSurfaceVariant,
fontSize = 12.sp
)
}
}
HorizontalDivider(color = colors.textSecondary.copy(alpha = 0.2f))
HorizontalDivider(color = scheme.onSurfaceVariant.copy(alpha = 0.2f))
}
@Composable
fun HomeDrawerNav(
primaryItems: List<HomeNavItem>,
secondaryItems: List<HomeNavItem>,
colors: HomeColors,
modifier: Modifier = Modifier,
) {
Column(
@@ -110,16 +108,16 @@ fun HomeDrawerNav(
.padding(vertical = 16.dp)
) {
primaryItems.forEach { item ->
HomeDrawerNavItem(item = item, colors = colors)
HomeDrawerNavItem(item = item)
}
if (secondaryItems.isNotEmpty()) {
HorizontalDivider(
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 12.dp),
color = colors.divider
color = MaterialTheme.colorScheme.outlineVariant
)
secondaryItems.forEach { item ->
HomeDrawerNavItem(item = item, colors = colors)
HomeDrawerNavItem(item = item)
}
}
}
@@ -128,12 +126,12 @@ fun HomeDrawerNav(
@Composable
fun HomeDrawerNavItem(
item: HomeNavItem,
colors: HomeColors,
modifier: Modifier = Modifier,
viewModel: HomePageViewModel = hiltViewModel(),
) {
val background = if (item.selected) colors.primary.copy(alpha = 0.12f) else Color.Transparent
val tint = if (item.selected) colors.primary else colors.textSecondary
val scheme = MaterialTheme.colorScheme
val background = if (item.selected) scheme.primary.copy(alpha = 0.12f) else Color.Transparent
val tint = if (item.selected) scheme.primary else scheme.onSurfaceVariant
Row(
modifier = modifier
.fillMaxWidth()
@@ -150,7 +148,7 @@ fun HomeDrawerNavItem(
)
Text(
text = item.label,
color = if (item.selected) colors.primary else colors.textPrimary,
color = if (item.selected) scheme.primary else scheme.onBackground,
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 12.dp)
@@ -162,30 +160,31 @@ fun HomeDrawerNavItem(
fun HomeDrawerFooter (
viewModel: HomePageViewModel = hiltViewModel(),
user: HomeUser,
colors: HomeColors,
modifier: Modifier = Modifier,
) {
val scheme = MaterialTheme.colorScheme
Row(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
.background(colors.drawerFooterBackground, RoundedCornerShape(12.dp))
.background(scheme.surfaceVariant, RoundedCornerShape(12.dp))
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
HomeAvatar(
size = 32.dp,
borderWidth = 1.dp,
borderColor = colors.divider,
backgroundColor = colors.avatarBackground,
borderColor = scheme.outlineVariant,
backgroundColor = scheme.primaryContainer,
icon = Icons.Outlined.Person,
iconTint = colors.textPrimary
iconTint = scheme.onBackground
)
Column(modifier = Modifier.padding(start = 12.dp)
.clickable {viewModel.logout()}) {
Text(
text = user.name,
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
@@ -193,7 +192,7 @@ fun HomeDrawerFooter (
)
Text(
text = user.plan,
color = colors.textSecondary,
color = scheme.onSurfaceVariant,
fontSize = 11.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis

View File

@@ -23,6 +23,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -36,9 +37,9 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import coil3.compose.AsyncImage
import hu.bbara.purefin.app.home.HomePageViewModel
import hu.bbara.purefin.common.ui.PosterCard
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.player.PlayerActivity
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
@@ -46,10 +47,12 @@ import kotlin.math.nextUp
@Composable
fun ContinueWatchingSection(
items: List<ContinueWatchingItem>, colors: HomeColors, modifier: Modifier = Modifier
items: List<ContinueWatchingItem>,
modifier: Modifier = Modifier
) {
SectionHeader(
title = "Continue Watching", action = null, colors = colors
title = "Continue Watching",
action = null
)
LazyRow(
modifier = modifier.fillMaxWidth(),
@@ -59,7 +62,7 @@ fun ContinueWatchingSection(
items(
items = items, key = { it.id }) { item ->
ContinueWatchingCard(
item = item, colors = colors
item = item
)
}
}
@@ -68,10 +71,11 @@ fun ContinueWatchingSection(
@Composable
fun ContinueWatchingCard(
item: ContinueWatchingItem,
colors: HomeColors,
modifier: Modifier = Modifier,
viewModel: HomePageViewModel = hiltViewModel()
) {
val scheme = MaterialTheme.colorScheme
val context = LocalContext.current
fun openItem(item: ContinueWatchingItem) {
@@ -92,9 +96,9 @@ fun ContinueWatchingCard(
.aspectRatio(16f / 9f)
.shadow(12.dp, RoundedCornerShape(16.dp))
.clip(RoundedCornerShape(16.dp))
.background(colors.card)
.background(scheme.surfaceVariant)
) {
AsyncImage(
PurefinAsyncImage(
model = viewModel.getImageUrl(itemId = item.id, type = ImageType.PRIMARY),
contentDescription = null,
modifier = Modifier
@@ -103,25 +107,24 @@ fun ContinueWatchingCard(
openItem(item)
},
contentScale = ContentScale.Crop,
)
)
Box(
modifier = Modifier
.matchParentSize()
.background(colors.textPrimary.copy(alpha = 0.2f))
.background(scheme.onBackground.copy(alpha = 0.2f))
)
Box(
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth()
.height(4.dp)
.background(colors.textPrimary.copy(alpha = 0.2f))
.background(scheme.onBackground.copy(alpha = 0.2f))
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(item.progress.toFloat().nextUp().div(100))
.background(colors.primary)
.background(scheme.primary)
)
}
Button(
@@ -136,7 +139,7 @@ fun ContinueWatchingCard(
Column(modifier = Modifier.padding(top = 12.dp)) {
Text(
text = item.primaryText,
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
@@ -144,7 +147,7 @@ fun ContinueWatchingCard(
)
Text(
text = item.secondaryText,
color = colors.textSecondary,
color = scheme.onSurfaceVariant,
fontSize = 13.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@@ -158,11 +161,11 @@ fun LibraryPosterSection(
title: String,
items: List<PosterItem>,
action: String?,
colors: HomeColors,
modifier: Modifier = Modifier
) {
SectionHeader(
title = title, action = action, colors = colors
title = title,
action = action
)
LazyRow(
modifier = modifier.fillMaxWidth(),
@@ -173,7 +176,6 @@ fun LibraryPosterSection(
items = items, key = { it.id }) { item ->
PosterCard(
item = item,
colors = colors,
)
}
}
@@ -183,10 +185,11 @@ fun LibraryPosterSection(
fun SectionHeader(
title: String,
action: String?,
colors: HomeColors,
modifier: Modifier = Modifier,
onActionClick: () -> Unit = {}
) {
val scheme = MaterialTheme.colorScheme
Row(
modifier = modifier
.fillMaxWidth()
@@ -195,12 +198,15 @@ fun SectionHeader(
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = title, color = colors.textPrimary, fontSize = 20.sp, fontWeight = FontWeight.Bold
text = title,
color = scheme.onBackground,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
if (action != null) {
Text(
text = action,
color = colors.primary,
color = scheme.primary,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.clickable { onActionClick() })

View File

@@ -1,40 +0,0 @@
package hu.bbara.purefin.app.home.ui
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
data class HomeColors(
val primary: Color,
val onPrimary: Color,
val background: Color,
val drawerBackground: Color,
val card: Color,
val textPrimary: Color,
val textSecondary: Color,
val divider: Color,
val avatarBackground: Color,
val avatarBorder: Color,
val drawerFooterBackground: Color
)
@Composable
fun rememberHomeColors(): HomeColors {
val scheme = MaterialTheme.colorScheme
return remember(scheme) {
HomeColors(
primary = scheme.primary,
onPrimary = scheme.onPrimary,
background = scheme.background,
drawerBackground = scheme.surface,
card = scheme.surfaceVariant,
textPrimary = scheme.onBackground,
textSecondary = scheme.onSurfaceVariant,
divider = scheme.outlineVariant,
avatarBackground = scheme.primaryContainer,
avatarBorder = scheme.outline,
drawerFooterBackground = scheme.surfaceVariant
)
}
}

View File

@@ -15,6 +15,7 @@ import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -30,24 +31,25 @@ import hu.bbara.purefin.app.home.HomePageViewModel
fun HomeTopBar(
viewModel: HomePageViewModel = hiltViewModel(),
title: String,
colors: HomeColors,
onMenuClick: () -> Unit,
modifier: Modifier = Modifier,
actions: @Composable RowScope.() -> Unit = {
HomeAvatar(
size = 36.dp,
borderWidth = 2.dp,
borderColor = colors.avatarBorder,
backgroundColor = colors.avatarBackground,
borderColor = MaterialTheme.colorScheme.outline,
backgroundColor = MaterialTheme.colorScheme.primaryContainer,
icon = Icons.Outlined.Person,
iconTint = colors.onPrimary
iconTint = MaterialTheme.colorScheme.onPrimary
)
}
) {
val scheme = MaterialTheme.colorScheme
Box(
modifier = modifier
.fillMaxWidth()
.background(colors.background.copy(alpha = 0.95f))
.background(scheme.background.copy(alpha = 0.95f))
.zIndex(1f)
) {
Row(
@@ -63,12 +65,12 @@ fun HomeTopBar(
Icon(
imageVector = Icons.Outlined.Menu,
contentDescription = "Menu",
tint = colors.textPrimary
tint = scheme.onBackground
)
}
Text(
text = title,
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)

View File

@@ -22,6 +22,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -36,94 +37,39 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import hu.bbara.purefin.app.content.episode.EpisodeColors
import hu.bbara.purefin.app.content.movie.MovieColors
import hu.bbara.purefin.app.content.series.SeriesColors
data class MediaDetailColors(
val primary: Color,
val onPrimary: Color,
val background: Color,
val surface: Color,
val surfaceAlt: Color,
val surfaceBorder: Color,
val textPrimary: Color,
val textSecondary: Color,
val textMuted: Color,
val textMutedStrong: Color
)
internal fun MovieColors.toMediaDetailColors() = MediaDetailColors(
primary = primary,
onPrimary = onPrimary,
background = background,
surface = surface,
surfaceAlt = surfaceAlt,
surfaceBorder = surfaceBorder,
textPrimary = textPrimary,
textSecondary = textSecondary,
textMuted = textMuted,
textMutedStrong = textMutedStrong
)
internal fun EpisodeColors.toMediaDetailColors() = MediaDetailColors(
primary = primary,
onPrimary = onPrimary,
background = background,
surface = surface,
surfaceAlt = surfaceAlt,
surfaceBorder = surfaceBorder,
textPrimary = textPrimary,
textSecondary = textSecondary,
textMuted = textMuted,
textMutedStrong = textMutedStrong
)
internal fun SeriesColors.toMediaDetailColors() = MediaDetailColors(
primary = primary,
onPrimary = onPrimary,
background = background,
surface = surface,
surfaceAlt = surfaceAlt,
surfaceBorder = surfaceBorder,
textPrimary = textPrimary,
textSecondary = textSecondary,
textMuted = textMuted,
textMutedStrong = textMutedStrong
)
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
@Composable
fun MediaGhostIconButton(
colors: MediaDetailColors,
icon: ImageVector,
contentDescription: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val scheme = MaterialTheme.colorScheme
Box(
modifier = modifier
.size(40.dp)
.clip(CircleShape)
.background(colors.background.copy(alpha = 0.4f))
.background(scheme.background.copy(alpha = 0.4f))
.clickable { onClick() },
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = colors.textPrimary
tint = scheme.onBackground
)
}
}
@Composable
fun MediaMetaChip(
colors: MediaDetailColors,
text: String,
background: Color = colors.surfaceAlt,
background: Color = MaterialTheme.colorScheme.surfaceVariant,
border: Color = Color.Transparent,
textColor: Color = colors.textSecondary,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier: Modifier = Modifier
) {
Box(
@@ -153,13 +99,15 @@ data class MediaCastMember(
@Composable
fun MediaCastRow(
colors: MediaDetailColors,
cast: List<MediaCastMember>,
modifier: Modifier = Modifier,
cardWidth: Dp = 96.dp,
nameSize: TextUnit = 12.sp,
roleSize: TextUnit = 10.sp
) {
val scheme = MaterialTheme.colorScheme
val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
LazyRow(
modifier = modifier,
contentPadding = PaddingValues(horizontal = 4.dp),
@@ -171,23 +119,23 @@ fun MediaCastRow(
modifier = Modifier
.aspectRatio(4f / 5f)
.clip(RoundedCornerShape(12.dp))
.background(colors.surfaceAlt)
.background(scheme.surfaceVariant)
) {
if (member.imageUrl == null) {
Box(
modifier = Modifier
.fillMaxSize()
.background(colors.surfaceAlt.copy(alpha = 0.6f)),
.background(scheme.surfaceVariant.copy(alpha = 0.6f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Outlined.Person,
contentDescription = null,
tint = colors.textMutedStrong
tint = mutedStrong
)
}
} else {
AsyncImage(
PurefinAsyncImage(
model = member.imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
@@ -198,7 +146,7 @@ fun MediaCastRow(
Spacer(modifier = Modifier.height(6.dp))
Text(
text = member.name,
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = nameSize,
fontWeight = FontWeight.Bold,
maxLines = 1,
@@ -206,7 +154,7 @@ fun MediaCastRow(
)
Text(
text = member.role,
color = colors.textMutedStrong,
color = mutedStrong,
fontSize = roleSize,
maxLines = 1,
overflow = TextOverflow.Ellipsis

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -18,21 +19,20 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import coil3.compose.AsyncImage
import hu.bbara.purefin.app.home.HomePageViewModel
import hu.bbara.purefin.app.home.ui.HomeColors
import hu.bbara.purefin.app.home.ui.PosterItem
import hu.bbara.purefin.app.home.ui.rememberHomeColors
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
@Composable
fun PosterCard(
item: PosterItem,
colors: HomeColors = rememberHomeColors(),
modifier: Modifier = Modifier,
viewModel: HomePageViewModel = hiltViewModel()
) {
val scheme = MaterialTheme.colorScheme
fun openItem(posterItem: PosterItem) {
when (posterItem.type) {
BaseItemKind.MOVIE -> viewModel.onMovieSelected(posterItem.id.toString())
@@ -45,20 +45,20 @@ fun PosterCard(
modifier = Modifier
.width(144.dp)
) {
AsyncImage(
PurefinAsyncImage(
model = viewModel.getImageUrl(item.imageItemId, ImageType.PRIMARY),
contentDescription = null,
modifier = Modifier
.aspectRatio(2f / 3f)
.shadow(10.dp, RoundedCornerShape(14.dp))
.clip(RoundedCornerShape(14.dp))
.background(colors.card)
.background(scheme.surfaceVariant)
.clickable(onClick = { openItem(item) }),
contentScale = ContentScale.Crop
)
Text(
text = item.title,
color = colors.textPrimary,
color = scheme.onBackground,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(top = 8.dp, start = 4.dp, end = 4.dp, bottom = 8.dp),
@@ -66,4 +66,4 @@ fun PosterCard(
overflow = TextOverflow.Ellipsis
)
}
}
}

View File

@@ -10,7 +10,6 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import coil3.compose.AsyncImage
@Composable
fun MediaHero(
@@ -24,7 +23,7 @@ fun MediaHero(
.height(height)
.background(backgroundColor)
) {
AsyncImage(
PurefinAsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),

View File

@@ -0,0 +1,32 @@
package hu.bbara.purefin.common.ui.components
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImage
/**
* Async image that falls back to theme-synced color blocks so loading/error states
* stay aligned with PurefinTheme's colorScheme.
*/
@Composable
fun PurefinAsyncImage(
model: Any?,
contentDescription: String?,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Crop
) {
val placeholderPainter = ColorPainter(MaterialTheme.colorScheme.surfaceVariant)
AsyncImage(
model = model,
contentDescription = contentDescription,
modifier = modifier,
contentScale = contentScale,
placeholder = placeholderPainter,
error = placeholderPainter,
fallback = placeholderPainter
)
}

View File

@@ -24,7 +24,7 @@ class PlayerActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
PurefinTheme {
PurefinTheme(darkTheme = false) {
val viewModel = hiltViewModel<PlayerViewModel>()
Box(
modifier = Modifier.fillMaxSize()

View File

@@ -51,7 +51,7 @@ private val DarkColorScheme = darkColorScheme(
@Composable
fun PurefinTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
@@ -69,4 +69,4 @@ fun PurefinTheme(
typography = Typography,
content = content
)
}
}