feat: addon confiuguration and reordering

This commit is contained in:
tapframe 2026-04-24 02:24:45 +05:30
parent 40588e0ed6
commit 57fc855180
2 changed files with 127 additions and 41 deletions

View file

@ -250,6 +250,27 @@ object AddonRepository {
pushToServer() pushToServer()
} }
fun moveAddon(fromIndex: Int, toIndex: Int) {
if (isUsingPrimaryAddonsFromSecondaryProfile()) return
_uiState.update { current ->
val addons = current.addons
if (
fromIndex !in addons.indices ||
toIndex !in addons.indices ||
fromIndex == toIndex
) {
return@update current
}
val reordered = addons.toMutableList()
val movingAddon = reordered.removeAt(fromIndex)
reordered.add(toIndex, movingAddon)
current.copy(addons = reordered)
}
persist()
pushToServer()
}
fun refreshAll() { fun refreshAll() {
_uiState.value.addons.distinctBy { it.manifestUrl }.forEach { addon -> _uiState.value.addons.distinctBy { it.manifestUrl }.forEach { addon ->
refreshAddon(addon.manifestUrl) refreshAddon(addon.manifestUrl)

View file

@ -12,12 +12,14 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowDownward
import androidx.compose.material.icons.rounded.ArrowUpward
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Extension import androidx.compose.material.icons.rounded.Extension
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -36,6 +38,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -80,6 +83,7 @@ internal fun AddonsSettingsPageContent(
} }
val uiState by AddonRepository.uiState.collectAsStateWithLifecycle() val uiState by AddonRepository.uiState.collectAsStateWithLifecycle()
val uriHandler = LocalUriHandler.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var addonUrl by rememberSaveable { mutableStateOf("") } var addonUrl by rememberSaveable { mutableStateOf("") }
var formMessage by rememberSaveable { mutableStateOf<String?>(null) } var formMessage by rememberSaveable { mutableStateOf<String?>(null) }
@ -131,10 +135,34 @@ internal fun AddonsSettingsPageContent(
if (uiState.addons.isEmpty()) { if (uiState.addons.isEmpty()) {
EmptyStateCard() EmptyStateCard()
} else { } else {
uiState.addons.forEach { addon -> val lastIndex = uiState.addons.lastIndex
uiState.addons.forEachIndexed { index, addon ->
val manifest = addon.manifest
val behaviorHints = manifest?.behaviorHints
val showConfigureAction = behaviorHints?.configurable == true || behaviorHints?.configurationRequired == true
val configureUrl = addon.manifestUrl.toConfigureUrl()
InstalledAddonCard( InstalledAddonCard(
addon = addon, addon = addon,
onMoveUpClick = if (index > 0) {
{ AddonRepository.moveAddon(index, index - 1) }
} else {
null
},
onMoveDownClick = if (index < lastIndex) {
{ AddonRepository.moveAddon(index, index + 1) }
} else {
null
},
onRefreshClick = { AddonRepository.refreshAddon(addon.manifestUrl) }, onRefreshClick = { AddonRepository.refreshAddon(addon.manifestUrl) },
onConfigureClick = if (showConfigureAction && !configureUrl.isNullOrBlank()) {
{
runCatching {
uriHandler.openUri(configureUrl)
}
}
} else {
null
},
onDeleteClick = { AddonRepository.removeAddon(addon.manifestUrl) }, onDeleteClick = { AddonRepository.removeAddon(addon.manifestUrl) },
) )
} }
@ -307,7 +335,10 @@ private fun EmptyStateCard() {
@Composable @Composable
private fun InstalledAddonCard( private fun InstalledAddonCard(
addon: ManagedAddon, addon: ManagedAddon,
onMoveUpClick: (() -> Unit)?,
onMoveDownClick: (() -> Unit)?,
onRefreshClick: () -> Unit, onRefreshClick: () -> Unit,
onConfigureClick: (() -> Unit)?,
onDeleteClick: () -> Unit, onDeleteClick: () -> Unit,
) { ) {
val manifest = addon.manifest val manifest = addon.manifest
@ -315,54 +346,79 @@ private fun InstalledAddonCard(
NuvioSurfaceCard { NuvioSurfaceCard {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top, verticalAlignment = Alignment.Top,
) { ) {
Row( AddonIconBadge(
modifier = Modifier.weight(1f), imageUrl = manifest?.logoUrl,
verticalAlignment = Alignment.Top, icon = Icons.Rounded.Extension,
) { tint = if (manifest != null) Color(0xFF71BDE8) else MaterialTheme.colorScheme.onSurfaceVariant,
AddonIconBadge( )
imageUrl = manifest?.logoUrl, Spacer(modifier = Modifier.width(16.dp))
icon = Icons.Rounded.Extension, Column(modifier = Modifier.weight(1f)) {
tint = if (manifest != null) Color(0xFF71BDE8) else MaterialTheme.colorScheme.onSurfaceVariant, Text(
text = addon.displayTitle,
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
) )
Spacer(modifier = Modifier.width(16.dp)) manifest?.version?.let { version ->
Column(modifier = Modifier.weight(1f)) { Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = addon.displayTitle, text = "Version $version",
style = MaterialTheme.typography.headlineLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
) )
manifest?.version?.let { version ->
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Version $version",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} }
} }
Row(verticalAlignment = Alignment.CenterVertically) {
NuvioIconActionButton(
icon = Icons.Rounded.Refresh,
contentDescription = "Refresh addon",
tint = MaterialTheme.colorScheme.primary,
onClick = onRefreshClick,
)
NuvioIconActionButton(
icon = Icons.Rounded.Delete,
contentDescription = "Delete addon",
tint = MaterialTheme.colorScheme.error,
onClick = onDeleteClick,
)
}
} }
Spacer(modifier = Modifier.height(18.dp)) Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
verticalAlignment = Alignment.CenterVertically,
) {
onMoveUpClick?.let { onMoveUp ->
NuvioIconActionButton(
icon = Icons.Rounded.ArrowUpward,
contentDescription = "Move addon up",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
onClick = onMoveUp,
)
}
onMoveDownClick?.let { onMoveDown ->
NuvioIconActionButton(
icon = Icons.Rounded.ArrowDownward,
contentDescription = "Move addon down",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
onClick = onMoveDown,
)
}
NuvioIconActionButton(
icon = Icons.Rounded.Refresh,
contentDescription = "Refresh addon",
tint = MaterialTheme.colorScheme.primary,
onClick = onRefreshClick,
)
onConfigureClick?.let { onConfigure ->
NuvioIconActionButton(
icon = Icons.Rounded.Settings,
contentDescription = "Configure addon",
tint = MaterialTheme.colorScheme.tertiary,
onClick = onConfigure,
)
}
NuvioIconActionButton(
icon = Icons.Rounded.Delete,
contentDescription = "Delete addon",
tint = MaterialTheme.colorScheme.error,
onClick = onDeleteClick,
)
}
Spacer(modifier = Modifier.height(12.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outline) HorizontalDivider(color = MaterialTheme.colorScheme.outline)
Spacer(modifier = Modifier.height(18.dp)) Spacer(modifier = Modifier.height(18.dp))
@ -484,3 +540,12 @@ private fun manifestSummary(manifest: AddonManifest): String {
} }
} }
} }
private fun String.toConfigureUrl(): String {
val base = substringBefore("?").trimEnd('/')
return if (base.endsWith("/manifest.json")) {
base.removeSuffix("/manifest.json") + "/configure"
} else {
"$base/configure"
}
}