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

View File

@@ -53,7 +53,8 @@ private fun SeriesScreenInternal(
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val colors = rememberSeriesColors() val scheme = MaterialTheme.colorScheme
val textMutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
@@ -83,7 +84,7 @@ private fun SeriesScreenInternal(
) { ) {
Text( Text(
text = series.title, text = series.title,
color = colors.textPrimary, color = scheme.onBackground,
fontSize = 30.sp, fontSize = 30.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
lineHeight = 36.sp lineHeight = 36.sp
@@ -95,20 +96,20 @@ private fun SeriesScreenInternal(
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Text( Text(
text = "Synopsis", text = "Synopsis",
color = colors.textPrimary, color = scheme.onBackground,
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = series.synopsis, text = series.synopsis,
color = colors.textMutedStrong, color = textMutedStrong,
fontSize = 13.sp, fontSize = 13.sp,
) )
Spacer(modifier = Modifier.height(28.dp)) Spacer(modifier = Modifier.height(28.dp))
Text( Text(
text = "Episodes", text = "Episodes",
color = colors.textPrimary, color = scheme.onBackground,
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
@@ -123,7 +124,7 @@ private fun SeriesScreenInternal(
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
Text( Text(
text = "Cast", text = "Cast",
color = colors.textPrimary, color = scheme.onBackground,
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.Bold 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.Movie
import androidx.compose.material.icons.outlined.Tv import androidx.compose.material.icons.outlined.Tv
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold 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.HomeMockData
import hu.bbara.purefin.app.home.ui.HomeNavItem import hu.bbara.purefin.app.home.ui.HomeNavItem
import hu.bbara.purefin.app.home.ui.HomeTopBar import hu.bbara.purefin.app.home.ui.HomeTopBar
import hu.bbara.purefin.app.home.ui.rememberHomeColors
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
@@ -32,7 +32,6 @@ fun HomePage(
viewModel: HomePageViewModel = hiltViewModel(), viewModel: HomePageViewModel = hiltViewModel(),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val colors = rememberHomeColors()
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -56,13 +55,12 @@ fun HomePage(
modifier = Modifier modifier = Modifier
.width(280.dp) .width(280.dp)
.fillMaxSize(), .fillMaxSize(),
drawerContainerColor = colors.drawerBackground, drawerContainerColor = MaterialTheme.colorScheme.surface,
drawerContentColor = colors.textPrimary drawerContentColor = MaterialTheme.colorScheme.onBackground
) { ) {
HomeDrawerContent( HomeDrawerContent(
title = "Jellyfin", title = "Jellyfin",
subtitle = "Library Dashboard", subtitle = "Library Dashboard",
colors = colors,
primaryNavItems = libraries, primaryNavItems = libraries,
secondaryNavItems = HomeMockData.secondaryNavItems, secondaryNavItems = HomeMockData.secondaryNavItems,
user = HomeMockData.user, user = HomeMockData.user,
@@ -72,18 +70,16 @@ fun HomePage(
) { ) {
Scaffold( Scaffold(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
containerColor = colors.background, containerColor = MaterialTheme.colorScheme.background,
contentColor = colors.textPrimary, contentColor = MaterialTheme.colorScheme.onBackground,
topBar = { topBar = {
HomeTopBar( HomeTopBar(
title = "Home", title = "Home",
colors = colors,
onMenuClick = { coroutineScope.launch { drawerState.open() } } onMenuClick = { coroutineScope.launch { drawerState.open() } }
) )
} }
) { innerPadding -> ) { innerPadding ->
HomeContent( HomeContent(
colors = colors,
continueWatching = continueWatching.value, continueWatching = continueWatching.value,
modifier = Modifier.padding(innerPadding) 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.layout.height
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -17,7 +18,6 @@ import hu.bbara.purefin.app.home.HomePageViewModel
@Composable @Composable
fun HomeContent( fun HomeContent(
viewModel: HomePageViewModel = hiltViewModel(), viewModel: HomePageViewModel = hiltViewModel(),
colors: HomeColors,
continueWatching: List<ContinueWatchingItem>, continueWatching: List<ContinueWatchingItem>,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -28,15 +28,14 @@ fun HomeContent(
LazyColumn( LazyColumn(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.background(colors.background) .background(MaterialTheme.colorScheme.background)
) { ) {
item { item {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
item { item {
ContinueWatchingSection( ContinueWatchingSection(
items = continueWatching, items = continueWatching
colors = colors
) )
} }
items( items(
@@ -47,7 +46,6 @@ fun HomeContent(
title = item.name, title = item.name,
items = libraryContent[item.id] ?: emptyList(), items = libraryContent[item.id] ?: emptyList(),
action = "See All", action = "See All",
colors = colors
) )
} }
item { item {

View File

@@ -16,6 +16,7 @@ import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -32,7 +33,6 @@ import hu.bbara.purefin.app.home.HomePageViewModel
fun HomeDrawerContent( fun HomeDrawerContent(
title: String, title: String,
subtitle: String, subtitle: String,
colors: HomeColors,
primaryNavItems: List<HomeNavItem>, primaryNavItems: List<HomeNavItem>,
secondaryNavItems: List<HomeNavItem>, secondaryNavItems: List<HomeNavItem>,
user: HomeUser, user: HomeUser,
@@ -41,16 +41,14 @@ fun HomeDrawerContent(
Column(modifier = modifier.fillMaxSize()) { Column(modifier = modifier.fillMaxSize()) {
HomeDrawerHeader( HomeDrawerHeader(
title = title, title = title,
subtitle = subtitle, subtitle = subtitle
colors = colors
) )
HomeDrawerNav( HomeDrawerNav(
primaryItems = primaryNavItems, primaryItems = primaryNavItems,
secondaryItems = secondaryNavItems, secondaryItems = secondaryNavItems
colors = colors,
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
HomeDrawerFooter(user = user, colors = colors) HomeDrawerFooter(user = user)
} }
} }
@@ -58,9 +56,10 @@ fun HomeDrawerContent(
fun HomeDrawerHeader( fun HomeDrawerHeader(
title: String, title: String,
subtitle: String, subtitle: String,
colors: HomeColors,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scheme = MaterialTheme.colorScheme
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@@ -70,38 +69,37 @@ fun HomeDrawerHeader(
Row( Row(
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
.background(colors.primary, RoundedCornerShape(12.dp)), .background(scheme.primary, RoundedCornerShape(12.dp)),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
Icon( Icon(
imageVector = Icons.Filled.PlayArrow, imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play", contentDescription = "Play",
tint = colors.onPrimary tint = scheme.onPrimary
) )
} }
Column(modifier = Modifier.padding(start = 12.dp)) { Column(modifier = Modifier.padding(start = 12.dp)) {
Text( Text(
text = title, text = title,
color = colors.textPrimary, color = scheme.onBackground,
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text( Text(
text = subtitle, text = subtitle,
color = colors.textSecondary, color = scheme.onSurfaceVariant,
fontSize = 12.sp fontSize = 12.sp
) )
} }
} }
HorizontalDivider(color = colors.textSecondary.copy(alpha = 0.2f)) HorizontalDivider(color = scheme.onSurfaceVariant.copy(alpha = 0.2f))
} }
@Composable @Composable
fun HomeDrawerNav( fun HomeDrawerNav(
primaryItems: List<HomeNavItem>, primaryItems: List<HomeNavItem>,
secondaryItems: List<HomeNavItem>, secondaryItems: List<HomeNavItem>,
colors: HomeColors,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
@@ -110,16 +108,16 @@ fun HomeDrawerNav(
.padding(vertical = 16.dp) .padding(vertical = 16.dp)
) { ) {
primaryItems.forEach { item -> primaryItems.forEach { item ->
HomeDrawerNavItem(item = item, colors = colors) HomeDrawerNavItem(item = item)
} }
if (secondaryItems.isNotEmpty()) { if (secondaryItems.isNotEmpty()) {
HorizontalDivider( HorizontalDivider(
modifier = Modifier modifier = Modifier
.padding(horizontal = 20.dp, vertical = 12.dp), .padding(horizontal = 20.dp, vertical = 12.dp),
color = colors.divider color = MaterialTheme.colorScheme.outlineVariant
) )
secondaryItems.forEach { item -> secondaryItems.forEach { item ->
HomeDrawerNavItem(item = item, colors = colors) HomeDrawerNavItem(item = item)
} }
} }
} }
@@ -128,12 +126,12 @@ fun HomeDrawerNav(
@Composable @Composable
fun HomeDrawerNavItem( fun HomeDrawerNavItem(
item: HomeNavItem, item: HomeNavItem,
colors: HomeColors,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: HomePageViewModel = hiltViewModel(), viewModel: HomePageViewModel = hiltViewModel(),
) { ) {
val background = if (item.selected) colors.primary.copy(alpha = 0.12f) else Color.Transparent val scheme = MaterialTheme.colorScheme
val tint = if (item.selected) colors.primary else colors.textSecondary 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( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@@ -150,7 +148,7 @@ fun HomeDrawerNavItem(
) )
Text( Text(
text = item.label, text = item.label,
color = if (item.selected) colors.primary else colors.textPrimary, color = if (item.selected) scheme.primary else scheme.onBackground,
fontSize = 15.sp, fontSize = 15.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 12.dp) modifier = Modifier.padding(start = 12.dp)
@@ -162,30 +160,31 @@ fun HomeDrawerNavItem(
fun HomeDrawerFooter ( fun HomeDrawerFooter (
viewModel: HomePageViewModel = hiltViewModel(), viewModel: HomePageViewModel = hiltViewModel(),
user: HomeUser, user: HomeUser,
colors: HomeColors,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scheme = MaterialTheme.colorScheme
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(16.dp)
.background(colors.drawerFooterBackground, RoundedCornerShape(12.dp)) .background(scheme.surfaceVariant, RoundedCornerShape(12.dp))
.padding(12.dp), .padding(12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
HomeAvatar( HomeAvatar(
size = 32.dp, size = 32.dp,
borderWidth = 1.dp, borderWidth = 1.dp,
borderColor = colors.divider, borderColor = scheme.outlineVariant,
backgroundColor = colors.avatarBackground, backgroundColor = scheme.primaryContainer,
icon = Icons.Outlined.Person, icon = Icons.Outlined.Person,
iconTint = colors.textPrimary iconTint = scheme.onBackground
) )
Column(modifier = Modifier.padding(start = 12.dp) Column(modifier = Modifier.padding(start = 12.dp)
.clickable {viewModel.logout()}) { .clickable {viewModel.logout()}) {
Text( Text(
text = user.name, text = user.name,
color = colors.textPrimary, color = scheme.onBackground,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
@@ -193,7 +192,7 @@ fun HomeDrawerFooter (
) )
Text( Text(
text = user.plan, text = user.plan,
color = colors.textSecondary, color = scheme.onSurfaceVariant,
fontSize = 11.sp, fontSize = 11.sp,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis 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.material.icons.outlined.PlayArrow
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import coil3.compose.AsyncImage
import hu.bbara.purefin.app.home.HomePageViewModel import hu.bbara.purefin.app.home.HomePageViewModel
import hu.bbara.purefin.common.ui.PosterCard import hu.bbara.purefin.common.ui.PosterCard
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.player.PlayerActivity import hu.bbara.purefin.player.PlayerActivity
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ImageType
@@ -46,10 +47,12 @@ import kotlin.math.nextUp
@Composable @Composable
fun ContinueWatchingSection( fun ContinueWatchingSection(
items: List<ContinueWatchingItem>, colors: HomeColors, modifier: Modifier = Modifier items: List<ContinueWatchingItem>,
modifier: Modifier = Modifier
) { ) {
SectionHeader( SectionHeader(
title = "Continue Watching", action = null, colors = colors title = "Continue Watching",
action = null
) )
LazyRow( LazyRow(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
@@ -59,7 +62,7 @@ fun ContinueWatchingSection(
items( items(
items = items, key = { it.id }) { item -> items = items, key = { it.id }) { item ->
ContinueWatchingCard( ContinueWatchingCard(
item = item, colors = colors item = item
) )
} }
} }
@@ -68,10 +71,11 @@ fun ContinueWatchingSection(
@Composable @Composable
fun ContinueWatchingCard( fun ContinueWatchingCard(
item: ContinueWatchingItem, item: ContinueWatchingItem,
colors: HomeColors,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: HomePageViewModel = hiltViewModel() viewModel: HomePageViewModel = hiltViewModel()
) { ) {
val scheme = MaterialTheme.colorScheme
val context = LocalContext.current val context = LocalContext.current
fun openItem(item: ContinueWatchingItem) { fun openItem(item: ContinueWatchingItem) {
@@ -92,9 +96,9 @@ fun ContinueWatchingCard(
.aspectRatio(16f / 9f) .aspectRatio(16f / 9f)
.shadow(12.dp, RoundedCornerShape(16.dp)) .shadow(12.dp, RoundedCornerShape(16.dp))
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.background(colors.card) .background(scheme.surfaceVariant)
) { ) {
AsyncImage( PurefinAsyncImage(
model = viewModel.getImageUrl(itemId = item.id, type = ImageType.PRIMARY), model = viewModel.getImageUrl(itemId = item.id, type = ImageType.PRIMARY),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
@@ -103,25 +107,24 @@ fun ContinueWatchingCard(
openItem(item) openItem(item)
}, },
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
) )
Box( Box(
modifier = Modifier modifier = Modifier
.matchParentSize() .matchParentSize()
.background(colors.textPrimary.copy(alpha = 0.2f)) .background(scheme.onBackground.copy(alpha = 0.2f))
) )
Box( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.fillMaxWidth() .fillMaxWidth()
.height(4.dp) .height(4.dp)
.background(colors.textPrimary.copy(alpha = 0.2f)) .background(scheme.onBackground.copy(alpha = 0.2f))
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.fillMaxWidth(item.progress.toFloat().nextUp().div(100)) .fillMaxWidth(item.progress.toFloat().nextUp().div(100))
.background(colors.primary) .background(scheme.primary)
) )
} }
Button( Button(
@@ -136,7 +139,7 @@ fun ContinueWatchingCard(
Column(modifier = Modifier.padding(top = 12.dp)) { Column(modifier = Modifier.padding(top = 12.dp)) {
Text( Text(
text = item.primaryText, text = item.primaryText,
color = colors.textPrimary, color = scheme.onBackground,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
@@ -144,7 +147,7 @@ fun ContinueWatchingCard(
) )
Text( Text(
text = item.secondaryText, text = item.secondaryText,
color = colors.textSecondary, color = scheme.onSurfaceVariant,
fontSize = 13.sp, fontSize = 13.sp,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@@ -158,11 +161,11 @@ fun LibraryPosterSection(
title: String, title: String,
items: List<PosterItem>, items: List<PosterItem>,
action: String?, action: String?,
colors: HomeColors,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
SectionHeader( SectionHeader(
title = title, action = action, colors = colors title = title,
action = action
) )
LazyRow( LazyRow(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
@@ -173,7 +176,6 @@ fun LibraryPosterSection(
items = items, key = { it.id }) { item -> items = items, key = { it.id }) { item ->
PosterCard( PosterCard(
item = item, item = item,
colors = colors,
) )
} }
} }
@@ -183,10 +185,11 @@ fun LibraryPosterSection(
fun SectionHeader( fun SectionHeader(
title: String, title: String,
action: String?, action: String?,
colors: HomeColors,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onActionClick: () -> Unit = {} onActionClick: () -> Unit = {}
) { ) {
val scheme = MaterialTheme.colorScheme
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@@ -195,12 +198,15 @@ fun SectionHeader(
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( 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) { if (action != null) {
Text( Text(
text = action, text = action,
color = colors.primary, color = scheme.primary,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
modifier = Modifier.clickable { onActionClick() }) 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.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -30,24 +31,25 @@ import hu.bbara.purefin.app.home.HomePageViewModel
fun HomeTopBar( fun HomeTopBar(
viewModel: HomePageViewModel = hiltViewModel(), viewModel: HomePageViewModel = hiltViewModel(),
title: String, title: String,
colors: HomeColors,
onMenuClick: () -> Unit, onMenuClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
actions: @Composable RowScope.() -> Unit = { actions: @Composable RowScope.() -> Unit = {
HomeAvatar( HomeAvatar(
size = 36.dp, size = 36.dp,
borderWidth = 2.dp, borderWidth = 2.dp,
borderColor = colors.avatarBorder, borderColor = MaterialTheme.colorScheme.outline,
backgroundColor = colors.avatarBackground, backgroundColor = MaterialTheme.colorScheme.primaryContainer,
icon = Icons.Outlined.Person, icon = Icons.Outlined.Person,
iconTint = colors.onPrimary iconTint = MaterialTheme.colorScheme.onPrimary
) )
} }
) { ) {
val scheme = MaterialTheme.colorScheme
Box( Box(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.background(colors.background.copy(alpha = 0.95f)) .background(scheme.background.copy(alpha = 0.95f))
.zIndex(1f) .zIndex(1f)
) { ) {
Row( Row(
@@ -63,12 +65,12 @@ fun HomeTopBar(
Icon( Icon(
imageVector = Icons.Outlined.Menu, imageVector = Icons.Outlined.Menu,
contentDescription = "Menu", contentDescription = "Menu",
tint = colors.textPrimary tint = scheme.onBackground
) )
} }
Text( Text(
text = title, text = title,
color = colors.textPrimary, color = scheme.onBackground,
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.Bold 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.Icons
import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment 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.TextUnit
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 coil3.compose.AsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
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
)
@Composable @Composable
fun MediaGhostIconButton( fun MediaGhostIconButton(
colors: MediaDetailColors,
icon: ImageVector, icon: ImageVector,
contentDescription: String, contentDescription: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scheme = MaterialTheme.colorScheme
Box( Box(
modifier = modifier modifier = modifier
.size(40.dp) .size(40.dp)
.clip(CircleShape) .clip(CircleShape)
.background(colors.background.copy(alpha = 0.4f)) .background(scheme.background.copy(alpha = 0.4f))
.clickable { onClick() }, .clickable { onClick() },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = contentDescription, contentDescription = contentDescription,
tint = colors.textPrimary tint = scheme.onBackground
) )
} }
} }
@Composable @Composable
fun MediaMetaChip( fun MediaMetaChip(
colors: MediaDetailColors,
text: String, text: String,
background: Color = colors.surfaceAlt, background: Color = MaterialTheme.colorScheme.surfaceVariant,
border: Color = Color.Transparent, border: Color = Color.Transparent,
textColor: Color = colors.textSecondary, textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Box( Box(
@@ -153,13 +99,15 @@ data class MediaCastMember(
@Composable @Composable
fun MediaCastRow( fun MediaCastRow(
colors: MediaDetailColors,
cast: List<MediaCastMember>, cast: List<MediaCastMember>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
cardWidth: Dp = 96.dp, cardWidth: Dp = 96.dp,
nameSize: TextUnit = 12.sp, nameSize: TextUnit = 12.sp,
roleSize: TextUnit = 10.sp roleSize: TextUnit = 10.sp
) { ) {
val scheme = MaterialTheme.colorScheme
val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
LazyRow( LazyRow(
modifier = modifier, modifier = modifier,
contentPadding = PaddingValues(horizontal = 4.dp), contentPadding = PaddingValues(horizontal = 4.dp),
@@ -171,23 +119,23 @@ fun MediaCastRow(
modifier = Modifier modifier = Modifier
.aspectRatio(4f / 5f) .aspectRatio(4f / 5f)
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.background(colors.surfaceAlt) .background(scheme.surfaceVariant)
) { ) {
if (member.imageUrl == null) { if (member.imageUrl == null) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(colors.surfaceAlt.copy(alpha = 0.6f)), .background(scheme.surfaceVariant.copy(alpha = 0.6f)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.Person, imageVector = Icons.Outlined.Person,
contentDescription = null, contentDescription = null,
tint = colors.textMutedStrong tint = mutedStrong
) )
} }
} else { } else {
AsyncImage( PurefinAsyncImage(
model = member.imageUrl, model = member.imageUrl,
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -198,7 +146,7 @@ fun MediaCastRow(
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
Text( Text(
text = member.name, text = member.name,
color = colors.textPrimary, color = scheme.onBackground,
fontSize = nameSize, fontSize = nameSize,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
maxLines = 1, maxLines = 1,
@@ -206,7 +154,7 @@ fun MediaCastRow(
) )
Text( Text(
text = member.role, text = member.role,
color = colors.textMutedStrong, color = mutedStrong,
fontSize = roleSize, fontSize = roleSize,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis 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.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import coil3.compose.AsyncImage
import hu.bbara.purefin.app.home.HomePageViewModel 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.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.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ImageType
@Composable @Composable
fun PosterCard( fun PosterCard(
item: PosterItem, item: PosterItem,
colors: HomeColors = rememberHomeColors(),
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: HomePageViewModel = hiltViewModel() viewModel: HomePageViewModel = hiltViewModel()
) { ) {
val scheme = MaterialTheme.colorScheme
fun openItem(posterItem: PosterItem) { fun openItem(posterItem: PosterItem) {
when (posterItem.type) { when (posterItem.type) {
BaseItemKind.MOVIE -> viewModel.onMovieSelected(posterItem.id.toString()) BaseItemKind.MOVIE -> viewModel.onMovieSelected(posterItem.id.toString())
@@ -45,20 +45,20 @@ fun PosterCard(
modifier = Modifier modifier = Modifier
.width(144.dp) .width(144.dp)
) { ) {
AsyncImage( PurefinAsyncImage(
model = viewModel.getImageUrl(item.imageItemId, ImageType.PRIMARY), model = viewModel.getImageUrl(item.imageItemId, ImageType.PRIMARY),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.aspectRatio(2f / 3f) .aspectRatio(2f / 3f)
.shadow(10.dp, RoundedCornerShape(14.dp)) .shadow(10.dp, RoundedCornerShape(14.dp))
.clip(RoundedCornerShape(14.dp)) .clip(RoundedCornerShape(14.dp))
.background(colors.card) .background(scheme.surfaceVariant)
.clickable(onClick = { openItem(item) }), .clickable(onClick = { openItem(item) }),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
Text( Text(
text = item.title, text = item.title,
color = colors.textPrimary, color = scheme.onBackground,
fontSize = 13.sp, fontSize = 13.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
modifier = Modifier.padding(top = 8.dp, start = 4.dp, end = 4.dp, bottom = 8.dp), modifier = Modifier.padding(top = 8.dp, start = 4.dp, end = 4.dp, bottom = 8.dp),

View File

@@ -10,7 +10,6 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import coil3.compose.AsyncImage
@Composable @Composable
fun MediaHero( fun MediaHero(
@@ -24,7 +23,7 @@ fun MediaHero(
.height(height) .height(height)
.background(backgroundColor) .background(backgroundColor)
) { ) {
AsyncImage( PurefinAsyncImage(
model = imageUrl, model = imageUrl,
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize(), 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) super.onCreate(savedInstanceState)
setContent { setContent {
PurefinTheme { PurefinTheme(darkTheme = false) {
val viewModel = hiltViewModel<PlayerViewModel>() val viewModel = hiltViewModel<PlayerViewModel>()
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

View File

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