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