feat: add DetailAdditionalInfoSection and DetailProductionSection components to enhance metadata display in MetaDetailsScreen

This commit is contained in:
tapframe 2026-03-30 20:32:44 +05:30
parent ca2be5fdb2
commit a4a4f3ced4
4 changed files with 253 additions and 49 deletions

View file

@ -37,10 +37,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.ui.NuvioBackButton
import com.nuvio.app.core.ui.nuvioPlatformExtraBottomPadding
import com.nuvio.app.features.details.components.DetailActionButtons
import com.nuvio.app.features.details.components.DetailAdditionalInfoSection
import com.nuvio.app.features.details.components.DetailCastSection
import com.nuvio.app.features.details.components.DetailFloatingHeader
import com.nuvio.app.features.details.components.DetailHero
import com.nuvio.app.features.details.components.DetailMetaInfo
import com.nuvio.app.features.details.components.DetailProductionSection
import com.nuvio.app.features.details.components.DetailSeriesContent
import com.nuvio.app.features.details.components.EpisodeWatchedActionSheet
import com.nuvio.app.features.library.LibraryRepository
@ -162,6 +164,17 @@ fun MetaDetailsScreen(
}?.overview
}
val hasEpisodes = meta.videos.any { it.season != null || it.episode != null }
val hasProductionSection = remember(meta) {
meta.productionCompanies.isNotEmpty() || meta.networks.isNotEmpty()
}
val hasAdditionalInfoSection = remember(meta) {
meta.status != null ||
meta.releaseInfo != null ||
meta.runtime != null ||
meta.ageRating != null ||
meta.country != null ||
meta.language != null
}
val playButtonLabel = remember(movieProgress, seriesAction, meta.type, hasEpisodes) {
when {
(meta.type == "series" || hasEpisodes) && seriesAction != null ->
@ -259,8 +272,16 @@ fun MetaDetailsScreen(
DetailMetaInfo(meta = meta)
if (hasEpisodes && hasProductionSection) {
DetailProductionSection(meta = meta)
}
DetailCastSection(cast = meta.cast)
if (!hasEpisodes && hasProductionSection) {
DetailProductionSection(meta = meta)
}
DetailSeriesContent(
meta = meta,
progressByVideoId = watchProgressUiState.byVideoId,
@ -298,6 +319,14 @@ fun MetaDetailsScreen(
},
)
if (hasEpisodes && hasAdditionalInfoSection) {
DetailAdditionalInfoSection(meta = meta)
}
if (!hasEpisodes && hasAdditionalInfoSection) {
DetailAdditionalInfoSection(meta = meta)
}
Spacer(modifier = Modifier.height(32.dp + nuvioPlatformExtraBottomPadding))
}
}

View file

@ -0,0 +1,98 @@
package com.nuvio.app.features.details.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.nuvio.app.features.details.MetaDetails
@Composable
fun DetailAdditionalInfoSection(
meta: MetaDetails,
modifier: Modifier = Modifier,
) {
val isSeriesLike = meta.type == "series" || meta.videos.any { it.season != null || it.episode != null }
val title = if (isSeriesLike) "Show Details" else "Movie Details"
val rows = buildList {
meta.status?.let { add("Status" to it) }
meta.releaseInfo?.let { add("Release Info" to it) }
meta.runtime?.let { add("Runtime" to it.uppercase()) }
meta.ageRating?.let { add("Certification" to it) }
meta.country?.let { add("Origin Country" to it) }
meta.language?.let { add("Original Language" to it.uppercase()) }
}
if (rows.isEmpty()) return
DetailSection(
title = title,
modifier = modifier,
) {
rows.forEachIndexed { index, (label, value) ->
DetailInfoRow(
label = label,
value = value,
showDivider = index < rows.lastIndex,
)
}
}
}
@Composable
private fun DetailInfoRow(
label: String,
value: String,
showDivider: Boolean,
) {
androidx.compose.foundation.layout.Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(0.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
fontWeight = FontWeight.SemiBold,
)
Text(
text = value,
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.Medium,
textAlign = TextAlign.End,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
if (showDivider) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.10f)),
)
}
}
}

View file

@ -3,8 +3,6 @@ package com.nuvio.app.features.details.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -29,7 +27,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nuvio.app.features.details.MetaDetails
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DetailMetaInfo(
meta: MetaDetails,
@ -88,22 +85,6 @@ fun DetailMetaInfo(
}
}
val detailChips = buildList {
meta.status?.let { add(it) }
meta.country?.let { add(it) }
meta.language?.let { add(it.uppercase()) }
}
if (detailChips.isNotEmpty()) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
detailChips.forEach { chip ->
DetailChip(label = chip)
}
}
}
if (meta.director.isNotEmpty()) {
MetaLabelValueRow(
label = "Director",
@ -118,20 +99,6 @@ fun DetailMetaInfo(
)
}
if (meta.productionCompanies.isNotEmpty()) {
MetaLabelValueRow(
label = "Production",
value = meta.productionCompanies.joinToString(", ") { it.name },
)
}
if (meta.networks.isNotEmpty()) {
MetaLabelValueRow(
label = "Network",
value = meta.networks.joinToString(", ") { it.name },
)
}
if (!meta.description.isNullOrBlank()) {
var expanded by remember { mutableStateOf(false) }
Column {
@ -175,21 +142,5 @@ private fun MetaLabelValueRow(
}
}
@Composable
private fun DetailChip(label: String) {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
shape = RoundedCornerShape(999.dp),
) {
Text(
text = label,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Medium,
)
}
}
private val ImdbYellow = Color(0xFFF5C518)
private val ImdbBlack = Color(0xFF000000)

View file

@ -0,0 +1,126 @@
package com.nuvio.app.features.details.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.nuvio.app.features.details.MetaCompany
import com.nuvio.app.features.details.MetaDetails
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DetailProductionSection(
meta: MetaDetails,
modifier: Modifier = Modifier,
) {
val isSeriesLike = meta.type == "series" || meta.videos.any { it.season != null || it.episode != null }
val sourceItems = if (isSeriesLike) {
meta.networks.ifEmpty { meta.productionCompanies }
} else {
meta.productionCompanies.ifEmpty { meta.networks }
}
if (sourceItems.isEmpty()) return
val displayItems = if (isSeriesLike) {
sourceItems.take(6)
} else {
val logosOnly = sourceItems.filter { !it.logo.isNullOrBlank() }
(if (logosOnly.isNotEmpty()) logosOnly else sourceItems).take(6)
}
if (displayItems.isEmpty()) return
DetailSection(
title = if (isSeriesLike) "Network" else "Production",
modifier = modifier,
) {
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
val chipHeight = when {
maxWidth >= 1024.dp -> 44.dp
maxWidth >= 720.dp -> 40.dp
else -> 36.dp
}
val logoWidth = when {
maxWidth >= 1024.dp -> 72.dp
maxWidth >= 720.dp -> 68.dp
else -> 64.dp
}
val logoHeight = when {
maxWidth >= 1024.dp -> 26.dp
maxWidth >= 720.dp -> 24.dp
else -> 22.dp
}
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
displayItems.forEach { item ->
ProductionChip(
item = item,
chipHeight = chipHeight,
logoWidth = logoWidth,
logoHeight = logoHeight,
)
}
}
}
}
}
@Composable
private fun ProductionChip(
item: MetaCompany,
chipHeight: androidx.compose.ui.unit.Dp,
logoWidth: androidx.compose.ui.unit.Dp,
logoHeight: androidx.compose.ui.unit.Dp,
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(color = ProductionChipBackground)
.padding(horizontal = 12.dp, vertical = 8.dp)
.height(chipHeight),
contentAlignment = Alignment.Center,
) {
if (!item.logo.isNullOrBlank()) {
AsyncImage(
model = item.logo,
contentDescription = item.name,
modifier = Modifier
.width(logoWidth)
.height(logoHeight),
contentScale = ContentScale.Fit,
)
} else {
Text(
text = item.name,
style = MaterialTheme.typography.labelMedium.copy(
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
),
color = ProductionTextColor,
)
}
}
}
private val ProductionChipBackground = androidx.compose.ui.graphics.Color(0xE6F5F5F5)
private val ProductionTextColor = androidx.compose.ui.graphics.Color(0xFF333333)