mirror of
https://github.com/bbara04/Purefin.git
synced 2026-04-01 01:30:08 +02:00
refactor media components to use shared UI elements and improve consistency
This commit is contained in:
@@ -1,99 +0,0 @@
|
|||||||
package hu.bbara.purefin.app.content.episode
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun EpisodeCard(
|
|
||||||
episode: EpisodeUiModel,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val colors = rememberEpisodeColors()
|
|
||||||
|
|
||||||
BoxWithConstraints(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(colors.background)
|
|
||||||
) {
|
|
||||||
val isWide = maxWidth >= 900.dp
|
|
||||||
val contentPadding = if (isWide) 32.dp else 20.dp
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
|
||||||
if (isWide) {
|
|
||||||
Row(modifier = Modifier.fillMaxSize()) {
|
|
||||||
EpisodeHero(
|
|
||||||
episode = episode,
|
|
||||||
height = 300.dp,
|
|
||||||
isWide = true,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.weight(0.5f)
|
|
||||||
)
|
|
||||||
EpisodeDetails(
|
|
||||||
episode = episode,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(0.5f)
|
|
||||||
.fillMaxHeight()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(
|
|
||||||
start = contentPadding,
|
|
||||||
end = contentPadding,
|
|
||||||
top = 96.dp,
|
|
||||||
bottom = 32.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
EpisodeHero(
|
|
||||||
episode = episode,
|
|
||||||
height = 400.dp,
|
|
||||||
isWide = false,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
EpisodeDetails(
|
|
||||||
episode = episode,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = contentPadding)
|
|
||||||
.offset(y = (-48).dp)
|
|
||||||
.padding(bottom = 96.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EpisodeTopBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isWide) {
|
|
||||||
FloatingPlayButton(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,60 +2,46 @@ package hu.bbara.purefin.app.content.episode
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
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.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material.icons.outlined.Add
|
|
||||||
import androidx.compose.material.icons.outlined.ArrowBack
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.Cast
|
import androidx.compose.material.icons.outlined.Cast
|
||||||
import androidx.compose.material.icons.outlined.ClosedCaption
|
|
||||||
import androidx.compose.material.icons.outlined.Download
|
|
||||||
import androidx.compose.material.icons.outlined.ExpandMore
|
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.Person
|
|
||||||
import androidx.compose.material.icons.outlined.Tune
|
|
||||||
import androidx.compose.material.icons.outlined.VolumeUp
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.remember
|
||||||
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.shadow
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
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
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import coil3.compose.AsyncImage
|
import hu.bbara.purefin.common.ui.MediaActionButtons
|
||||||
|
import hu.bbara.purefin.common.ui.MediaCastMember
|
||||||
|
import hu.bbara.purefin.common.ui.MediaCastRow
|
||||||
|
import hu.bbara.purefin.common.ui.MediaFloatingPlayButton
|
||||||
|
import hu.bbara.purefin.common.ui.MediaGhostIconButton
|
||||||
|
import hu.bbara.purefin.common.ui.MediaHero
|
||||||
|
import hu.bbara.purefin.common.ui.MediaMetaChip
|
||||||
|
import hu.bbara.purefin.common.ui.MediaPlaybackSettings
|
||||||
|
import hu.bbara.purefin.common.ui.toMediaDetailColors
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -63,100 +49,44 @@ internal fun EpisodeTopBar(
|
|||||||
viewModel: EpisodeScreenViewModel = hiltViewModel(),
|
viewModel: EpisodeScreenViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val colors = rememberEpisodeColors().toMediaDetailColors()
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
GhostIconButton(
|
MediaGhostIconButton(
|
||||||
onClick = { viewModel.onBack() },
|
colors = colors,
|
||||||
icon = Icons.Outlined.ArrowBack,
|
icon = Icons.Outlined.ArrowBack,
|
||||||
contentDescription = "Back"
|
contentDescription = "Back",
|
||||||
|
onClick = { viewModel.onBack() }
|
||||||
)
|
)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast")
|
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
||||||
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More")
|
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun GhostIconButton(
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
icon: ImageVector,
|
|
||||||
contentDescription: String,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val colors = rememberEpisodeColors()
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(colors.background.copy(alpha = 0.4f))
|
|
||||||
.clickable { onClick() },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = contentDescription,
|
|
||||||
tint = colors.textPrimary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun EpisodeHero(
|
internal fun EpisodeHero(
|
||||||
episode: EpisodeUiModel,
|
episode: EpisodeUiModel,
|
||||||
height: Dp,
|
height: Dp,
|
||||||
isWide: Boolean,
|
isWide: Boolean,
|
||||||
|
onPlayClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val colors = rememberEpisodeColors()
|
val colors = rememberEpisodeColors().toMediaDetailColors()
|
||||||
Box(
|
MediaHero(
|
||||||
modifier = modifier
|
imageUrl = episode.heroImageUrl,
|
||||||
.height(height)
|
colors = colors,
|
||||||
.background(colors.background)
|
height = height,
|
||||||
) {
|
isWide = isWide,
|
||||||
AsyncImage(
|
modifier = modifier,
|
||||||
model = episode.heroImageUrl,
|
showPlayButton = true,
|
||||||
contentDescription = null,
|
playButtonSize = if (isWide) 96.dp else 80.dp,
|
||||||
modifier = Modifier.fillMaxSize(),
|
onPlayClick = onPlayClick
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
)
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.background(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
colors.background.copy(alpha = 0.4f),
|
|
||||||
colors.background
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (isWide) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.background(
|
|
||||||
Brush.horizontalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
colors.background.copy(alpha = 0.8f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
PlayButton(size = if (isWide) 96.dp else 80.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@@ -165,7 +95,7 @@ internal fun EpisodeDetails(
|
|||||||
episode: EpisodeUiModel,
|
episode: EpisodeUiModel,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val colors = rememberEpisodeColors()
|
val colors = rememberEpisodeColors().toMediaDetailColors()
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Text(
|
Text(
|
||||||
text = episode.title,
|
text = episode.title,
|
||||||
@@ -179,10 +109,11 @@ internal fun EpisodeDetails(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
MetaChip(text = episode.releaseDate)
|
MediaMetaChip(colors = colors, text = episode.releaseDate)
|
||||||
MetaChip(text = episode.rating)
|
MediaMetaChip(colors = colors, text = episode.rating)
|
||||||
MetaChip(text = episode.runtime)
|
MediaMetaChip(colors = colors, text = episode.runtime)
|
||||||
MetaChip(
|
MediaMetaChip(
|
||||||
|
colors = colors,
|
||||||
text = episode.format,
|
text = episode.format,
|
||||||
background = colors.primary.copy(alpha = 0.2f),
|
background = colors.primary.copy(alpha = 0.2f),
|
||||||
border = colors.primary.copy(alpha = 0.3f),
|
border = colors.primary.copy(alpha = 0.3f),
|
||||||
@@ -191,7 +122,11 @@ internal fun EpisodeDetails(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
PlaybackSettings(episode = episode)
|
MediaPlaybackSettings(
|
||||||
|
colors = colors,
|
||||||
|
audioTrack = episode.audioTrack,
|
||||||
|
subtitles = episode.subtitles
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -209,7 +144,7 @@ internal fun EpisodeDetails(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
ActionButtons()
|
MediaActionButtons(colors = colors)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(28.dp))
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -219,269 +154,108 @@ internal fun EpisodeDetails(
|
|||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
CastRow(cast = episode.cast)
|
MediaCastRow(
|
||||||
}
|
colors = colors,
|
||||||
}
|
cast = episode.cast.map { it.toMediaCastMember() }
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MetaChip(
|
|
||||||
text: String,
|
|
||||||
background: Color? = null,
|
|
||||||
border: Color? = null,
|
|
||||||
textColor: Color? = null
|
|
||||||
) {
|
|
||||||
val colors = rememberEpisodeColors()
|
|
||||||
val resolvedBackground = background ?: colors.surfaceAlt
|
|
||||||
val resolvedBorder = border ?: Color.Transparent
|
|
||||||
val resolvedTextColor = textColor ?: colors.textSecondary
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(28.dp)
|
|
||||||
.wrapContentHeight(Alignment.CenterVertically)
|
|
||||||
.clip(RoundedCornerShape(6.dp))
|
|
||||||
.background(resolvedBackground)
|
|
||||||
.border(width = 1.dp, color = resolvedBorder, shape = RoundedCornerShape(6.dp))
|
|
||||||
.padding(horizontal = 12.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
color = resolvedTextColor,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PlaybackSettings(episode: EpisodeUiModel) {
|
fun EpisodeCard(
|
||||||
val colors = rememberEpisodeColors()
|
episode: EpisodeUiModel,
|
||||||
Column(
|
modifier: Modifier = Modifier,
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.background(colors.surfaceAlt)
|
|
||||||
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(16.dp))
|
|
||||||
.padding(20.dp)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
val colors = rememberEpisodeColors().toMediaDetailColors()
|
||||||
verticalAlignment = Alignment.CenterVertically
|
val context = LocalContext.current
|
||||||
) {
|
val playAction = remember(episode.id) {
|
||||||
Icon(
|
{
|
||||||
imageVector = Icons.Outlined.Tune,
|
val intent = Intent(context, PlayerActivity::class.java)
|
||||||
contentDescription = null,
|
intent.putExtra("MEDIA_ID", episode.id.toString())
|
||||||
tint = colors.primary
|
context.startActivity(intent)
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = "Playback Settings",
|
|
||||||
color = colors.textMuted,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
letterSpacing = 2.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
||||||
SettingDropdown(
|
|
||||||
label = "Audio Track",
|
|
||||||
icon = Icons.Outlined.VolumeUp,
|
|
||||||
value = episode.audioTrack
|
|
||||||
)
|
|
||||||
SettingDropdown(
|
|
||||||
label = "Subtitles",
|
|
||||||
icon = Icons.Outlined.ClosedCaption,
|
|
||||||
value = episode.subtitles
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
BoxWithConstraints(
|
||||||
private fun SettingDropdown(
|
|
||||||
label: String,
|
|
||||||
icon: ImageVector,
|
|
||||||
value: String
|
|
||||||
) {
|
|
||||||
val colors = rememberEpisodeColors()
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
color = colors.textMutedStrong,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
modifier = Modifier.padding(start = 4.dp, bottom = 6.dp)
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(48.dp)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(colors.surface)
|
|
||||||
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Icon(imageVector = icon, contentDescription = null, tint = colors.textMutedStrong)
|
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
|
||||||
Text(text = value, color = colors.textPrimary, fontSize = 14.sp)
|
|
||||||
}
|
|
||||||
Icon(imageVector = Icons.Outlined.ExpandMore, contentDescription = null, tint = colors.textMutedStrong)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ActionButtons() {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
ActionButton(
|
|
||||||
text = "Watchlist",
|
|
||||||
icon = Icons.Outlined.Add,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
ActionButton(
|
|
||||||
text = "Download",
|
|
||||||
icon = Icons.Outlined.Download,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ActionButton(
|
|
||||||
text: String,
|
|
||||||
icon: ImageVector,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val colors = rememberEpisodeColors()
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.height(48.dp)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(colors.surfaceAlt.copy(alpha = 0.6f))
|
|
||||||
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
|
|
||||||
.clickable { },
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Icon(imageVector = icon, contentDescription = null, tint = colors.textPrimary)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(text = text, color = colors.textPrimary, fontSize = 14.sp, fontWeight = FontWeight.Bold)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CastRow(cast: List<CastMember>) {
|
|
||||||
val colors = rememberEpisodeColors()
|
|
||||||
LazyRow(
|
|
||||||
contentPadding = PaddingValues(horizontal = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
items(cast) { member ->
|
|
||||||
Column(modifier = Modifier.width(96.dp)) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.aspectRatio(4f / 5f)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(colors.surfaceAlt)
|
|
||||||
) {
|
|
||||||
if (member.imageUrl == null) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(colors.surfaceAlt.copy(alpha = 0.6f)),
|
.background(colors.background)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
val isWide = maxWidth >= 900.dp
|
||||||
imageVector = Icons.Outlined.Person,
|
val contentPadding = if (isWide) 32.dp else 20.dp
|
||||||
contentDescription = null,
|
|
||||||
tint = colors.textMutedStrong
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
if (isWide) {
|
||||||
|
Row(modifier = Modifier.fillMaxSize()) {
|
||||||
|
EpisodeHero(
|
||||||
|
episode = episode,
|
||||||
|
height = 300.dp,
|
||||||
|
isWide = true,
|
||||||
|
onPlayClick = playAction,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(0.5f)
|
||||||
|
)
|
||||||
|
EpisodeDetails(
|
||||||
|
episode = episode,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(0.5f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(
|
||||||
|
start = contentPadding,
|
||||||
|
end = contentPadding,
|
||||||
|
top = 96.dp,
|
||||||
|
bottom = 32.dp
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
AsyncImage(
|
Column(
|
||||||
model = member.imageUrl,
|
modifier = Modifier
|
||||||
contentDescription = null,
|
.fillMaxSize()
|
||||||
modifier = Modifier.fillMaxSize(),
|
.verticalScroll(rememberScrollState())
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
Text(
|
|
||||||
text = member.name,
|
|
||||||
color = colors.textPrimary,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = member.role,
|
|
||||||
color = colors.textMutedStrong,
|
|
||||||
fontSize = 10.sp,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PlayButton(
|
|
||||||
size: Dp,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
viewModel: EpisodeScreenViewModel = hiltViewModel()
|
|
||||||
) {
|
) {
|
||||||
val colors = rememberEpisodeColors()
|
EpisodeHero(
|
||||||
val context = LocalContext.current
|
episode = episode,
|
||||||
val episodeItem = viewModel.episode.collectAsState()
|
height = 400.dp,
|
||||||
|
isWide = false,
|
||||||
Box(
|
onPlayClick = playAction,
|
||||||
modifier = modifier
|
modifier = Modifier.fillMaxWidth()
|
||||||
.size(size)
|
)
|
||||||
.shadow(24.dp, CircleShape)
|
EpisodeDetails(
|
||||||
.clip(CircleShape)
|
episode = episode,
|
||||||
.background(colors.primary)
|
modifier = Modifier
|
||||||
.clickable {
|
.fillMaxWidth()
|
||||||
val intent = Intent(context, PlayerActivity::class.java)
|
.padding(horizontal = contentPadding)
|
||||||
intent.putExtra("MEDIA_ID", episodeItem.value!!.id.toString())
|
.offset(y = (-48).dp)
|
||||||
context.startActivity(intent)
|
.padding(bottom = 96.dp)
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.PlayArrow,
|
|
||||||
contentDescription = "Play",
|
|
||||||
tint = colors.onPrimary,
|
|
||||||
modifier = Modifier.size(42.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
EpisodeTopBar(
|
||||||
internal fun FloatingPlayButton(modifier: Modifier = Modifier) {
|
modifier = Modifier
|
||||||
val colors = rememberEpisodeColors()
|
.fillMaxWidth()
|
||||||
Box(
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
modifier = modifier
|
)
|
||||||
.size(56.dp)
|
|
||||||
.shadow(20.dp, CircleShape)
|
if (!isWide) {
|
||||||
.clip(CircleShape)
|
MediaFloatingPlayButton(
|
||||||
.background(colors.primary),
|
colors = colors,
|
||||||
contentAlignment = Alignment.Center
|
onClick = playAction,
|
||||||
) {
|
modifier = Modifier
|
||||||
Icon(
|
.align(Alignment.BottomEnd)
|
||||||
imageVector = Icons.Filled.PlayArrow,
|
.padding(20.dp)
|
||||||
contentDescription = "Play",
|
|
||||||
tint = colors.onPrimary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CastMember.toMediaCastMember() = MediaCastMember(
|
||||||
|
name = name,
|
||||||
|
role = role,
|
||||||
|
imageUrl = imageUrl
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
package hu.bbara.purefin.app.content.movie
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MovieCard(
|
|
||||||
movie: MovieUiModel,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val colors = rememberMovieColors()
|
|
||||||
|
|
||||||
BoxWithConstraints(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(colors.background)
|
|
||||||
) {
|
|
||||||
val isWide = maxWidth >= 900.dp
|
|
||||||
val contentPadding = if (isWide) 32.dp else 20.dp
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
|
||||||
if (isWide) {
|
|
||||||
Row(modifier = Modifier.fillMaxSize()) {
|
|
||||||
MovieHero(
|
|
||||||
movie = movie,
|
|
||||||
height = 300.dp,
|
|
||||||
isWide = true,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.weight(0.5f)
|
|
||||||
)
|
|
||||||
MovieDetails(
|
|
||||||
movie = movie,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(0.5f)
|
|
||||||
.fillMaxHeight()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(
|
|
||||||
start = contentPadding,
|
|
||||||
end = contentPadding,
|
|
||||||
top = 96.dp,
|
|
||||||
bottom = 32.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
MovieHero(
|
|
||||||
movie = movie,
|
|
||||||
height = 400.dp,
|
|
||||||
isWide = false,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
MovieDetails(
|
|
||||||
movie = movie,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = contentPadding)
|
|
||||||
.offset(y = (-48).dp)
|
|
||||||
.padding(bottom = 96.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieTopBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isWide) {
|
|
||||||
FloatingPlayButton(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,60 +2,46 @@ package hu.bbara.purefin.app.content.movie
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
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.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material.icons.outlined.Add
|
|
||||||
import androidx.compose.material.icons.outlined.ArrowBack
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.Cast
|
import androidx.compose.material.icons.outlined.Cast
|
||||||
import androidx.compose.material.icons.outlined.ClosedCaption
|
|
||||||
import androidx.compose.material.icons.outlined.Download
|
|
||||||
import androidx.compose.material.icons.outlined.ExpandMore
|
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.Person
|
|
||||||
import androidx.compose.material.icons.outlined.Tune
|
|
||||||
import androidx.compose.material.icons.outlined.VolumeUp
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.remember
|
||||||
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.shadow
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
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
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import coil3.compose.AsyncImage
|
import hu.bbara.purefin.common.ui.MediaActionButtons
|
||||||
|
import hu.bbara.purefin.common.ui.MediaCastMember
|
||||||
|
import hu.bbara.purefin.common.ui.MediaCastRow
|
||||||
|
import hu.bbara.purefin.common.ui.MediaFloatingPlayButton
|
||||||
|
import hu.bbara.purefin.common.ui.MediaGhostIconButton
|
||||||
|
import hu.bbara.purefin.common.ui.MediaHero
|
||||||
|
import hu.bbara.purefin.common.ui.MediaMetaChip
|
||||||
|
import hu.bbara.purefin.common.ui.MediaPlaybackSettings
|
||||||
|
import hu.bbara.purefin.common.ui.toMediaDetailColors
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -63,99 +49,44 @@ internal fun MovieTopBar(
|
|||||||
viewModel: MovieScreenViewModel = hiltViewModel(),
|
viewModel: MovieScreenViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val colors = rememberMovieColors().toMediaDetailColors()
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
GhostIconButton(
|
MediaGhostIconButton(
|
||||||
onClick = { viewModel.onBack() },
|
colors = colors,
|
||||||
icon = Icons.Outlined.ArrowBack,
|
icon = Icons.Outlined.ArrowBack,
|
||||||
contentDescription = "Back"
|
contentDescription = "Back",
|
||||||
|
onClick = { viewModel.onBack() }
|
||||||
)
|
)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast")
|
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
||||||
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More")
|
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun GhostIconButton(
|
|
||||||
icon: ImageVector,
|
|
||||||
contentDescription: String,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val colors = rememberMovieColors()
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(colors.background.copy(alpha = 0.4f))
|
|
||||||
.clickable { onClick() },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = contentDescription,
|
|
||||||
tint = colors.textPrimary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MovieHero(
|
internal fun MovieHero(
|
||||||
movie: MovieUiModel,
|
movie: MovieUiModel,
|
||||||
height: Dp,
|
height: Dp,
|
||||||
isWide: Boolean,
|
isWide: Boolean,
|
||||||
|
onPlayClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val colors = rememberMovieColors()
|
val colors = rememberMovieColors().toMediaDetailColors()
|
||||||
Box(
|
MediaHero(
|
||||||
modifier = modifier
|
imageUrl = movie.heroImageUrl,
|
||||||
.height(height)
|
colors = colors,
|
||||||
.background(colors.background)
|
height = height,
|
||||||
) {
|
isWide = isWide,
|
||||||
AsyncImage(
|
modifier = modifier,
|
||||||
model = movie.heroImageUrl,
|
showPlayButton = true,
|
||||||
contentDescription = null,
|
playButtonSize = if (isWide) 96.dp else 80.dp,
|
||||||
modifier = Modifier.fillMaxSize(),
|
onPlayClick = onPlayClick
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
)
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.background(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
colors.background.copy(alpha = 0.4f),
|
|
||||||
colors.background
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (isWide) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.background(
|
|
||||||
Brush.horizontalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent, colors.background.copy(alpha = 0.8f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
PlayButton(size = if (isWide) 96.dp else 80.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@@ -164,7 +95,7 @@ internal fun MovieDetails(
|
|||||||
movie: MovieUiModel,
|
movie: MovieUiModel,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val colors = rememberMovieColors()
|
val colors = rememberMovieColors().toMediaDetailColors()
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Text(
|
Text(
|
||||||
text = movie.title,
|
text = movie.title,
|
||||||
@@ -178,10 +109,11 @@ internal fun MovieDetails(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
MetaChip(text = movie.year)
|
MediaMetaChip(colors = colors, text = movie.year)
|
||||||
MetaChip(text = movie.rating)
|
MediaMetaChip(colors = colors, text = movie.rating)
|
||||||
MetaChip(text = movie.runtime)
|
MediaMetaChip(colors = colors, text = movie.runtime)
|
||||||
MetaChip(
|
MediaMetaChip(
|
||||||
|
colors = colors,
|
||||||
text = movie.format,
|
text = movie.format,
|
||||||
background = colors.primary.copy(alpha = 0.2f),
|
background = colors.primary.copy(alpha = 0.2f),
|
||||||
border = colors.primary.copy(alpha = 0.3f),
|
border = colors.primary.copy(alpha = 0.3f),
|
||||||
@@ -190,7 +122,11 @@ internal fun MovieDetails(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
PlaybackSettings(movie = movie)
|
MediaPlaybackSettings(
|
||||||
|
colors = colors,
|
||||||
|
audioTrack = movie.audioTrack,
|
||||||
|
subtitles = movie.subtitles
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -208,7 +144,7 @@ internal fun MovieDetails(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
ActionButtons()
|
MediaActionButtons(colors = colors)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(28.dp))
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -218,269 +154,108 @@ internal fun MovieDetails(
|
|||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
CastRow(cast = movie.cast)
|
MediaCastRow(
|
||||||
}
|
colors = colors,
|
||||||
}
|
cast = movie.cast.map { it.toMediaCastMember() }
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MetaChip(
|
|
||||||
text: String,
|
|
||||||
background: Color? = null,
|
|
||||||
border: Color? = null,
|
|
||||||
textColor: Color? = null
|
|
||||||
) {
|
|
||||||
val colors = rememberMovieColors()
|
|
||||||
val resolvedBackground = background ?: colors.surfaceAlt
|
|
||||||
val resolvedBorder = border ?: Color.Transparent
|
|
||||||
val resolvedTextColor = textColor ?: colors.textSecondary
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(28.dp)
|
|
||||||
.wrapContentHeight(Alignment.CenterVertically)
|
|
||||||
.clip(RoundedCornerShape(6.dp))
|
|
||||||
.background(resolvedBackground)
|
|
||||||
.border(width = 1.dp, color = resolvedBorder, shape = RoundedCornerShape(6.dp))
|
|
||||||
.padding(horizontal = 12.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
color = resolvedTextColor,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PlaybackSettings(movie: MovieUiModel) {
|
fun MovieCard(
|
||||||
val colors = rememberMovieColors()
|
movie: MovieUiModel,
|
||||||
Column(
|
modifier: Modifier = Modifier,
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.background(colors.surfaceAlt)
|
|
||||||
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(16.dp))
|
|
||||||
.padding(20.dp)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
val colors = rememberMovieColors().toMediaDetailColors()
|
||||||
verticalAlignment = Alignment.CenterVertically
|
val context = LocalContext.current
|
||||||
) {
|
val playAction = remember(movie.id) {
|
||||||
Icon(
|
{
|
||||||
imageVector = Icons.Outlined.Tune,
|
val intent = Intent(context, PlayerActivity::class.java)
|
||||||
contentDescription = null,
|
intent.putExtra("MEDIA_ID", movie.id.toString())
|
||||||
tint = colors.primary
|
context.startActivity(intent)
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = "Playback Settings",
|
|
||||||
color = colors.textMuted,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
letterSpacing = 2.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
||||||
SettingDropdown(
|
|
||||||
label = "Audio Track",
|
|
||||||
icon = Icons.Outlined.VolumeUp,
|
|
||||||
value = movie.audioTrack
|
|
||||||
)
|
|
||||||
SettingDropdown(
|
|
||||||
label = "Subtitles",
|
|
||||||
icon = Icons.Outlined.ClosedCaption,
|
|
||||||
value = movie.subtitles
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
BoxWithConstraints(
|
||||||
private fun SettingDropdown(
|
|
||||||
label: String,
|
|
||||||
icon: ImageVector,
|
|
||||||
value: String
|
|
||||||
) {
|
|
||||||
val colors = rememberMovieColors()
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
color = colors.textMutedStrong,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
modifier = Modifier.padding(start = 4.dp, bottom = 6.dp)
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(48.dp)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(colors.surface)
|
|
||||||
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Icon(imageVector = icon, contentDescription = null, tint = colors.textMutedStrong)
|
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
|
||||||
Text(text = value, color = colors.textPrimary, fontSize = 14.sp)
|
|
||||||
}
|
|
||||||
Icon(imageVector = Icons.Outlined.ExpandMore, contentDescription = null, tint = colors.textMutedStrong)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ActionButtons() {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
ActionButton(
|
|
||||||
text = "Watchlist",
|
|
||||||
icon = Icons.Outlined.Add,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
ActionButton(
|
|
||||||
text = "Download",
|
|
||||||
icon = Icons.Outlined.Download,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ActionButton(
|
|
||||||
text: String,
|
|
||||||
icon: ImageVector,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val colors = rememberMovieColors()
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.height(48.dp)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(colors.surfaceAlt.copy(alpha = 0.6f))
|
|
||||||
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
|
|
||||||
.clickable { },
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Icon(imageVector = icon, contentDescription = null, tint = colors.textPrimary)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(text = text, color = colors.textPrimary, fontSize = 14.sp, fontWeight = FontWeight.Bold)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CastRow(cast: List<CastMember>) {
|
|
||||||
val colors = rememberMovieColors()
|
|
||||||
LazyRow(
|
|
||||||
contentPadding = PaddingValues(horizontal = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
items(cast) { member ->
|
|
||||||
Column(modifier = Modifier.width(96.dp)) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.aspectRatio(4f / 5f)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(colors.surfaceAlt)
|
|
||||||
) {
|
|
||||||
if (member.imageUrl == null) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(colors.surfaceAlt.copy(alpha = 0.6f)),
|
.background(colors.background)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
val isWide = maxWidth >= 900.dp
|
||||||
imageVector = Icons.Outlined.Person,
|
val contentPadding = if (isWide) 32.dp else 20.dp
|
||||||
contentDescription = null,
|
|
||||||
tint = colors.textMutedStrong
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
if (isWide) {
|
||||||
|
Row(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MovieHero(
|
||||||
|
movie = movie,
|
||||||
|
height = 300.dp,
|
||||||
|
isWide = true,
|
||||||
|
onPlayClick = playAction,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(0.5f)
|
||||||
|
)
|
||||||
|
MovieDetails(
|
||||||
|
movie = movie,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(0.5f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(
|
||||||
|
start = contentPadding,
|
||||||
|
end = contentPadding,
|
||||||
|
top = 96.dp,
|
||||||
|
bottom = 32.dp
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
AsyncImage(
|
Column(
|
||||||
model = member.imageUrl,
|
modifier = Modifier
|
||||||
contentDescription = null,
|
.fillMaxSize()
|
||||||
modifier = Modifier.fillMaxSize(),
|
.verticalScroll(rememberScrollState())
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
Text(
|
|
||||||
text = member.name,
|
|
||||||
color = colors.textPrimary,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = member.role,
|
|
||||||
color = colors.textMutedStrong,
|
|
||||||
fontSize = 10.sp,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PlayButton(
|
|
||||||
size: Dp,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
viewModel: MovieScreenViewModel = hiltViewModel()
|
|
||||||
) {
|
) {
|
||||||
val colors = rememberMovieColors()
|
MovieHero(
|
||||||
val context = LocalContext.current
|
movie = movie,
|
||||||
val movieId = viewModel.movie.collectAsState()
|
height = 400.dp,
|
||||||
|
isWide = false,
|
||||||
Box(
|
onPlayClick = playAction,
|
||||||
modifier = modifier
|
modifier = Modifier.fillMaxWidth()
|
||||||
.size(size)
|
)
|
||||||
.shadow(24.dp, CircleShape)
|
MovieDetails(
|
||||||
.clip(CircleShape)
|
movie = movie,
|
||||||
.background(colors.primary)
|
modifier = Modifier
|
||||||
.clickable {
|
.fillMaxWidth()
|
||||||
val intent = Intent(context, PlayerActivity::class.java)
|
.padding(horizontal = contentPadding)
|
||||||
intent.putExtra("MEDIA_ID", movieId.value!!.id.toString())
|
.offset(y = (-48).dp)
|
||||||
context.startActivity(intent)
|
.padding(bottom = 96.dp)
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.PlayArrow,
|
|
||||||
contentDescription = "Play",
|
|
||||||
tint = colors.onPrimary,
|
|
||||||
modifier = Modifier.size(42.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
MovieTopBar(
|
||||||
internal fun FloatingPlayButton(modifier: Modifier = Modifier) {
|
modifier = Modifier
|
||||||
val colors = rememberMovieColors()
|
.fillMaxWidth()
|
||||||
Box(
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
modifier = modifier
|
)
|
||||||
.size(56.dp)
|
|
||||||
.shadow(20.dp, CircleShape)
|
if (!isWide) {
|
||||||
.clip(CircleShape)
|
MediaFloatingPlayButton(
|
||||||
.background(colors.primary),
|
colors = colors,
|
||||||
contentAlignment = Alignment.Center
|
onClick = playAction,
|
||||||
) {
|
modifier = Modifier
|
||||||
Icon(
|
.align(Alignment.BottomEnd)
|
||||||
imageVector = Icons.Filled.PlayArrow,
|
.padding(20.dp)
|
||||||
contentDescription = "Play",
|
|
||||||
tint = colors.onPrimary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CastMember.toMediaCastMember() = MediaCastMember(
|
||||||
|
name = name,
|
||||||
|
role = role,
|
||||||
|
imageUrl = imageUrl
|
||||||
|
)
|
||||||
|
|||||||
@@ -19,20 +19,14 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material.icons.outlined.Add
|
|
||||||
import androidx.compose.material.icons.outlined.ArrowBack
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.Cast
|
import androidx.compose.material.icons.outlined.Cast
|
||||||
import androidx.compose.material.icons.outlined.Download
|
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.Person
|
|
||||||
import androidx.compose.material.icons.outlined.PlayCircle
|
import androidx.compose.material.icons.outlined.PlayCircle
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -40,10 +34,7 @@ import androidx.compose.runtime.Composable
|
|||||||
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.shadow
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
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
|
||||||
@@ -52,98 +43,68 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import hu.bbara.purefin.common.ui.MediaActionButtons
|
||||||
|
import hu.bbara.purefin.common.ui.MediaCastMember
|
||||||
|
import hu.bbara.purefin.common.ui.MediaCastRow
|
||||||
|
import hu.bbara.purefin.common.ui.MediaGhostIconButton
|
||||||
|
import hu.bbara.purefin.common.ui.MediaHero
|
||||||
|
import hu.bbara.purefin.common.ui.MediaMetaChip
|
||||||
|
import hu.bbara.purefin.common.ui.toMediaDetailColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SeriesTopBar(
|
internal fun SeriesTopBar(
|
||||||
viewModel: SeriesViewModel = hiltViewModel(),
|
viewModel: SeriesViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val colors = rememberSeriesColors().toMediaDetailColors()
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
GhostIconButton(
|
MediaGhostIconButton(
|
||||||
|
colors = colors,
|
||||||
onClick = { viewModel.onBack() },
|
onClick = { viewModel.onBack() },
|
||||||
icon = Icons.Outlined.ArrowBack,
|
icon = Icons.Outlined.ArrowBack,
|
||||||
contentDescription = "Back")
|
contentDescription = "Back")
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast")
|
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
||||||
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More")
|
MediaGhostIconButton(colors = colors, icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun GhostIconButton(
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
icon: ImageVector,
|
|
||||||
contentDescription: String,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val colors = rememberSeriesColors()
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(colors.background.copy(alpha = 0.4f))
|
|
||||||
.clickable { onClick() },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = contentDescription,
|
|
||||||
tint = colors.textPrimary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SeriesHero(
|
internal fun SeriesHero(
|
||||||
imageUrl: String,
|
imageUrl: String,
|
||||||
height: Dp,
|
height: Dp,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val colors = rememberSeriesColors()
|
val colors = rememberSeriesColors().toMediaDetailColors()
|
||||||
Box(
|
MediaHero(
|
||||||
modifier = modifier
|
imageUrl = imageUrl,
|
||||||
.height(height)
|
colors = colors,
|
||||||
.background(colors.background)
|
height = height,
|
||||||
) {
|
isWide = false,
|
||||||
AsyncImage(
|
modifier = modifier,
|
||||||
model = imageUrl,
|
showPlayButton = false,
|
||||||
contentDescription = null,
|
horizontalGradientOnWide = false
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
)
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.background(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
colors.background.copy(alpha = 0.4f),
|
|
||||||
colors.background
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SeriesMetaChips(series: SeriesUiModel) {
|
internal fun SeriesMetaChips(series: SeriesUiModel) {
|
||||||
val colors = rememberSeriesColors()
|
val colors = rememberSeriesColors().toMediaDetailColors()
|
||||||
FlowRow(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
MetaChip(text = series.year)
|
MediaMetaChip(colors = colors, text = series.year)
|
||||||
MetaChip(text = series.rating)
|
MediaMetaChip(colors = colors, text = series.rating)
|
||||||
MetaChip(text = series.seasons)
|
MediaMetaChip(colors = colors, text = series.seasons)
|
||||||
MetaChip(
|
MediaMetaChip(
|
||||||
|
colors = colors,
|
||||||
text = series.format,
|
text = series.format,
|
||||||
background = colors.primary.copy(alpha = 0.2f),
|
background = colors.primary.copy(alpha = 0.2f),
|
||||||
border = colors.primary.copy(alpha = 0.3f),
|
border = colors.primary.copy(alpha = 0.3f),
|
||||||
@@ -152,76 +113,15 @@ internal fun SeriesMetaChips(series: SeriesUiModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MetaChip(
|
|
||||||
text: String,
|
|
||||||
background: Color? = null,
|
|
||||||
border: Color? = null,
|
|
||||||
textColor: Color? = null
|
|
||||||
) {
|
|
||||||
val colors = rememberSeriesColors()
|
|
||||||
val resolvedBackground = background ?: colors.surfaceAlt
|
|
||||||
val resolvedBorder = border ?: colors.surfaceBorder
|
|
||||||
val resolvedTextColor = textColor ?: colors.textSecondary
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(28.dp)
|
|
||||||
.wrapContentHeight(Alignment.CenterVertically)
|
|
||||||
.clip(RoundedCornerShape(6.dp))
|
|
||||||
.background(resolvedBackground)
|
|
||||||
.border(width = 1.dp, color = resolvedBorder, shape = RoundedCornerShape(6.dp))
|
|
||||||
.padding(horizontal = 12.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
color = resolvedTextColor,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SeriesActionButtons(modifier: Modifier = Modifier) {
|
internal fun SeriesActionButtons(modifier: Modifier = Modifier) {
|
||||||
Row(
|
val colors = rememberSeriesColors().toMediaDetailColors()
|
||||||
modifier = modifier.fillMaxWidth(),
|
MediaActionButtons(
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
colors = colors,
|
||||||
) {
|
modifier = modifier,
|
||||||
ActionButton(
|
height = 44.dp,
|
||||||
text = "Watchlist",
|
textSize = 13.sp
|
||||||
icon = Icons.Outlined.Add,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
ActionButton(
|
|
||||||
text = "Download",
|
|
||||||
icon = Icons.Outlined.Download,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ActionButton(
|
|
||||||
text: String,
|
|
||||||
icon: ImageVector,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val colors = rememberSeriesColors()
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.height(44.dp)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(colors.surfaceAlt.copy(alpha = 0.6f))
|
|
||||||
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
|
|
||||||
.clickable { },
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Icon(imageVector = icon, contentDescription = null, tint = colors.textPrimary, modifier = Modifier.size(18.dp))
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(text = text, color = colors.textPrimary, fontSize = 13.sp, fontWeight = FontWeight.Bold)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -360,88 +260,19 @@ private fun EpisodeCard(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CastRow(cast: List<SeriesCastMemberUiModel>, modifier: Modifier = Modifier) {
|
internal fun CastRow(cast: List<SeriesCastMemberUiModel>, modifier: Modifier = Modifier) {
|
||||||
LazyRow(
|
val colors = rememberSeriesColors().toMediaDetailColors()
|
||||||
|
MediaCastRow(
|
||||||
|
colors = colors,
|
||||||
|
cast = cast.map { it.toMediaCastMember() },
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp),
|
cardWidth = 84.dp,
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
nameSize = 11.sp,
|
||||||
) {
|
roleSize = 10.sp
|
||||||
items(cast) { member ->
|
)
|
||||||
CastCard(member = member)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
private fun SeriesCastMemberUiModel.toMediaCastMember() = MediaCastMember(
|
||||||
private fun CastCard(member: SeriesCastMemberUiModel) {
|
name = name,
|
||||||
val colors = rememberSeriesColors()
|
role = role,
|
||||||
Column(
|
imageUrl = imageUrl
|
||||||
modifier = Modifier.width(84.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.aspectRatio(4f / 5f)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(colors.surfaceAlt)
|
|
||||||
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
|
|
||||||
) {
|
|
||||||
if (member.imageUrl == null) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(colors.surfaceAlt.copy(alpha = 0.6f)),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Person,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = colors.textMutedStrong
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AsyncImage(
|
|
||||||
model = member.imageUrl,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = member.name,
|
|
||||||
color = colors.textPrimary,
|
|
||||||
fontSize = 11.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = member.role,
|
|
||||||
color = colors.textMutedStrong,
|
|
||||||
fontSize = 10.sp,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PlayButton(size: Dp, modifier: Modifier = Modifier) {
|
|
||||||
val colors = rememberSeriesColors()
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(size)
|
|
||||||
.shadow(24.dp, CircleShape)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(colors.primary)
|
|
||||||
.clickable { },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.PlayArrow,
|
|
||||||
contentDescription = "Play",
|
|
||||||
tint = colors.onPrimary,
|
|
||||||
modifier = Modifier.size(36.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,491 @@
|
|||||||
|
package hu.bbara.purefin.common.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
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.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.ClosedCaption
|
||||||
|
import androidx.compose.material.icons.outlined.Download
|
||||||
|
import androidx.compose.material.icons.outlined.ExpandMore
|
||||||
|
import androidx.compose.material.icons.outlined.Person
|
||||||
|
import androidx.compose.material.icons.outlined.Tune
|
||||||
|
import androidx.compose.material.icons.outlined.VolumeUp
|
||||||
|
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.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
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 coil3.compose.AsyncImage
|
||||||
|
import hu.bbara.purefin.app.content.episode.EpisodeColors
|
||||||
|
import hu.bbara.purefin.app.content.movie.MovieColors
|
||||||
|
import hu.bbara.purefin.app.content.series.SeriesColors
|
||||||
|
|
||||||
|
data class MediaDetailColors(
|
||||||
|
val primary: Color,
|
||||||
|
val onPrimary: Color,
|
||||||
|
val background: Color,
|
||||||
|
val surface: Color,
|
||||||
|
val surfaceAlt: Color,
|
||||||
|
val surfaceBorder: Color,
|
||||||
|
val textPrimary: Color,
|
||||||
|
val textSecondary: Color,
|
||||||
|
val textMuted: Color,
|
||||||
|
val textMutedStrong: Color
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun MovieColors.toMediaDetailColors() = MediaDetailColors(
|
||||||
|
primary = primary,
|
||||||
|
onPrimary = onPrimary,
|
||||||
|
background = background,
|
||||||
|
surface = surface,
|
||||||
|
surfaceAlt = surfaceAlt,
|
||||||
|
surfaceBorder = surfaceBorder,
|
||||||
|
textPrimary = textPrimary,
|
||||||
|
textSecondary = textSecondary,
|
||||||
|
textMuted = textMuted,
|
||||||
|
textMutedStrong = textMutedStrong
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun EpisodeColors.toMediaDetailColors() = MediaDetailColors(
|
||||||
|
primary = primary,
|
||||||
|
onPrimary = onPrimary,
|
||||||
|
background = background,
|
||||||
|
surface = surface,
|
||||||
|
surfaceAlt = surfaceAlt,
|
||||||
|
surfaceBorder = surfaceBorder,
|
||||||
|
textPrimary = textPrimary,
|
||||||
|
textSecondary = textSecondary,
|
||||||
|
textMuted = textMuted,
|
||||||
|
textMutedStrong = textMutedStrong
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun SeriesColors.toMediaDetailColors() = MediaDetailColors(
|
||||||
|
primary = primary,
|
||||||
|
onPrimary = onPrimary,
|
||||||
|
background = background,
|
||||||
|
surface = surface,
|
||||||
|
surfaceAlt = surfaceAlt,
|
||||||
|
surfaceBorder = surfaceBorder,
|
||||||
|
textPrimary = textPrimary,
|
||||||
|
textSecondary = textSecondary,
|
||||||
|
textMuted = textMuted,
|
||||||
|
textMutedStrong = textMutedStrong
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaGhostIconButton(
|
||||||
|
colors: MediaDetailColors,
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(colors.background.copy(alpha = 0.4f))
|
||||||
|
.clickable { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = colors.textPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaHero(
|
||||||
|
imageUrl: String,
|
||||||
|
colors: MediaDetailColors,
|
||||||
|
height: Dp,
|
||||||
|
isWide: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
showPlayButton: Boolean = false,
|
||||||
|
playButtonSize: Dp = 80.dp,
|
||||||
|
onPlayClick: (() -> Unit)? = null,
|
||||||
|
horizontalGradientOnWide: Boolean = true
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.height(height)
|
||||||
|
.background(colors.background)
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = imageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
colors.background.copy(alpha = 0.4f),
|
||||||
|
colors.background
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (horizontalGradientOnWide && isWide) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(
|
||||||
|
Brush.horizontalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
colors.background.copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showPlayButton && onPlayClick != null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
MediaPlayButton(
|
||||||
|
colors = colors,
|
||||||
|
size = playButtonSize,
|
||||||
|
onClick = onPlayClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaMetaChip(
|
||||||
|
colors: MediaDetailColors,
|
||||||
|
text: String,
|
||||||
|
background: Color = colors.surfaceAlt,
|
||||||
|
border: Color = Color.Transparent,
|
||||||
|
textColor: Color = colors.textSecondary,
|
||||||
|
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 MediaPlaybackSettings(
|
||||||
|
colors: MediaDetailColors,
|
||||||
|
audioTrack: String,
|
||||||
|
subtitles: String,
|
||||||
|
headerIcon: ImageVector = Icons.Outlined.Tune,
|
||||||
|
audioIcon: ImageVector = Icons.Outlined.VolumeUp,
|
||||||
|
subtitleIcon: ImageVector = Icons.Outlined.ClosedCaption,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(colors.surfaceAlt)
|
||||||
|
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(16.dp))
|
||||||
|
.padding(20.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = headerIcon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = colors.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Playback Settings",
|
||||||
|
color = colors.textMuted,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
letterSpacing = 2.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
MediaSettingDropdown(
|
||||||
|
colors = colors,
|
||||||
|
label = "Audio Track",
|
||||||
|
value = audioTrack,
|
||||||
|
icon = audioIcon
|
||||||
|
)
|
||||||
|
MediaSettingDropdown(
|
||||||
|
colors = colors,
|
||||||
|
label = "Subtitles",
|
||||||
|
value = subtitles,
|
||||||
|
icon = subtitleIcon
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MediaSettingDropdown(
|
||||||
|
colors: MediaDetailColors,
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
icon: ImageVector? = null
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = colors.textMutedStrong,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(start = 4.dp, bottom = 6.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(colors.surface)
|
||||||
|
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
if (icon != null) {
|
||||||
|
Icon(imageVector = icon, contentDescription = null, tint = colors.textMutedStrong)
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
}
|
||||||
|
Text(text = value, color = colors.textPrimary, fontSize = 14.sp)
|
||||||
|
}
|
||||||
|
Icon(imageVector = Icons.Outlined.ExpandMore, contentDescription = null, tint = colors.textMutedStrong)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaActionButtons(
|
||||||
|
colors: MediaDetailColors,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
height: Dp = 48.dp,
|
||||||
|
textSize: TextUnit = 14.sp,
|
||||||
|
watchlistIcon: ImageVector = Icons.Outlined.Add,
|
||||||
|
downloadIcon: ImageVector = Icons.Outlined.Download
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
MediaActionButton(
|
||||||
|
colors = colors,
|
||||||
|
text = "Watchlist",
|
||||||
|
icon = watchlistIcon,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
height = height,
|
||||||
|
textSize = textSize
|
||||||
|
)
|
||||||
|
MediaActionButton(
|
||||||
|
colors = colors,
|
||||||
|
text = "Download",
|
||||||
|
icon = downloadIcon,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
height = height,
|
||||||
|
textSize = textSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MediaActionButton(
|
||||||
|
colors: MediaDetailColors,
|
||||||
|
text: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
height: Dp,
|
||||||
|
textSize: TextUnit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.height(height)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(colors.surfaceAlt.copy(alpha = 0.6f))
|
||||||
|
.border(1.dp, colors.surfaceBorder, RoundedCornerShape(12.dp))
|
||||||
|
.clickable { },
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(imageVector = icon, contentDescription = null, tint = colors.textPrimary)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = text, color = colors.textPrimary, fontSize = textSize, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MediaCastMember(
|
||||||
|
val name: String,
|
||||||
|
val role: String,
|
||||||
|
val imageUrl: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaCastRow(
|
||||||
|
colors: MediaDetailColors,
|
||||||
|
cast: List<MediaCastMember>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
cardWidth: Dp = 96.dp,
|
||||||
|
nameSize: TextUnit = 12.sp,
|
||||||
|
roleSize: TextUnit = 10.sp
|
||||||
|
) {
|
||||||
|
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(colors.surfaceAlt)
|
||||||
|
) {
|
||||||
|
if (member.imageUrl == null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(colors.surfaceAlt.copy(alpha = 0.6f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = colors.textMutedStrong
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AsyncImage(
|
||||||
|
model = member.imageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
text = member.name,
|
||||||
|
color = colors.textPrimary,
|
||||||
|
fontSize = nameSize,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = member.role,
|
||||||
|
color = colors.textMutedStrong,
|
||||||
|
fontSize = roleSize,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaPlayButton(
|
||||||
|
colors: MediaDetailColors,
|
||||||
|
size: Dp,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.shadow(24.dp, CircleShape)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(colors.primary)
|
||||||
|
.clickable { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
|
contentDescription = "Play",
|
||||||
|
tint = colors.onPrimary,
|
||||||
|
modifier = Modifier.size(42.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MediaFloatingPlayButton(
|
||||||
|
colors: MediaDetailColors,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(56.dp)
|
||||||
|
.shadow(20.dp, CircleShape)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(colors.primary)
|
||||||
|
.clickable { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
|
contentDescription = "Play",
|
||||||
|
tint = colors.onPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,6 @@ fun PosterCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(144.dp)
|
.width(144.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = viewModel.getImageUrl(item.id, ImageType.PRIMARY),
|
model = viewModel.getImageUrl(item.id, ImageType.PRIMARY),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|||||||
Reference in New Issue
Block a user