fix(#548): stop System tab button text truncation + fix two wrong labels (#559)

* fix(#548): stop System tab button text truncation + fix two wrong labels

* fix(system): remove inline icons from narrow system action buttons to prevent text cutoff

* fix(system): restore action icons and implement dynamic font scaling for button text

* fix: stack icon above text in system buttons to prevent label clipping

Replace horizontal Icon+Text layout with vertical Column stacking
(icon on top, text centered below) — the same pattern Material 3
NavigationBar items use. Gives text the full button width so labels
like 'Security Audit' and 'Config Migrate' never get cut off.

* fix: add missing ImageVector import for system buttons fix

---------

Co-authored-by: M57 <m57@hermes-control.app>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-12 01:08:52 +03:00
1 parent 9156064d2b
commit cc917afcbd
2 files changed
+150 -89

No files matched your search

@@ -59,12 +59,15 @@ 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
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -378,32 +381,32 @@ private fun LazyListScope.hostSection(
onClick = { viewModel.checkForUpdate(false) }, onClick = { viewModel.checkForUpdate(false) },
enabled = !state.checkingUpdate, enabled = !state.checkingUpdate,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
if (state.checkingUpdate) { if (state.checkingUpdate) {
androidx.compose.material3.CircularProgressIndicator( ActionButtonContent(
modifier = Modifier.size(16.dp), icon = null,
strokeWidth = 2.dp, text = stringResource(R.string.system_action_check_updates),
loading = true,
) )
Spacer(modifier = Modifier.size(spacing.sm))
} else { } else {
Icon(Icons.Filled.Update, contentDescription = null, modifier = Modifier.size(18.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.Update,
text = stringResource(R.string.system_action_check_updates),
)
} }
Text(stringResource(R.string.system_action_check_updates), maxLines = 1)
} }
state.updateInfo?.let { info -> state.updateInfo?.let { info ->
if (info.update_available == true && info.can_apply == true) { if (info.update_available == true && info.can_apply == true) {
Button( Button(
onClick = { viewModel.openUpdateConfirm() }, onClick = { viewModel.openUpdateConfirm() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon( ActionButtonContent(
Icons.Filled.PlayArrow, icon = Icons.Filled.PlayArrow,
contentDescription = null, text = stringResource(R.string.system_action_update_now),
modifier = Modifier.size(18.dp),
) )
Spacer(modifier = Modifier.size(spacing.xs))
Text(stringResource(R.string.system_action_update_now), maxLines = 1)
} }
} }
} }
@@ -571,7 +574,11 @@ private fun LazyListScope.portalSection(
) { ) {
Icon(Icons.Filled.Link, contentDescription = null, modifier = Modifier.size(16.dp)) Icon(Icons.Filled.Link, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.size(spacing.xs)) Spacer(modifier = Modifier.size(spacing.xs))
Text(stringResource(R.string.system_portal_manage_subscription)) Text(
stringResource(R.string.system_portal_manage_subscription),
maxLines = 1,
softWrap = false,
)
} }
} }
@@ -668,28 +675,29 @@ private fun LazyListScope.curatorSection(
OutlinedButton( OutlinedButton(
onClick = { viewModel.toggleCuratorPaused() }, onClick = { viewModel.toggleCuratorPaused() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
if (curator.paused == true) { if (curator.paused == true) {
Icon( ActionButtonContent(
Icons.Filled.PlayArrow, icon = Icons.Filled.PlayArrow,
contentDescription = null, text = stringResource(R.string.system_curator_resume),
modifier = Modifier.size(18.dp),
) )
Spacer(modifier = Modifier.size(spacing.xs))
Text(stringResource(R.string.system_curator_resume), maxLines = 1)
} else { } else {
Icon(Icons.Filled.Pause, contentDescription = null, modifier = Modifier.size(18.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.Pause,
Text(stringResource(R.string.system_curator_pause), maxLines = 1) text = stringResource(R.string.system_curator_pause),
)
} }
} }
Button( Button(
onClick = { viewModel.runCuratorNow() }, onClick = { viewModel.runCuratorNow() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.Refresh, contentDescription = null, modifier = Modifier.size(18.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.Refresh,
Text(stringResource(R.string.system_curator_run_now), maxLines = 1) text = stringResource(R.string.system_curator_run_now),
)
} }
} }
} }
@@ -812,40 +820,35 @@ private fun LazyListScope.gatewaySection(
Button( Button(
onClick = { viewModel.startGateway() }, onClick = { viewModel.startGateway() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.PlayArrow, contentDescription = null, modifier = Modifier.size(18.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.PlayArrow,
Text(stringResource(R.string.system_gateway_start), maxLines = 1) text = stringResource(R.string.system_gateway_start),
)
} }
} }
if (isRunning) { if (isRunning) {
Button( Button(
onClick = { viewModel.restartGateway() }, onClick = { viewModel.restartGateway() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon( ActionButtonContent(
Icons.Filled.RestartAlt, icon = Icons.Filled.RestartAlt,
contentDescription = null, text = stringResource(R.string.system_gateway_restart),
modifier = Modifier.size(18.dp),
) )
Spacer(modifier = Modifier.size(spacing.xs))
Text(stringResource(R.string.system_gateway_restart), maxLines = 1)
} }
OutlinedButton( OutlinedButton(
onClick = { viewModel.stopGateway() }, onClick = { viewModel.stopGateway() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon( ActionButtonContent(
Icons.Filled.Stop, icon = Icons.Filled.Stop,
contentDescription = null, text = stringResource(R.string.system_gateway_stop),
modifier = Modifier.size(18.dp), iconTint = MaterialTheme.colorScheme.error,
tint = MaterialTheme.colorScheme.error, textColor = MaterialTheme.colorScheme.error,
)
Spacer(modifier = Modifier.size(spacing.xs))
Text(
stringResource(R.string.system_gateway_stop),
color = MaterialTheme.colorScheme.error,
maxLines = 1,
) )
} }
} }
@@ -915,30 +918,32 @@ private fun LazyListScope.memorySection(
FilledTonalButton( FilledTonalButton(
onClick = { onResetRequest("memory") }, onClick = { onResetRequest("memory") },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.Delete, contentDescription = null, modifier = Modifier.size(14.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.Delete,
Text(stringResource(R.string.system_memory_reset_memory), maxLines = 1) text = stringResource(R.string.system_memory_reset_memory),
)
} }
FilledTonalButton( FilledTonalButton(
onClick = { onResetRequest("user") }, onClick = { onResetRequest("user") },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.Delete, contentDescription = null, modifier = Modifier.size(14.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.Delete,
Text(stringResource(R.string.system_memory_reset_user), maxLines = 1) text = stringResource(R.string.system_memory_reset_user),
)
} }
FilledTonalButton( FilledTonalButton(
onClick = { onResetRequest("all") }, onClick = { onResetRequest("all") },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon( ActionButtonContent(
Icons.Filled.DeleteForever, icon = Icons.Filled.DeleteForever,
contentDescription = null, text = stringResource(R.string.system_memory_reset_all),
modifier = Modifier.size(14.dp),
) )
Spacer(modifier = Modifier.size(spacing.xs))
Text(stringResource(R.string.system_memory_reset_all), maxLines = 1)
} }
} }
} }
@@ -1032,7 +1037,7 @@ private fun LazyListScope.credentialsSection(
Icon(Icons.Filled.Add, contentDescription = null, modifier = Modifier.size(18.dp)) Icon(Icons.Filled.Add, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.size(spacing.xs)) Spacer(modifier = Modifier.size(spacing.xs))
} }
Text(stringResource(R.string.system_credentials_add_key)) Text(stringResource(R.string.system_credentials_add_key), maxLines = 1, softWrap = false)
} }
HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm)) HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm))
@@ -1098,26 +1103,32 @@ private fun LazyListScope.operationsSection(
viewModel.loadAll() viewModel.loadAll()
}, },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.HealthAndSafety, contentDescription = null, modifier = Modifier.size(16.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.HealthAndSafety,
Text(stringResource(R.string.system_op_doctor), maxLines = 1) text = stringResource(R.string.system_op_doctor),
)
} }
FilledTonalButton( FilledTonalButton(
onClick = { viewModel.runSecurityAudit() }, onClick = { viewModel.runSecurityAudit() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.VerifiedUser, contentDescription = null, modifier = Modifier.size(16.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.VerifiedUser,
Text(stringResource(R.string.system_op_security_audit), maxLines = 1) text = stringResource(R.string.system_op_security_audit),
)
} }
FilledTonalButton( FilledTonalButton(
onClick = { viewModel.runUpdateSkills() }, onClick = { viewModel.runUpdateSkills() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.Refresh, contentDescription = null, modifier = Modifier.size(16.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.Refresh,
Text(stringResource(R.string.system_op_update_skills), maxLines = 1) text = stringResource(R.string.system_op_update_skills),
)
} }
} }
@@ -1131,26 +1142,32 @@ private fun LazyListScope.operationsSection(
FilledTonalButton( FilledTonalButton(
onClick = { viewModel.runPromptSize() }, onClick = { viewModel.runPromptSize() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.HealthAndSafety, contentDescription = null, modifier = Modifier.size(16.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.HealthAndSafety,
Text(stringResource(R.string.system_op_prompt_size), maxLines = 1) text = stringResource(R.string.system_op_prompt_size),
)
} }
FilledTonalButton( FilledTonalButton(
onClick = { viewModel.runDump() }, onClick = { viewModel.runDump() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.Build, contentDescription = null, modifier = Modifier.size(16.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.Build,
Text(stringResource(R.string.system_op_dump), maxLines = 1) text = stringResource(R.string.system_op_dump),
)
} }
FilledTonalButton( FilledTonalButton(
onClick = { viewModel.runConfigMigrate() }, onClick = { viewModel.runConfigMigrate() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.Build, contentDescription = null, modifier = Modifier.size(16.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.Build,
Text(stringResource(R.string.system_op_config_migrate), maxLines = 1) text = stringResource(R.string.system_op_config_migrate),
)
} }
} }
@@ -1158,7 +1175,7 @@ private fun LazyListScope.operationsSection(
// Backup section // Backup section
Text( Text(
text = stringResource(R.string.system_op_backup_create), text = stringResource(R.string.system_sec_backup),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
) )
@@ -1171,26 +1188,30 @@ private fun LazyListScope.operationsSection(
Button( Button(
onClick = { viewModel.triggerBackup() }, onClick = { viewModel.triggerBackup() },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.Backup, contentDescription = null, modifier = Modifier.size(18.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.Backup,
Text(stringResource(R.string.system_op_backup_create), maxLines = 1) text = stringResource(R.string.system_op_backup_create),
)
} }
OutlinedButton( OutlinedButton(
onClick = { viewModel.downloadBackup() }, onClick = { viewModel.downloadBackup() },
enabled = state.backupArchive != null, enabled = state.backupArchive != null,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentPadding = actionButtonPadding,
) { ) {
Icon(Icons.Filled.CloudDownload, contentDescription = null, modifier = Modifier.size(18.dp)) ActionButtonContent(
Spacer(modifier = Modifier.size(spacing.xs)) icon = Icons.Filled.CloudDownload,
Text(stringResource(R.string.system_op_backup_download), maxLines = 1) text = stringResource(R.string.system_op_backup_download),
)
} }
} }
// Restore from uploaded zip // Restore from uploaded zip
Spacer(modifier = Modifier.height(spacing.sm)) Spacer(modifier = Modifier.height(spacing.sm))
Text( Text(
text = stringResource(R.string.system_op_backup_restore_upload), text = stringResource(R.string.system_sec_restore),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
) )
@@ -1219,7 +1240,7 @@ private fun LazyListScope.operationsSection(
) { ) {
Icon(Icons.Filled.RestartAlt, contentDescription = null, modifier = Modifier.size(18.dp)) Icon(Icons.Filled.RestartAlt, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.size(spacing.xs)) Spacer(modifier = Modifier.size(spacing.xs))
Text(stringResource(R.string.system_op_restore_upload)) Text(stringResource(R.string.system_op_restore_upload), maxLines = 1, softWrap = false)
} }
HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm)) HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm))
@@ -1270,6 +1291,8 @@ private fun LazyListScope.operationsSection(
} else { } else {
stringResource(R.string.system_debug_share_generate) stringResource(R.string.system_debug_share_generate)
}, },
maxLines = 1,
softWrap = false,
) )
} }
@@ -1288,7 +1311,7 @@ private fun LazyListScope.operationsSection(
if (share.ok == true) { if (share.ok == true) {
stringResource(R.string.system_debug_share_uploaded) stringResource(R.string.system_debug_share_uploaded)
} else { } else {
stringResource(R.string.system_status_stopped) stringResource(R.string.system_debug_share_failed)
}, },
status = if (share.ok == true) StatusBadgeType.SUCCESS else StatusBadgeType.ERROR, status = if (share.ok == true) StatusBadgeType.SUCCESS else StatusBadgeType.ERROR,
) )
@@ -1384,7 +1407,7 @@ private fun LazyListScope.checkpointsSection(
) { ) {
Icon(Icons.Filled.DeleteForever, contentDescription = null, modifier = Modifier.size(18.dp)) Icon(Icons.Filled.DeleteForever, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.size(spacing.xs)) Spacer(modifier = Modifier.size(spacing.xs))
Text(stringResource(R.string.system_checkpoints_prune)) Text(stringResource(R.string.system_checkpoints_prune), maxLines = 1, softWrap = false)
} }
} }
} }
@@ -1406,7 +1429,7 @@ private fun LazyListScope.shellHooksSection(
FilledTonalButton(onClick = { viewModel.toggleHookModal() }) { FilledTonalButton(onClick = { viewModel.toggleHookModal() }) {
Icon(Icons.Filled.Add, contentDescription = null, modifier = Modifier.size(16.dp)) Icon(Icons.Filled.Add, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.size(spacing.xs)) Spacer(modifier = Modifier.size(spacing.xs))
Text(stringResource(R.string.system_hooks_new)) Text(stringResource(R.string.system_hooks_new), maxLines = 1, softWrap = false)
} }
}, },
) )
@@ -1612,3 +1635,38 @@ private fun LazyListScope.actionLogSection(
} }
} }
} }
private val actionButtonPadding =
androidx.compose.foundation.layout.PaddingValues(horizontal = 8.dp, vertical = 8.dp)
@Composable
private fun ActionButtonContent(
icon: ImageVector?,
text: String,
iconTint: androidx.compose.ui.graphics.Color = androidx.compose.material3.LocalContentColor.current,
textColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Unspecified,
loading: Boolean = false,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(),
) {
if (loading) {
androidx.compose.material3.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,
)
}
}
+3
View File
@@ -396,6 +396,8 @@
<string name="system_op_backup_create">Create backup</string> <string name="system_op_backup_create">Create backup</string>
<string name="system_op_backup_download">Download backup</string> <string name="system_op_backup_download">Download backup</string>
<string name="system_op_backup_restore_upload">Restore from backup upload</string> <string name="system_op_backup_restore_upload">Restore from backup upload</string>
<string name="system_sec_backup">Backup</string>
<string name="system_sec_restore">Restore from upload</string>
<string name="system_op_choose_zip">Choose restore zip</string> <string name="system_op_choose_zip">Choose restore zip</string>
<string name="system_op_restore_upload">Restore upload</string> <string name="system_op_restore_upload">Restore upload</string>
<string name="system_op_restore_path_label">Restore from backups path</string> <string name="system_op_restore_path_label">Restore from backups path</string>
@@ -416,6 +418,7 @@
<string name="system_debug_share_not_redacted">not redacted</string> <string name="system_debug_share_not_redacted">not redacted</string>
<string name="system_debug_share_auto_delete">auto-deletes in %1$dh</string> <string name="system_debug_share_auto_delete">auto-deletes in %1$dh</string>
<string name="system_debug_share_copy_all">Copy all</string> <string name="system_debug_share_copy_all">Copy all</string>
<string name="system_debug_share_failed">upload failed</string>
<!-- Checkpoints section --> <!-- Checkpoints section -->
<string name="system_sec_checkpoints">Checkpoints</string> <string name="system_sec_checkpoints">Checkpoints</string>