mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat: Update MetaDetails and DetailCastSection to use MetaPerson for cast representation
This commit is contained in:
parent
80751fe25b
commit
83cfcccbfe
3 changed files with 143 additions and 18 deletions
|
|
@ -13,7 +13,7 @@ data class MetaDetails(
|
|||
val runtime: String? = null,
|
||||
val genres: List<String> = emptyList(),
|
||||
val director: List<String> = emptyList(),
|
||||
val cast: List<String> = emptyList(),
|
||||
val cast: List<MetaPerson> = emptyList(),
|
||||
val country: String? = null,
|
||||
val awards: String? = null,
|
||||
val language: String? = null,
|
||||
|
|
@ -22,6 +22,12 @@ data class MetaDetails(
|
|||
val videos: List<MetaVideo> = emptyList(),
|
||||
)
|
||||
|
||||
data class MetaPerson(
|
||||
val name: String,
|
||||
val role: String? = null,
|
||||
val photo: String? = null,
|
||||
)
|
||||
|
||||
data class MetaLink(
|
||||
val name: String,
|
||||
val category: String,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.nuvio.app.features.details
|
|||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
|
|
@ -14,6 +15,7 @@ internal object MetaDetailsParser {
|
|||
fun parse(payload: String): MetaDetails {
|
||||
val root = json.parseToJsonElement(payload).jsonObject
|
||||
val meta = root["meta"]?.jsonObject ?: error("Missing 'meta' in response")
|
||||
val links = meta.links()
|
||||
|
||||
return MetaDetails(
|
||||
id = meta.requiredString("id"),
|
||||
|
|
@ -27,13 +29,13 @@ internal object MetaDetailsParser {
|
|||
imdbRating = meta.string("imdbRating"),
|
||||
runtime = meta.string("runtime"),
|
||||
genres = meta.stringList("genres"),
|
||||
director = meta.stringList("director"),
|
||||
cast = meta.stringList("cast"),
|
||||
director = meta.directors(links),
|
||||
cast = meta.cast(links),
|
||||
country = meta.string("country"),
|
||||
awards = meta.string("awards"),
|
||||
language = meta.string("language"),
|
||||
website = meta.string("website"),
|
||||
links = meta.links(),
|
||||
links = links,
|
||||
videos = meta.videos(),
|
||||
)
|
||||
}
|
||||
|
|
@ -50,6 +52,19 @@ internal object MetaDetailsParser {
|
|||
private fun JsonObject.stringList(name: String): List<String> =
|
||||
array(name).mapNotNull { it.jsonPrimitive.contentOrNull?.takeIf(String::isNotBlank) }
|
||||
|
||||
private fun JsonObject.stringListOrCsv(name: String): List<String> {
|
||||
val value = this[name] ?: return emptyList()
|
||||
return when (value) {
|
||||
is JsonArray -> value.mapNotNull { it.jsonPrimitive.contentOrNull?.takeIf(String::isNotBlank) }
|
||||
is JsonPrimitive -> value.contentOrNull
|
||||
?.split(',')
|
||||
?.map(String::trim)
|
||||
?.filter(String::isNotBlank)
|
||||
.orEmpty()
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun JsonObject.links(): List<MetaLink> =
|
||||
array("links").mapNotNull { element ->
|
||||
val link = element as? JsonObject ?: return@mapNotNull null
|
||||
|
|
@ -62,6 +77,94 @@ internal object MetaDetailsParser {
|
|||
private fun JsonObject.int(name: String): Int? =
|
||||
this[name]?.jsonPrimitive?.intOrNull
|
||||
|
||||
private fun JsonObject.directors(links: List<MetaLink>): List<String> {
|
||||
val appExtras = this["app_extras"] as? JsonObject
|
||||
val topLevel = stringListOrCsv("director")
|
||||
val extraDirectors = appExtras.personNameList("directors")
|
||||
val linkDirectors = links.filter { link ->
|
||||
link.category.equals("director", ignoreCase = true) ||
|
||||
link.category.equals("directors", ignoreCase = true)
|
||||
}.map(MetaLink::name)
|
||||
|
||||
return (topLevel + extraDirectors + linkDirectors)
|
||||
.map(String::trim)
|
||||
.filter(String::isNotBlank)
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private fun JsonObject.cast(links: List<MetaLink>): List<MetaPerson> {
|
||||
val appExtras = this["app_extras"] as? JsonObject
|
||||
val appExtraCast = appExtras.people("cast")
|
||||
val topLevelCast = stringListOrCsv("cast").map { name ->
|
||||
MetaPerson(name = name)
|
||||
}
|
||||
val linkedCast = links.filter { link ->
|
||||
link.category.equals("cast", ignoreCase = true) ||
|
||||
link.category.equals("actor", ignoreCase = true) ||
|
||||
link.category.equals("actors", ignoreCase = true)
|
||||
}.map { link ->
|
||||
MetaPerson(name = link.name)
|
||||
}
|
||||
|
||||
return mergePeople(appExtraCast, topLevelCast, linkedCast)
|
||||
}
|
||||
|
||||
private fun JsonObject?.personNameList(name: String): List<String> =
|
||||
people(name).map(MetaPerson::name)
|
||||
|
||||
private fun JsonObject?.people(name: String): List<MetaPerson> {
|
||||
if (this == null) return emptyList()
|
||||
return when (val value = this[name]) {
|
||||
is JsonArray -> value.mapNotNull { element ->
|
||||
when (element) {
|
||||
is JsonObject -> {
|
||||
val personName = element.string("name")?.trim()
|
||||
?.takeIf(String::isNotBlank)
|
||||
?: return@mapNotNull null
|
||||
MetaPerson(
|
||||
name = personName,
|
||||
role = element.string("character")?.trim()?.takeIf(String::isNotBlank),
|
||||
photo = element.string("photo")?.trim()?.takeIf(String::isNotBlank),
|
||||
)
|
||||
}
|
||||
is JsonPrimitive -> element.contentOrNull
|
||||
?.trim()
|
||||
?.takeIf(String::isNotBlank)
|
||||
?.let(::MetaPerson)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
is JsonPrimitive -> value.contentOrNull
|
||||
?.split(',')
|
||||
?.map(String::trim)
|
||||
?.filter(String::isNotBlank)
|
||||
?.map(::MetaPerson)
|
||||
.orEmpty()
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mergePeople(vararg groups: List<MetaPerson>): List<MetaPerson> {
|
||||
val merged = linkedMapOf<String, MetaPerson>()
|
||||
groups.forEach { group ->
|
||||
group.forEach { person ->
|
||||
val normalizedName = person.name.trim()
|
||||
if (normalizedName.isBlank()) return@forEach
|
||||
val key = normalizedName.lowercase()
|
||||
val existing = merged[key]
|
||||
merged[key] = if (existing == null) {
|
||||
person.copy(name = normalizedName)
|
||||
} else {
|
||||
existing.copy(
|
||||
role = existing.role ?: person.role,
|
||||
photo = existing.photo ?: person.photo,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged.values.toList()
|
||||
}
|
||||
|
||||
private fun JsonObject.videos(): List<MetaVideo> =
|
||||
array("videos").mapNotNull { element ->
|
||||
val video = element as? JsonObject ?: return@mapNotNull null
|
||||
|
|
|
|||
|
|
@ -10,21 +10,25 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.layout.ContentScale
|
||||
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.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.features.details.MetaPerson
|
||||
|
||||
@Composable
|
||||
fun DetailCastSection(
|
||||
cast: List<String>,
|
||||
cast: List<MetaPerson>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (cast.isEmpty()) return
|
||||
|
|
@ -39,9 +43,12 @@ fun DetailCastSection(
|
|||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(sizing.avatarGap),
|
||||
) {
|
||||
items(cast) { name ->
|
||||
items(
|
||||
items = cast,
|
||||
key = { it.name },
|
||||
) { person ->
|
||||
CastItem(
|
||||
name = name,
|
||||
person = person,
|
||||
sizing = sizing,
|
||||
)
|
||||
}
|
||||
|
|
@ -52,9 +59,8 @@ fun DetailCastSection(
|
|||
|
||||
@Composable
|
||||
private fun CastItem(
|
||||
name: String,
|
||||
person: MetaPerson,
|
||||
modifier: Modifier = Modifier,
|
||||
subLabel: String? = null,
|
||||
sizing: CastSectionSizing,
|
||||
) {
|
||||
Column(
|
||||
|
|
@ -65,21 +71,31 @@ private fun CastItem(
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.size(sizing.avatarSize)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = CircleShape,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = name.initials(),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
if (person.photo != null) {
|
||||
AsyncImage(
|
||||
model = person.photo,
|
||||
contentDescription = person.name,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = person.name.initials(),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = name,
|
||||
text = person.name,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontSize = sizing.nameLabelSize,
|
||||
),
|
||||
|
|
@ -88,9 +104,9 @@ private fun CastItem(
|
|||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (!subLabel.isNullOrBlank()) {
|
||||
if (!person.role.isNullOrBlank()) {
|
||||
Text(
|
||||
text = subLabel,
|
||||
text = person.role,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
fontSize = sizing.subLabelSize,
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue