Orphaned duplicate of SkillsScreen's private InstalledSkillsView. Zero references in the repo (no imports, call sites, or tests). Shared helpers (CATEGORY_ALL, SkillFilter, SkillsUiState) live in SkillsScreen.kt / SkillsViewModel.kt, not this file. No behavior change. Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
87442e1a92
commit
e639e33188
1 file changed
-411
@@ -1,411 +0,0 @@
|
|||||||
package com.m57.hermescontrol.ui.skills.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.CloudOff
|
|
||||||
import androidx.compose.material.icons.filled.Edit
|
|
||||||
import androidx.compose.material.icons.filled.Extension
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import com.m57.hermescontrol.R
|
|
||||||
import com.m57.hermescontrol.data.model.Skill
|
|
||||||
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.ErrorState
|
|
||||||
import com.m57.hermescontrol.ui.common.FilterChipRow
|
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
|
||||||
import com.m57.hermescontrol.ui.common.NavIcon
|
|
||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
|
||||||
import com.m57.hermescontrol.ui.common.SkeletonListState
|
|
||||||
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.SkillFilter
|
|
||||||
import com.m57.hermescontrol.ui.skills.SkillsUiState
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun InstalledSkillsView(
|
|
||||||
state: SkillsUiState,
|
|
||||||
query: String,
|
|
||||||
onQueryChange: (String) -> Unit,
|
|
||||||
selectedStatus: SkillFilter,
|
|
||||||
onStatusChange: (SkillFilter) -> Unit,
|
|
||||||
selectedCategory: String?,
|
|
||||||
onCategoryChange: (String) -> Unit,
|
|
||||||
onToggle: (Skill) -> Unit,
|
|
||||||
onEdit: (String) -> Unit,
|
|
||||||
onUninstall: (String) -> Unit,
|
|
||||||
onSetSourceFilter: (String?) -> Unit,
|
|
||||||
sourceFilter: String?,
|
|
||||||
isUninstalling: Boolean,
|
|
||||||
uninstallingSkillName: String?,
|
|
||||||
onRefresh: () -> Unit,
|
|
||||||
) {
|
|
||||||
var showDetail by remember { mutableStateOf<Skill?>(null) }
|
|
||||||
|
|
||||||
val categories =
|
|
||||||
remember(state.skills) {
|
|
||||||
state.skills
|
|
||||||
.mapNotNull { it.category }
|
|
||||||
.distinct()
|
|
||||||
.sorted()
|
|
||||||
}
|
|
||||||
val sources =
|
|
||||||
remember(state.skills) {
|
|
||||||
state.skills
|
|
||||||
.mapNotNull { it.source }
|
|
||||||
.distinct()
|
|
||||||
.sorted()
|
|
||||||
}
|
|
||||||
|
|
||||||
val filteredSkills =
|
|
||||||
remember(state.skills, query, selectedStatus, selectedCategory, sourceFilter) {
|
|
||||||
state.skills.filter { skill ->
|
|
||||||
(
|
|
||||||
query.isBlank() || skill.name.contains(query, ignoreCase = true) ||
|
|
||||||
skill.description?.contains(query, ignoreCase = true) == true
|
|
||||||
) &&
|
|
||||||
(
|
|
||||||
selectedStatus == SkillFilter.ALL_STATUSES ||
|
|
||||||
(selectedStatus == SkillFilter.ENABLED && skill.enabled) ||
|
|
||||||
(selectedStatus == SkillFilter.DISABLED && !skill.enabled)
|
|
||||||
) &&
|
|
||||||
(selectedCategory == CATEGORY_ALL || skill.category == selectedCategory) &&
|
|
||||||
(sourceFilter == null || skill.source == sourceFilter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
|
||||||
SearchBar(
|
|
||||||
query = query,
|
|
||||||
onQueryChange = onQueryChange,
|
|
||||||
placeholder = stringResource(R.string.skills_search_placeholder),
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
|
||||||
)
|
|
||||||
|
|
||||||
FilterChipRow(
|
|
||||||
chips = SkillFilter.entries.toList(),
|
|
||||||
selectedChip = selectedStatus,
|
|
||||||
onChipSelected = onStatusChange,
|
|
||||||
chipLabel = { chip ->
|
|
||||||
Text(
|
|
||||||
text =
|
|
||||||
if (chip == SkillFilter.ALL_STATUSES) {
|
|
||||||
stringResource(R.string.skills_category_all)
|
|
||||||
} else {
|
|
||||||
stringResource(chip.labelRes)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (categories.isNotEmpty() || sources.isNotEmpty()) {
|
|
||||||
FilterChipRow(
|
|
||||||
chips = listOf(stringResource(R.string.skills_category_all)) + categories,
|
|
||||||
selectedChip = selectedCategory,
|
|
||||||
onChipSelected = onCategoryChange,
|
|
||||||
)
|
|
||||||
if (sources.isNotEmpty()) {
|
|
||||||
FilterChipRow(
|
|
||||||
chips = listOf<String?>(null) + sources,
|
|
||||||
selectedChip = sourceFilter,
|
|
||||||
onChipSelected = onSetSourceFilter,
|
|
||||||
chipLabel = { source ->
|
|
||||||
Text(
|
|
||||||
text = source ?: stringResource(R.string.skills_source_all),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
|
||||||
state.isLoading -> {
|
|
||||||
SkeletonListState()
|
|
||||||
}
|
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
|
||||||
ErrorState(
|
|
||||||
message = state.errorMessage,
|
|
||||||
onRetry = onRefresh,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredSkills.isEmpty() -> {
|
|
||||||
EmptyState(
|
|
||||||
icon = Icons.Filled.Extension,
|
|
||||||
title = stringResource(R.string.skills_empty_message),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
|
|
||||||
verticalArrangement = listItemSpacing,
|
|
||||||
) {
|
|
||||||
itemsIndexed(
|
|
||||||
filteredSkills,
|
|
||||||
key = { index, skill -> "${skill.name}:${skill.source ?: "unknown"}:$index" },
|
|
||||||
) { _, skill ->
|
|
||||||
SkillCard(
|
|
||||||
skill = skill,
|
|
||||||
onToggle = { onToggle(skill) },
|
|
||||||
onAction = { onEdit(skill.name) },
|
|
||||||
onUninstall =
|
|
||||||
if (skill.source == "hub") {
|
|
||||||
{ onUninstall(skill.name) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
isUninstalling = isUninstalling && uninstallingSkillName == skill.name,
|
|
||||||
onClick = {
|
|
||||||
showDetail = skill
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showDetail?.let { skill ->
|
|
||||||
DetailDialog(
|
|
||||||
title = skill.name,
|
|
||||||
rows = skill.toDetailRows(),
|
|
||||||
onDismiss = { showDetail = null },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SkillCard(
|
|
||||||
skill: Skill,
|
|
||||||
onToggle: () -> Unit,
|
|
||||||
onAction: () -> Unit,
|
|
||||||
onUninstall: (() -> Unit)?,
|
|
||||||
isUninstalling: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
|
|
||||||
colors =
|
|
||||||
CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
),
|
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp, vertical = 8.dp),
|
|
||||||
) {
|
|
||||||
// ── Top row: name + source badge + toggle ──
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = skill.name,
|
|
||||||
style =
|
|
||||||
MaterialTheme.typography.titleMedium.copy(
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
skill.source?.let { source ->
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
SourceBadge(source = source)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Switch(
|
|
||||||
checked = skill.enabled,
|
|
||||||
onCheckedChange = { onToggle() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Category ──
|
|
||||||
skill.category?.let { cat ->
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
|
||||||
Text(
|
|
||||||
text = cat,
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Description ──
|
|
||||||
skill.description?.let { desc ->
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = desc,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Action buttons row ──
|
|
||||||
if (onUninstall != null) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
IconButton(onClick = onAction) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Edit,
|
|
||||||
contentDescription = stringResource(R.string.content_desc_edit_skill),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = onUninstall,
|
|
||||||
enabled = !isUninstalling,
|
|
||||||
) {
|
|
||||||
if (isUninstalling) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(18.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val statusColors = LocalHermesStatusColors.current
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.CloudOff,
|
|
||||||
contentDescription = stringResource(R.string.content_desc_uninstall_skill),
|
|
||||||
tint = statusColors.error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
) {
|
|
||||||
IconButton(onClick = onAction) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Edit,
|
|
||||||
contentDescription = stringResource(R.string.content_desc_edit_skill),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SkillPreviewDialog(
|
|
||||||
skillName: String,
|
|
||||||
isLoading: Boolean,
|
|
||||||
content: String?,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp),
|
|
||||||
colors =
|
|
||||||
CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
HermesScaffold(
|
|
||||||
title = { Text(stringResource(R.string.skills_preview_title, skillName)) },
|
|
||||||
navigationIcon = NavIcon.Back(onBack = onDismiss),
|
|
||||||
) { padding ->
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
when {
|
|
||||||
isLoading -> {
|
|
||||||
LoadingState()
|
|
||||||
}
|
|
||||||
|
|
||||||
content != null -> {
|
|
||||||
Column(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = content,
|
|
||||||
style =
|
|
||||||
MaterialTheme.typography.bodyMedium.copy(
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
EmptyState(
|
|
||||||
icon = Icons.Filled.Extension,
|
|
||||||
title = stringResource(R.string.skills_preview_empty),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user