feat: add content screens to TV app (movies, series, episodes)
- Created MovieScreen, MovieComponents for movie detail view - Created SeriesScreen, SeriesComponents for series detail with season selection - Created EpisodeScreen, EpisodeComponents for episode detail view - Copied all required common UI components from mobile app - Updated TvNavigationModule with route entry builders - Updated TvRouteEntryBuilder with new screen entries - Added missing launcher resources for TV app build - All screens reuse ViewModels from :feature:shared module - TV app now supports browsing movies, series, and episodes
@@ -0,0 +1,152 @@
|
|||||||
|
package hu.bbara.purefin.app.content.episode
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.Cast
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
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 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.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.core.model.Episode
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun EpisodeTopBar(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
GhostIconButton(
|
||||||
|
icon = Icons.Outlined.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
onClick = onBack
|
||||||
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
||||||
|
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun EpisodeDetails(
|
||||||
|
episode: Episode,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
MediaSynopsis(
|
||||||
|
synopsis = episode.synopsis
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row() {
|
||||||
|
MediaResumeButton(
|
||||||
|
text = if (episode.progress == null) "Play" else "Resume",
|
||||||
|
progress = episode.progress?.div(100)?.toFloat() ?: 0f,
|
||||||
|
onClick = {},
|
||||||
|
modifier = Modifier.sizeIn(maxWidth = 200.dp)
|
||||||
|
)
|
||||||
|
VerticalDivider(
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
thickness = 4.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
Row() {
|
||||||
|
MediaActionButton(
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.secondary,
|
||||||
|
iconColor = MaterialTheme.colorScheme.onSecondary,
|
||||||
|
icon = Icons.Outlined.Add,
|
||||||
|
height = 48.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
MediaPlaybackSettings(
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.surface,
|
||||||
|
foregroundColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
audioTrack = "ENG",
|
||||||
|
subtitles = "ENG"
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package hu.bbara.purefin.app.content.episode
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||||
|
import hu.bbara.purefin.common.ui.components.MediaHero
|
||||||
|
import hu.bbara.purefin.core.data.navigation.EpisodeDto
|
||||||
|
import hu.bbara.purefin.core.model.Episode
|
||||||
|
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EpisodeScreen(
|
||||||
|
episode: EpisodeDto,
|
||||||
|
viewModel: EpisodeScreenViewModel = hiltViewModel(),
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
|
||||||
|
LaunchedEffect(episode) {
|
||||||
|
viewModel.selectEpisode(
|
||||||
|
seriesId = episode.seriesId,
|
||||||
|
seasonId = episode.seasonId,
|
||||||
|
episodeId = episode.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val episode = viewModel.episode.collectAsState()
|
||||||
|
|
||||||
|
if (episode.value == null) {
|
||||||
|
PurefinWaitingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
EpisodeScreenInternal(
|
||||||
|
episode = episode.value!!,
|
||||||
|
onBack = viewModel::onBack,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EpisodeScreenInternal(
|
||||||
|
episode: Episode,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
|
topBar = {
|
||||||
|
EpisodeTopBar(
|
||||||
|
onBack = onBack,
|
||||||
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
MediaHero(
|
||||||
|
imageUrl = episode.heroImageUrl,
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.background,
|
||||||
|
heightFraction = 0.30f,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
EpisodeDetails(
|
||||||
|
episode = episode,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package hu.bbara.purefin.app.content.movie
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.Cast
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
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 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.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
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MovieTopBar(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
GhostIconButton(
|
||||||
|
icon = Icons.Outlined.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
onClick = onBack
|
||||||
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
||||||
|
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun MovieDetails(
|
||||||
|
movie: MovieUiModel,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
MediaSynopsis(
|
||||||
|
synopsis = movie.synopsis
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row() {
|
||||||
|
MediaResumeButton(
|
||||||
|
text = if (movie.progress == null) "Play" else "Resume",
|
||||||
|
progress = movie.progress?.div(100)?.toFloat() ?: 0f,
|
||||||
|
onClick = {},
|
||||||
|
modifier = Modifier.sizeIn(maxWidth = 200.dp)
|
||||||
|
)
|
||||||
|
VerticalDivider(
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
thickness = 4.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
Row() {
|
||||||
|
MediaActionButton(
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.secondary,
|
||||||
|
iconColor = MaterialTheme.colorScheme.onSecondary,
|
||||||
|
icon = Icons.Outlined.Add,
|
||||||
|
height = 48.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
MediaPlaybackSettings(
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.surface,
|
||||||
|
foregroundColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
audioTrack = movie.audioTrack,
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package hu.bbara.purefin.app.content.movie
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||||
|
import hu.bbara.purefin.common.ui.components.MediaHero
|
||||||
|
import hu.bbara.purefin.core.data.navigation.MovieDto
|
||||||
|
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
||||||
|
import hu.bbara.purefin.feature.shared.content.movie.MovieUiModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MovieScreen(
|
||||||
|
movie: MovieDto, viewModel: MovieScreenViewModel = hiltViewModel(), modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
LaunchedEffect(movie.id) {
|
||||||
|
viewModel.selectMovie(movie.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
val movieItem = viewModel.movie.collectAsState()
|
||||||
|
|
||||||
|
if (movieItem.value != null) {
|
||||||
|
MovieScreenInternal(
|
||||||
|
movie = movieItem.value!!,
|
||||||
|
onBack = viewModel::onBack,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PurefinWaitingScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MovieScreenInternal(
|
||||||
|
movie: MovieUiModel,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
|
topBar = {
|
||||||
|
MovieTopBar(
|
||||||
|
onBack = onBack,
|
||||||
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
MediaHero(
|
||||||
|
imageUrl = movie.heroImageUrl,
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.background,
|
||||||
|
heightFraction = 0.30f,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
MovieDetails(
|
||||||
|
movie = movie,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
package hu.bbara.purefin.app.content.series
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
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.layout.size
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.Cast
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.material.icons.outlined.PlayCircle
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
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.components.GhostIconButton
|
||||||
|
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
||||||
|
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
||||||
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
||||||
|
import hu.bbara.purefin.core.model.CastMember
|
||||||
|
import hu.bbara.purefin.core.model.Episode
|
||||||
|
import hu.bbara.purefin.core.model.Season
|
||||||
|
import hu.bbara.purefin.core.model.Series
|
||||||
|
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SeriesTopBar(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
GhostIconButton(
|
||||||
|
onClick = onBack,
|
||||||
|
icon = Icons.Outlined.ArrowBack,
|
||||||
|
contentDescription = "Back")
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
||||||
|
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun SeriesMetaChips(series: Series) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
MediaMetaChip(text = series.year)
|
||||||
|
MediaMetaChip(text = "${series.seasonCount} Seasons")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SeriesActionButtons(modifier: Modifier = Modifier) {
|
||||||
|
Row() {
|
||||||
|
MediaActionButton(
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.secondary,
|
||||||
|
iconColor = MaterialTheme.colorScheme.onSecondary,
|
||||||
|
icon = Icons.Outlined.Add,
|
||||||
|
height = 32.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SeasonTabs(
|
||||||
|
seasons: List<Season>,
|
||||||
|
selectedSeason: Season?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onSelect: (Season) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState()),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(20.dp)
|
||||||
|
) {
|
||||||
|
seasons.forEach { season ->
|
||||||
|
SeasonTab(
|
||||||
|
name = season.name,
|
||||||
|
isSelected = season == selectedSeason,
|
||||||
|
modifier = Modifier.clickable { onSelect(season) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SeasonTab(
|
||||||
|
name: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
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
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
color = color,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(2.dp)
|
||||||
|
.width(52.dp)
|
||||||
|
.background(borderColor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun EpisodeCarousel(episodes: List<Episode>, modifier: Modifier = Modifier) {
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
LaunchedEffect(episodes) {
|
||||||
|
val firstUnwatchedIndex = episodes.indexOfFirst { !it.watched }.let { if (it == -1) 0 else it }
|
||||||
|
if (firstUnwatchedIndex != 0) {
|
||||||
|
listState.animateScrollToItem(firstUnwatchedIndex)
|
||||||
|
} else {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
state = listState,
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(episodes) { episode ->
|
||||||
|
EpisodeCard(episode = episode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EpisodeCard(
|
||||||
|
viewModel: SeriesViewModel = hiltViewModel(),
|
||||||
|
episode: Episode
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(260.dp)
|
||||||
|
.clickable { viewModel.onSelectEpisode(
|
||||||
|
seriesId = episode.seriesId,
|
||||||
|
seasonId = episode.seasonId,
|
||||||
|
episodeId = episode.id
|
||||||
|
) },
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(16f / 9f)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(scheme.surface)
|
||||||
|
.border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp))
|
||||||
|
) {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = episode.heroImageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(scheme.background.copy(alpha = 0.2f))
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.PlayCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = scheme.onBackground,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(32.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(6.dp)
|
||||||
|
.background(scheme.background.copy(alpha = 0.8f), RoundedCornerShape(6.dp))
|
||||||
|
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = episode.runtime,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (episode.watched.not() && (episode.progress ?: 0.0) > 0) {
|
||||||
|
MediaProgressBar(
|
||||||
|
progress = (episode.progress ?: 0.0).toFloat().div(100),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
WatchStateIndicator(
|
||||||
|
watched = episode.watched,
|
||||||
|
started = (episode.progress ?: 0.0) > 0.0,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = episode.title,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Episode ${episode.index}",
|
||||||
|
color = mutedStrong,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun CastRow(cast: List<CastMember>, modifier: Modifier = Modifier) {
|
||||||
|
MediaCastRow(
|
||||||
|
cast = cast,
|
||||||
|
modifier = modifier,
|
||||||
|
cardWidth = 84.dp,
|
||||||
|
nameSize = 11.sp,
|
||||||
|
roleSize = 10.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package hu.bbara.purefin.app.content.series
|
||||||
|
|
||||||
|
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.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
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.MediaSynopsis
|
||||||
|
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||||
|
import hu.bbara.purefin.common.ui.components.MediaHero
|
||||||
|
import hu.bbara.purefin.core.data.navigation.SeriesDto
|
||||||
|
import hu.bbara.purefin.core.model.Season
|
||||||
|
import hu.bbara.purefin.core.model.Series
|
||||||
|
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SeriesScreen(
|
||||||
|
series: SeriesDto,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: SeriesViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
LaunchedEffect(series.id) {
|
||||||
|
viewModel.selectSeries(series.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
val series = viewModel.series.collectAsState()
|
||||||
|
|
||||||
|
val seriesData = series.value
|
||||||
|
if (seriesData != null && seriesData.seasons.isNotEmpty()) {
|
||||||
|
SeriesScreenInternal(
|
||||||
|
series = seriesData,
|
||||||
|
onBack = viewModel::onBack,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PurefinWaitingScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SeriesScreenInternal(
|
||||||
|
series: Series,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val textMutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||||
|
|
||||||
|
fun getDefaultSeason() : Season {
|
||||||
|
for (season in series.seasons) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { 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())
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = series.name,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 30.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
lineHeight = 36.sp
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package hu.bbara.purefin.common.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Person
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.core.model.CastMember
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaMetaChip(
|
||||||
|
text: String,
|
||||||
|
background: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
border: Color = Color.Transparent,
|
||||||
|
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.height(28.dp)
|
||||||
|
.wrapContentHeight(Alignment.CenterVertically)
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
.background(background)
|
||||||
|
.border(width = 1.dp, color = border, shape = RoundedCornerShape(6.dp))
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaCastRow(
|
||||||
|
cast: List<CastMember>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
cardWidth: Dp = 96.dp,
|
||||||
|
nameSize: TextUnit = 12.sp,
|
||||||
|
roleSize: TextUnit = 10.sp
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
modifier = modifier,
|
||||||
|
contentPadding = PaddingValues(horizontal = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(cast) { member ->
|
||||||
|
Column(modifier = Modifier.width(cardWidth)) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(4f / 5f)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(scheme.surfaceVariant)
|
||||||
|
) {
|
||||||
|
if (member.imageUrl == null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(scheme.surfaceVariant.copy(alpha = 0.6f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = mutedStrong
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = member.imageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
text = member.name,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = nameSize,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = member.role,
|
||||||
|
color = mutedStrong,
|
||||||
|
fontSize = roleSize,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package hu.bbara.purefin.common.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.TextUnit.Companion.Unspecified
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaSynopsis(
|
||||||
|
synopsis: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String = "Synopsis",
|
||||||
|
titleColor: Color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
bodyColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
titleFontSize: TextUnit = 18.sp,
|
||||||
|
bodyFontSize: TextUnit = 15.sp,
|
||||||
|
bodyLineHeight: TextUnit? = 22.sp,
|
||||||
|
titleSpacing: Dp = 12.dp,
|
||||||
|
collapsedLines: Int = 3,
|
||||||
|
collapseInitially: Boolean = true
|
||||||
|
) {
|
||||||
|
var isExpanded by remember(synopsis) { mutableStateOf(!collapseInitially) }
|
||||||
|
var isOverflowing by remember(synopsis) { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val containerModifier = if (isOverflowing) {
|
||||||
|
modifier.clickable(role = Role.Button) { isExpanded = !isExpanded }
|
||||||
|
} else {
|
||||||
|
modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = containerModifier) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = titleColor,
|
||||||
|
fontSize = titleFontSize,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(titleSpacing))
|
||||||
|
Text(
|
||||||
|
text = synopsis,
|
||||||
|
color = bodyColor,
|
||||||
|
fontSize = bodyFontSize,
|
||||||
|
lineHeight = bodyLineHeight ?: Unspecified,
|
||||||
|
maxLines = if (isExpanded) Int.MAX_VALUE else collapsedLines,
|
||||||
|
overflow = if (isExpanded) TextOverflow.Clip else TextOverflow.Ellipsis,
|
||||||
|
onTextLayout = { result ->
|
||||||
|
val overflowed = if (isExpanded) {
|
||||||
|
result.lineCount > collapsedLines
|
||||||
|
} else {
|
||||||
|
result.hasVisualOverflow || result.lineCount > collapsedLines
|
||||||
|
}
|
||||||
|
if (overflowed != isOverflowing) {
|
||||||
|
isOverflowing = overflowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ fun PurefinTextButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
content: @Composable RowScope.() -> Unit
|
content: @Composable RowScope.() -> Unit // Slot API
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
@@ -23,4 +23,4 @@ fun PurefinTextButton(
|
|||||||
),
|
),
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GhostIconButton(
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(52.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(scheme.background.copy(alpha = 0.65f))
|
||||||
|
.clickable { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = scheme.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaActionButton(
|
||||||
|
backgroundColor: Color,
|
||||||
|
iconColor: Color,
|
||||||
|
icon: ImageVector,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
height: Dp,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(height)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(backgroundColor.copy(alpha = 0.6f))
|
||||||
|
.clickable { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(imageVector = icon, contentDescription = null, tint = iconColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaHero(
|
||||||
|
imageUrl: String,
|
||||||
|
backgroundColor: Color,
|
||||||
|
heightFraction: Float = 0.4f,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
|
||||||
|
val heroHeight = screenHeight * heightFraction
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.height(heroHeight)
|
||||||
|
.background(backgroundColor)
|
||||||
|
) {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = imageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
backgroundColor.copy(alpha = 0.5f),
|
||||||
|
backgroundColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.TextUnitType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaPlayButton(
|
||||||
|
backgroundColor: Color,
|
||||||
|
foregroundColor: Color,
|
||||||
|
text: String = "Play",
|
||||||
|
subText: String? = null,
|
||||||
|
size: Dp,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.height(size)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(backgroundColor)
|
||||||
|
.padding(start = 16.dp, end = 32.dp)
|
||||||
|
.clickable { onClick() },
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
|
contentDescription = "Play",
|
||||||
|
tint = foregroundColor,
|
||||||
|
modifier = Modifier.size(42.dp)
|
||||||
|
)
|
||||||
|
Column() {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = foregroundColor,
|
||||||
|
fontSize = TextUnit(
|
||||||
|
value = 16f,
|
||||||
|
type = TextUnitType.Sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
subText?.let {
|
||||||
|
Text(
|
||||||
|
text = subText,
|
||||||
|
color = foregroundColor.copy(alpha = 0.7f),
|
||||||
|
fontSize = TextUnit(
|
||||||
|
value = 14f,
|
||||||
|
type = TextUnitType.Sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ClosedCaption
|
||||||
|
import androidx.compose.material.icons.outlined.ExpandMore
|
||||||
|
import androidx.compose.material.icons.outlined.VolumeUp
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaPlaybackSettings(
|
||||||
|
backgroundColor: Color,
|
||||||
|
foregroundColor: Color,
|
||||||
|
audioTrack: String,
|
||||||
|
subtitles: String,
|
||||||
|
audioIcon: ImageVector = Icons.Outlined.VolumeUp,
|
||||||
|
subtitleIcon: ImageVector = Icons.Outlined.ClosedCaption,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
MediaSettingDropdown(
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
foregroundColor = foregroundColor,
|
||||||
|
label = "Audio Track",
|
||||||
|
value = audioTrack,
|
||||||
|
icon = audioIcon
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
MediaSettingDropdown(
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
foregroundColor = foregroundColor,
|
||||||
|
label = "Subtitles",
|
||||||
|
value = subtitles,
|
||||||
|
icon = subtitleIcon
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MediaSettingDropdown(
|
||||||
|
backgroundColor: Color,
|
||||||
|
foregroundColor: Color,
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
icon: ImageVector
|
||||||
|
) {
|
||||||
|
Row (
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = foregroundColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(38.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(backgroundColor)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon, contentDescription = null, tint = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
Text(text = value, color = foregroundColor, fontSize = 14.sp)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.ExpandMore,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = foregroundColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,4 +45,4 @@ fun MediaProgressBar(
|
|||||||
.background(foregroundColor)
|
.background(foregroundColor)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
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.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.graphics.drawscope.clipRect
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaResumeButton(
|
||||||
|
text: String,
|
||||||
|
progress: Float = 0f,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
val onPrimaryColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = modifier
|
||||||
|
.height(52.dp)
|
||||||
|
.clip(RoundedCornerShape(50))
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
) {
|
||||||
|
// Bottom layer: inverted colors (visible for the remaining %)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(onPrimaryColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
ButtonContent(text = text, color = primaryColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top layer: primary colors, clipped to the progress %
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.drawWithContent {
|
||||||
|
val clipWidth = size.width * progress
|
||||||
|
clipRect(
|
||||||
|
left = 0f,
|
||||||
|
top = 0f,
|
||||||
|
right = clipWidth,
|
||||||
|
bottom = size.height
|
||||||
|
) {
|
||||||
|
this@drawWithContent.drawContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(primaryColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
ButtonContent(text = text, color = onPrimaryColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ButtonContent(text: String, color: androidx.compose.ui.graphics.Color) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(text, color = color, fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Icon(Icons.Filled.PlayArrow, null, tint = color)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,4 +45,4 @@ fun SearchField(
|
|||||||
focusedTextColor = textColor,
|
focusedTextColor = textColor,
|
||||||
unfocusedTextColor = textColor,
|
unfocusedTextColor = textColor,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.EnterTransition
|
||||||
|
import androidx.compose.animation.ExitTransition
|
||||||
|
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.Modifier
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composable that displays content for a specified duration after the value becomes null.
|
||||||
|
*
|
||||||
|
* @param value The value to display. When set to a non-null value, it will be shown immediately.
|
||||||
|
* When set to null, the previously shown value will remain visible for [hideAfterMillis]
|
||||||
|
* before being hidden.
|
||||||
|
* @param hideAfterMillis The duration in milliseconds to keep showing the last value after [value] becomes null.
|
||||||
|
* Defaults to 1000ms (1 second).
|
||||||
|
* @param content The composable content to display, receiving the current non-null value.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T> EmptyValueTimedVisibility(
|
||||||
|
value: T?,
|
||||||
|
hideAfterMillis: Long = 1_000,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable (T) -> Unit
|
||||||
|
) {
|
||||||
|
val shownValue = remember { mutableStateOf<T?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(value) {
|
||||||
|
if (value == null) {
|
||||||
|
delay(hideAfterMillis)
|
||||||
|
shownValue.value = null
|
||||||
|
}
|
||||||
|
shownValue.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
shownValue.value?.let {
|
||||||
|
content(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays [content] whenever [value] changes and hides it after [hideAfterMillis]
|
||||||
|
* milliseconds without further updates.
|
||||||
|
*
|
||||||
|
* @param value The value whose changes should trigger visibility.
|
||||||
|
* @param hideAfterMillis Duration in milliseconds after which the content will be hidden
|
||||||
|
* if [value] has not changed again.
|
||||||
|
* @param content The composable to render while visible.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T> ValueChangeTimedVisibility(
|
||||||
|
value: T,
|
||||||
|
hideAfterMillis: Long = 1_000,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable (T) -> Unit
|
||||||
|
) {
|
||||||
|
var displayedValue by remember { mutableStateOf(value) }
|
||||||
|
var isVisible by remember { mutableStateOf(false) }
|
||||||
|
var hasInitialValue by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(value) {
|
||||||
|
displayedValue = value
|
||||||
|
if (!hasInitialValue) {
|
||||||
|
hasInitialValue = true
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
isVisible = true
|
||||||
|
delay(hideAfterMillis)
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isVisible,
|
||||||
|
modifier = modifier,
|
||||||
|
enter = EnterTransition.None,
|
||||||
|
exit = ExitTransition.None
|
||||||
|
) {
|
||||||
|
content(displayedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,4 +43,4 @@ fun UnwatchedEpisodeIndicator(
|
|||||||
fontSize = 15.sp
|
fontSize = 15.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,4 +78,4 @@ private fun WatchStateIndicatorPreview() {
|
|||||||
started = true
|
started = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,4 +23,22 @@ object TvNavigationModule {
|
|||||||
fun provideTvLoginEntryBuilder(): EntryProviderScope<Route>.() -> Unit = {
|
fun provideTvLoginEntryBuilder(): EntryProviderScope<Route>.() -> Unit = {
|
||||||
tvLoginSection()
|
tvLoginSection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IntoSet
|
||||||
|
@Provides
|
||||||
|
fun provideTvMovieEntryBuilder(): EntryProviderScope<Route>.() -> Unit = {
|
||||||
|
tvMovieSection()
|
||||||
|
}
|
||||||
|
|
||||||
|
@IntoSet
|
||||||
|
@Provides
|
||||||
|
fun provideTvSeriesEntryBuilder(): EntryProviderScope<Route>.() -> Unit = {
|
||||||
|
tvSeriesSection()
|
||||||
|
}
|
||||||
|
|
||||||
|
@IntoSet
|
||||||
|
@Provides
|
||||||
|
fun provideTvEpisodeEntryBuilder(): EntryProviderScope<Route>.() -> Unit = {
|
||||||
|
tvEpisodeSection()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package hu.bbara.purefin.tv.navigation
|
package hu.bbara.purefin.tv.navigation
|
||||||
|
|
||||||
import androidx.navigation3.runtime.EntryProviderScope
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
|
import hu.bbara.purefin.app.content.episode.EpisodeScreen
|
||||||
|
import hu.bbara.purefin.app.content.movie.MovieScreen
|
||||||
|
import hu.bbara.purefin.app.content.series.SeriesScreen
|
||||||
import hu.bbara.purefin.core.data.navigation.Route
|
import hu.bbara.purefin.core.data.navigation.Route
|
||||||
import hu.bbara.purefin.login.ui.LoginScreen
|
import hu.bbara.purefin.login.ui.LoginScreen
|
||||||
import hu.bbara.purefin.tv.home.TvHomePage
|
import hu.bbara.purefin.tv.home.TvHomePage
|
||||||
@@ -16,3 +19,21 @@ fun EntryProviderScope<Route>.tvLoginSection() {
|
|||||||
LoginScreen()
|
LoginScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun EntryProviderScope<Route>.tvMovieSection() {
|
||||||
|
entry<Route.MovieRoute> { route ->
|
||||||
|
MovieScreen(movie = route.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EntryProviderScope<Route>.tvSeriesSection() {
|
||||||
|
entry<Route.SeriesRoute> { route ->
|
||||||
|
SeriesScreen(series = route.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EntryProviderScope<Route>.tvEpisodeSection() {
|
||||||
|
entry<Route.EpisodeRoute> { route ->
|
||||||
|
EpisodeScreen(episode = route.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
170
app-tv/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
||||||
30
app-tv/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
||||||
6
app-tv/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app-tv/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app-tv/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app-tv/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app-tv/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app-tv/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app-tv/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app-tv/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
10
app-tv/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Purefin</string>
|
<string name="app_name">Purefin</string>
|
||||||
|
<string name="download_channel_name">Downloads</string>
|
||||||
|
<string name="download_channel_description">Media download progress</string>
|
||||||
</resources>
|
</resources>
|
||||||