From cc917afcbd922380750cd96785e689fc99f35782 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:08:52 +0300 Subject: [PATCH] fix(#548): stop System tab button text truncation + fix two wrong labels (#559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../hermescontrol/ui/system/SystemScreen.kt | 236 +++++++++++------- app/src/main/res/values/strings.xml | 3 + 2 files changed, 150 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt index f0178ce0..e0f3bd64 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt @@ -59,12 +59,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight 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.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -378,32 +381,32 @@ private fun LazyListScope.hostSection( onClick = { viewModel.checkForUpdate(false) }, enabled = !state.checkingUpdate, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { if (state.checkingUpdate) { - androidx.compose.material3.CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, + ActionButtonContent( + icon = null, + text = stringResource(R.string.system_action_check_updates), + loading = true, ) - Spacer(modifier = Modifier.size(spacing.sm)) } else { - Icon(Icons.Filled.Update, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) + ActionButtonContent( + 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 -> if (info.update_available == true && info.can_apply == true) { Button( onClick = { viewModel.openUpdateConfirm() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon( - Icons.Filled.PlayArrow, - contentDescription = null, - modifier = Modifier.size(18.dp), + ActionButtonContent( + icon = Icons.Filled.PlayArrow, + text = stringResource(R.string.system_action_update_now), ) - 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)) 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( onClick = { viewModel.toggleCuratorPaused() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { if (curator.paused == true) { - Icon( - Icons.Filled.PlayArrow, - contentDescription = null, - modifier = Modifier.size(18.dp), + ActionButtonContent( + icon = Icons.Filled.PlayArrow, + text = stringResource(R.string.system_curator_resume), ) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_curator_resume), maxLines = 1) } else { - Icon(Icons.Filled.Pause, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_curator_pause), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.Pause, + text = stringResource(R.string.system_curator_pause), + ) } } Button( onClick = { viewModel.runCuratorNow() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.Refresh, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_curator_run_now), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.Refresh, + text = stringResource(R.string.system_curator_run_now), + ) } } } @@ -812,40 +820,35 @@ private fun LazyListScope.gatewaySection( Button( onClick = { viewModel.startGateway() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.PlayArrow, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_gateway_start), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.PlayArrow, + text = stringResource(R.string.system_gateway_start), + ) } } if (isRunning) { Button( onClick = { viewModel.restartGateway() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon( - Icons.Filled.RestartAlt, - contentDescription = null, - modifier = Modifier.size(18.dp), + ActionButtonContent( + icon = Icons.Filled.RestartAlt, + text = stringResource(R.string.system_gateway_restart), ) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_gateway_restart), maxLines = 1) } OutlinedButton( onClick = { viewModel.stopGateway() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon( - Icons.Filled.Stop, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.error, - ) - Spacer(modifier = Modifier.size(spacing.xs)) - Text( - stringResource(R.string.system_gateway_stop), - color = MaterialTheme.colorScheme.error, - maxLines = 1, + ActionButtonContent( + icon = Icons.Filled.Stop, + text = stringResource(R.string.system_gateway_stop), + iconTint = MaterialTheme.colorScheme.error, + textColor = MaterialTheme.colorScheme.error, ) } } @@ -915,30 +918,32 @@ private fun LazyListScope.memorySection( FilledTonalButton( onClick = { onResetRequest("memory") }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.Delete, contentDescription = null, modifier = Modifier.size(14.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_memory_reset_memory), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.Delete, + text = stringResource(R.string.system_memory_reset_memory), + ) } FilledTonalButton( onClick = { onResetRequest("user") }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.Delete, contentDescription = null, modifier = Modifier.size(14.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_memory_reset_user), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.Delete, + text = stringResource(R.string.system_memory_reset_user), + ) } FilledTonalButton( onClick = { onResetRequest("all") }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon( - Icons.Filled.DeleteForever, - contentDescription = null, - modifier = Modifier.size(14.dp), + ActionButtonContent( + icon = Icons.Filled.DeleteForever, + text = stringResource(R.string.system_memory_reset_all), ) - 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)) 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)) @@ -1098,26 +1103,32 @@ private fun LazyListScope.operationsSection( viewModel.loadAll() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.HealthAndSafety, contentDescription = null, modifier = Modifier.size(16.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_op_doctor), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.HealthAndSafety, + text = stringResource(R.string.system_op_doctor), + ) } FilledTonalButton( onClick = { viewModel.runSecurityAudit() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.VerifiedUser, contentDescription = null, modifier = Modifier.size(16.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_op_security_audit), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.VerifiedUser, + text = stringResource(R.string.system_op_security_audit), + ) } FilledTonalButton( onClick = { viewModel.runUpdateSkills() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.Refresh, contentDescription = null, modifier = Modifier.size(16.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_op_update_skills), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.Refresh, + text = stringResource(R.string.system_op_update_skills), + ) } } @@ -1131,26 +1142,32 @@ private fun LazyListScope.operationsSection( FilledTonalButton( onClick = { viewModel.runPromptSize() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.HealthAndSafety, contentDescription = null, modifier = Modifier.size(16.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_op_prompt_size), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.HealthAndSafety, + text = stringResource(R.string.system_op_prompt_size), + ) } FilledTonalButton( onClick = { viewModel.runDump() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.Build, contentDescription = null, modifier = Modifier.size(16.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_op_dump), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.Build, + text = stringResource(R.string.system_op_dump), + ) } FilledTonalButton( onClick = { viewModel.runConfigMigrate() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.Build, contentDescription = null, modifier = Modifier.size(16.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_op_config_migrate), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.Build, + text = stringResource(R.string.system_op_config_migrate), + ) } } @@ -1158,7 +1175,7 @@ private fun LazyListScope.operationsSection( // Backup section Text( - text = stringResource(R.string.system_op_backup_create), + text = stringResource(R.string.system_sec_backup), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, ) @@ -1171,26 +1188,30 @@ private fun LazyListScope.operationsSection( Button( onClick = { viewModel.triggerBackup() }, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.Backup, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_op_backup_create), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.Backup, + text = stringResource(R.string.system_op_backup_create), + ) } OutlinedButton( onClick = { viewModel.downloadBackup() }, enabled = state.backupArchive != null, modifier = Modifier.weight(1f), + contentPadding = actionButtonPadding, ) { - Icon(Icons.Filled.CloudDownload, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.size(spacing.xs)) - Text(stringResource(R.string.system_op_backup_download), maxLines = 1) + ActionButtonContent( + icon = Icons.Filled.CloudDownload, + text = stringResource(R.string.system_op_backup_download), + ) } } // Restore from uploaded zip Spacer(modifier = Modifier.height(spacing.sm)) Text( - text = stringResource(R.string.system_op_backup_restore_upload), + text = stringResource(R.string.system_sec_restore), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, ) @@ -1219,7 +1240,7 @@ private fun LazyListScope.operationsSection( ) { 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)) + Text(stringResource(R.string.system_op_restore_upload), maxLines = 1, softWrap = false) } HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm)) @@ -1270,6 +1291,8 @@ private fun LazyListScope.operationsSection( } else { stringResource(R.string.system_debug_share_generate) }, + maxLines = 1, + softWrap = false, ) } @@ -1288,7 +1311,7 @@ private fun LazyListScope.operationsSection( if (share.ok == true) { stringResource(R.string.system_debug_share_uploaded) } else { - stringResource(R.string.system_status_stopped) + stringResource(R.string.system_debug_share_failed) }, 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)) 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() }) { Icon(Icons.Filled.Add, contentDescription = null, modifier = Modifier.size(16.dp)) 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, + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85d0d902..7dd5af03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -396,6 +396,8 @@ Create backup Download backup Restore from backup upload + Backup + Restore from upload Choose restore zip Restore upload Restore from backups path @@ -416,6 +418,7 @@ not redacted auto-deletes in %1$dh Copy all + upload failed Checkpoints