mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
refactor: update TvMediaDetailScaffold to use artwork parameters and improve layout
This commit is contained in:
@@ -77,15 +77,16 @@ internal fun EpisodeScreenContent(
|
||||
onPlay: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val backFocusRequester = remember { FocusRequester() }
|
||||
val playFocusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(episode.id) {
|
||||
backFocusRequester.requestFocus()
|
||||
playFocusRequester.requestFocus()
|
||||
}
|
||||
|
||||
TvMediaDetailScaffold(
|
||||
heroImageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
artworkImageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
artworkWidth = 280.dp,
|
||||
artworkAspectRatio = 16f / 9f,
|
||||
resetScrollKey = episode.id,
|
||||
modifier = modifier,
|
||||
heroContent = {
|
||||
|
||||
@@ -53,15 +53,16 @@ internal fun MovieScreenContent(
|
||||
onPlay: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val backFocusRequester = remember { FocusRequester() }
|
||||
val playFocusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(movie.id) {
|
||||
backFocusRequester.requestFocus()
|
||||
playFocusRequester.requestFocus()
|
||||
}
|
||||
|
||||
TvMediaDetailScaffold(
|
||||
heroImageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
|
||||
artworkImageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
|
||||
artworkWidth = 200.dp,
|
||||
artworkAspectRatio = 2f / 3f,
|
||||
resetScrollKey = movie.id,
|
||||
modifier = modifier,
|
||||
heroContent = {
|
||||
|
||||
@@ -70,16 +70,21 @@ internal fun SeriesScreenContent(
|
||||
season.episodes.firstOrNull { !it.watched }
|
||||
} ?: series.seasons.firstOrNull()?.episodes?.firstOrNull()
|
||||
}
|
||||
val backFocusRequester = remember { FocusRequester() }
|
||||
val playFocusRequester = remember { FocusRequester() }
|
||||
val firstContentFocusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(series.id) {
|
||||
backFocusRequester.requestFocus()
|
||||
LaunchedEffect(series.id, nextUpEpisode?.id) {
|
||||
if (nextUpEpisode != null) {
|
||||
playFocusRequester.requestFocus()
|
||||
} else {
|
||||
firstContentFocusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
TvMediaDetailScaffold(
|
||||
heroImageUrl = JellyfinImageHelper.finishImageUrl(series.imageUrlPrefix, ImageType.PRIMARY),
|
||||
artworkImageUrl = JellyfinImageHelper.finishImageUrl(series.imageUrlPrefix, ImageType.PRIMARY),
|
||||
artworkWidth = 200.dp,
|
||||
artworkAspectRatio = 2f / 3f,
|
||||
resetScrollKey = series.id,
|
||||
modifier = modifier,
|
||||
heroContent = {
|
||||
|
||||
@@ -2,13 +2,18 @@ package hu.bbara.purefin.common.ui.components
|
||||
|
||||
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.ColumnScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
@@ -16,20 +21,36 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
||||
|
||||
internal val MediaDetailHorizontalPadding = 48.dp
|
||||
private val MediaDetailHeaderTopPadding = 104.dp
|
||||
private val MediaDetailHeaderBottomPadding = 36.dp
|
||||
private val MediaDetailCornerArtworkTopPadding = 40.dp
|
||||
private val MediaDetailCornerArtworkShape = RoundedCornerShape(24.dp)
|
||||
private val MediaDetailMinimumContentWidth = 280.dp
|
||||
private val MediaDetailContentArtworkGap = 32.dp
|
||||
|
||||
@Composable
|
||||
internal fun TvMediaDetailScaffold(
|
||||
heroImageUrl: String,
|
||||
artworkImageUrl: String,
|
||||
artworkWidth: Dp,
|
||||
artworkAspectRatio: Float,
|
||||
resetScrollKey: Any,
|
||||
modifier: Modifier = Modifier,
|
||||
heroHeightFraction: Float = 0.48f,
|
||||
headerHeightFraction: Float = 0.48f,
|
||||
heroContent: @Composable ColumnScope.() -> Unit,
|
||||
bodyContent: LazyListScope.(Modifier) -> Unit = { _ -> }
|
||||
) {
|
||||
@@ -51,27 +72,124 @@ internal fun TvMediaDetailScaffold(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
MediaHero(
|
||||
imageUrl = heroImageUrl,
|
||||
backgroundColor = scheme.background,
|
||||
heightFraction = heroHeightFraction,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
MediaHeroScrimOverlay()
|
||||
Column(
|
||||
modifier = contentPadding
|
||||
.padding(top = 104.dp, bottom = 36.dp)
|
||||
) {
|
||||
heroContent()
|
||||
}
|
||||
}
|
||||
TvMediaDetailHeader(
|
||||
artworkImageUrl = artworkImageUrl,
|
||||
artworkWidth = artworkWidth,
|
||||
artworkAspectRatio = artworkAspectRatio,
|
||||
headerHeightFraction = headerHeightFraction,
|
||||
heroContent = heroContent
|
||||
)
|
||||
}
|
||||
bodyContent(contentPadding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TvMediaDetailHeader(
|
||||
artworkImageUrl: String,
|
||||
artworkWidth: Dp,
|
||||
artworkAspectRatio: Float,
|
||||
headerHeightFraction: Float,
|
||||
heroContent: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
|
||||
val headerHeight = screenHeight * headerHeightFraction
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = headerHeight)
|
||||
) {
|
||||
val contentMaxWidth = (
|
||||
maxWidth -
|
||||
(MediaDetailHorizontalPadding * 2) -
|
||||
artworkWidth -
|
||||
MediaDetailContentArtworkGap
|
||||
).coerceAtLeast(MediaDetailMinimumContentWidth)
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
TvMediaDetailCornerArtwork(
|
||||
artworkImageUrl = artworkImageUrl,
|
||||
artworkWidth = artworkWidth,
|
||||
artworkAspectRatio = artworkAspectRatio,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(
|
||||
top = MediaDetailCornerArtworkTopPadding,
|
||||
end = MediaDetailHorizontalPadding
|
||||
)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(
|
||||
start = MediaDetailHorizontalPadding,
|
||||
top = MediaDetailHeaderTopPadding,
|
||||
end = MediaDetailHorizontalPadding,
|
||||
bottom = MediaDetailHeaderBottomPadding
|
||||
)
|
||||
.widthIn(max = contentMaxWidth)
|
||||
) {
|
||||
heroContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TvMediaDetailCornerArtwork(
|
||||
artworkImageUrl: String,
|
||||
artworkWidth: Dp,
|
||||
artworkAspectRatio: Float,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(artworkWidth)
|
||||
.aspectRatio(artworkAspectRatio)
|
||||
.clip(MediaDetailCornerArtworkShape)
|
||||
.background(scheme.surfaceVariant.copy(alpha = 0.28f))
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
model = artworkImageUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer { alpha = 0.52f },
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
colorStops = arrayOf(
|
||||
0.0f to scheme.background.copy(alpha = 0.08f),
|
||||
0.55f to scheme.background.copy(alpha = 0.2f),
|
||||
1.0f to scheme.background.copy(alpha = 0.56f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colorStops = arrayOf(
|
||||
0.0f to scheme.background.copy(alpha = 0.06f),
|
||||
0.65f to scheme.background.copy(alpha = 0.22f),
|
||||
1.0f to scheme.background.copy(alpha = 0.74f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MediaDetailSectionTitle(
|
||||
text: String,
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
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.platform.LocalConfiguration
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun MediaHero(
|
||||
imageUrl: String,
|
||||
backgroundColor: Color,
|
||||
heightFraction: Float = 0.4f,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
|
||||
val heroHeight = screenHeight * heightFraction
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(heroHeight)
|
||||
.background(backgroundColor)
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
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.5f),
|
||||
backgroundColor
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
|
||||
@Composable
|
||||
internal fun MediaHeroScrimOverlay(modifier: Modifier = Modifier) {
|
||||
val background = MaterialTheme.colorScheme.background
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
background,
|
||||
background.copy(alpha = 0.95f),
|
||||
background.copy(alpha = 0.7f),
|
||||
background.copy(alpha = 0.15f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
background.copy(alpha = 0.05f),
|
||||
background.copy(alpha = 0.2f),
|
||||
background
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package hu.bbara.purefin.common.ui.components
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
@@ -43,18 +45,30 @@ fun MediaResumeButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val onPrimaryColor = MaterialTheme.colorScheme.onPrimary
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val primaryColor = scheme.primary
|
||||
val onPrimaryColor = scheme.onPrimary
|
||||
val focusShape = RoundedCornerShape(50)
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.05f else 1.0f, label = "scale")
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.08f else 1.0f, label = "scale")
|
||||
val focusBorderColor by animateColorAsState(
|
||||
targetValue = if (isFocused) scheme.onBackground else Color.Transparent,
|
||||
label = "focus-border"
|
||||
)
|
||||
val focusHaloColor by animateColorAsState(
|
||||
targetValue = if (isFocused) scheme.primary.copy(alpha = 0.22f) else Color.Transparent,
|
||||
label = "focus-halo"
|
||||
)
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
.height(52.dp)
|
||||
.border(2.5.dp, if (isFocused) onPrimaryColor else Color.Transparent, RoundedCornerShape(50))
|
||||
.clip(RoundedCornerShape(50))
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.background(focusHaloColor, focusShape)
|
||||
.border(3.dp, focusBorderColor, focusShape)
|
||||
.clip(focusShape)
|
||||
.onFocusChanged { isFocused = it.isFocused || it.hasFocus }
|
||||
.focusable()
|
||||
.clickable(onClick = onClick)
|
||||
) {
|
||||
// Bottom layer: inverted colors (visible for the remaining %)
|
||||
@@ -87,6 +101,14 @@ fun MediaResumeButton(
|
||||
) {
|
||||
ButtonContent(text = text, color = onPrimaryColor)
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.border(2.dp, scheme.primary.copy(alpha = 0.95f), focusShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user