adjusting addonscreen UI

This commit is contained in:
tapframe 2026-03-30 12:32:01 +05:30
parent 7ea57547a8
commit 930ef3503a
4 changed files with 130 additions and 68 deletions

View file

@ -27,6 +27,7 @@ internal object AddonManifestParser {
name = root.requiredString("name"),
description = root.requiredString("description"),
version = root.requiredString("version"),
logoUrl = root.optionalString("logo")?.resolveAgainstManifest(manifestUrl),
resources = root.resources(defaultTypes, defaultPrefixes),
types = defaultTypes,
idPrefixes = defaultPrefixes,
@ -110,3 +111,25 @@ internal object AddonManifestParser {
private fun JsonObject.int(name: String): Int? =
this[name]?.jsonPrimitive?.contentOrNull?.toIntOrNull()
}
private fun String.resolveAgainstManifest(manifestUrl: String): String =
when {
startsWith("http://") || startsWith("https://") || startsWith("data:") -> this
startsWith("//") -> "https:$this"
else -> {
val manifestBase = manifestUrl.substringBefore("?").substringBeforeLast('/', "")
if (startsWith('/')) {
val origin = manifestBase.substringBefore("/", missingDelimiterValue = manifestBase)
val schemeAndHost = if (origin.contains("://")) {
val scheme = origin.substringBefore("://")
val host = manifestBase.substringAfter("://").substringBefore("/")
"$scheme://$host"
} else {
manifestBase
}
"$schemeAndHost$this"
} else {
"$manifestBase/$this"
}
}
}

View file

@ -5,6 +5,7 @@ data class AddonManifest(
val name: String,
val description: String,
val version: String,
val logoUrl: String? = null,
val resources: List<AddonResource>,
val types: List<String>,
val idPrefixes: List<String> = emptyList(),

View file

@ -17,7 +17,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Extension
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.SwapVert
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -35,9 +34,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.nuvio.app.core.ui.NuvioIconActionButton
import com.nuvio.app.core.ui.NuvioInfoBadge
import com.nuvio.app.core.ui.NuvioInputField
@ -81,15 +82,10 @@ internal fun AddonsSettingsPageContent(
val coroutineScope = rememberCoroutineScope()
var addonUrl by rememberSaveable { mutableStateOf("") }
var formMessage by rememberSaveable { mutableStateOf<String?>(null) }
var sortAscending by rememberSaveable { mutableStateOf(true) }
var installModalState by remember { mutableStateOf<AddonInstallModalState?>(null) }
val sortedAddons = remember(uiState.addons, sortAscending) {
if (sortAscending) {
uiState.addons.sortedBy { it.displayTitle.lowercase() }
} else {
uiState.addons.sortedByDescending { it.displayTitle.lowercase() }
}
val sortedAddons = remember(uiState.addons) {
uiState.addons.sortedBy { it.displayTitle.lowercase() }
}
val overview = remember(sortedAddons) { sortedAddons.toOverview() }
@ -98,22 +94,6 @@ internal fun AddonsSettingsPageContent(
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
SectionHeader("OVERVIEW")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
NuvioIconActionButton(
icon = Icons.Rounded.SwapVert,
contentDescription = if (sortAscending) "Sort descending" else "Sort ascending",
onClick = { sortAscending = !sortAscending },
)
NuvioIconActionButton(
icon = Icons.Rounded.Refresh,
contentDescription = "Refresh all addons",
onClick = { AddonRepository.refreshAll() },
)
}
OverviewCard(overview = overview)
SectionHeader("ADD ADDON")
@ -345,6 +325,7 @@ private fun InstalledAddonCard(
verticalAlignment = Alignment.Top,
) {
AddonIconBadge(
imageUrl = manifest?.logoUrl,
icon = Icons.Rounded.Extension,
tint = if (manifest != null) Color(0xFF71BDE8) else MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -357,12 +338,6 @@ private fun InstalledAddonCard(
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = addon.manifestUrl,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
manifest?.version?.let { version ->
Spacer(modifier = Modifier.height(8.dp))
Text(
@ -462,6 +437,7 @@ private fun InstalledAddonCard(
@Composable
private fun AddonIconBadge(
imageUrl: String?,
icon: ImageVector,
tint: Color,
) {
@ -472,12 +448,21 @@ private fun AddonIconBadge(
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = tint,
modifier = Modifier.size(34.dp),
)
if (!imageUrl.isNullOrBlank()) {
AsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier.matchParentSize(),
contentScale = ContentScale.Crop,
)
} else {
Icon(
imageVector = icon,
contentDescription = null,
tint = tint,
modifier = Modifier.size(34.dp),
)
}
}
}

View file

@ -1,10 +1,17 @@
package com.nuvio.app.features.settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -51,42 +58,19 @@ internal fun LazyListScope.homescreenSettingsContent(
}
}
item {
if (items.isNotEmpty()) {
if (heroEnabled && items.isNotEmpty()) {
var heroSourcesExpanded by remember { mutableStateOf(false) }
SettingsSection(
title = "HERO SOURCES",
isTablet = isTablet,
) {
SettingsGroup(isTablet = isTablet) {
Text(
text = "$selectedHeroSourceCount of ${HomeCatalogSettingsRepository.HERO_SOURCE_SELECTION_LIMIT} selected",
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (items.isNotEmpty()) {
SettingsGroupDivider(isTablet = isTablet)
}
items.forEachIndexed { index, item ->
if (index > 0) {
SettingsGroupDivider(isTablet = isTablet)
}
SettingsSwitchRow(
title = item.displayTitle,
description = if (!item.heroSourceEnabled &&
selectedHeroSourceCount >= HomeCatalogSettingsRepository.HERO_SOURCE_SELECTION_LIMIT
) {
"${item.addonName} • Limit reached (max 2)"
} else {
item.addonName
},
checked = item.heroSourceEnabled,
enabled = item.heroSourceEnabled ||
selectedHeroSourceCount < HomeCatalogSettingsRepository.HERO_SOURCE_SELECTION_LIMIT,
isTablet = isTablet,
onCheckedChange = { HomeCatalogSettingsRepository.setHeroSourceEnabled(item.key, it) },
)
}
}
HeroSourcesDropdown(
isTablet = isTablet,
items = items,
selectedHeroSourceCount = selectedHeroSourceCount,
expanded = heroSourcesExpanded,
onExpandedChange = { heroSourcesExpanded = it },
)
}
}
}
@ -111,6 +95,75 @@ internal fun LazyListScope.homescreenSettingsContent(
}
}
@Composable
private fun HeroSourcesDropdown(
isTablet: Boolean,
items: List<HomeCatalogSettingsItem>,
selectedHeroSourceCount: Int,
expanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
) {
SettingsGroup(isTablet = isTablet) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 14.dp)
.clickable { onExpandedChange(!expanded) },
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = "$selectedHeroSourceCount of ${HomeCatalogSettingsRepository.HERO_SOURCE_SELECTION_LIMIT} selected",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.Medium,
)
Text(
text = items.filter { it.heroSourceEnabled }
.joinToString(separator = ", ") { it.displayTitle }
.ifBlank { "No hero sources selected" },
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Icon(
imageVector = if (expanded) Icons.Rounded.KeyboardArrowUp else Icons.Rounded.KeyboardArrowDown,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
AnimatedVisibility(visible = expanded) {
Column {
SettingsGroupDivider(isTablet = isTablet)
items.forEachIndexed { index, item ->
if (index > 0) {
SettingsGroupDivider(isTablet = isTablet)
}
SettingsSwitchRow(
title = item.displayTitle,
description = if (!item.heroSourceEnabled &&
selectedHeroSourceCount >= HomeCatalogSettingsRepository.HERO_SOURCE_SELECTION_LIMIT
) {
"${item.addonName} • Limit reached (max 2)"
} else {
item.addonName
},
checked = item.heroSourceEnabled,
enabled = item.heroSourceEnabled ||
selectedHeroSourceCount < HomeCatalogSettingsRepository.HERO_SOURCE_SELECTION_LIMIT,
isTablet = isTablet,
onCheckedChange = { HomeCatalogSettingsRepository.setHeroSourceEnabled(item.key, it) },
)
}
}
}
}
}
@Composable
private fun HomescreenSummaryCard(
isTablet: Boolean,