refactor color management to use MaterialTheme and implement dynamic color schemes

This commit is contained in:
2026-01-20 16:53:11 +01:00
parent c4347e610c
commit 00cf71e65e
12 changed files with 362 additions and 190 deletions

View File

@@ -22,11 +22,12 @@ fun EpisodeCard(
episode: EpisodeUiModel,
modifier: Modifier = Modifier,
) {
val colors = rememberEpisodeColors()
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(EpisodeBackgroundDark)
.background(colors.background)
) {
val isWide = maxWidth >= 900.dp
val contentPadding = if (isWide) 32.dp else 20.dp

View File

@@ -1,10 +1,38 @@
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 val EpisodePrimary = Color(0xFFDDA73C)
internal val EpisodeBackgroundDark = Color(0xFF141414)
internal val EpisodeSurfaceDark = Color(0xFF1F1F1F)
internal val EpisodeSurfaceBorder = Color(0x1AFFFFFF)
internal val EpisodeMuted = Color(0x99FFFFFF)
internal val EpisodeMutedStrong = Color(0x66FFFFFF)
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

@@ -87,18 +87,19 @@ private fun GhostIconButton(
contentDescription: String,
modifier: Modifier = Modifier
) {
val colors = rememberEpisodeColors()
Box(
modifier = modifier
.size(40.dp)
.clip(CircleShape)
.background(EpisodeBackgroundDark.copy(alpha = 0.4f))
.background(colors.background.copy(alpha = 0.4f))
.clickable { onClick() },
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = Color.White
tint = colors.textPrimary
)
}
}
@@ -110,10 +111,11 @@ internal fun EpisodeHero(
isWide: Boolean,
modifier: Modifier = Modifier
) {
val colors = rememberEpisodeColors()
Box(
modifier = modifier
.height(height)
.background(EpisodeBackgroundDark)
.background(colors.background)
) {
AsyncImage(
model = episode.heroImageUrl,
@@ -128,8 +130,8 @@ internal fun EpisodeHero(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
EpisodeBackgroundDark.copy(alpha = 0.4f),
EpisodeBackgroundDark
colors.background.copy(alpha = 0.4f),
colors.background
)
)
)
@@ -142,7 +144,7 @@ internal fun EpisodeHero(
Brush.horizontalGradient(
colors = listOf(
Color.Transparent,
EpisodeBackgroundDark.copy(alpha = 0.8f)
colors.background.copy(alpha = 0.8f)
)
)
)
@@ -163,10 +165,11 @@ internal fun EpisodeDetails(
episode: EpisodeUiModel,
modifier: Modifier = Modifier
) {
val colors = rememberEpisodeColors()
Column(modifier = modifier) {
Text(
text = episode.title,
color = Color.White,
color = colors.textPrimary,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
lineHeight = 38.sp
@@ -181,9 +184,9 @@ internal fun EpisodeDetails(
MetaChip(text = episode.runtime)
MetaChip(
text = episode.format,
background = EpisodePrimary.copy(alpha = 0.2f),
border = EpisodePrimary.copy(alpha = 0.3f),
textColor = EpisodePrimary
background = colors.primary.copy(alpha = 0.2f),
border = colors.primary.copy(alpha = 0.3f),
textColor = colors.primary
)
}
@@ -193,14 +196,14 @@ internal fun EpisodeDetails(
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Synopsis",
color = Color.White,
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = episode.synopsis,
color = EpisodeMuted,
color = colors.textMuted,
fontSize = 15.sp,
lineHeight = 22.sp
)
@@ -211,7 +214,7 @@ internal fun EpisodeDetails(
Spacer(modifier = Modifier.height(28.dp))
Text(
text = "Cast",
color = Color.White,
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
@@ -223,23 +226,27 @@ internal fun EpisodeDetails(
@Composable
private fun MetaChip(
text: String,
background: Color = Color.White.copy(alpha = 0.1f),
border: Color = Color.Transparent,
textColor: Color = Color.White
background: Color? = null,
border: Color? = null,
textColor: Color? = null
) {
val colors = rememberEpisodeColors()
val resolvedBackground = background ?: colors.surfaceAlt
val resolvedBorder = border ?: Color.Transparent
val resolvedTextColor = textColor ?: colors.textSecondary
Box(
modifier = Modifier
.height(28.dp)
.wrapContentHeight(Alignment.CenterVertically)
.clip(RoundedCornerShape(6.dp))
.background(background)
.border(width = 1.dp, color = border, shape = RoundedCornerShape(6.dp))
.background(resolvedBackground)
.border(width = 1.dp, color = resolvedBorder, shape = RoundedCornerShape(6.dp))
.padding(horizontal = 12.dp),
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = textColor,
color = resolvedTextColor,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
@@ -248,12 +255,13 @@ private fun MetaChip(
@Composable
private fun PlaybackSettings(episode: EpisodeUiModel) {
val colors = rememberEpisodeColors()
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(EpisodeSurfaceDark)
.border(1.dp, EpisodeSurfaceBorder, RoundedCornerShape(16.dp))
.background(colors.surfaceAlt)
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(16.dp))
.padding(20.dp)
) {
Row(
@@ -262,12 +270,12 @@ private fun PlaybackSettings(episode: EpisodeUiModel) {
Icon(
imageVector = Icons.Outlined.Tune,
contentDescription = null,
tint = EpisodePrimary
tint = colors.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Playback Settings",
color = EpisodeMuted,
color = colors.textMuted,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
@@ -295,10 +303,11 @@ private fun SettingDropdown(
icon: ImageVector,
value: String
) {
val colors = rememberEpisodeColors()
Column {
Text(
text = label,
color = EpisodeMutedStrong,
color = colors.textMutedStrong,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 4.dp, bottom = 6.dp)
@@ -308,18 +317,18 @@ private fun SettingDropdown(
.fillMaxWidth()
.height(48.dp)
.clip(RoundedCornerShape(12.dp))
.background(EpisodeBackgroundDark)
.border(1.dp, EpisodeSurfaceBorder, RoundedCornerShape(12.dp))
.background(colors.surface)
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(imageVector = icon, contentDescription = null, tint = EpisodeMutedStrong)
Icon(imageVector = icon, contentDescription = null, tint = colors.textMutedStrong)
Spacer(modifier = Modifier.width(10.dp))
Text(text = value, color = Color.White, fontSize = 14.sp)
Text(text = value, color = colors.textPrimary, fontSize = 14.sp)
}
Icon(imageVector = Icons.Outlined.ExpandMore, contentDescription = null, tint = EpisodeMutedStrong)
Icon(imageVector = Icons.Outlined.ExpandMore, contentDescription = null, tint = colors.textMutedStrong)
}
}
}
@@ -351,24 +360,26 @@ private fun ActionButton(
icon: ImageVector,
modifier: Modifier = Modifier
) {
val colors = rememberEpisodeColors()
Row(
modifier = modifier
.height(48.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color.White.copy(alpha = 0.05f))
.border(1.dp, EpisodeSurfaceBorder, RoundedCornerShape(12.dp))
.background(colors.surfaceAlt.copy(alpha = 0.6f))
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
.clickable { },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(imageVector = icon, contentDescription = null, tint = Color.White)
Icon(imageVector = icon, contentDescription = null, tint = colors.textPrimary)
Spacer(modifier = Modifier.width(8.dp))
Text(text = text, color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Bold)
Text(text = text, color = colors.textPrimary, fontSize = 14.sp, fontWeight = FontWeight.Bold)
}
}
@Composable
private fun CastRow(cast: List<CastMember>) {
val colors = rememberEpisodeColors()
LazyRow(
contentPadding = PaddingValues(horizontal = 4.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
@@ -379,19 +390,19 @@ private fun CastRow(cast: List<CastMember>) {
modifier = Modifier
.aspectRatio(4f / 5f)
.clip(RoundedCornerShape(12.dp))
.background(EpisodeSurfaceDark)
.background(colors.surfaceAlt)
) {
if (member.imageUrl == null) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White.copy(alpha = 0.05f)),
.background(colors.surfaceAlt.copy(alpha = 0.6f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Outlined.Person,
contentDescription = null,
tint = EpisodeMutedStrong
tint = colors.textMutedStrong
)
}
} else {
@@ -406,7 +417,7 @@ private fun CastRow(cast: List<CastMember>) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = member.name,
color = Color.White,
color = colors.textPrimary,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
@@ -414,7 +425,7 @@ private fun CastRow(cast: List<CastMember>) {
)
Text(
text = member.role,
color = EpisodeMutedStrong,
color = colors.textMutedStrong,
fontSize = 10.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@@ -430,6 +441,7 @@ private fun PlayButton(
modifier: Modifier = Modifier,
viewModel: EpisodeScreenViewModel = hiltViewModel()
) {
val colors = rememberEpisodeColors()
val context = LocalContext.current
val episodeItem = viewModel.episode.collectAsState()
@@ -438,7 +450,7 @@ private fun PlayButton(
.size(size)
.shadow(24.dp, CircleShape)
.clip(CircleShape)
.background(EpisodePrimary)
.background(colors.primary)
.clickable {
val intent = Intent(context, PlayerActivity::class.java)
intent.putExtra("MEDIA_ID", episodeItem.value!!.id.toString())
@@ -449,7 +461,7 @@ private fun PlayButton(
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = EpisodeBackgroundDark,
tint = colors.onPrimary,
modifier = Modifier.size(42.dp)
)
}
@@ -457,18 +469,19 @@ private fun PlayButton(
@Composable
internal fun FloatingPlayButton(modifier: Modifier = Modifier) {
val colors = rememberEpisodeColors()
Box(
modifier = modifier
.size(56.dp)
.shadow(20.dp, CircleShape)
.clip(CircleShape)
.background(EpisodePrimary),
.background(colors.primary),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = EpisodeBackgroundDark
tint = colors.onPrimary
)
}
}

View File

@@ -22,11 +22,12 @@ fun MovieCard(
movie: MovieUiModel,
modifier: Modifier = Modifier,
) {
val colors = rememberMovieColors()
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(MovieBackgroundDark)
.background(colors.background)
) {
val isWide = maxWidth >= 900.dp
val contentPadding = if (isWide) 32.dp else 20.dp

View File

@@ -1,10 +1,38 @@
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 val MoviePrimary = Color(0xFFDDA73C)
internal val MovieBackgroundDark = Color(0xFF141414)
internal val MovieSurfaceDark = Color(0xFF1F1F1F)
internal val MovieSurfaceBorder = Color(0x1AFFFFFF)
internal val MovieMuted = Color(0x99FFFFFF)
internal val MovieMutedStrong = Color(0x66FFFFFF)
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

@@ -87,18 +87,19 @@ private fun GhostIconButton(
onClick: () -> Unit = {},
modifier: Modifier = Modifier
) {
val colors = rememberMovieColors()
Box(
modifier = modifier
.size(40.dp)
.clip(CircleShape)
.background(MovieBackgroundDark.copy(alpha = 0.4f))
.background(colors.background.copy(alpha = 0.4f))
.clickable { onClick() },
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = Color.White
tint = colors.textPrimary
)
}
}
@@ -110,10 +111,11 @@ internal fun MovieHero(
isWide: Boolean,
modifier: Modifier = Modifier
) {
val colors = rememberMovieColors()
Box(
modifier = modifier
.height(height)
.background(MovieBackgroundDark)
.background(colors.background)
) {
AsyncImage(
model = movie.heroImageUrl,
@@ -128,8 +130,8 @@ internal fun MovieHero(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
MovieBackgroundDark.copy(alpha = 0.4f),
MovieBackgroundDark
colors.background.copy(alpha = 0.4f),
colors.background
)
)
)
@@ -141,7 +143,7 @@ internal fun MovieHero(
.background(
Brush.horizontalGradient(
colors = listOf(
Color.Transparent, MovieBackgroundDark.copy(alpha = 0.8f)
Color.Transparent, colors.background.copy(alpha = 0.8f)
)
)
)
@@ -162,10 +164,11 @@ internal fun MovieDetails(
movie: MovieUiModel,
modifier: Modifier = Modifier
) {
val colors = rememberMovieColors()
Column(modifier = modifier) {
Text(
text = movie.title,
color = Color.White,
color = colors.textPrimary,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
lineHeight = 38.sp
@@ -180,9 +183,9 @@ internal fun MovieDetails(
MetaChip(text = movie.runtime)
MetaChip(
text = movie.format,
background = MoviePrimary.copy(alpha = 0.2f),
border = MoviePrimary.copy(alpha = 0.3f),
textColor = MoviePrimary
background = colors.primary.copy(alpha = 0.2f),
border = colors.primary.copy(alpha = 0.3f),
textColor = colors.primary
)
}
@@ -192,14 +195,14 @@ internal fun MovieDetails(
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Synopsis",
color = Color.White,
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = movie.synopsis,
color = MovieMuted,
color = colors.textMuted,
fontSize = 15.sp,
lineHeight = 22.sp
)
@@ -210,7 +213,7 @@ internal fun MovieDetails(
Spacer(modifier = Modifier.height(28.dp))
Text(
text = "Cast",
color = Color.White,
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
@@ -222,23 +225,27 @@ internal fun MovieDetails(
@Composable
private fun MetaChip(
text: String,
background: Color = Color.White.copy(alpha = 0.1f),
border: Color = Color.Transparent,
textColor: Color = Color.White
background: Color? = null,
border: Color? = null,
textColor: Color? = null
) {
val colors = rememberMovieColors()
val resolvedBackground = background ?: colors.surfaceAlt
val resolvedBorder = border ?: Color.Transparent
val resolvedTextColor = textColor ?: colors.textSecondary
Box(
modifier = Modifier
.height(28.dp)
.wrapContentHeight(Alignment.CenterVertically)
.clip(RoundedCornerShape(6.dp))
.background(background)
.border(width = 1.dp, color = border, shape = RoundedCornerShape(6.dp))
.background(resolvedBackground)
.border(width = 1.dp, color = resolvedBorder, shape = RoundedCornerShape(6.dp))
.padding(horizontal = 12.dp),
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = textColor,
color = resolvedTextColor,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
@@ -247,12 +254,13 @@ private fun MetaChip(
@Composable
private fun PlaybackSettings(movie: MovieUiModel) {
val colors = rememberMovieColors()
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(MovieSurfaceDark)
.border(1.dp, MovieSurfaceBorder, RoundedCornerShape(16.dp))
.background(colors.surfaceAlt)
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(16.dp))
.padding(20.dp)
) {
Row(
@@ -261,12 +269,12 @@ private fun PlaybackSettings(movie: MovieUiModel) {
Icon(
imageVector = Icons.Outlined.Tune,
contentDescription = null,
tint = MoviePrimary
tint = colors.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Playback Settings",
color = MovieMuted,
color = colors.textMuted,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
@@ -294,10 +302,11 @@ private fun SettingDropdown(
icon: ImageVector,
value: String
) {
val colors = rememberMovieColors()
Column {
Text(
text = label,
color = MovieMutedStrong,
color = colors.textMutedStrong,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 4.dp, bottom = 6.dp)
@@ -307,18 +316,18 @@ private fun SettingDropdown(
.fillMaxWidth()
.height(48.dp)
.clip(RoundedCornerShape(12.dp))
.background(MovieBackgroundDark)
.border(1.dp, MovieSurfaceBorder, RoundedCornerShape(12.dp))
.background(colors.surface)
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(imageVector = icon, contentDescription = null, tint = MovieMutedStrong)
Icon(imageVector = icon, contentDescription = null, tint = colors.textMutedStrong)
Spacer(modifier = Modifier.width(10.dp))
Text(text = value, color = Color.White, fontSize = 14.sp)
Text(text = value, color = colors.textPrimary, fontSize = 14.sp)
}
Icon(imageVector = Icons.Outlined.ExpandMore, contentDescription = null, tint = MovieMutedStrong)
Icon(imageVector = Icons.Outlined.ExpandMore, contentDescription = null, tint = colors.textMutedStrong)
}
}
}
@@ -350,24 +359,26 @@ private fun ActionButton(
icon: ImageVector,
modifier: Modifier = Modifier
) {
val colors = rememberMovieColors()
Row(
modifier = modifier
.height(48.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color.White.copy(alpha = 0.05f))
.border(1.dp, MovieSurfaceBorder, RoundedCornerShape(12.dp))
.background(colors.surfaceAlt.copy(alpha = 0.6f))
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
.clickable { },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(imageVector = icon, contentDescription = null, tint = Color.White)
Icon(imageVector = icon, contentDescription = null, tint = colors.textPrimary)
Spacer(modifier = Modifier.width(8.dp))
Text(text = text, color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Bold)
Text(text = text, color = colors.textPrimary, fontSize = 14.sp, fontWeight = FontWeight.Bold)
}
}
@Composable
private fun CastRow(cast: List<CastMember>) {
val colors = rememberMovieColors()
LazyRow(
contentPadding = PaddingValues(horizontal = 4.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
@@ -378,19 +389,19 @@ private fun CastRow(cast: List<CastMember>) {
modifier = Modifier
.aspectRatio(4f / 5f)
.clip(RoundedCornerShape(12.dp))
.background(MovieSurfaceDark)
.background(colors.surfaceAlt)
) {
if (member.imageUrl == null) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White.copy(alpha = 0.05f)),
.background(colors.surfaceAlt.copy(alpha = 0.6f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Outlined.Person,
contentDescription = null,
tint = MovieMutedStrong
tint = colors.textMutedStrong
)
}
} else {
@@ -405,7 +416,7 @@ private fun CastRow(cast: List<CastMember>) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = member.name,
color = Color.White,
color = colors.textPrimary,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
@@ -413,7 +424,7 @@ private fun CastRow(cast: List<CastMember>) {
)
Text(
text = member.role,
color = MovieMutedStrong,
color = colors.textMutedStrong,
fontSize = 10.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@@ -429,6 +440,7 @@ private fun PlayButton(
modifier: Modifier = Modifier,
viewModel: MovieScreenViewModel = hiltViewModel()
) {
val colors = rememberMovieColors()
val context = LocalContext.current
val movieId = viewModel.movie.collectAsState()
@@ -437,7 +449,7 @@ private fun PlayButton(
.size(size)
.shadow(24.dp, CircleShape)
.clip(CircleShape)
.background(MoviePrimary)
.background(colors.primary)
.clickable {
val intent = Intent(context, PlayerActivity::class.java)
intent.putExtra("MEDIA_ID", movieId.value!!.id.toString())
@@ -448,7 +460,7 @@ private fun PlayButton(
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = MovieBackgroundDark,
tint = colors.onPrimary,
modifier = Modifier.size(42.dp)
)
}
@@ -456,18 +468,19 @@ private fun PlayButton(
@Composable
internal fun FloatingPlayButton(modifier: Modifier = Modifier) {
val colors = rememberMovieColors()
Box(
modifier = modifier
.size(56.dp)
.shadow(20.dp, CircleShape)
.clip(CircleShape)
.background(MoviePrimary),
.background(colors.primary),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = MovieBackgroundDark
tint = colors.onPrimary
)
}
}

View File

@@ -15,7 +15,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -26,11 +25,12 @@ fun SeriesCard(
series: SeriesUiModel,
modifier: Modifier = Modifier,
) {
val colors = rememberSeriesColors()
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(SeriesBackgroundDark)
.background(colors.background)
) {
val heroHeight = maxHeight * 0.4f
Column(
@@ -54,7 +54,7 @@ fun SeriesCard(
) {
Text(
text = series.title,
color = Color.White,
color = colors.textPrimary,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
lineHeight = 36.sp
@@ -66,20 +66,20 @@ fun SeriesCard(
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Synopsis",
color = Color.White,
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = series.synopsis,
color = SeriesMutedStrong,
color = colors.textMutedStrong,
fontSize = 13.sp,
)
Spacer(modifier = Modifier.height(28.dp))
Text(
text = "Episodes",
color = Color.White,
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
@@ -101,7 +101,7 @@ fun SeriesCard(
) {
Text(
text = "Cast",
color = Color.White,
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 20.dp)

View File

@@ -1,10 +1,38 @@
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 val SeriesPrimary = Color(0xFFDDA73C)
internal val SeriesBackgroundDark = Color(0xFF141414)
internal val SeriesSurfaceDark = Color(0xFF1F1F1F)
internal val SeriesSurfaceBorder = Color(0x1AFFFFFF)
internal val SeriesMuted = Color(0xB3FFFFFF)
internal val SeriesMutedStrong = Color(0x99FFFFFF)
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

@@ -81,18 +81,19 @@ private fun GhostIconButton(
contentDescription: String,
modifier: Modifier = Modifier
) {
val colors = rememberSeriesColors()
Box(
modifier = modifier
.size(40.dp)
.clip(CircleShape)
.background(SeriesBackgroundDark.copy(alpha = 0.4f))
.background(colors.background.copy(alpha = 0.4f))
.clickable { onClick() },
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = Color.White
tint = colors.textPrimary
)
}
}
@@ -103,10 +104,11 @@ internal fun SeriesHero(
height: Dp,
modifier: Modifier = Modifier
) {
val colors = rememberSeriesColors()
Box(
modifier = modifier
.height(height)
.background(SeriesBackgroundDark)
.background(colors.background)
) {
AsyncImage(
model = imageUrl,
@@ -121,8 +123,8 @@ internal fun SeriesHero(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
SeriesBackgroundDark.copy(alpha = 0.4f),
SeriesBackgroundDark
colors.background.copy(alpha = 0.4f),
colors.background
)
)
)
@@ -133,6 +135,7 @@ internal fun SeriesHero(
@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun SeriesMetaChips(series: SeriesUiModel) {
val colors = rememberSeriesColors()
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
@@ -142,9 +145,9 @@ internal fun SeriesMetaChips(series: SeriesUiModel) {
MetaChip(text = series.seasons)
MetaChip(
text = series.format,
background = SeriesPrimary.copy(alpha = 0.2f),
border = SeriesPrimary.copy(alpha = 0.3f),
textColor = SeriesPrimary
background = colors.primary.copy(alpha = 0.2f),
border = colors.primary.copy(alpha = 0.3f),
textColor = colors.primary
)
}
}
@@ -152,23 +155,27 @@ internal fun SeriesMetaChips(series: SeriesUiModel) {
@Composable
private fun MetaChip(
text: String,
background: Color = Color.White.copy(alpha = 0.1f),
border: Color = Color.White.copy(alpha = 0.05f),
textColor: Color = Color.White
background: Color? = null,
border: Color? = null,
textColor: Color? = null
) {
val colors = rememberSeriesColors()
val resolvedBackground = background ?: colors.surfaceAlt
val resolvedBorder = border ?: colors.surfaceBorder
val resolvedTextColor = textColor ?: colors.textSecondary
Box(
modifier = Modifier
.height(28.dp)
.wrapContentHeight(Alignment.CenterVertically)
.clip(RoundedCornerShape(6.dp))
.background(background)
.border(width = 1.dp, color = border, shape = RoundedCornerShape(6.dp))
.background(resolvedBackground)
.border(width = 1.dp, color = resolvedBorder, shape = RoundedCornerShape(6.dp))
.padding(horizontal = 12.dp),
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = textColor,
color = resolvedTextColor,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
@@ -200,19 +207,20 @@ private fun ActionButton(
icon: ImageVector,
modifier: Modifier = Modifier
) {
val colors = rememberSeriesColors()
Row(
modifier = modifier
.height(44.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color.White.copy(alpha = 0.1f))
.border(1.dp, SeriesSurfaceBorder, RoundedCornerShape(12.dp))
.background(colors.surfaceAlt.copy(alpha = 0.6f))
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
.clickable { },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(imageVector = icon, contentDescription = null, tint = Color.White, modifier = Modifier.size(18.dp))
Icon(imageVector = icon, contentDescription = null, tint = colors.textPrimary, modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(text = text, color = Color.White, fontSize = 13.sp, fontWeight = FontWeight.Bold)
Text(text = text, color = colors.textPrimary, fontSize = 13.sp, fontWeight = FontWeight.Bold)
}
}
@@ -232,8 +240,9 @@ internal fun SeasonTabs(seasons: List<SeriesSeasonUiModel>, modifier: Modifier =
@Composable
private fun SeasonTab(name: String, isSelected: Boolean) {
val color = if (isSelected) SeriesPrimary else SeriesMutedStrong
val borderColor = if (isSelected) SeriesPrimary else Color.Transparent
val colors = rememberSeriesColors()
val color = if (isSelected) colors.primary else colors.textMutedStrong
val borderColor = if (isSelected) colors.primary else Color.Transparent
Column(
modifier = Modifier
.padding(bottom = 8.dp)
@@ -273,12 +282,13 @@ private fun EpisodeCard(
viewModel: SeriesViewModel = hiltViewModel(),
episode: SeriesEpisodeUiModel
) {
val colors = rememberSeriesColors()
Column(
modifier = Modifier
.width(260.dp)
.clip(RoundedCornerShape(16.dp))
.background(SeriesSurfaceDark.copy(alpha = 0.3f))
.border(1.dp, SeriesSurfaceBorder, RoundedCornerShape(16.dp))
.background(colors.surfaceAlt.copy(alpha = 0.6f))
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(16.dp))
.padding(12.dp)
.clickable { viewModel.onSelectEpisode(episode.id) },
verticalArrangement = Arrangement.spacedBy(12.dp)
@@ -288,8 +298,8 @@ private fun EpisodeCard(
.fillMaxWidth()
.aspectRatio(16f / 9f)
.clip(RoundedCornerShape(12.dp))
.background(SeriesSurfaceDark)
.border(1.dp, SeriesSurfaceBorder, RoundedCornerShape(12.dp))
.background(colors.surface)
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
) {
AsyncImage(
model = episode.imageUrl,
@@ -300,12 +310,12 @@ private fun EpisodeCard(
Box(
modifier = Modifier
.matchParentSize()
.background(Color.Black.copy(alpha = 0.2f))
.background(colors.background.copy(alpha = 0.2f))
)
Icon(
imageVector = Icons.Outlined.PlayCircle,
contentDescription = null,
tint = Color.White,
tint = colors.textPrimary,
modifier = Modifier
.align(Alignment.Center)
.size(32.dp)
@@ -314,12 +324,12 @@ private fun EpisodeCard(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(6.dp)
.background(Color.Black.copy(alpha = 0.8f), RoundedCornerShape(6.dp))
.background(colors.background.copy(alpha = 0.8f), RoundedCornerShape(6.dp))
.padding(horizontal = 6.dp, vertical = 2.dp)
) {
Text(
text = episode.duration,
color = Color.White,
color = colors.textPrimary,
fontSize = 10.sp,
fontWeight = FontWeight.Bold
)
@@ -330,7 +340,7 @@ private fun EpisodeCard(
) {
Text(
text = episode.title,
color = Color.White,
color = colors.textPrimary,
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
@@ -338,7 +348,7 @@ private fun EpisodeCard(
)
Text(
text = episode.description,
color = SeriesMutedStrong,
color = colors.textMutedStrong,
fontSize = 11.sp,
lineHeight = 16.sp,
maxLines = 2,
@@ -363,6 +373,7 @@ internal fun CastRow(cast: List<SeriesCastMemberUiModel>, modifier: Modifier = M
@Composable
private fun CastCard(member: SeriesCastMemberUiModel) {
val colors = rememberSeriesColors()
Column(
modifier = Modifier.width(84.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
@@ -371,20 +382,20 @@ private fun CastCard(member: SeriesCastMemberUiModel) {
modifier = Modifier
.aspectRatio(4f / 5f)
.clip(RoundedCornerShape(12.dp))
.background(SeriesSurfaceDark)
.border(1.dp, SeriesSurfaceBorder, RoundedCornerShape(12.dp))
.background(colors.surfaceAlt)
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
) {
if (member.imageUrl == null) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White.copy(alpha = 0.05f)),
.background(colors.surfaceAlt.copy(alpha = 0.6f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Outlined.Person,
contentDescription = null,
tint = SeriesMutedStrong
tint = colors.textMutedStrong
)
}
} else {
@@ -398,7 +409,7 @@ private fun CastCard(member: SeriesCastMemberUiModel) {
}
Text(
text = member.name,
color = Color.White,
color = colors.textPrimary,
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
@@ -406,7 +417,7 @@ private fun CastCard(member: SeriesCastMemberUiModel) {
)
Text(
text = member.role,
color = SeriesMutedStrong,
color = colors.textMutedStrong,
fontSize = 10.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@@ -416,19 +427,20 @@ private fun CastCard(member: SeriesCastMemberUiModel) {
@Composable
private fun PlayButton(size: Dp, modifier: Modifier = Modifier) {
val colors = rememberSeriesColors()
Box(
modifier = modifier
.size(size)
.shadow(24.dp, CircleShape)
.clip(CircleShape)
.background(SeriesPrimary)
.background(colors.primary)
.clickable { },
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = SeriesBackgroundDark,
tint = colors.onPrimary,
modifier = Modifier.size(36.dp)
)
}

View File

@@ -1,6 +1,6 @@
package hu.bbara.purefin.app.home.ui
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
@@ -20,21 +20,21 @@ data class HomeColors(
)
@Composable
fun rememberHomeColors(isDark: Boolean = isSystemInDarkTheme()): HomeColors {
val primary = Color(0xFFDDA73C)
return remember(isDark) {
fun rememberHomeColors(): HomeColors {
val scheme = MaterialTheme.colorScheme
return remember(scheme) {
HomeColors(
primary = primary,
onPrimary = Color(0xFF17171C),
background = if (isDark) Color(0xFF17171C) else Color(0xFFF8F7F6),
drawerBackground = if (isDark) Color(0xFF1E1E24) else Color(0xFFF8F7F6),
card = Color(0xFF24242B),
textPrimary = if (isDark) Color.White else Color(0xFF141517),
textSecondary = if (isDark) Color(0xFF9AA0A6) else Color(0xFF6B7280),
divider = if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.08f),
avatarBackground = Color(0xFF3A3A46),
avatarBorder = primary.copy(alpha = 0.3f),
drawerFooterBackground = if (isDark) Color.Black.copy(alpha = 0.2f) else Color.Black.copy(alpha = 0.05f)
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

@@ -2,10 +2,44 @@ package hu.bbara.purefin.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
// Light Mode Palette
val LightPrimary = Color(0xFF30D0F8) // The main brand color
val LightOnPrimary = Color(0xFFFFFFFF)
val LightPrimaryContainer = Color(0xFFD4FCFF)
val LightOnPrimaryContainer = Color(0xFF03405E)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val LightSecondary = Color(0xFF0F81AB) // A darker teal/blue from the end of the light list
val LightOnSecondary = Color(0xFFFFFFFF)
val LightSecondaryContainer = Color(0xFFABF7FF)
val LightOnSecondaryContainer = Color(0xFF055E85)
val LightTertiary = Color(0xFF1DA7D1) // Mid-tone blue
val LightOnTertiary = Color(0xFFFFFFFF)
val LightTertiaryContainer = Color(0xFFF0FEFF) // Very light blue
val LightOnTertiaryContainer = Color(0xFF03405E)
val LightBackground = Color(0xFFF0FEFF) // The lightest swatch
val LightOnBackground = Color(0xFF03405E) // The darkest swatch for text
val LightSurface = Color(0xFFF0FEFF)
val LightOnSurface = Color(0xFF03405E)
// Dark Mode Palette
val DarkPrimary = Color(0xFF30D0F8) // Main brand color stays vibrant
val DarkOnPrimary = Color(0xFF003546) // Dark text on bright primary
val DarkPrimaryContainer = Color(0xFF1C4C58)
val DarkOnPrimaryContainer = Color(0xFFABF7FF)
val DarkSecondary = Color(0xFF52CEE8) // Lighter blue for secondary in dark mode
val DarkOnSecondary = Color(0xFF003546)
val DarkSecondaryContainer = Color(0xFF21697B)
val DarkOnSecondaryContainer = Color(0xFFD4FCFF)
val DarkTertiary = Color(0xFF7DE3F3) // Another light accent
val DarkOnTertiary = Color(0xFF003546)
val DarkTertiaryContainer = Color(0xFF268EA8)
val DarkOnTertiaryContainer = Color(0xFFD0F7FA)
val DarkBackground = Color(0xFF13242B) // Darkest swatch from dark mode list
val DarkOnBackground = Color(0xFFD0F7FA) // Lightest swatch for text
val DarkSurface = Color(0xFF13242B)
val DarkOnSurface = Color(0xFFD0F7FA)

View File

@@ -1,6 +1,5 @@
package hu.bbara.purefin.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -11,32 +10,47 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
private val LightColorScheme = lightColorScheme(
primary = LightPrimary,
onPrimary = LightOnPrimary,
primaryContainer = LightPrimaryContainer,
onPrimaryContainer = LightOnPrimaryContainer,
secondary = LightSecondary,
onSecondary = LightOnSecondary,
secondaryContainer = LightSecondaryContainer,
onSecondaryContainer = LightOnSecondaryContainer,
tertiary = LightTertiary,
onTertiary = LightOnTertiary,
tertiaryContainer = LightTertiaryContainer,
onTertiaryContainer = LightOnTertiaryContainer,
background = LightBackground,
onBackground = LightOnBackground,
surface = LightSurface,
onSurface = LightOnSurface,
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
private val DarkColorScheme = darkColorScheme(
primary = DarkPrimary,
onPrimary = DarkOnPrimary,
primaryContainer = DarkPrimaryContainer,
onPrimaryContainer = DarkOnPrimaryContainer,
secondary = DarkSecondary,
onSecondary = DarkOnSecondary,
secondaryContainer = DarkSecondaryContainer,
onSecondaryContainer = DarkOnSecondaryContainer,
tertiary = DarkTertiary,
onTertiary = DarkOnTertiary,
tertiaryContainer = DarkTertiaryContainer,
onTertiaryContainer = DarkOnTertiaryContainer,
background = DarkBackground,
onBackground = DarkOnBackground,
surface = DarkSurface,
onSurface = DarkOnSurface,
)
@Composable
fun PurefinTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {