refactor(system): decompose SystemScreen into focused section components
Extract Host, Portal, Curator, Gateway, Credentials, Operations, Checkpoints, ShellHooks, ActionLog, and HookCreateDialog into components/.
This commit is contained in:
1 parent
5ecc2e7d88
commit
35dc778702
12 files changed
+1794
-1507
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,55 @@
|
|||||||
|
package com.m57.hermescontrol.ui.system.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
val ActionButtonPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ActionButtonContent(
|
||||||
|
icon: ImageVector?,
|
||||||
|
text: String,
|
||||||
|
iconTint: Color = LocalContentColor.current,
|
||||||
|
textColor: Color = Color.Unspecified,
|
||||||
|
loading: Boolean = false,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
if (loading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
} else if (icon != null) {
|
||||||
|
Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp), tint = iconTint)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = textColor,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package com.m57.hermescontrol.ui.system.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
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.unit.dp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.Spacing
|
||||||
|
import com.m57.hermescontrol.ui.common.SectionHeader
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemUiState
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemViewModel
|
||||||
|
|
||||||
|
fun LazyListScope.actionLogSection(
|
||||||
|
state: SystemUiState,
|
||||||
|
spacing: Spacing,
|
||||||
|
viewModel: SystemViewModel,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SectionHeader(
|
||||||
|
title = stringResource(R.string.system_action_log_title),
|
||||||
|
trailing = {
|
||||||
|
TextButton(onClick = { viewModel.closeActionLog() }) {
|
||||||
|
Icon(Icons.Filled.Close, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
Text(stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(spacing.md)) {
|
||||||
|
// Action title
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = state.activeAction ?: "",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
val actionLog = state.actionLog
|
||||||
|
if (actionLog != null) {
|
||||||
|
if (actionLog.running == true) {
|
||||||
|
StatusBadge(
|
||||||
|
text = stringResource(R.string.system_action_log_running),
|
||||||
|
status = StatusBadgeType.INFO,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
StatusBadge(
|
||||||
|
text = stringResource(R.string.system_action_log_done),
|
||||||
|
status = StatusBadgeType.SUCCESS,
|
||||||
|
)
|
||||||
|
actionLog.exit_code?.let { code ->
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
StatusBadge(
|
||||||
|
text = stringResource(R.string.system_action_log_exit, code),
|
||||||
|
status =
|
||||||
|
if (code == 0) StatusBadgeType.SUCCESS else StatusBadgeType.ERROR,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StatusBadge(
|
||||||
|
text = stringResource(R.string.system_action_log_starting),
|
||||||
|
status = StatusBadgeType.NEUTRAL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output lines
|
||||||
|
state.actionLog?.lines?.let { lines ->
|
||||||
|
if (lines.isNotEmpty()) {
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm))
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(spacing.sm)) {
|
||||||
|
lines.take(20).forEach { line ->
|
||||||
|
Text(
|
||||||
|
text = line,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (lines.size > 20) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_more_lines, lines.size - 20),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.m57.hermescontrol.ui.system.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.lazy.LazyListScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.DeleteForever
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.Spacing
|
||||||
|
import com.m57.hermescontrol.ui.common.InfoRow
|
||||||
|
import com.m57.hermescontrol.ui.common.SectionHeader
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemUiState
|
||||||
|
import com.m57.hermescontrol.util.formatBytes
|
||||||
|
|
||||||
|
fun LazyListScope.checkpointsSection(
|
||||||
|
state: SystemUiState,
|
||||||
|
spacing: Spacing,
|
||||||
|
onPruneConfirm: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
state.checkpoints?.let { checkpoints ->
|
||||||
|
item {
|
||||||
|
SectionHeader(title = stringResource(R.string.system_sec_checkpoints))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(spacing.md)) {
|
||||||
|
val sessionsCount = checkpoints.sessions?.size ?: 0
|
||||||
|
val totalBytes = checkpoints.total_bytes ?: 0L
|
||||||
|
InfoRow(
|
||||||
|
label = "",
|
||||||
|
value =
|
||||||
|
stringResource(
|
||||||
|
R.string.system_checkpoints_summary,
|
||||||
|
sessionsCount,
|
||||||
|
formatBytes(totalBytes),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
Button(
|
||||||
|
onClick = { onPruneConfirm(true) },
|
||||||
|
enabled = sessionsCount > 0,
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onError,
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.DeleteForever, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
Text(stringResource(R.string.system_checkpoints_prune), maxLines = 1, softWrap = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package com.m57.hermescontrol.ui.system.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.Spacing
|
||||||
|
import com.m57.hermescontrol.ui.common.SectionHeader
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemUiState
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemViewModel
|
||||||
|
|
||||||
|
fun LazyListScope.credentialsSection(
|
||||||
|
state: SystemUiState,
|
||||||
|
spacing: Spacing,
|
||||||
|
viewModel: SystemViewModel,
|
||||||
|
onRemoveRequest: (Pair<String, Int>) -> Unit,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SectionHeader(
|
||||||
|
title = stringResource(R.string.system_sec_credentials),
|
||||||
|
trailing = {
|
||||||
|
val hasCreds = state.credentials.any { it.entries?.isNotEmpty() == true }
|
||||||
|
if (hasCreds) {
|
||||||
|
Text(
|
||||||
|
text = "${state.credentials.sumOf { it.entries?.size ?: 0 }} key(s)",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(spacing.md)) {
|
||||||
|
// Add credential form
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.credProvider,
|
||||||
|
onValueChange = viewModel::updateCredProvider,
|
||||||
|
label = { Text(stringResource(R.string.system_credentials_provider)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.credKey,
|
||||||
|
onValueChange = viewModel::updateCredKey,
|
||||||
|
label = { Text(stringResource(R.string.system_credentials_api_key)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.credLabel,
|
||||||
|
onValueChange = viewModel::updateCredLabel,
|
||||||
|
label = { Text(stringResource(R.string.system_credentials_label)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.addCredential() },
|
||||||
|
enabled = !state.addingCred && state.credKey.isNotBlank(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
if (state.addingCred) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(spacing.sm))
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Filled.Add, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
}
|
||||||
|
Text(stringResource(R.string.system_credentials_add_key), maxLines = 1, softWrap = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm))
|
||||||
|
|
||||||
|
// Provider list with entries
|
||||||
|
if (state.credentials.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_credentials_empty),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(vertical = spacing.sm),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.credentials.forEach { provider ->
|
||||||
|
Text(
|
||||||
|
text = provider.provider ?: "?",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
modifier = Modifier.padding(vertical = spacing.xs),
|
||||||
|
)
|
||||||
|
provider.entries?.forEach { entry ->
|
||||||
|
CredentialEntryRow(
|
||||||
|
entry = entry,
|
||||||
|
providerName = provider.provider ?: "",
|
||||||
|
onRemove = { onRemoveRequest(Pair(provider.provider ?: "", entry.index ?: 0)) },
|
||||||
|
spacing = spacing,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.m57.hermescontrol.ui.system.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Pause
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.Spacing
|
||||||
|
import com.m57.hermescontrol.ui.common.InfoRow
|
||||||
|
import com.m57.hermescontrol.ui.common.SectionHeader
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemUiState
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemViewModel
|
||||||
|
|
||||||
|
fun LazyListScope.curatorSection(
|
||||||
|
state: SystemUiState,
|
||||||
|
spacing: Spacing,
|
||||||
|
viewModel: SystemViewModel,
|
||||||
|
) {
|
||||||
|
state.curator?.let { curator ->
|
||||||
|
item {
|
||||||
|
SectionHeader(title = stringResource(R.string.system_sec_curator))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(spacing.md)) {
|
||||||
|
// Status
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
val statusText: String
|
||||||
|
val statusType: StatusBadgeType
|
||||||
|
when {
|
||||||
|
curator.enabled != true -> {
|
||||||
|
statusText = stringResource(R.string.system_status_disabled)
|
||||||
|
statusType = StatusBadgeType.ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
curator.paused == true -> {
|
||||||
|
statusText = stringResource(R.string.system_status_paused)
|
||||||
|
statusType = StatusBadgeType.WARNING
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
statusText = stringResource(R.string.system_status_active)
|
||||||
|
statusType = StatusBadgeType.SUCCESS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StatusBadge(text = statusText, status = statusType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interval info
|
||||||
|
curator.interval_hours?.let { hrs ->
|
||||||
|
InfoRow(
|
||||||
|
label = stringResource(R.string.system_curator_interval, hrs),
|
||||||
|
value = "",
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(spacing.xs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last run
|
||||||
|
curator.last_run_at?.let { lastRun ->
|
||||||
|
InfoRow(
|
||||||
|
label = stringResource(R.string.system_curator_last_run, lastRun),
|
||||||
|
value = "",
|
||||||
|
)
|
||||||
|
} ?: InfoRow(
|
||||||
|
label = stringResource(R.string.system_curator_never_run),
|
||||||
|
value = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
if (curator.enabled == true) {
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { viewModel.toggleCuratorPaused() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
if (curator.paused == true) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.PlayArrow,
|
||||||
|
text = stringResource(R.string.system_curator_resume),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.Pause,
|
||||||
|
text = stringResource(R.string.system_curator_pause),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.runCuratorNow() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.Refresh,
|
||||||
|
text = stringResource(R.string.system_curator_run_now),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package com.m57.hermescontrol.ui.system.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.lazy.LazyListScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Devices
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.RestartAlt
|
||||||
|
import androidx.compose.material.icons.filled.Stop
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.Spacing
|
||||||
|
import com.m57.hermescontrol.ui.common.InfoRow
|
||||||
|
import com.m57.hermescontrol.ui.common.SectionHeader
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemUiState
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemViewModel
|
||||||
|
|
||||||
|
fun LazyListScope.gatewaySection(
|
||||||
|
state: SystemUiState,
|
||||||
|
spacing: Spacing,
|
||||||
|
viewModel: SystemViewModel,
|
||||||
|
) {
|
||||||
|
state.status?.let { status ->
|
||||||
|
item {
|
||||||
|
SectionHeader(title = stringResource(R.string.system_sec_gateway))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(spacing.md)) {
|
||||||
|
// Status badge + version
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
val isRunning = status.gateway_running == true
|
||||||
|
StatusBadge(
|
||||||
|
text =
|
||||||
|
if (isRunning) {
|
||||||
|
stringResource(R.string.system_status_running)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.system_status_stopped)
|
||||||
|
},
|
||||||
|
status = if (isRunning) StatusBadgeType.SUCCESS else StatusBadgeType.ERROR,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version · Sessions · PID
|
||||||
|
status.version?.let { ver ->
|
||||||
|
val parts = mutableListOf(ver)
|
||||||
|
status.active_sessions?.let { parts.add("$it session(s)") }
|
||||||
|
state.doctorReport?.let { report ->
|
||||||
|
if (report.ok && report.pid != null) {
|
||||||
|
parts.add("pid ${report.pid}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InfoRow(
|
||||||
|
label = stringResource(R.string.system_label_hermes_version),
|
||||||
|
value = parts.joinToString(" · "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth required
|
||||||
|
status.auth_required?.let { auth ->
|
||||||
|
InfoRow(
|
||||||
|
label = stringResource(R.string.system_auth_required),
|
||||||
|
value = if (auth) "yes" else "no",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active platforms
|
||||||
|
status.gateway_platforms?.let { platforms ->
|
||||||
|
if (platforms.isNotEmpty()) {
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm))
|
||||||
|
platforms.forEach { (name, platform) ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = spacing.xs),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Devices,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
platform?.state?.let { platformState ->
|
||||||
|
StatusBadge(
|
||||||
|
text = platformState,
|
||||||
|
status =
|
||||||
|
when (platformState.lowercase()) {
|
||||||
|
"connected", "running", "ready" -> StatusBadgeType.SUCCESS
|
||||||
|
"error", "disconnected", "failed" -> StatusBadgeType.ERROR
|
||||||
|
else -> StatusBadgeType.NEUTRAL
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
val isRunning = status.gateway_running == true
|
||||||
|
if (!isRunning) {
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.startGateway() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.PlayArrow,
|
||||||
|
text = stringResource(R.string.system_gateway_start),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isRunning) {
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.restartGateway() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.RestartAlt,
|
||||||
|
text = stringResource(R.string.system_gateway_restart),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { viewModel.stopGateway() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.Stop,
|
||||||
|
text = stringResource(R.string.system_gateway_stop),
|
||||||
|
iconTint = MaterialTheme.colorScheme.error,
|
||||||
|
textColor = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.m57.hermescontrol.ui.system.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HookCreateDialog(
|
||||||
|
isOpen: Boolean,
|
||||||
|
event: String,
|
||||||
|
command: String,
|
||||||
|
matcher: String,
|
||||||
|
timeout: String,
|
||||||
|
approve: Boolean,
|
||||||
|
isCreating: Boolean,
|
||||||
|
onEventChange: (String) -> Unit,
|
||||||
|
onCommandChange: (String) -> Unit,
|
||||||
|
onMatcherChange: (String) -> Unit,
|
||||||
|
onTimeoutChange: (String) -> Unit,
|
||||||
|
onApproveChange: (Boolean) -> Unit,
|
||||||
|
onCreate: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
if (!isOpen) return
|
||||||
|
val spacing = LocalSpacing.current
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(R.string.system_hooks_modal_title)) },
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(spacing.sm)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = event,
|
||||||
|
onValueChange = onEventChange,
|
||||||
|
label = { Text(stringResource(R.string.system_hooks_field_event)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = command,
|
||||||
|
onValueChange = onCommandChange,
|
||||||
|
label = { Text(stringResource(R.string.system_hooks_field_command)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = matcher,
|
||||||
|
onValueChange = onMatcherChange,
|
||||||
|
label = { Text(stringResource(R.string.system_hooks_field_matcher)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = timeout,
|
||||||
|
onValueChange = onTimeoutChange,
|
||||||
|
label = { Text(stringResource(R.string.system_hooks_field_timeout)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
)
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Switch(
|
||||||
|
checked = approve,
|
||||||
|
onCheckedChange = onApproveChange,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(spacing.sm))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_hooks_field_approve),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_hooks_warning),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = onCreate,
|
||||||
|
enabled = !isCreating && command.isNotBlank(),
|
||||||
|
) {
|
||||||
|
if (isCreating) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(spacing.sm))
|
||||||
|
}
|
||||||
|
Text(stringResource(R.string.system_hooks_create))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.system_confirm_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package com.m57.hermescontrol.ui.system.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Memory
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.Speed
|
||||||
|
import androidx.compose.material.icons.filled.Storage
|
||||||
|
import androidx.compose.material.icons.filled.Update
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.Spacing
|
||||||
|
import com.m57.hermescontrol.ui.common.InfoRow
|
||||||
|
import com.m57.hermescontrol.ui.common.SectionHeader
|
||||||
|
import com.m57.hermescontrol.ui.common.StatCard
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemUiState
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemViewModel
|
||||||
|
import com.m57.hermescontrol.util.formatBytes
|
||||||
|
import com.m57.hermescontrol.util.formatDuration
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
fun LazyListScope.hostSection(
|
||||||
|
state: SystemUiState,
|
||||||
|
spacing: Spacing,
|
||||||
|
viewModel: SystemViewModel,
|
||||||
|
) {
|
||||||
|
state.stats?.let { stats ->
|
||||||
|
item {
|
||||||
|
SectionHeader(title = stringResource(R.string.system_sec_host))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(spacing.md)) {
|
||||||
|
// OS, Arch, Hostname
|
||||||
|
stats.os?.let { InfoRow(stringResource(R.string.system_label_os), it) }
|
||||||
|
stats.arch?.let { InfoRow(stringResource(R.string.system_label_arch), it) }
|
||||||
|
stats.hostname?.let { InfoRow(stringResource(R.string.system_label_hostname), it) }
|
||||||
|
stats.python_version?.let { python ->
|
||||||
|
val impl = stats.python_impl?.let { " ($it)" } ?: ""
|
||||||
|
InfoRow(stringResource(R.string.system_label_python), "$python$impl")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hermes version + update badge
|
||||||
|
stats.hermes_version?.let { ver ->
|
||||||
|
val updateBadge =
|
||||||
|
state.updateInfo?.let { info ->
|
||||||
|
when {
|
||||||
|
info.update_available == true && info.behind != null && info.behind > 0 -> {
|
||||||
|
" (${stringResource(
|
||||||
|
R.string.system_version_update_available,
|
||||||
|
)}: ${stringResource(R.string.system_version_behind, info.behind)})"
|
||||||
|
}
|
||||||
|
|
||||||
|
info.update_available == true -> {
|
||||||
|
" (${stringResource(
|
||||||
|
R.string.system_version_update_available,
|
||||||
|
)})"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
" (${stringResource(R.string.system_version_latest)})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: ""
|
||||||
|
InfoRow(stringResource(R.string.system_label_hermes_version), "$ver$updateBadge")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = spacing.sm),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { viewModel.checkForUpdate(false) },
|
||||||
|
enabled = !state.checkingUpdate,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
if (state.checkingUpdate) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = null,
|
||||||
|
text = stringResource(R.string.system_action_check_updates),
|
||||||
|
loading = true,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.Update,
|
||||||
|
text = stringResource(R.string.system_action_check_updates),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.updateInfo?.let { info ->
|
||||||
|
if (info.update_available == true && info.can_apply == true) {
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.openUpdateConfirm() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.PlayArrow,
|
||||||
|
text = stringResource(R.string.system_action_update_now),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm))
|
||||||
|
|
||||||
|
// CPU / Memory / Disk stat cards
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
stats.cpu_percent?.let { cpu ->
|
||||||
|
StatCard(
|
||||||
|
label = stringResource(R.string.system_label_cpu),
|
||||||
|
value = "${cpu.toInt()}%",
|
||||||
|
icon = Icons.Filled.Speed,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
stats.memory?.percent?.let { mem ->
|
||||||
|
StatCard(
|
||||||
|
label = stringResource(R.string.system_label_memory),
|
||||||
|
value = "${mem.toInt()}%",
|
||||||
|
icon = Icons.Filled.Memory,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
stats.disk?.percent?.let { disk ->
|
||||||
|
StatCard(
|
||||||
|
label = stringResource(R.string.system_label_disk),
|
||||||
|
value = "${disk.toInt()}%",
|
||||||
|
icon = Icons.Filled.Storage,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory details
|
||||||
|
stats.memory?.let { mem ->
|
||||||
|
InfoRow(
|
||||||
|
label = stringResource(R.string.system_label_memory),
|
||||||
|
value =
|
||||||
|
stringResource(
|
||||||
|
R.string.system_label_used_total_percent,
|
||||||
|
formatBytes(mem.used ?: 0L),
|
||||||
|
formatBytes(mem.total ?: 0L),
|
||||||
|
"${(mem.percent ?: 0.0).toInt()}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disk details
|
||||||
|
stats.disk?.let { disk ->
|
||||||
|
InfoRow(
|
||||||
|
label = stringResource(R.string.system_label_disk),
|
||||||
|
value =
|
||||||
|
stringResource(
|
||||||
|
R.string.system_label_used_total_percent,
|
||||||
|
formatBytes(disk.used ?: 0L),
|
||||||
|
formatBytes(disk.total ?: 0L),
|
||||||
|
"${(disk.percent ?: 0.0).toInt()}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
stats.uptime_seconds?.let { secs ->
|
||||||
|
InfoRow(
|
||||||
|
label = stringResource(R.string.system_label_uptime),
|
||||||
|
value = formatDuration(secs),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load avg
|
||||||
|
stats.load_avg?.let { loads ->
|
||||||
|
if (loads.isNotEmpty()) {
|
||||||
|
val coresText =
|
||||||
|
stats.cpu_count?.let {
|
||||||
|
" ($it ${stringResource(
|
||||||
|
R.string.system_label_cores,
|
||||||
|
)})"
|
||||||
|
} ?: ""
|
||||||
|
InfoRow(
|
||||||
|
label = stringResource(R.string.system_label_load_avg),
|
||||||
|
value = loads.joinToString(", ") { "%.2f".format(it) } + coresText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: stats.cpu_count?.let { cores ->
|
||||||
|
InfoRow(
|
||||||
|
label = stringResource(R.string.system_label_cpu),
|
||||||
|
value = "$cores ${stringResource(R.string.system_label_cores)}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// psutil warning
|
||||||
|
if (stats.psutil == false) {
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
StatusBadge(
|
||||||
|
text = stringResource(R.string.system_psutil_warning),
|
||||||
|
status = StatusBadgeType.WARNING,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
package com.m57.hermescontrol.ui.system.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.lazy.LazyListScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Backup
|
||||||
|
import androidx.compose.material.icons.filled.Build
|
||||||
|
import androidx.compose.material.icons.filled.CloudDownload
|
||||||
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
|
import androidx.compose.material.icons.filled.Description
|
||||||
|
import androidx.compose.material.icons.filled.HealthAndSafety
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material.icons.filled.RestartAlt
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material.icons.filled.UploadFile
|
||||||
|
import androidx.compose.material.icons.filled.VerifiedUser
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.Spacing
|
||||||
|
import com.m57.hermescontrol.ui.common.SectionHeader
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemUiState
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemViewModel
|
||||||
|
|
||||||
|
fun LazyListScope.operationsSection(
|
||||||
|
state: SystemUiState,
|
||||||
|
spacing: Spacing,
|
||||||
|
viewModel: SystemViewModel,
|
||||||
|
onImportConfirm: (Boolean) -> Unit,
|
||||||
|
onImportPick: () -> Unit,
|
||||||
|
downloadBackup: () -> Unit,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SectionHeader(title = stringResource(R.string.system_sec_operations))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(spacing.md)) {
|
||||||
|
// Operation buttons row 1
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = { viewModel.loadAll() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.HealthAndSafety,
|
||||||
|
text = stringResource(R.string.system_op_doctor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = { viewModel.runSecurityAudit() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.VerifiedUser,
|
||||||
|
text = stringResource(R.string.system_op_security_audit),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = { viewModel.runUpdateSkills() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.Refresh,
|
||||||
|
text = stringResource(R.string.system_op_update_skills),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
|
||||||
|
// Operation buttons row 2
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = { viewModel.runPromptSize() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.HealthAndSafety,
|
||||||
|
text = stringResource(R.string.system_op_prompt_size),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = { viewModel.runDump() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.Build,
|
||||||
|
text = stringResource(R.string.system_op_dump),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = { viewModel.runConfigMigrate() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.Build,
|
||||||
|
text = stringResource(R.string.system_op_config_migrate),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm))
|
||||||
|
|
||||||
|
// Backup section
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_sec_backup),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.triggerBackup() },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
ActionButtonContent(
|
||||||
|
icon = Icons.Filled.Backup,
|
||||||
|
text = stringResource(R.string.system_op_backup_create),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = downloadBackup,
|
||||||
|
enabled = state.backupArchive != null && !state.isDownloading,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
if (state.isDownloading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.CloudDownload,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_op_backup_download),
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore from picked archive (issue #786: SAF picker → upload)
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_sec_restore),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onImportPick,
|
||||||
|
enabled = !state.isImporting,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentPadding = ActionButtonPadding,
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.UploadFile, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_op_restore_pick),
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.importFileName?.let { name ->
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.xs),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Description,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { onImportConfirm(true) },
|
||||||
|
enabled = state.importFileName != null && !state.isImporting,
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onError,
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
if (state.isImporting) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onError,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Filled.RestartAlt, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
}
|
||||||
|
Text(stringResource(R.string.system_op_restore_upload), maxLines = 1, softWrap = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm))
|
||||||
|
|
||||||
|
// Debug share section
|
||||||
|
SectionHeader(title = stringResource(R.string.system_sec_debug_share))
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_debug_share_description),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
|
||||||
|
Switch(
|
||||||
|
checked = state.shareRedact,
|
||||||
|
onCheckedChange = { viewModel.toggleShareRedact() },
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(spacing.sm))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_debug_share_redact),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(start = spacing.xs),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.runDebugShare() },
|
||||||
|
enabled = !state.sharing,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
if (state.sharing) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(spacing.sm))
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Filled.Share, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
if (state.sharing) {
|
||||||
|
stringResource(R.string.system_debug_share_uploading)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.system_debug_share_generate)
|
||||||
|
},
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug share results
|
||||||
|
state.debugShare?.let { share ->
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
StatusBadge(
|
||||||
|
text =
|
||||||
|
if (share.ok == true) {
|
||||||
|
stringResource(R.string.system_debug_share_uploaded)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.system_debug_share_failed)
|
||||||
|
},
|
||||||
|
status = if (share.ok == true) StatusBadgeType.SUCCESS else StatusBadgeType.ERROR,
|
||||||
|
)
|
||||||
|
share.redacted?.let { redacted ->
|
||||||
|
StatusBadge(
|
||||||
|
text =
|
||||||
|
if (redacted) {
|
||||||
|
stringResource(R.string.system_debug_share_redacted)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.system_debug_share_not_redacted)
|
||||||
|
},
|
||||||
|
status = StatusBadgeType.INFO,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
share.auto_delete_seconds?.let { secs ->
|
||||||
|
StatusBadge(
|
||||||
|
text = stringResource(R.string.system_debug_share_auto_delete, secs / 3600),
|
||||||
|
status = StatusBadgeType.NEUTRAL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
share.urls?.forEach { (name, url) ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = spacing.xs),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$name: $url",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
IconButton(onClick = {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
clipboardManager.setText(AnnotatedString(url))
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.ContentCopy,
|
||||||
|
contentDescription = stringResource(R.string.system_debug_share_copy_all),
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.m57.hermescontrol.ui.system.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Link
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.UriHandler
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.Spacing
|
||||||
|
import com.m57.hermescontrol.ui.common.InfoRow
|
||||||
|
import com.m57.hermescontrol.ui.common.SectionHeader
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||||
|
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemUiState
|
||||||
|
|
||||||
|
fun LazyListScope.portalSection(
|
||||||
|
state: SystemUiState,
|
||||||
|
spacing: Spacing,
|
||||||
|
uriHandler: UriHandler,
|
||||||
|
) {
|
||||||
|
state.portal?.let { portal ->
|
||||||
|
item {
|
||||||
|
SectionHeader(title = stringResource(R.string.system_sec_portal))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(spacing.md)) {
|
||||||
|
// Login status
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
val loggedIn = portal.logged_in == true
|
||||||
|
StatusBadge(
|
||||||
|
text =
|
||||||
|
if (loggedIn) {
|
||||||
|
stringResource(R.string.system_status_logged_in)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.system_status_not_logged_in)
|
||||||
|
},
|
||||||
|
status = if (loggedIn) StatusBadgeType.SUCCESS else StatusBadgeType.WARNING,
|
||||||
|
)
|
||||||
|
if (!loggedIn) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_portal_login_hint),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider
|
||||||
|
portal.provider?.let { provider ->
|
||||||
|
InfoRow(
|
||||||
|
label = stringResource(R.string.system_portal_provider, provider),
|
||||||
|
value = "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage subscription link
|
||||||
|
portal.subscription_url?.let { url ->
|
||||||
|
TextButton(
|
||||||
|
onClick = { uriHandler.openUri(url) },
|
||||||
|
modifier = Modifier.padding(top = spacing.xs),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Link, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.system_portal_manage_subscription),
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feature routing
|
||||||
|
portal.features?.let { features ->
|
||||||
|
if (features.isNotEmpty()) {
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm))
|
||||||
|
features.forEach { feature ->
|
||||||
|
InfoRow(
|
||||||
|
label = feature.label ?: stringResource(R.string.system_portal_feature_routing),
|
||||||
|
value = feature.state ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.m57.hermescontrol.ui.system.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.Spacing
|
||||||
|
import com.m57.hermescontrol.ui.common.SectionHeader
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemUiState
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemViewModel
|
||||||
|
|
||||||
|
fun LazyListScope.shellHooksSection(
|
||||||
|
state: SystemUiState,
|
||||||
|
spacing: Spacing,
|
||||||
|
viewModel: SystemViewModel,
|
||||||
|
) {
|
||||||
|
val hooks = state.hooks?.hooks ?: emptyList()
|
||||||
|
|
||||||
|
item {
|
||||||
|
SectionHeader(
|
||||||
|
title = stringResource(R.string.system_sec_hooks),
|
||||||
|
trailing = {
|
||||||
|
FilledTonalButton(onClick = { viewModel.toggleHookModal() }) {
|
||||||
|
Icon(Icons.Filled.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
Text(stringResource(R.string.system_hooks_new), maxLines = 1, softWrap = false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hooks.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_hooks_empty),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(horizontal = spacing.md, vertical = spacing.sm),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(hooks, key = { "${it.event}:${it.command}" }) { hook ->
|
||||||
|
HookCard(hook = hook, spacing = spacing, onDelete = {
|
||||||
|
viewModel.deleteHook(hook.event ?: "", hook.command ?: "")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user