refactor media screens to use Scaffold and standard layout components

- Replace `BoxWithConstraints` with `Scaffold` in `MovieScreen`, `EpisodeScreen`, and `SeriesScreen`.
- Move `MovieTopBar`, `EpisodeTopBar`, and `SeriesTopBar` into the `topBar` slot of `Scaffold`.
- Implement `statusBarsPadding` and standard padding in top bar components.
- Decouple top bar components from ViewModels by passing an `onBack` callback.
- Simplify hero image heights and remove complex offsets and conditional wide-screen layouts.
- Use `floatingActionButton` slot for the play button in movie and episode screens.
- Standardize background handling using `MaterialTheme.colorScheme.background`.
This commit is contained in:
2026-01-21 22:43:50 +01:00
parent decb8cd0cb
commit ce45139f74
6 changed files with 171 additions and 248 deletions

View File

@@ -6,7 +6,10 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Cast
@@ -18,7 +21,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaActionButtons
import hu.bbara.purefin.common.ui.MediaCastMember
import hu.bbara.purefin.common.ui.MediaCastRow
@@ -29,12 +31,15 @@ import hu.bbara.purefin.common.ui.toMediaDetailColors
@Composable
internal fun EpisodeTopBar(
viewModel: EpisodeScreenViewModel = hiltViewModel(),
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val colors = rememberEpisodeColors().toMediaDetailColors()
Row(
modifier = modifier,
modifier = modifier
.fillMaxWidth()
.statusBarsPadding()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -42,7 +47,7 @@ internal fun EpisodeTopBar(
colors = colors,
icon = Icons.Outlined.ArrowBack,
contentDescription = "Back",
onClick = { viewModel.onBack() }
onClick = onBack
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })

View File

@@ -1,26 +1,19 @@
package hu.bbara.purefin.app.content.episode
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -29,7 +22,6 @@ import hu.bbara.purefin.app.content.ContentMockData
import hu.bbara.purefin.common.ui.MediaFloatingPlayButton
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.common.ui.toMediaDetailColors
import hu.bbara.purefin.navigation.ItemDto
import hu.bbara.purefin.player.PlayerActivity
@@ -53,18 +45,17 @@ fun EpisodeScreen(
EpisodeScreenInternal(
episode = episode.value!!,
modifier = modifier,
backGroundColor = MaterialTheme.colorScheme.background
onBack = viewModel::onBack,
modifier = modifier
)
}
@Composable
private fun EpisodeScreenInternal(
episode: EpisodeUiModel,
backGroundColor: Color,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
val colors = rememberEpisodeColors().toMediaDetailColors()
val context = LocalContext.current
val playAction = remember(episode.id) {
{
@@ -74,40 +65,25 @@ private fun EpisodeScreenInternal(
}
}
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(colors.background)
) {
val isWide = maxWidth >= 900.dp
val contentPadding = if (isWide) 32.dp else 20.dp
Box(modifier = Modifier.fillMaxSize()) {
if (isWide) {
Row(modifier = Modifier.fillMaxSize()) {
MediaHero(
imageUrl = episode.heroImageUrl,
height = 300.dp,
backgroundColor = backGroundColor,
Scaffold(
modifier = modifier,
containerColor = MaterialTheme.colorScheme.background,
topBar = {
EpisodeTopBar(
onBack = onBack,
modifier = Modifier
.fillMaxHeight()
.weight(0.5f)
)
EpisodeDetails(
episode = episode,
},
floatingActionButton = {
MediaFloatingPlayButton(
containerColor = MaterialTheme.colorScheme.primary,
onContainerColor = MaterialTheme.colorScheme.onPrimary,
onClick = playAction,
modifier = Modifier
.weight(0.5f)
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.padding(
start = contentPadding,
end = contentPadding,
top = 96.dp,
bottom = 32.dp
)
.padding(16.dp)
)
}
} else {
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
@@ -115,39 +91,19 @@ private fun EpisodeScreenInternal(
) {
MediaHero(
imageUrl = episode.heroImageUrl,
backgroundColor = backGroundColor,
height = 400.dp,
backgroundColor = MaterialTheme.colorScheme.background,
height = 200.dp,
modifier = Modifier.fillMaxWidth()
)
EpisodeDetails(
episode = episode,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = contentPadding)
.offset(y = (-48).dp)
.padding(bottom = 96.dp)
.padding(horizontal = 16.dp)
.padding(bottom = innerPadding.calculateBottomPadding())
)
}
}
EpisodeTopBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
)
if (!isWide) {
MediaFloatingPlayButton(
containerColor = colors.primary,
onContainerColor = colors.onPrimary,
onClick = playAction,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(20.dp)
)
}
}
}
}
@Preview
@@ -155,6 +111,6 @@ private fun EpisodeScreenInternal(
fun EpisodeScreenPreview() {
EpisodeScreenInternal(
episode = ContentMockData.episode(),
backGroundColor = MaterialTheme.colorScheme.background
onBack = {}
)
}

View File

@@ -6,7 +6,10 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Cast
@@ -18,7 +21,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaActionButtons
import hu.bbara.purefin.common.ui.MediaCastMember
import hu.bbara.purefin.common.ui.MediaCastRow
@@ -29,12 +31,15 @@ import hu.bbara.purefin.common.ui.toMediaDetailColors
@Composable
internal fun MovieTopBar(
viewModel: MovieScreenViewModel = hiltViewModel(),
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val colors = rememberMovieColors().toMediaDetailColors()
Row(
modifier = modifier,
modifier = modifier
.fillMaxWidth()
.statusBarsPadding()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -42,7 +47,7 @@ internal fun MovieTopBar(
colors = colors,
icon = Icons.Outlined.ArrowBack,
contentDescription = "Back",
onClick = { viewModel.onBack() }
onClick = onBack
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })

View File

@@ -1,24 +1,18 @@
package hu.bbara.purefin.app.content.movie
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
@@ -43,7 +37,9 @@ fun MovieScreen(
if (movieItem.value != null) {
MovieScreenInternal(
movie = movieItem.value!!, modifier = modifier
movie = movieItem.value!!,
onBack = viewModel::onBack,
modifier = modifier
)
} else {
PurefinWaitingScreen()
@@ -53,6 +49,7 @@ fun MovieScreen(
@Composable
private fun MovieScreenInternal(
movie: MovieUiModel,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
@@ -64,40 +61,25 @@ private fun MovieScreenInternal(
}
}
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
val isWide = maxWidth >= 900.dp
val contentPadding = if (isWide) 32.dp else 20.dp
Box(modifier = Modifier.fillMaxSize()) {
if (isWide) {
Row(modifier = Modifier.fillMaxSize()) {
MediaHero(
imageUrl = movie.heroImageUrl,
backgroundColor = MaterialTheme.colorScheme.background,
height = 300.dp,
Scaffold(
modifier = modifier,
containerColor = MaterialTheme.colorScheme.background,
topBar = {
MovieTopBar(
onBack = onBack,
modifier = Modifier
.fillMaxHeight()
.weight(0.5f)
)
MovieDetails(
movie = movie,
},
floatingActionButton = {
MediaFloatingPlayButton(
containerColor = MaterialTheme.colorScheme.primary,
onContainerColor = MaterialTheme.colorScheme.onPrimary,
onClick = playAction,
modifier = Modifier
.weight(0.5f)
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.padding(
start = contentPadding,
end = contentPadding,
top = 96.dp,
bottom = 32.dp
)
.padding(16.dp)
)
}
} else {
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
@@ -105,44 +87,27 @@ private fun MovieScreenInternal(
) {
MediaHero(
imageUrl = movie.heroImageUrl,
height = 400.dp,
backgroundColor = MaterialTheme.colorScheme.background,
height = 200.dp,
modifier = Modifier.fillMaxWidth()
)
MovieDetails(
movie = movie,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = contentPadding)
.offset(y = (-48).dp)
.padding(bottom = 96.dp)
.padding(horizontal = 16.dp)
.padding(bottom = innerPadding.calculateBottomPadding())
)
}
}
MovieTopBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
)
if (!isWide) {
MediaFloatingPlayButton(
containerColor = MaterialTheme.colorScheme.primary,
onContainerColor = MaterialTheme.colorScheme.onPrimary,
onClick = playAction,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(20.dp)
)
}
}
}
}
@Preview
@Composable
fun MovieScreenPreview() {
MovieScreenInternal(movie = ContentMockData.movie())
MovieScreenInternal(
movie = ContentMockData.movie(),
onBack = {}
)
}

View File

@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
@@ -54,18 +55,21 @@ import hu.bbara.purefin.common.ui.toMediaDetailColors
@Composable
internal fun SeriesTopBar(
viewModel: SeriesViewModel = hiltViewModel(),
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val colors = rememberSeriesColors().toMediaDetailColors()
Row(
modifier = modifier,
modifier = modifier
.fillMaxWidth()
.statusBarsPadding()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
MediaGhostIconButton(
colors = colors,
onClick = { viewModel.onBack() },
onClick = onBack,
icon = Icons.Outlined.ArrowBack,
contentDescription = "Back")
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {

View File

@@ -1,21 +1,19 @@
package hu.bbara.purefin.app.content.series
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
@@ -41,6 +39,7 @@ fun SeriesScreen(
if (series.value != null) {
SeriesScreenInternal(
series = series.value!!,
onBack = viewModel::onBack,
modifier = modifier
)
} else {
@@ -51,16 +50,21 @@ fun SeriesScreen(
@Composable
private fun SeriesScreenInternal(
series: SeriesUiModel,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
val colors = rememberSeriesColors()
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(colors.background)
) {
val heroHeight = maxHeight * 0.4f
Scaffold(
modifier = modifier,
containerColor = MaterialTheme.colorScheme.background,
topBar = {
SeriesTopBar(
onBack = onBack,
modifier = Modifier
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
@@ -68,17 +72,14 @@ private fun SeriesScreenInternal(
) {
SeriesHero(
imageUrl = series.heroImageUrl,
height = heroHeight
height = 200.dp,
modifier = Modifier.fillMaxWidth()
)
Column(
modifier = Modifier
.fillMaxWidth()
.offset(y = (-96).dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.padding(horizontal = 16.dp)
.padding(bottom = innerPadding.calculateBottomPadding())
) {
Text(
text = series.title,
@@ -114,43 +115,30 @@ private fun SeriesScreenInternal(
Spacer(modifier = Modifier.height(28.dp))
SeasonTabs(seasons = series.seasonTabs)
Spacer(modifier = Modifier.height(16.dp))
}
EpisodeCarousel(
episodes = series.seasonTabs.firstOrNull { it.isSelected }?.episodes
?: series.seasonTabs.firstOrNull()?.episodes
?: emptyList()
)
Spacer(modifier = Modifier.height(32.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 0.dp, bottom = 0.dp)
) {
Text(
text = "Cast",
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 20.dp)
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
CastRow(cast = series.cast)
}
}
}
SeriesTopBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
.align(Alignment.TopCenter)
)
}
}
@Preview
@Composable
fun SeriesScreenPreview() {
SeriesScreenInternal(series = ContentMockData.series())
SeriesScreenInternal(
series = ContentMockData.series(),
onBack = {}
)
}