refactor: Extract and generalize MediaHero component. Now MovieScreen, SeriesScreen and EpisodeScreen also use the same Component.

This commit is contained in:
2026-01-21 20:48:53 +01:00
parent 6fa8ab1c56
commit d2f5f8547a
6 changed files with 94 additions and 141 deletions

View File

@@ -27,9 +27,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.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
@@ -38,9 +38,9 @@ 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.MediaFloatingPlayButton import hu.bbara.purefin.common.ui.MediaFloatingPlayButton
import hu.bbara.purefin.common.ui.MediaGhostIconButton import hu.bbara.purefin.common.ui.MediaGhostIconButton
import hu.bbara.purefin.common.ui.MediaHero
import hu.bbara.purefin.common.ui.MediaMetaChip import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.MediaPlaybackSettings import hu.bbara.purefin.common.ui.MediaPlaybackSettings
import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.common.ui.toMediaDetailColors import hu.bbara.purefin.common.ui.toMediaDetailColors
import hu.bbara.purefin.player.PlayerActivity import hu.bbara.purefin.player.PlayerActivity
@@ -68,27 +68,6 @@ internal fun EpisodeTopBar(
} }
} }
@Composable
internal fun EpisodeHero(
episode: EpisodeUiModel,
height: Dp,
isWide: Boolean,
onPlayClick: () -> Unit,
modifier: Modifier = Modifier
) {
val colors = rememberEpisodeColors().toMediaDetailColors()
MediaHero(
imageUrl = episode.heroImageUrl,
colors = colors,
height = height,
isWide = isWide,
modifier = modifier,
showPlayButton = true,
playButtonSize = if (isWide) 96.dp else 80.dp,
onPlayClick = onPlayClick
)
}
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
internal fun EpisodeDetails( internal fun EpisodeDetails(
@@ -164,6 +143,7 @@ internal fun EpisodeDetails(
@Composable @Composable
fun EpisodeCard( fun EpisodeCard(
episode: EpisodeUiModel, episode: EpisodeUiModel,
backGroundColor: Color,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val colors = rememberEpisodeColors().toMediaDetailColors() val colors = rememberEpisodeColors().toMediaDetailColors()
@@ -187,11 +167,10 @@ fun EpisodeCard(
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
if (isWide) { if (isWide) {
Row(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.fillMaxSize()) {
EpisodeHero( MediaHero(
episode = episode, imageUrl = episode.heroImageUrl,
height = 300.dp, height = 300.dp,
isWide = true, backgroundColor = backGroundColor,
onPlayClick = playAction,
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.weight(0.5f) .weight(0.5f)
@@ -216,11 +195,10 @@ fun EpisodeCard(
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
EpisodeHero( MediaHero(
episode = episode, imageUrl = episode.heroImageUrl,
backgroundColor = backGroundColor,
height = 400.dp, height = 400.dp,
isWide = false,
onPlayClick = playAction,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
EpisodeDetails( EpisodeDetails(
@@ -242,7 +220,8 @@ fun EpisodeCard(
if (!isWide) { if (!isWide) {
MediaFloatingPlayButton( MediaFloatingPlayButton(
colors = colors, containerColor = colors.primary,
onContainerColor = colors.onPrimary,
onClick = playAction, onClick = playAction,
modifier = Modifier modifier = Modifier
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)

View File

@@ -1,5 +1,6 @@
package hu.bbara.purefin.app.content.episode package hu.bbara.purefin.app.content.episode
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -24,7 +25,8 @@ fun EpisodeScreen(
if (episode.value != null) { if (episode.value != null) {
EpisodeCard( EpisodeCard(
episode = episode.value!!, episode = episode.value!!,
modifier = modifier modifier = modifier,
backGroundColor = MaterialTheme.colorScheme.background
) )
} else { } else {
PurefinWaitingScreen() PurefinWaitingScreen()

View File

@@ -22,6 +22,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Cast import androidx.compose.material.icons.outlined.Cast
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
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.runtime.remember import androidx.compose.runtime.remember
@@ -29,7 +30,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.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
@@ -38,9 +38,9 @@ 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.MediaFloatingPlayButton import hu.bbara.purefin.common.ui.MediaFloatingPlayButton
import hu.bbara.purefin.common.ui.MediaGhostIconButton import hu.bbara.purefin.common.ui.MediaGhostIconButton
import hu.bbara.purefin.common.ui.MediaHero
import hu.bbara.purefin.common.ui.MediaMetaChip import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.MediaPlaybackSettings import hu.bbara.purefin.common.ui.MediaPlaybackSettings
import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.common.ui.toMediaDetailColors import hu.bbara.purefin.common.ui.toMediaDetailColors
import hu.bbara.purefin.player.PlayerActivity import hu.bbara.purefin.player.PlayerActivity
@@ -68,27 +68,6 @@ internal fun MovieTopBar(
} }
} }
@Composable
internal fun MovieHero(
movie: MovieUiModel,
height: Dp,
isWide: Boolean,
onPlayClick: () -> Unit,
modifier: Modifier = Modifier
) {
val colors = rememberMovieColors().toMediaDetailColors()
MediaHero(
imageUrl = movie.heroImageUrl,
colors = colors,
height = height,
isWide = isWide,
modifier = modifier,
showPlayButton = true,
playButtonSize = if (isWide) 96.dp else 80.dp,
onPlayClick = onPlayClick
)
}
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
internal fun MovieDetails( internal fun MovieDetails(
@@ -161,12 +140,13 @@ internal fun MovieDetails(
} }
} }
@Composable @Composable
fun MovieCard( fun MovieCard(
movie: MovieUiModel, movie: MovieUiModel,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val colors = rememberMovieColors().toMediaDetailColors()
val context = LocalContext.current val context = LocalContext.current
val playAction = remember(movie.id) { val playAction = remember(movie.id) {
{ {
@@ -179,7 +159,7 @@ fun MovieCard(
BoxWithConstraints( BoxWithConstraints(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.background(colors.background) .background(MaterialTheme.colorScheme.background)
) { ) {
val isWide = maxWidth >= 900.dp val isWide = maxWidth >= 900.dp
val contentPadding = if (isWide) 32.dp else 20.dp val contentPadding = if (isWide) 32.dp else 20.dp
@@ -187,11 +167,10 @@ fun MovieCard(
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
if (isWide) { if (isWide) {
Row(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.fillMaxSize()) {
MovieHero( MediaHero(
movie = movie, imageUrl = movie.heroImageUrl,
backgroundColor = MaterialTheme.colorScheme.background,
height = 300.dp, height = 300.dp,
isWide = true,
onPlayClick = playAction,
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.weight(0.5f) .weight(0.5f)
@@ -216,11 +195,10 @@ fun MovieCard(
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
MovieHero( MediaHero(
movie = movie, imageUrl = movie.heroImageUrl,
height = 400.dp, height = 400.dp,
isWide = false, backgroundColor = MaterialTheme.colorScheme.background,
onPlayClick = playAction,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
MovieDetails( MovieDetails(
@@ -242,7 +220,9 @@ fun MovieCard(
if (!isWide) { if (!isWide) {
MediaFloatingPlayButton( MediaFloatingPlayButton(
colors = colors, containerColor = MaterialTheme.colorScheme.primary,
onContainerColor = MaterialTheme.colorScheme.onPrimary,
onClick = playAction, onClick = playAction,
modifier = Modifier modifier = Modifier
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)

View File

@@ -29,6 +29,7 @@ import androidx.compose.material.icons.outlined.Cast
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.PlayCircle import androidx.compose.material.icons.outlined.PlayCircle
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
@@ -47,8 +48,8 @@ import hu.bbara.purefin.common.ui.MediaActionButtons
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.MediaHero
import hu.bbara.purefin.common.ui.MediaMetaChip import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.common.ui.toMediaDetailColors import hu.bbara.purefin.common.ui.toMediaDetailColors
@Composable @Composable
@@ -83,12 +84,9 @@ internal fun SeriesHero(
val colors = rememberSeriesColors().toMediaDetailColors() val colors = rememberSeriesColors().toMediaDetailColors()
MediaHero( MediaHero(
imageUrl = imageUrl, imageUrl = imageUrl,
colors = colors, backgroundColor = MaterialTheme.colorScheme.background,
height = height, height = height,
isWide = false,
modifier = modifier, modifier = modifier,
showPlayButton = false,
horizontalGradientOnWide = false
) )
} }

View File

@@ -37,7 +37,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@@ -128,71 +127,6 @@ fun MediaGhostIconButton(
} }
} }
@Composable
fun MediaHero(
imageUrl: String,
colors: MediaDetailColors,
height: Dp,
isWide: Boolean,
modifier: Modifier = Modifier,
showPlayButton: Boolean = false,
playButtonSize: Dp = 80.dp,
onPlayClick: (() -> Unit)? = null,
horizontalGradientOnWide: Boolean = true
) {
Box(
modifier = modifier
.height(height)
.background(colors.background)
) {
AsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
colors.background.copy(alpha = 0.4f),
colors.background
)
)
)
)
if (horizontalGradientOnWide && isWide) {
Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.horizontalGradient(
colors = listOf(
Color.Transparent,
colors.background.copy(alpha = 0.8f)
)
)
)
)
}
if (showPlayButton && onPlayClick != null) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
MediaPlayButton(
colors = colors,
size = playButtonSize,
onClick = onPlayClick
)
}
}
}
}
@Composable @Composable
fun MediaMetaChip( fun MediaMetaChip(
colors: MediaDetailColors, colors: MediaDetailColors,
@@ -469,7 +403,8 @@ fun MediaPlayButton(
@Composable @Composable
fun MediaFloatingPlayButton( fun MediaFloatingPlayButton(
colors: MediaDetailColors, containerColor: Color,
onContainerColor: Color,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -478,14 +413,14 @@ fun MediaFloatingPlayButton(
.size(56.dp) .size(56.dp)
.shadow(20.dp, CircleShape) .shadow(20.dp, CircleShape)
.clip(CircleShape) .clip(CircleShape)
.background(colors.primary) .background(containerColor)
.clickable { onClick() }, .clickable { onClick() },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
imageVector = Icons.Filled.PlayArrow, imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play", contentDescription = "Play",
tint = colors.onPrimary tint = onContainerColor
) )
} }
} }

View File

@@ -0,0 +1,59 @@
package hu.bbara.purefin.common.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import coil3.compose.AsyncImage
@Composable
fun MediaHero(
imageUrl: String,
backgroundColor: Color,
height: Dp,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.height(height)
.background(backgroundColor)
) {
AsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
backgroundColor.copy(alpha = 0.4f),
backgroundColor
)
)
)
)
Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.horizontalGradient(
colors = listOf(
Color.Transparent,
backgroundColor.copy(alpha = 0.8f)
)
)
)
)
}
}