From 01ff49198eb36e31cb6b7b32ae0c44478892d79d Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:12:27 +0530 Subject: [PATCH] feat: Update DetailCastSection and DetailSeriesContent for improved layout; enhance StreamsScreen with animated filter chips --- .../details/components/DetailCastSection.kt | 10 ++- .../details/components/DetailSeriesContent.kt | 4 +- .../app/features/streams/StreamsScreen.kt | 80 ++++++++++--------- 3 files changed, 55 insertions(+), 39 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt index e0bc9790e..352eedb47 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape @@ -64,7 +65,7 @@ private fun CastItem( sizing: CastSectionSizing, ) { Column( - modifier = modifier, + modifier = modifier.width(sizing.itemWidth), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -96,6 +97,7 @@ private fun CastItem( } Text( text = person.name, + modifier = Modifier.fillMaxWidth(), style = MaterialTheme.typography.bodyMedium.copy( fontSize = sizing.nameLabelSize, ), @@ -107,6 +109,7 @@ private fun CastItem( if (!person.role.isNullOrBlank()) { Text( text = person.role, + modifier = Modifier.fillMaxWidth(), style = MaterialTheme.typography.bodySmall.copy( fontSize = sizing.subLabelSize, ), @@ -121,6 +124,7 @@ private fun CastItem( private data class CastSectionSizing( val avatarSize: androidx.compose.ui.unit.Dp, + val itemWidth: androidx.compose.ui.unit.Dp, val avatarGap: androidx.compose.ui.unit.Dp, val nameLabelSize: TextUnit, val subLabelSize: TextUnit, @@ -130,24 +134,28 @@ private fun castSectionSizing(maxWidthDp: Float): CastSectionSizing = when { maxWidthDp >= 1200f -> CastSectionSizing( avatarSize = 100.dp, + itemWidth = 112.dp, avatarGap = 20.dp, nameLabelSize = 16.sp, subLabelSize = 14.sp, ) maxWidthDp >= 840f -> CastSectionSizing( avatarSize = 90.dp, + itemWidth = 102.dp, avatarGap = 18.dp, nameLabelSize = 15.sp, subLabelSize = 13.sp, ) maxWidthDp >= 600f -> CastSectionSizing( avatarSize = 85.dp, + itemWidth = 98.dp, avatarGap = 16.dp, nameLabelSize = 14.sp, subLabelSize = 12.sp, ) else -> CastSectionSizing( avatarSize = 80.dp, + itemWidth = 92.dp, avatarGap = 16.dp, nameLabelSize = 14.sp, subLabelSize = 12.sp, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt index d7cc8fe6d..5edb88b80 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt @@ -250,8 +250,8 @@ private fun EpisodeCard( Box( modifier = Modifier - .align(Alignment.BottomEnd) - .padding(bottom = 8.dp, end = 4.dp) + .align(Alignment.TopStart) + .padding(start = 8.dp, top = 8.dp) .clip(RoundedCornerShape(sizing.badgeRadius)) .background(Color.Black.copy(alpha = 0.85f)) .border( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt index fc8e6598f..8263d854d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt @@ -1,6 +1,9 @@ package com.nuvio.app.features.streams import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background @@ -49,6 +52,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -57,6 +61,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState import com.nuvio.app.core.ui.NuvioBackButton import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage @@ -369,7 +375,7 @@ private fun EpisodeHeroBlock( ) { // Episode label Text( - text = "S${seasonNumber} ยท E${episodeNumber}", + text = "S${seasonNumber} E${episodeNumber}", style = MaterialTheme.typography.labelMedium.copy( fontSize = 14.sp, fontWeight = FontWeight.Bold, @@ -447,14 +453,44 @@ private fun FilterChip( isSelected: Boolean, onClick: () -> Unit, ) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.96f else 1f, + animationSpec = tween(durationMillis = 140), + label = "filter_chip_scale", + ) + val containerColor by animateColorAsState( + targetValue = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f) + }, + animationSpec = tween(durationMillis = 180), + label = "filter_chip_container", + ) + val contentColor by animateColorAsState( + targetValue = if (isSelected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurface + }, + animationSpec = tween(durationMillis = 180), + label = "filter_chip_content", + ) Box( modifier = Modifier + .graphicsLayer { + scaleX = scale + scaleY = scale + } .clip(RoundedCornerShape(16.dp)) - .background( - if (isSelected) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f), + .background(containerColor) + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onClick, ) - .clickable(onClick = onClick) .padding(horizontal = 14.dp, vertical = 8.dp), ) { Text( @@ -464,8 +500,7 @@ private fun FilterChip( fontWeight = if (isSelected) FontWeight.Bold else FontWeight.SemiBold, letterSpacing = 0.1.sp, ), - color = if (isSelected) MaterialTheme.colorScheme.onPrimary - else MaterialTheme.colorScheme.onSurface, + color = contentColor, maxLines = 1, ) } @@ -619,10 +654,10 @@ private fun StreamCard( .fillMaxWidth() .heightIn(min = 68.dp) .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.58f)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.9f)) .border( width = 1.dp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), shape = RoundedCornerShape(12.dp), ) .clickable(enabled = isEnabled, onClick = onClick) @@ -660,39 +695,12 @@ private fun StreamCard( Spacer(modifier = Modifier.height(6.dp)) Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - StreamSourceBadge(stream = stream) StreamFileSizeBadge(stream = stream) } } } } -@Composable -private fun StreamSourceBadge(stream: StreamItem) { - val (label, color) = when { - stream.url != null -> "HTTP" to MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) - stream.infoHash != null -> "TORRENT" to Color(0xFF4CAF50).copy(alpha = 0.9f) - stream.externalUrl != null -> "EXTERNAL" to MaterialTheme.colorScheme.secondary.copy(alpha = 0.8f) - else -> return - } - Box( - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background(color.copy(alpha = 0.15f)) - .padding(horizontal = 8.dp, vertical = 3.dp), - ) { - Text( - text = label, - style = MaterialTheme.typography.labelSmall.copy( - fontSize = 11.sp, - fontWeight = FontWeight.SemiBold, - letterSpacing = 0.2.sp, - ), - color = color, - ) - } -} - @Composable private fun StreamFileSizeBadge(stream: StreamItem) { val bytes = stream.behaviorHints.videoSize ?: return