mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
feat: Add TV focus states and refactor content screens to LazyColumn
- Add animated focus feedback (scale, border, background) to all interactive TV UI components: PosterCard, GhostIconButton, MediaActionButton, MediaResumeButton, PurefinIconButton, TvHomeDrawer nav items, TvHomeSections cards, TvPlayerScreen track/queue panels - Refactor EpisodeScreen, MovieScreen, and SeriesScreen from Scaffold + verticalScroll to LazyColumn layout with Box-overlaid top bar - Switch MovieComponents and MovieScreen from MovieUiModel to core Movie model - Conditionally render cast sections only when cast is non-empty
This commit is contained in:
@@ -1,21 +1,41 @@
|
||||
package hu.bbara.purefin.app.content.episode
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.MediaCastRow
|
||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
||||
import hu.bbara.purefin.common.ui.components.MediaHero
|
||||
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
|
||||
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
||||
import hu.bbara.purefin.core.data.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
||||
@@ -26,7 +46,6 @@ fun EpisodeScreen(
|
||||
viewModel: EpisodeScreenViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
LaunchedEffect(episode) {
|
||||
viewModel.selectEpisode(
|
||||
seriesId = episode.seriesId,
|
||||
@@ -50,6 +69,7 @@ fun EpisodeScreen(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun EpisodeScreenInternal(
|
||||
episode: Episode,
|
||||
@@ -57,36 +77,116 @@ private fun EpisodeScreenInternal(
|
||||
onPlay: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val hPad = Modifier.padding(horizontal = 16.dp)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
EpisodeTopBar(
|
||||
onBack = onBack,
|
||||
modifier = Modifier
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(scheme.background)
|
||||
) {
|
||||
item {
|
||||
Box {
|
||||
MediaHero(
|
||||
imageUrl = episode.heroImageUrl,
|
||||
backgroundColor = scheme.background,
|
||||
heightFraction = 0.30f,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
EpisodeTopBar(onBack = onBack)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Column(modifier = hPad) {
|
||||
Text(
|
||||
text = episode.title,
|
||||
color = scheme.onBackground,
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
lineHeight = 38.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Episode ${episode.index}",
|
||||
color = scheme.onBackground,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MediaMetaChip(text = episode.releaseDate)
|
||||
MediaMetaChip(text = episode.rating)
|
||||
MediaMetaChip(text = episode.runtime)
|
||||
MediaMetaChip(
|
||||
text = episode.format,
|
||||
background = scheme.primary.copy(alpha = 0.2f),
|
||||
border = scheme.primary.copy(alpha = 0.3f),
|
||||
textColor = scheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
MediaSynopsis(
|
||||
synopsis = episode.synopsis,
|
||||
modifier = hPad
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
MediaHero(
|
||||
imageUrl = episode.heroImageUrl,
|
||||
backgroundColor = MaterialTheme.colorScheme.background,
|
||||
heightFraction = 0.30f,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
EpisodeDetails(
|
||||
episode = episode,
|
||||
onPlay = onPlay,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Row(modifier = hPad) {
|
||||
MediaResumeButton(
|
||||
text = if (episode.progress == null) "Play" else "Resume",
|
||||
progress = episode.progress?.div(100)?.toFloat() ?: 0f,
|
||||
onClick = onPlay,
|
||||
modifier = Modifier.sizeIn(maxWidth = 200.dp)
|
||||
)
|
||||
VerticalDivider(
|
||||
color = scheme.secondary,
|
||||
thickness = 4.dp,
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
Row {
|
||||
MediaActionButton(
|
||||
backgroundColor = scheme.secondary,
|
||||
iconColor = scheme.onSecondary,
|
||||
icon = Icons.Outlined.Add,
|
||||
height = 48.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
MediaPlaybackSettings(
|
||||
backgroundColor = scheme.surface,
|
||||
foregroundColor = scheme.onSurface,
|
||||
audioTrack = "ENG",
|
||||
subtitles = "ENG",
|
||||
modifier = hPad
|
||||
)
|
||||
}
|
||||
if (episode.cast.isNotEmpty()) {
|
||||
item {
|
||||
Column(modifier = hPad) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = "Cast",
|
||||
color = scheme.onBackground,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
MediaCastRow(cast = episode.cast)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import hu.bbara.purefin.common.ui.components.GhostIconButton
|
||||
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
||||
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
|
||||
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
||||
import hu.bbara.purefin.feature.shared.content.movie.MovieUiModel
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
|
||||
@Composable
|
||||
internal fun MovieTopBar(
|
||||
@@ -64,7 +64,7 @@ internal fun MovieTopBar(
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
internal fun MovieDetails(
|
||||
movie: MovieUiModel,
|
||||
movie: Movie,
|
||||
onPlay: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -132,16 +132,18 @@ internal fun MovieDetails(
|
||||
subtitles = movie.subtitles
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = "Cast",
|
||||
color = scheme.onBackground,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
MediaCastRow(
|
||||
cast = emptyList()
|
||||
)
|
||||
if (movie.cast.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = "Cast",
|
||||
color = scheme.onBackground,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
MediaCastRow(
|
||||
cast = movie.cast
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,44 @@
|
||||
package hu.bbara.purefin.app.content.movie
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.MediaCastRow
|
||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
||||
import hu.bbara.purefin.common.ui.components.MediaHero
|
||||
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
|
||||
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
||||
import hu.bbara.purefin.core.data.navigation.MovieDto
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
||||
import hu.bbara.purefin.feature.shared.content.movie.MovieUiModel
|
||||
|
||||
@Composable
|
||||
fun MovieScreen(
|
||||
@@ -42,42 +62,117 @@ fun MovieScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun MovieScreenInternal(
|
||||
movie: MovieUiModel,
|
||||
movie: Movie,
|
||||
onBack: () -> Unit,
|
||||
onPlay: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
MovieTopBar(
|
||||
onBack = onBack,
|
||||
modifier = Modifier
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val hPad = Modifier.padding(horizontal = 16.dp)
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(scheme.background)
|
||||
) {
|
||||
item {
|
||||
Box {
|
||||
MediaHero(
|
||||
imageUrl = movie.heroImageUrl,
|
||||
backgroundColor = scheme.background,
|
||||
heightFraction = 0.30f,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
MovieTopBar(onBack = onBack)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Column(modifier = hPad) {
|
||||
Text(
|
||||
text = movie.title,
|
||||
color = scheme.onBackground,
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
lineHeight = 38.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MediaMetaChip(text = movie.year)
|
||||
MediaMetaChip(text = movie.rating)
|
||||
MediaMetaChip(text = movie.runtime)
|
||||
MediaMetaChip(
|
||||
text = movie.format,
|
||||
background = scheme.primary.copy(alpha = 0.2f),
|
||||
border = scheme.primary.copy(alpha = 0.3f),
|
||||
textColor = scheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
MediaSynopsis(
|
||||
synopsis = movie.synopsis,
|
||||
modifier = hPad
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
MediaHero(
|
||||
imageUrl = movie.heroImageUrl,
|
||||
backgroundColor = MaterialTheme.colorScheme.background,
|
||||
heightFraction = 0.30f,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
MovieDetails(
|
||||
movie = movie,
|
||||
onPlay = onPlay,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Row(modifier = hPad) {
|
||||
MediaResumeButton(
|
||||
text = if (movie.progress == null) "Play" else "Resume",
|
||||
progress = movie.progress?.div(100)?.toFloat() ?: 0f,
|
||||
onClick = onPlay,
|
||||
modifier = Modifier.sizeIn(maxWidth = 200.dp)
|
||||
)
|
||||
VerticalDivider(
|
||||
color = scheme.secondary,
|
||||
thickness = 4.dp,
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
Row {
|
||||
MediaActionButton(
|
||||
backgroundColor = scheme.secondary,
|
||||
iconColor = scheme.onSecondary,
|
||||
icon = Icons.Outlined.Add,
|
||||
height = 48.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
MediaPlaybackSettings(
|
||||
backgroundColor = scheme.surface,
|
||||
foregroundColor = scheme.onSurface,
|
||||
audioTrack = movie.audioTrack,
|
||||
subtitles = movie.subtitles,
|
||||
modifier = hPad
|
||||
)
|
||||
}
|
||||
if (movie.cast.isNotEmpty()) {
|
||||
item {
|
||||
Column(modifier = hPad) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = "Cast",
|
||||
color = scheme.onBackground,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
MediaCastRow(cast = movie.cast)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package hu.bbara.purefin.app.content.series
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -35,10 +36,16 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -124,7 +131,7 @@ internal fun SeasonTabs(
|
||||
SeasonTab(
|
||||
name = season.name,
|
||||
isSelected = season == selectedSeason,
|
||||
modifier = Modifier.clickable { onSelect(season) }
|
||||
onSelect = { onSelect(season) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -134,28 +141,34 @@ internal fun SeasonTabs(
|
||||
private fun SeasonTab(
|
||||
name: String,
|
||||
isSelected: Boolean,
|
||||
onSelect: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
val color = if (isSelected) scheme.primary else mutedStrong
|
||||
val borderColor = if (isSelected) scheme.primary else Color.Transparent
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val color = if (isSelected || isFocused) scheme.primary else mutedStrong
|
||||
val underlineColor = if (isSelected || isFocused) scheme.primary else Color.Transparent
|
||||
val underlineHeight = if (isFocused) 3.dp else 2.dp
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(bottom = 8.dp)
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable { onSelect() }
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
color = color,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium
|
||||
fontWeight = if (isSelected || isFocused) FontWeight.Bold else FontWeight.Medium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(2.dp)
|
||||
.height(underlineHeight)
|
||||
.width(52.dp)
|
||||
.background(borderColor)
|
||||
.background(underlineColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -191,9 +204,14 @@ private fun EpisodeCard(
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(260.dp)
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable { viewModel.onSelectEpisode(
|
||||
seriesId = episode.seriesId,
|
||||
seasonId = episode.seasonId,
|
||||
@@ -207,7 +225,11 @@ private fun EpisodeCard(
|
||||
.aspectRatio(16f / 9f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(scheme.surface)
|
||||
.border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp))
|
||||
.border(
|
||||
width = if (isFocused) 2.dp else 1.dp,
|
||||
color = if (isFocused) scheme.primary else scheme.outlineVariant,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
model = episode.heroImageUrl,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package hu.bbara.purefin.app.content.series
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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
|
||||
@@ -61,47 +61,35 @@ private fun SeriesScreenInternal(
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val textMutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
val hPad = Modifier.padding(horizontal = 16.dp)
|
||||
|
||||
fun getDefaultSeason() : Season {
|
||||
fun getDefaultSeason(): Season {
|
||||
for (season in series.seasons) {
|
||||
val firstUnwatchedEpisode = season.episodes.firstOrNull {
|
||||
it.watched.not()
|
||||
}
|
||||
if (firstUnwatchedEpisode != null) {
|
||||
return season
|
||||
}
|
||||
val firstUnwatchedEpisode = season.episodes.firstOrNull { it.watched.not() }
|
||||
if (firstUnwatchedEpisode != null) return season
|
||||
}
|
||||
return series.seasons.first()
|
||||
}
|
||||
val selectedSeason = remember { mutableStateOf<Season>(getDefaultSeason()) }
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
SeriesTopBar(
|
||||
onBack = onBack,
|
||||
modifier = Modifier
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(scheme.background)
|
||||
) {
|
||||
item {
|
||||
Box {
|
||||
MediaHero(
|
||||
imageUrl = series.heroImageUrl,
|
||||
heightFraction = 0.30f,
|
||||
backgroundColor = scheme.background,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SeriesTopBar(onBack = onBack)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
MediaHero(
|
||||
imageUrl = series.heroImageUrl,
|
||||
heightFraction = 0.30f,
|
||||
backgroundColor = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||
) {
|
||||
item {
|
||||
Column(modifier = hPad) {
|
||||
Text(
|
||||
text = series.name,
|
||||
color = scheme.onBackground,
|
||||
@@ -111,34 +99,52 @@ private fun SeriesScreenInternal(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
SeriesMetaChips(series = series)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
SeriesActionButtons()
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
MediaSynopsis(
|
||||
synopsis = series.synopsis,
|
||||
bodyColor = textMutedStrong,
|
||||
bodyFontSize = 13.sp,
|
||||
bodyLineHeight = null,
|
||||
titleSpacing = 8.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
SeasonTabs(
|
||||
seasons = series.seasons,
|
||||
selectedSeason = selectedSeason.value,
|
||||
onSelect = { selectedSeason.value = it }
|
||||
)
|
||||
EpisodeCarousel(
|
||||
episodes = selectedSeason.value.episodes,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Cast",
|
||||
color = scheme.onBackground,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
CastRow(cast = series.cast)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
SeriesActionButtons(modifier = hPad)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
MediaSynopsis(
|
||||
synopsis = series.synopsis,
|
||||
bodyColor = textMutedStrong,
|
||||
bodyFontSize = 13.sp,
|
||||
bodyLineHeight = null,
|
||||
titleSpacing = 8.dp,
|
||||
modifier = hPad
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
SeasonTabs(
|
||||
seasons = series.seasons,
|
||||
selectedSeason = selectedSeason.value,
|
||||
onSelect = { selectedSeason.value = it },
|
||||
modifier = hPad
|
||||
)
|
||||
}
|
||||
item {
|
||||
EpisodeCarousel(
|
||||
episodes = selectedSeason.value.episodes,
|
||||
modifier = hPad
|
||||
)
|
||||
}
|
||||
if (series.cast.isNotEmpty()) {
|
||||
item {
|
||||
Column(modifier = hPad) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Cast",
|
||||
color = scheme.onBackground,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
CastRow(cast = series.cast)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package hu.bbara.purefin.common.ui
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -12,9 +13,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -41,6 +48,8 @@ fun PosterCard(
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
|
||||
|
||||
val posterWidth = 144.dp
|
||||
val posterHeight = posterWidth * 3 / 2
|
||||
@@ -64,6 +73,7 @@ fun PosterCard(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(posterWidth)
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
) {
|
||||
Box() {
|
||||
PurefinAsyncImage(
|
||||
@@ -72,8 +82,13 @@ fun PosterCard(
|
||||
modifier = Modifier
|
||||
.aspectRatio(2f / 3f)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.border(1.dp, scheme.outlineVariant.copy(alpha = 0.3f), RoundedCornerShape(14.dp))
|
||||
.border(
|
||||
width = if (isFocused) 2.dp else 1.dp,
|
||||
color = if (isFocused) scheme.primary else scheme.outlineVariant.copy(alpha = 0.3f),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
.background(scheme.surfaceVariant)
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable(onClick = { openItem(item) }),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -8,9 +11,16 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -22,12 +32,18 @@ fun GhostIconButton(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.1f else 1.0f, label = "scale")
|
||||
val borderColor by animateColorAsState(targetValue = if (isFocused) scheme.primary else Color.Transparent, label = "border")
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
.size(52.dp)
|
||||
.border(if (isFocused) 2.5.dp else 0.dp, borderColor, CircleShape)
|
||||
.clip(CircleShape)
|
||||
.background(scheme.background.copy(alpha = 0.65f))
|
||||
.background(if (isFocused) scheme.primary.copy(alpha = 0.25f) else scheme.background.copy(alpha = 0.65f))
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -37,4 +53,4 @@ fun GhostIconButton(
|
||||
tint = scheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
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.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun MediaActionButton(
|
||||
@@ -23,14 +34,22 @@ fun MediaActionButton(
|
||||
height: Dp,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.1f else 1.0f, label = "scale")
|
||||
val borderColor by animateColorAsState(targetValue = if (isFocused) scheme.primary else Color.Transparent, label = "border")
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
.size(height)
|
||||
.border(if (isFocused) 2.5.dp else 0.dp, borderColor, CircleShape)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundColor.copy(alpha = 0.6f))
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(imageVector = icon, contentDescription = null, tint = iconColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package hu.bbara.purefin.common.ui.components
|
||||
|
||||
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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -18,11 +20,18 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -36,11 +45,16 @@ fun MediaResumeButton(
|
||||
) {
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val onPrimaryColor = MaterialTheme.colorScheme.onPrimary
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.05f else 1.0f, label = "scale")
|
||||
|
||||
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 }
|
||||
.clickable(onClick = onClick)
|
||||
) {
|
||||
// Bottom layer: inverted colors (visible for the remaining %)
|
||||
@@ -77,7 +91,7 @@ fun MediaResumeButton(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ButtonContent(text: String, color: androidx.compose.ui.graphics.Color) {
|
||||
private fun ButtonContent(text: String, color: Color) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -8,9 +11,16 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -23,12 +33,17 @@ fun PurefinIconButton(
|
||||
size: Int = 52
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.1f else 1.0f, label = "scale")
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
.size(size.dp)
|
||||
.border(if (isFocused) 2.5.dp else 0.dp, if (isFocused) scheme.onPrimary else Color.Transparent, CircleShape)
|
||||
.clip(CircleShape)
|
||||
.background(scheme.secondary)
|
||||
.background(if (isFocused) scheme.primary else scheme.secondary)
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package hu.bbara.purefin.tv.home.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -19,8 +20,13 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -132,13 +138,20 @@ fun TvHomeDrawerNavItem(
|
||||
onLibrarySelected: (TvHomeNavItem) -> Unit
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val background = if (item.selected) scheme.primary.copy(alpha = 0.12f) else Color.Transparent
|
||||
val tint = if (item.selected) scheme.primary else scheme.onSurfaceVariant
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val background = when {
|
||||
isFocused -> scheme.primary.copy(alpha = 0.28f)
|
||||
item.selected -> scheme.primary.copy(alpha = 0.12f)
|
||||
else -> Color.Transparent
|
||||
}
|
||||
val tint = if (item.selected || isFocused) scheme.primary else scheme.onSurfaceVariant
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.then(if (isFocused) Modifier.border(2.dp, scheme.primary, RoundedCornerShape(12.dp)) else Modifier)
|
||||
.background(background, RoundedCornerShape(12.dp))
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable { onLibrarySelected(item) }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -150,7 +163,7 @@ fun TvHomeDrawerNavItem(
|
||||
)
|
||||
Text(
|
||||
text = item.label,
|
||||
color = if (item.selected) scheme.primary else scheme.onBackground,
|
||||
color = if (item.selected || isFocused) scheme.primary else scheme.onBackground,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package hu.bbara.purefin.tv.home.ui
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -27,9 +28,15 @@ import androidx.compose.material3.IconButtonColors
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -87,6 +94,8 @@ fun TvContinueWatchingCard(
|
||||
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
|
||||
|
||||
val imageUrl = when (item.type) {
|
||||
BaseItemKind.MOVIE -> item.movie?.heroImageUrl
|
||||
@@ -118,13 +127,18 @@ fun TvContinueWatchingCard(
|
||||
modifier = modifier
|
||||
.width(cardWidth)
|
||||
.wrapContentHeight()
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(cardWidth)
|
||||
.aspectRatio(16f / 9f)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.border(1.dp, scheme.outlineVariant.copy(alpha = 0.3f), RoundedCornerShape(16.dp))
|
||||
.border(
|
||||
width = if (isFocused) 2.dp else 1.dp,
|
||||
color = if (isFocused) scheme.primary else scheme.outlineVariant.copy(alpha = 0.3f),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
.background(scheme.surfaceVariant)
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
@@ -132,6 +146,7 @@ fun TvContinueWatchingCard(
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable {
|
||||
openItem(item)
|
||||
},
|
||||
@@ -201,6 +216,8 @@ fun TvNextUpCard(
|
||||
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
|
||||
|
||||
val imageUrl = item.episode.heroImageUrl
|
||||
|
||||
@@ -221,13 +238,18 @@ fun TvNextUpCard(
|
||||
modifier = modifier
|
||||
.width(cardWidth)
|
||||
.wrapContentHeight()
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(cardWidth)
|
||||
.aspectRatio(16f / 9f)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.border(1.dp, scheme.outlineVariant.copy(alpha = 0.3f), RoundedCornerShape(16.dp))
|
||||
.border(
|
||||
width = if (isFocused) 2.dp else 1.dp,
|
||||
color = if (isFocused) scheme.primary else scheme.outlineVariant.copy(alpha = 0.3f),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
.background(scheme.surfaceVariant)
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
@@ -235,6 +257,7 @@ fun TvNextUpCard(
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable {
|
||||
openItem(item)
|
||||
},
|
||||
|
||||
@@ -8,7 +8,10 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -62,10 +65,12 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -650,14 +655,18 @@ private fun TvTrackSelectionPanel(
|
||||
) {
|
||||
options.forEach { option ->
|
||||
val selected = option.id == selectedId
|
||||
var isTrackFocused by remember { mutableStateOf(false) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isTrackFocused) Modifier.border(2.dp, scheme.primary, RoundedCornerShape(12.dp)) else Modifier)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
if (selected) scheme.primary.copy(alpha = 0.15f)
|
||||
if (isTrackFocused) scheme.primary.copy(alpha = 0.3f)
|
||||
else if (selected) scheme.primary.copy(alpha = 0.15f)
|
||||
else scheme.surfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
.onFocusChanged { isTrackFocused = it.isFocused }
|
||||
.clickable { onSelect(option) }
|
||||
.padding(horizontal = 20.dp, vertical = 14.dp)
|
||||
) {
|
||||
@@ -724,14 +733,18 @@ private fun TvQueuePanel(
|
||||
} else {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
items(uiState.queue) { item ->
|
||||
var isQueueFocused by remember { mutableStateOf(false) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isQueueFocused) Modifier.border(2.dp, scheme.primary, RoundedCornerShape(12.dp)) else Modifier)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
if (item.isCurrent) scheme.primary.copy(alpha = 0.15f)
|
||||
if (isQueueFocused) scheme.primary.copy(alpha = 0.35f)
|
||||
else if (item.isCurrent) scheme.primary.copy(alpha = 0.15f)
|
||||
else scheme.surfaceVariant.copy(alpha = 0.8f)
|
||||
)
|
||||
.onFocusChanged { isQueueFocused = it.isFocused }
|
||||
.clickable { onSelect(item.id) }
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -790,12 +803,19 @@ private fun TvIconButton(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.1f else 1.0f, label = "scale")
|
||||
val borderColor by animateColorAsState(targetValue = if (isFocused) scheme.primary else Color.Transparent, label = "border")
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
.widthIn(min = size.dp)
|
||||
.height(size.dp)
|
||||
.border(if (isFocused) 2.dp else 0.dp, borderColor, RoundedCornerShape(50))
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(scheme.background.copy(alpha = 0.65f))
|
||||
.background(if (isFocused) scheme.primary.copy(alpha = 0.5f) else scheme.background.copy(alpha = 0.65f))
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user