refactor(skills): extract SkillListFilter and SkillScanPresentation

Separate pure skill filtering and scan presentation helpers.
Wire into SkillsScreen with unit test coverage.
This commit is contained in:
Hy4ri committed 2026-09-11 17:58:41 +03:00
1 parent 6876db0393
commit 9eaf9663f3
5 files changed
+323 -54

No files matched your search

@@ -0,0 +1,39 @@
package com.m57.hermescontrol.ui.skills
import com.m57.hermescontrol.data.model.Skill
object SkillListFilter {
/**
* Filters skills by search query, enabled/disabled status, category, and source.
*/
fun filterSkills(
skills: List<Skill>,
query: String,
selectedStatus: SkillFilter,
selectedCategory: String?,
sourceFilter: String?,
): List<Skill> =
skills.filter { skill ->
val matchesQuery =
query.isBlank() ||
skill.name.contains(query, ignoreCase = true) ||
skill.description?.contains(query, ignoreCase = true) == true
val matchesStatus =
when (selectedStatus) {
SkillFilter.ALL_STATUSES -> true
SkillFilter.ENABLED -> skill.enabled
SkillFilter.DISABLED -> !skill.enabled
}
val matchesCategory =
selectedCategory == null ||
selectedCategory == CATEGORY_ALL ||
skill.category == selectedCategory
val matchesSource = sourceFilter == null || skill.source == sourceFilter
matchesQuery && matchesStatus && matchesCategory && matchesSource
}
fun extractCategories(skills: List<Skill>): List<String> = skills.mapNotNull { it.category }.distinct().sorted()
fun extractSources(skills: List<Skill>): List<String> = skills.mapNotNull { it.source }.distinct().sorted()
}
@@ -0,0 +1,75 @@
package com.m57.hermescontrol.ui.skills
import androidx.compose.ui.graphics.Color
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.HermesStatusColors
enum class ScanVerdictKind {
SAFE,
DANGEROUS,
WARNING,
}
enum class ScanPolicyKind {
ALLOW,
ASK,
BLOCK,
OTHER,
}
object SkillScanPresentation {
fun resolveVerdictKind(verdict: String?): ScanVerdictKind =
when (verdict) {
"safe" -> ScanVerdictKind.SAFE
"dangerous" -> ScanVerdictKind.DANGEROUS
else -> ScanVerdictKind.WARNING
}
fun resolveVerdictColor(
verdict: String?,
statusColors: HermesStatusColors,
): Color =
when (resolveVerdictKind(verdict)) {
ScanVerdictKind.SAFE -> statusColors.success
ScanVerdictKind.DANGEROUS -> statusColors.error
ScanVerdictKind.WARNING -> statusColors.warning
}
fun resolvePolicyKind(policy: String?): ScanPolicyKind =
when (policy) {
"allow" -> ScanPolicyKind.ALLOW
"ask" -> ScanPolicyKind.ASK
"block" -> ScanPolicyKind.BLOCK
else -> ScanPolicyKind.OTHER
}
fun resolvePolicyLabelRes(policy: String?): Int? =
when (policy) {
"allow" -> R.string.skills_hub_scan_policy_allow
"ask" -> R.string.skills_hub_scan_policy_ask
"block" -> R.string.skills_hub_scan_policy_block
else -> null
}
fun resolvePolicyColor(
policy: String?,
statusColors: HermesStatusColors,
): Color =
when (policy) {
"allow" -> statusColors.success
"ask" -> statusColors.warning
"block" -> statusColors.error
else -> statusColors.neutral
}
fun resolveSeverityColor(
severity: String?,
statusColors: HermesStatusColors,
): Color =
when (severity) {
"critical", "high" -> statusColors.error
"medium" -> statusColors.warning
"low" -> statusColors.neutral
else -> statusColors.neutral
}
}
@@ -256,34 +256,22 @@ private fun InstalledSkillsView(
val categories =
remember(state.skills) {
state.skills
.mapNotNull { it.category }
.distinct()
.sorted()
SkillListFilter.extractCategories(state.skills)
}
val sources =
remember(state.skills) {
state.skills
.mapNotNull { it.source }
.distinct()
.sorted()
SkillListFilter.extractSources(state.skills)
}
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)
}
SkillListFilter.filterSkills(
skills = state.skills,
query = query,
selectedStatus = selectedStatus,
selectedCategory = selectedCategory,
sourceFilter = sourceFilter,
)
}
Column(modifier = Modifier.fillMaxSize()) {
@@ -891,26 +879,11 @@ private fun SkillScanSection(
@Composable
private fun ScanResultCard(result: SkillScanResponse) {
val statusColors = LocalHermesStatusColors.current
val verdictColor =
when (result.verdict) {
"safe" -> statusColors.success
"dangerous" -> statusColors.error
else -> statusColors.warning
}
val verdictColor = SkillScanPresentation.resolveVerdictColor(result.verdict, statusColors)
val policyLabel =
when (result.policy) {
"allow" -> stringResource(R.string.skills_hub_scan_policy_allow)
"ask" -> stringResource(R.string.skills_hub_scan_policy_ask)
"block" -> stringResource(R.string.skills_hub_scan_policy_block)
else -> result.policy.orEmpty()
}
val policyColor =
when (result.policy) {
"allow" -> statusColors.success
"ask" -> statusColors.warning
"block" -> statusColors.error
else -> statusColors.neutral
}
SkillScanPresentation.resolvePolicyLabelRes(result.policy)?.let { stringResource(it) }
?: result.policy.orEmpty()
val policyColor = SkillScanPresentation.resolvePolicyColor(result.policy, statusColors)
Card(
modifier = Modifier.fillMaxWidth(),
@@ -979,13 +952,6 @@ private fun ScanResultCard(result: SkillScanResponse) {
}
// ── Severity tally ──
val severityColors =
mapOf(
"critical" to statusColors.error,
"high" to statusColors.error,
"medium" to statusColors.warning,
"low" to statusColors.neutral,
)
Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(6.dp),
@@ -993,7 +959,7 @@ private fun ScanResultCard(result: SkillScanResponse) {
listOf("critical", "high", "medium", "low").forEach { sev ->
val count = result.severityCounts[sev] ?: 0
if (count > 0) {
val sevColor = severityColors[sev] ?: statusColors.neutral
val sevColor = SkillScanPresentation.resolveSeverityColor(sev, statusColors)
Surface(
color = sevColor.copy(alpha = 0.15f),
shape = RoundedCornerShape(50),
@@ -1039,12 +1005,7 @@ private fun ScanResultCard(result: SkillScanResponse) {
@Composable
private fun ScanFindingRow(finding: SkillScanFinding) {
val statusColors = LocalHermesStatusColors.current
val severityColor =
when (finding.severity) {
"critical", "high" -> statusColors.error
"medium" -> statusColors.warning
else -> statusColors.neutral
}
val severityColor = SkillScanPresentation.resolveSeverityColor(finding.severity, statusColors)
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,