mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
feat: addon confiuguration and reordering
This commit is contained in:
parent
40588e0ed6
commit
57fc855180
2 changed files with 127 additions and 41 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue