* feat(#500): tap-to-reveal detail dialog for truncated cards Add shared DetailDialog composable and wire tap-to-open full metadata dialogs across Skills (installed + hub), MCP servers, Plugins, Toolsets, and Webhooks screens. Action buttons stay on the card (sticky); tapping the card body reveals untruncated description + key metadata. Refs: #500 * fix(#500): resolve compile errors from missing runtime imports + dialog scope - add missing androidx.compose.runtime.mutableStateOf/remember imports in InstalledSkillsTab + HubSkillsTab (caused 'Unresolved reference' cascade on compileDebugKotlin) - move PluginsScreen detail dialog inside the composable function scope (was nested in OrphanPluginCard -> showDetail unresolved) - DetailDialog rows type: Pair<String?,String?> -> Pair<String,String?> (key always non-null; fixes type-inference failure) - drop unnecessary safe call on non-null label Verified locally with the Android SDK: ./gradlew :app:compileDebugKotlin -> BUILD SUCCESSFUL ./gradlew :app:testDebugUnitTest -> BUILD SUCCESSFUL Refs: #500 * refactor(#500): extract toDetailRows() mappers + semantic status tones; wire SkillsScreen hub browse - add DetailRows.kt with @Composable extension mappers for Skill/HubSkill/ McpServer/PluginInfo/Toolset/WebhookSubscription -> List<DetailRow> - DetailDialog now renders status rows via LocalHermesStatusColors (review #2): SUCCESS/NEUTRAL/WARNING/ERROR/INFO tones instead of plain onSurfaceVariant - wire SkillsScreen.HubBrowseView/HubSkillCard tap -> DetailDialog (same UX as other screens) — user request - remove JVM-signature-clashing Pair overload; drop unused Color import - add detail_dialog_trust_level string (HubSkill has no version field) - verified: compileDebugKotlin + testDebugUnitTest PASS locally w/ ANDROID_HOME * fix(#500): full hub description in dialog + no name truncation - HubBrowseView (both SkillsScreen + HubSkillsTab): on card tap, fetch previewHubSkill(identifier) to get the FULL SkillContentResponse.content (search snippet was truncated); dialog shows full content once loaded, with a spinner while previewing. Falls back to snippet if no identifier. - SkillsViewModel: add hubPreviewContent/isHubPreviewing/hubPreviewError state + previewHubSkill()/clearHubPreview() (uses existing preview endpoint) - DetailDialog: title now wraps fully (removed maxLines=2 ellipsis cap) so long skill names are never cut off - verified: compileDebugKotlin PASS + ktlint clean (local ANDROID_HOME)
This commit is contained in:
11 files changed
+522
-13
No files matched your search
@@ -0,0 +1,124 @@
|
|||||||
|
package com.m57.hermescontrol.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semantic tone for a detail row's value text.
|
||||||
|
* NONEl uses the default on-surface variant; the rest pull from
|
||||||
|
* [LocalHermesStatusColors] so status feedback stays brand-correct
|
||||||
|
* regardless of the wallpaper-derived Material palette.
|
||||||
|
*/
|
||||||
|
enum class DetailRowTone {
|
||||||
|
NONE,
|
||||||
|
SUCCESS,
|
||||||
|
WARNING,
|
||||||
|
ERROR,
|
||||||
|
INFO,
|
||||||
|
NEUTRAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single label/value row shown in [DetailDialog].
|
||||||
|
*
|
||||||
|
* @param label Non-null resource string (already resolved by the mapper).
|
||||||
|
* @param value Display value; null/blank rows are skipped at render time.
|
||||||
|
* @param tone Semantic tone for the value text (defaults to [DetailRowTone.NONE]).
|
||||||
|
*/
|
||||||
|
data class DetailRow(
|
||||||
|
val label: String,
|
||||||
|
val value: String?,
|
||||||
|
val tone: DetailRowTone = DetailRowTone.NONE,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable tap-to-reveal detail dialog: a title plus a scrollable list of
|
||||||
|
* label/value rows. Null/blank rows are skipped automatically. An optional
|
||||||
|
* [actions] slot renders after the rows (inside the scroll area) so primary
|
||||||
|
* actions stay visible without the user dismissing the dialog.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DetailDialog(
|
||||||
|
title: String,
|
||||||
|
rows: List<DetailRow>,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
actions: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
val statusColors = LocalHermesStatusColors.current
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
HermesScaffold(
|
||||||
|
title = { Text(title, style = MaterialTheme.typography.titleLarge) },
|
||||||
|
navigationIcon = NavIcon.Back(onDismiss),
|
||||||
|
isRefreshing = false,
|
||||||
|
) { padding ->
|
||||||
|
val visibleRows = rows.filter { !it.value.isNullOrBlank() }
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
visibleRows.forEachIndexed { index, (label, value, tone) ->
|
||||||
|
if (index > 0) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value ?: "",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color =
|
||||||
|
when (tone) {
|
||||||
|
DetailRowTone.SUCCESS -> statusColors.success
|
||||||
|
DetailRowTone.WARNING -> statusColors.warning
|
||||||
|
DetailRowTone.ERROR -> statusColors.error
|
||||||
|
DetailRowTone.INFO -> statusColors.info
|
||||||
|
DetailRowTone.NEUTRAL -> statusColors.neutral
|
||||||
|
DetailRowTone.NONE -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actions != null) {
|
||||||
|
if (visibleRows.isNotEmpty()) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
actions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.m57.hermescontrol.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.data.model.HubSkill
|
||||||
|
import com.m57.hermescontrol.data.model.McpServer
|
||||||
|
import com.m57.hermescontrol.data.model.PluginInfo
|
||||||
|
import com.m57.hermescontrol.data.model.Skill
|
||||||
|
import com.m57.hermescontrol.data.model.Toolset
|
||||||
|
import com.m57.hermescontrol.data.model.WebhookSubscription
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mappers that turn a domain model into the rows shown by [DetailDialog].
|
||||||
|
* Keeping these here (instead of inline in each screen) avoids the
|
||||||
|
* showDetail + DetailDialog wiring being copied across six screens.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Skill.toDetailRows(): List<DetailRow> =
|
||||||
|
listOf(
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_category), category),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_source), source),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_description), description),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HubSkill.toDetailRows(): List<DetailRow> =
|
||||||
|
listOf(
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_category), category),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_source), source),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_description), description),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_tags), tags.orEmpty().joinToString(", ")),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_trust_level), trustLevel),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun McpServer.toDetailRows(): List<DetailRow> {
|
||||||
|
val commandSummary =
|
||||||
|
buildString {
|
||||||
|
if (command?.isNotBlank() == true) append(command)
|
||||||
|
if (!args.isNullOrEmpty()) append(" " + args.joinToString(" "))
|
||||||
|
}.trim().ifBlank { null }
|
||||||
|
|
||||||
|
return listOf(
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_transport), transport),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_status), status, toneForStatus(status)),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_url), url),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_command), commandSummary),
|
||||||
|
DetailRow(
|
||||||
|
stringResource(R.string.detail_dialog_env_vars),
|
||||||
|
env?.entries?.joinToString("\n") { "${it.key}=${it.value}" },
|
||||||
|
),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_error), error),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PluginInfo.toDetailRows(): List<DetailRow> =
|
||||||
|
listOf(
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_version), version),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_source), source),
|
||||||
|
DetailRow(
|
||||||
|
stringResource(R.string.detail_dialog_status),
|
||||||
|
runtimeStatus,
|
||||||
|
toneForStatus(runtimeStatus),
|
||||||
|
),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_description), description),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Toolset.toDetailRows(): List<DetailRow> =
|
||||||
|
listOf(
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_label), label),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_description), description),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_tools), tools.orEmpty().joinToString(", ")),
|
||||||
|
DetailRow(
|
||||||
|
stringResource(R.string.detail_dialog_status),
|
||||||
|
if (enabled) {
|
||||||
|
stringResource(R.string.detail_dialog_status_enabled)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.detail_dialog_status_disabled)
|
||||||
|
},
|
||||||
|
if (enabled) DetailRowTone.SUCCESS else DetailRowTone.NEUTRAL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WebhookSubscription.toDetailRows(): List<DetailRow> =
|
||||||
|
listOf(
|
||||||
|
DetailRow(
|
||||||
|
stringResource(R.string.detail_dialog_status),
|
||||||
|
if (enabled == true) {
|
||||||
|
stringResource(R.string.detail_dialog_status_enabled)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.detail_dialog_status_disabled)
|
||||||
|
},
|
||||||
|
if (enabled == true) DetailRowTone.SUCCESS else DetailRowTone.NEUTRAL,
|
||||||
|
),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_deliver), deliver),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_description), description),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_events), events.orEmpty().joinToString(", ")),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_prompt), prompt),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_skills), skills.orEmpty().joinToString(", ")),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_url), url),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_created_at), created_at),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a free-form status string to a semantic tone. Recognises the app's
|
||||||
|
* common status vocabulary (enabled/active/running/ok → success, etc.).
|
||||||
|
*/
|
||||||
|
private fun toneForStatus(status: String?): DetailRowTone {
|
||||||
|
if (status.isNullOrBlank()) return DetailRowTone.NONE
|
||||||
|
return when (status.lowercase()) {
|
||||||
|
"enabled", "active", "running", "ok", "online", "connected" -> DetailRowTone.SUCCESS
|
||||||
|
"disabled", "inactive", "offline", "disconnected" -> DetailRowTone.NEUTRAL
|
||||||
|
"error", "failed", "unreachable" -> DetailRowTone.ERROR
|
||||||
|
"warning", "degraded" -> DetailRowTone.WARNING
|
||||||
|
else -> DetailRowTone.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.m57.hermescontrol.ui.mcp
|
package com.m57.hermescontrol.ui.mcp
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -57,6 +58,7 @@ import com.m57.hermescontrol.R
|
|||||||
import com.m57.hermescontrol.data.model.McpCatalogEntry
|
import com.m57.hermescontrol.data.model.McpCatalogEntry
|
||||||
import com.m57.hermescontrol.data.model.McpServer
|
import com.m57.hermescontrol.data.model.McpServer
|
||||||
import com.m57.hermescontrol.theme.LocalSpacing
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
|
import com.m57.hermescontrol.ui.common.DetailDialog
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -69,6 +71,7 @@ import com.m57.hermescontrol.ui.common.StatusBadgeType
|
|||||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
import com.m57.hermescontrol.ui.common.listContentPadding
|
import com.m57.hermescontrol.ui.common.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
import com.m57.hermescontrol.ui.common.toDetailRows
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun McpServersScreen(
|
fun McpServersScreen(
|
||||||
@@ -79,6 +82,7 @@ fun McpServersScreen(
|
|||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val spacing = LocalSpacing.current
|
val spacing = LocalSpacing.current
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
var showDetail by remember { mutableStateOf<McpServer?>(null) }
|
||||||
|
|
||||||
val filteredServers =
|
val filteredServers =
|
||||||
remember(query, state.servers) {
|
remember(query, state.servers) {
|
||||||
@@ -162,7 +166,13 @@ fun McpServersScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(filteredServers, key = { "server:${it.name}" }) { server ->
|
items(filteredServers, key = { "server:${it.name}" }) { server ->
|
||||||
ServerCard(server = server, state = state, viewModel = viewModel, spacing = spacing)
|
ServerCard(
|
||||||
|
server = server,
|
||||||
|
state = state,
|
||||||
|
viewModel = viewModel,
|
||||||
|
spacing = spacing,
|
||||||
|
onClick = { showDetail = server },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 4. Catalog ──────────────────────────────────────
|
// ── 4. Catalog ──────────────────────────────────────
|
||||||
@@ -178,6 +188,14 @@ fun McpServersScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDetail?.let { server ->
|
||||||
|
DetailDialog(
|
||||||
|
title = server.name,
|
||||||
|
rows = server.toDetailRows(),
|
||||||
|
onDismiss = { showDetail = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sections ──────────────────────────────────────────────────────
|
// ── Sections ──────────────────────────────────────────────────────
|
||||||
@@ -279,11 +297,12 @@ private fun ServerCard(
|
|||||||
state: McpServersUiState,
|
state: McpServersUiState,
|
||||||
viewModel: McpServersViewModel,
|
viewModel: McpServersViewModel,
|
||||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||||
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var showEnv by remember { mutableStateOf(false) }
|
var showEnv by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor =
|
containerColor =
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.m57.hermescontrol.ui.plugins
|
package com.m57.hermescontrol.ui.plugins
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -51,6 +52,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.data.model.PluginInfo
|
import com.m57.hermescontrol.data.model.PluginInfo
|
||||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||||
|
import com.m57.hermescontrol.ui.common.DetailDialog
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -60,6 +62,7 @@ import com.m57.hermescontrol.ui.common.SearchBar
|
|||||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
import com.m57.hermescontrol.ui.common.listContentPadding
|
import com.m57.hermescontrol.ui.common.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
import com.m57.hermescontrol.ui.common.toDetailRows
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -71,6 +74,7 @@ fun PluginsScreen(
|
|||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
var showDetail by remember { mutableStateOf<PluginInfo?>(null) }
|
||||||
|
|
||||||
val filteredPlugins =
|
val filteredPlugins =
|
||||||
remember(query, state.plugins) {
|
remember(query, state.plugins) {
|
||||||
@@ -176,7 +180,12 @@ fun PluginsScreen(
|
|||||||
|
|
||||||
// Plugin list
|
// Plugin list
|
||||||
items(filteredPlugins, key = { it.name }) { plugin ->
|
items(filteredPlugins, key = { it.name }) { plugin ->
|
||||||
PluginCard(plugin = plugin, state = state, viewModel = viewModel)
|
PluginCard(
|
||||||
|
plugin = plugin,
|
||||||
|
state = state,
|
||||||
|
viewModel = viewModel,
|
||||||
|
onClick = { showDetail = plugin },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Orphan dashboard plugins section
|
// Orphan dashboard plugins section
|
||||||
@@ -191,7 +200,7 @@ fun PluginsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
items(state.orphanPlugins, key = { "orphan-${it.name}" }) { plugin ->
|
items(state.orphanPlugins, key = { "orphan-${it.name}" }) { plugin ->
|
||||||
OrphanPluginCard(plugin = plugin)
|
OrphanPluginCard(plugin = plugin, onClick = { showDetail = plugin })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,6 +208,14 @@ fun PluginsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDetail?.let { plugin ->
|
||||||
|
DetailDialog(
|
||||||
|
title = plugin.name,
|
||||||
|
rows = plugin.toDetailRows(),
|
||||||
|
onDismiss = { showDetail = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -422,11 +439,12 @@ private fun PluginCard(
|
|||||||
plugin: PluginInfo,
|
plugin: PluginInfo,
|
||||||
state: PluginsUiState,
|
state: PluginsUiState,
|
||||||
viewModel: PluginsViewModel,
|
viewModel: PluginsViewModel,
|
||||||
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val busy = state.rowBusy == plugin.name
|
val busy = state.rowBusy == plugin.name
|
||||||
val statusColors = LocalHermesStatusColors.current
|
val statusColors = LocalHermesStatusColors.current
|
||||||
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth().clickable(onClick = onClick)) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
// Header row: name + toggle
|
// Header row: name + toggle
|
||||||
Row(
|
Row(
|
||||||
@@ -588,8 +606,11 @@ private fun PluginCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun OrphanPluginCard(plugin: PluginInfo) {
|
private fun OrphanPluginCard(
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
plugin: PluginInfo,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(modifier = Modifier.fillMaxWidth().clickable(onClick = onClick)) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(12.dp),
|
modifier = Modifier.padding(12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.m57.hermescontrol.ui.skills
|
package com.m57.hermescontrol.ui.skills
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -64,6 +65,8 @@ import com.m57.hermescontrol.R
|
|||||||
import com.m57.hermescontrol.data.model.HubSkill
|
import com.m57.hermescontrol.data.model.HubSkill
|
||||||
import com.m57.hermescontrol.data.model.Skill
|
import com.m57.hermescontrol.data.model.Skill
|
||||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||||
|
import com.m57.hermescontrol.ui.common.DetailDialog
|
||||||
|
import com.m57.hermescontrol.ui.common.DetailRow
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.FilterChipRow
|
import com.m57.hermescontrol.ui.common.FilterChipRow
|
||||||
@@ -74,6 +77,7 @@ import com.m57.hermescontrol.ui.common.SearchBar
|
|||||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
import com.m57.hermescontrol.ui.common.listContentPadding
|
import com.m57.hermescontrol.ui.common.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
import com.m57.hermescontrol.ui.common.toDetailRows
|
||||||
|
|
||||||
internal const val CATEGORY_ALL = "All"
|
internal const val CATEGORY_ALL = "All"
|
||||||
|
|
||||||
@@ -156,6 +160,7 @@ fun SkillsScreen(
|
|||||||
onSearch = viewModel::searchHub,
|
onSearch = viewModel::searchHub,
|
||||||
onClearSearch = viewModel::clearHubSearch,
|
onClearSearch = viewModel::clearHubSearch,
|
||||||
onInstall = viewModel::installSkill,
|
onInstall = viewModel::installSkill,
|
||||||
|
onPreviewHubSkill = viewModel::previewHubSkill,
|
||||||
isInstalling = state.isInstalling,
|
isInstalling = state.isInstalling,
|
||||||
installingSkillName = state.installingSkillName,
|
installingSkillName = state.installingSkillName,
|
||||||
)
|
)
|
||||||
@@ -214,6 +219,8 @@ private fun InstalledSkillsView(
|
|||||||
uninstallingSkillName: String?,
|
uninstallingSkillName: String?,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
var showDetail by remember { mutableStateOf<Skill?>(null) }
|
||||||
|
|
||||||
val categories =
|
val categories =
|
||||||
remember(state.skills) {
|
remember(state.skills) {
|
||||||
state.skills
|
state.skills
|
||||||
@@ -347,12 +354,21 @@ private fun InstalledSkillsView(
|
|||||||
null
|
null
|
||||||
},
|
},
|
||||||
isUninstalling = isUninstalling && uninstallingSkillName == skill.name,
|
isUninstalling = isUninstalling && uninstallingSkillName == skill.name,
|
||||||
|
onClick = { showDetail = skill },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDetail?.let { skill ->
|
||||||
|
DetailDialog(
|
||||||
|
title = skill.name,
|
||||||
|
rows = skill.toDetailRows(),
|
||||||
|
onDismiss = { showDetail = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Skill Card ─────────────────────────────────────────────────────────
|
// ── Skill Card ─────────────────────────────────────────────────────────
|
||||||
@@ -364,9 +380,10 @@ private fun SkillCard(
|
|||||||
onAction: () -> Unit,
|
onAction: () -> Unit,
|
||||||
onUninstall: (() -> Unit)?,
|
onUninstall: (() -> Unit)?,
|
||||||
isUninstalling: Boolean,
|
isUninstalling: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).clickable(onClick = onClick),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
@@ -514,9 +531,12 @@ private fun HubBrowseView(
|
|||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
onClearSearch: () -> Unit,
|
onClearSearch: () -> Unit,
|
||||||
onInstall: (String) -> Unit,
|
onInstall: (String) -> Unit,
|
||||||
|
onPreviewHubSkill: (String) -> Unit,
|
||||||
isInstalling: Boolean,
|
isInstalling: Boolean,
|
||||||
installingSkillName: String?,
|
installingSkillName: String?,
|
||||||
) {
|
) {
|
||||||
|
var hubShowDetail by remember { mutableStateOf<HubSkill?>(null) }
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = hubQuery,
|
value = hubQuery,
|
||||||
@@ -597,6 +617,10 @@ private fun HubBrowseView(
|
|||||||
hubSkill = hubSkill,
|
hubSkill = hubSkill,
|
||||||
onInstall = { onInstall(hubSkill.name) },
|
onInstall = { onInstall(hubSkill.name) },
|
||||||
isInstalling = isInstalling && installingSkillName == hubSkill.name,
|
isInstalling = isInstalling && installingSkillName == hubSkill.name,
|
||||||
|
onClick = {
|
||||||
|
hubShowDetail = hubSkill
|
||||||
|
hubSkill.identifier?.let { onPreviewHubSkill(it) }
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -610,6 +634,34 @@ private fun HubBrowseView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hubShowDetail?.let { hubSkill ->
|
||||||
|
val previewReady = state.hubPreviewIdentifier == hubSkill.identifier
|
||||||
|
val fullContent =
|
||||||
|
if (previewReady) {
|
||||||
|
state.hubPreviewContent ?: hubSkill.description
|
||||||
|
} else {
|
||||||
|
hubSkill.description
|
||||||
|
}
|
||||||
|
DetailDialog(
|
||||||
|
title = hubSkill.name,
|
||||||
|
rows =
|
||||||
|
listOf(
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_category), hubSkill.category),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_source), hubSkill.source),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_description), fullContent),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_tags), hubSkill.tags.orEmpty().joinToString(", ")),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_trust_level), hubSkill.trustLevel),
|
||||||
|
),
|
||||||
|
actions =
|
||||||
|
if (previewReady && state.isHubPreviewing) {
|
||||||
|
{ CircularProgressIndicator(modifier = Modifier.padding(top = 8.dp)) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onDismiss = { hubShowDetail = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hub Skill Card ─────────────────────────────────────────────────────
|
// ── Hub Skill Card ─────────────────────────────────────────────────────
|
||||||
@@ -619,9 +671,10 @@ private fun HubSkillCard(
|
|||||||
hubSkill: HubSkill,
|
hubSkill: HubSkill,
|
||||||
onInstall: () -> Unit,
|
onInstall: () -> Unit,
|
||||||
isInstalling: Boolean,
|
isInstalling: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).clickable(onClick = onClick),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ data class SkillsUiState(
|
|||||||
val hubResults: List<HubSkill> = emptyList(),
|
val hubResults: List<HubSkill> = emptyList(),
|
||||||
val isHubSearching: Boolean = false,
|
val isHubSearching: Boolean = false,
|
||||||
val hubSearchError: String? = null,
|
val hubSearchError: String? = null,
|
||||||
|
// Hub detail preview (full description/content via preview endpoint)
|
||||||
|
val hubPreviewIdentifier: String? = null,
|
||||||
|
val hubPreviewContent: String? = null,
|
||||||
|
val isHubPreviewing: Boolean = false,
|
||||||
|
val hubPreviewError: String? = null,
|
||||||
// Hub install
|
// Hub install
|
||||||
val isInstalling: Boolean = false,
|
val isInstalling: Boolean = false,
|
||||||
val installingSkillName: String? = null,
|
val installingSkillName: String? = null,
|
||||||
@@ -153,6 +158,60 @@ class SkillsViewModel(application: Application) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Hub detail preview ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the full skill content via the preview endpoint so the detail
|
||||||
|
* dialog can show the complete description (search results only carry a
|
||||||
|
* truncated snippet).
|
||||||
|
*/
|
||||||
|
fun previewHubSkill(identifier: String) {
|
||||||
|
if (identifier.isBlank()) return
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
hubPreviewIdentifier = identifier,
|
||||||
|
isHubPreviewing = true,
|
||||||
|
hubPreviewError = null,
|
||||||
|
hubPreviewContent = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall { ApiClient.hermesApi.previewHubSkill(identifier = identifier) }
|
||||||
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isHubPreviewing = false,
|
||||||
|
hubPreviewContent = result.data.content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isHubPreviewing = false,
|
||||||
|
hubPreviewError = result.error.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearHubPreview() {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
hubPreviewIdentifier = null,
|
||||||
|
hubPreviewContent = null,
|
||||||
|
isHubPreviewing = false,
|
||||||
|
hubPreviewError = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Hub install ─────────────────────────────────────────────────────────
|
// ── Hub install ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fun installSkill(identifier: String) {
|
fun installSkill(identifier: String) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.m57.hermescontrol.ui.skills.components
|
package com.m57.hermescontrol.ui.skills.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -31,6 +32,8 @@ import androidx.compose.material3.OutlinedTextField
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -41,6 +44,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.data.model.HubSkill
|
import com.m57.hermescontrol.data.model.HubSkill
|
||||||
|
import com.m57.hermescontrol.ui.common.DetailDialog
|
||||||
|
import com.m57.hermescontrol.ui.common.DetailRow
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
@@ -56,9 +61,12 @@ internal fun HubBrowseView(
|
|||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
onClearSearch: () -> Unit,
|
onClearSearch: () -> Unit,
|
||||||
onInstall: (String) -> Unit,
|
onInstall: (String) -> Unit,
|
||||||
|
onPreviewHubSkill: (String) -> Unit,
|
||||||
isInstalling: Boolean,
|
isInstalling: Boolean,
|
||||||
installingSkillName: String?,
|
installingSkillName: String?,
|
||||||
) {
|
) {
|
||||||
|
var showDetail by remember { mutableStateOf<HubSkill?>(null) }
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = hubQuery,
|
value = hubQuery,
|
||||||
@@ -139,6 +147,10 @@ internal fun HubBrowseView(
|
|||||||
hubSkill = hubSkill,
|
hubSkill = hubSkill,
|
||||||
onInstall = { onInstall(hubSkill.name) },
|
onInstall = { onInstall(hubSkill.name) },
|
||||||
isInstalling = isInstalling && installingSkillName == hubSkill.name,
|
isInstalling = isInstalling && installingSkillName == hubSkill.name,
|
||||||
|
onClick = {
|
||||||
|
showDetail = hubSkill
|
||||||
|
hubSkill.identifier?.let { onPreviewHubSkill(it) }
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,6 +164,34 @@ internal fun HubBrowseView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDetail?.let { hubSkill ->
|
||||||
|
val previewReady = state.hubPreviewIdentifier == hubSkill.identifier
|
||||||
|
val fullContent =
|
||||||
|
if (previewReady) {
|
||||||
|
state.hubPreviewContent ?: hubSkill.description
|
||||||
|
} else {
|
||||||
|
hubSkill.description
|
||||||
|
}
|
||||||
|
DetailDialog(
|
||||||
|
title = hubSkill.name,
|
||||||
|
rows =
|
||||||
|
listOf(
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_category), hubSkill.category),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_source), hubSkill.source),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_description), fullContent),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_tags), hubSkill.tags.orEmpty().joinToString(", ")),
|
||||||
|
DetailRow(stringResource(R.string.detail_dialog_trust_level), hubSkill.trustLevel),
|
||||||
|
),
|
||||||
|
actions =
|
||||||
|
if (previewReady && state.isHubPreviewing) {
|
||||||
|
{ CircularProgressIndicator(modifier = Modifier.padding(top = 8.dp)) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onDismiss = { showDetail = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -159,9 +199,10 @@ private fun HubSkillCard(
|
|||||||
hubSkill: HubSkill,
|
hubSkill: HubSkill,
|
||||||
onInstall: () -> Unit,
|
onInstall: () -> Unit,
|
||||||
isInstalling: Boolean,
|
isInstalling: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).clickable(onClick = onClick),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.m57.hermescontrol.ui.skills.components
|
package com.m57.hermescontrol.ui.skills.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -30,6 +31,7 @@ import androidx.compose.material3.Switch
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -44,6 +46,7 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.data.model.Skill
|
import com.m57.hermescontrol.data.model.Skill
|
||||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||||
|
import com.m57.hermescontrol.ui.common.DetailDialog
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.FilterChipRow
|
import com.m57.hermescontrol.ui.common.FilterChipRow
|
||||||
@@ -53,6 +56,7 @@ import com.m57.hermescontrol.ui.common.NavIcon
|
|||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
import com.m57.hermescontrol.ui.common.SearchBar
|
||||||
import com.m57.hermescontrol.ui.common.listContentPadding
|
import com.m57.hermescontrol.ui.common.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
import com.m57.hermescontrol.ui.common.toDetailRows
|
||||||
import com.m57.hermescontrol.ui.skills.CATEGORY_ALL
|
import com.m57.hermescontrol.ui.skills.CATEGORY_ALL
|
||||||
import com.m57.hermescontrol.ui.skills.SkillFilter
|
import com.m57.hermescontrol.ui.skills.SkillFilter
|
||||||
import com.m57.hermescontrol.ui.skills.SkillsUiState
|
import com.m57.hermescontrol.ui.skills.SkillsUiState
|
||||||
@@ -75,6 +79,8 @@ internal fun InstalledSkillsView(
|
|||||||
uninstallingSkillName: String?,
|
uninstallingSkillName: String?,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
var showDetail by remember { mutableStateOf<Skill?>(null) }
|
||||||
|
|
||||||
val categories =
|
val categories =
|
||||||
remember(state.skills) {
|
remember(state.skills) {
|
||||||
state.skills
|
state.skills
|
||||||
@@ -208,12 +214,23 @@ internal fun InstalledSkillsView(
|
|||||||
null
|
null
|
||||||
},
|
},
|
||||||
isUninstalling = isUninstalling && uninstallingSkillName == skill.name,
|
isUninstalling = isUninstalling && uninstallingSkillName == skill.name,
|
||||||
|
onClick = {
|
||||||
|
showDetail = skill
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDetail?.let { skill ->
|
||||||
|
DetailDialog(
|
||||||
|
title = skill.name,
|
||||||
|
rows = skill.toDetailRows(),
|
||||||
|
onDismiss = { showDetail = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -223,9 +240,10 @@ private fun SkillCard(
|
|||||||
onAction: () -> Unit,
|
onAction: () -> Unit,
|
||||||
onUninstall: (() -> Unit)?,
|
onUninstall: (() -> Unit)?,
|
||||||
isUninstalling: Boolean,
|
isUninstalling: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).clickable(onClick = onClick),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.m57.hermescontrol.ui.toolsets
|
package com.m57.hermescontrol.ui.toolsets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -38,6 +39,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.data.model.Toolset
|
||||||
|
import com.m57.hermescontrol.ui.common.DetailDialog
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -47,6 +50,7 @@ import com.m57.hermescontrol.ui.common.SearchBar
|
|||||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
import com.m57.hermescontrol.ui.common.listContentPadding
|
import com.m57.hermescontrol.ui.common.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
import com.m57.hermescontrol.ui.common.toDetailRows
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -58,6 +62,7 @@ fun ToolsetsScreen(
|
|||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
var showDetail by remember { mutableStateOf<Toolset?>(null) }
|
||||||
|
|
||||||
val filteredToolsets =
|
val filteredToolsets =
|
||||||
remember(query, state.toolsets) {
|
remember(query, state.toolsets) {
|
||||||
@@ -136,7 +141,7 @@ fun ToolsetsScreen(
|
|||||||
}
|
}
|
||||||
items(filteredToolsets, key = { it.name }) { toolset ->
|
items(filteredToolsets, key = { it.name }) { toolset ->
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().clickable(onClick = { showDetail = toolset }),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor =
|
containerColor =
|
||||||
@@ -208,5 +213,13 @@ fun ToolsetsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDetail?.let { toolset ->
|
||||||
|
DetailDialog(
|
||||||
|
title = toolset.label ?: toolset.name,
|
||||||
|
rows = toolset.toDetailRows(),
|
||||||
|
onDismiss = { showDetail = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.m57.hermescontrol.ui.webhooks
|
package com.m57.hermescontrol.ui.webhooks
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
@@ -48,6 +49,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.data.model.WebhookSubscription
|
import com.m57.hermescontrol.data.model.WebhookSubscription
|
||||||
|
import com.m57.hermescontrol.ui.common.DetailDialog
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -59,6 +61,7 @@ import com.m57.hermescontrol.ui.common.StatusBadgeType
|
|||||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
import com.m57.hermescontrol.ui.common.listContentPadding
|
import com.m57.hermescontrol.ui.common.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
import com.m57.hermescontrol.ui.common.toDetailRows
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -70,6 +73,7 @@ fun WebhooksScreen(
|
|||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
var showDetail by remember { mutableStateOf<WebhookSubscription?>(null) }
|
||||||
|
|
||||||
val filteredSubscriptions =
|
val filteredSubscriptions =
|
||||||
remember(query, state.subscriptions) {
|
remember(query, state.subscriptions) {
|
||||||
@@ -356,6 +360,7 @@ fun WebhooksScreen(
|
|||||||
isToggling = state.togglingName == sub.name,
|
isToggling = state.togglingName == sub.name,
|
||||||
onToggle = { enabled -> viewModel.toggleSubscription(sub.name, enabled) },
|
onToggle = { enabled -> viewModel.toggleSubscription(sub.name, enabled) },
|
||||||
onDelete = { viewModel.promptDeleteSubscription(sub) },
|
onDelete = { viewModel.promptDeleteSubscription(sub) },
|
||||||
|
onClick = { showDetail = sub },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,6 +368,14 @@ fun WebhooksScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDetail?.let { sub ->
|
||||||
|
DetailDialog(
|
||||||
|
title = sub.name,
|
||||||
|
rows = sub.toDetailRows(),
|
||||||
|
onDismiss = { showDetail = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -371,11 +384,12 @@ private fun SubscriptionCard(
|
|||||||
isToggling: Boolean,
|
isToggling: Boolean,
|
||||||
onToggle: (Boolean) -> Unit,
|
onToggle: (Boolean) -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val isEnabled = sub.enabled ?: true
|
val isEnabled = sub.enabled ?: true
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor =
|
containerColor =
|
||||||
|
|||||||
@@ -630,6 +630,31 @@
|
|||||||
<string name="plugins_show_in_sidebar">Show in sidebar</string>
|
<string name="plugins_show_in_sidebar">Show in sidebar</string>
|
||||||
<string name="plugins_hide_from_sidebar">Hide from sidebar</string>
|
<string name="plugins_hide_from_sidebar">Hide from sidebar</string>
|
||||||
|
|
||||||
|
<!-- Shared detail dialog (issue #500) -->
|
||||||
|
<string name="detail_dialog_no_info">No additional details available.</string>
|
||||||
|
<string name="detail_dialog_category">Category</string>
|
||||||
|
<string name="detail_dialog_source">Source</string>
|
||||||
|
<string name="detail_dialog_version">Version</string>
|
||||||
|
<string name="detail_dialog_status">Status</string>
|
||||||
|
<string name="detail_dialog_description">Description</string>
|
||||||
|
<string name="detail_dialog_tags">Tags</string>
|
||||||
|
<string name="detail_dialog_transport">Transport</string>
|
||||||
|
<string name="detail_dialog_label">Label</string>
|
||||||
|
<string name="detail_dialog_url">URL</string>
|
||||||
|
<string name="detail_dialog_command">Command</string>
|
||||||
|
<string name="detail_dialog_args">Arguments</string>
|
||||||
|
<string name="detail_dialog_env_vars">Environment Variables</string>
|
||||||
|
<string name="detail_dialog_tools">Tools</string>
|
||||||
|
<string name="detail_dialog_events">Events</string>
|
||||||
|
<string name="detail_dialog_deliver">Deliver to</string>
|
||||||
|
<string name="detail_dialog_prompt">Prompt</string>
|
||||||
|
<string name="detail_dialog_skills">Skills</string>
|
||||||
|
<string name="detail_dialog_created_at">Created</string>
|
||||||
|
<string name="detail_dialog_error">Error</string>
|
||||||
|
<string name="detail_dialog_status_enabled">Enabled</string>
|
||||||
|
<string name="detail_dialog_status_disabled">Disabled</string>
|
||||||
|
<string name="detail_dialog_trust_level">Trust Level</string>
|
||||||
|
|
||||||
<!-- McpServers Screen -->
|
<!-- McpServers Screen -->
|
||||||
<string name="mcp_servers_label_transport">Transport: %1$s</string>
|
<string name="mcp_servers_label_transport">Transport: %1$s</string>
|
||||||
<string name="mcp_servers_label_command">Command:</string>
|
<string name="mcp_servers_label_command">Command:</string>
|
||||||
|
|||||||
Reference in new issue
Block a user