From a4a4f3ced49cc5f1812b3d010bd17b8bbb3e7836 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:32:44 +0530 Subject: [PATCH] feat: add DetailAdditionalInfoSection and DetailProductionSection components to enhance metadata display in MetaDetailsScreen --- .../app/features/details/MetaDetailsScreen.kt | 29 ++++ .../components/DetailAdditionalInfoSection.kt | 98 ++++++++++++++ .../details/components/DetailMetaInfo.kt | 49 ------- .../components/DetailProductionSection.kt | 126 ++++++++++++++++++ 4 files changed, 253 insertions(+), 49 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailAdditionalInfoSection.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailProductionSection.kt diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt index 140c77a2..d7ac430c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt @@ -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)) } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailAdditionalInfoSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailAdditionalInfoSection.kt new file mode 100644 index 00000000..6476eb39 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailAdditionalInfoSection.kt @@ -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)), + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt index 5ad3bfcc..dcef3d4f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt @@ -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) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailProductionSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailProductionSection.kt new file mode 100644 index 00000000..fe77bd3a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailProductionSection.kt @@ -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)