refactor(#78): extract UI strings for chat, settings, connect, kanban, and skills screens

This commit is contained in:
Hy4ri committed 2026-06-21 11:38:33 +03:00
1 parent acd6cbf751
commit cdad31b555
9 files changed
+348 -149

No files matched your search

@@ -54,6 +54,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
@@ -64,6 +65,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.AssistantBubble
import com.m57.hermescontrol.theme.AssistantBubbleLight
import com.m57.hermescontrol.theme.StatusGreen
@@ -232,7 +234,7 @@ private fun UserBubble(
) {
Icon(
Icons.Filled.ContentCopy,
contentDescription = "Copy",
contentDescription = stringResource(R.string.content_desc_copy),
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
@@ -347,7 +349,7 @@ private fun AssistantBubble(
) {
Icon(
Icons.Filled.ContentCopy,
contentDescription = "Copy",
contentDescription = stringResource(R.string.content_desc_copy),
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
@@ -489,7 +491,7 @@ private fun ParsedToolContent(
}
parsed.stderr?.let {
Text(
text = "Error Output (stderr):\n$it",
text = stringResource(R.string.chat_tool_stderr, it),
style =
MaterialTheme.typography.bodySmall.copy(
color = StatusRed.copy(alpha = 0.9f),
@@ -500,7 +502,7 @@ private fun ParsedToolContent(
}
parsed.error?.let {
Text(
text = "Execution Error: $it",
text = stringResource(R.string.chat_tool_execution_error, it),
style =
MaterialTheme.typography.bodySmall.copy(
color = StatusRed,
@@ -513,7 +515,7 @@ private fun ParsedToolContent(
parsed.exitCode?.let { code ->
if (code != 0) {
Text(
text = "Exit Code: $code",
text = stringResource(R.string.chat_tool_exit_code, code),
style =
MaterialTheme.typography.labelSmall.copy(
color = StatusRed,
@@ -619,7 +621,7 @@ private fun ToolBubble(
}
Text(
text = message.toolName ?: "Tool",
text = message.toolName ?: stringResource(R.string.chat_tool_fallback),
style =
MaterialTheme.typography.labelMedium.copy(
color = contentColor,
@@ -635,7 +637,7 @@ private fun ToolBubble(
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Show Raw JSON",
text = stringResource(R.string.chat_tool_show_raw),
style =
MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.primary,
@@ -660,7 +662,7 @@ private fun ToolBubble(
if (parsedOutput != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Show Parsed Output",
text = stringResource(R.string.chat_tool_show_parsed),
style =
MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.primary,
@@ -293,14 +293,20 @@ fun ChatScreen(
imageVector =
if (state.isSearchActive) Icons.Filled.Close else Icons.Filled.Search,
contentDescription =
if (state.isSearchActive) "Close search" else "Search",
if (state.isSearchActive) {
stringResource(
R.string.chat_action_close_search,
)
} else {
stringResource(R.string.chat_action_search)
},
)
}
IconButton(onClick = { viewModel.createNewSession() }) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "New Chat",
contentDescription = stringResource(R.string.content_desc_new_chat),
)
}
},
@@ -335,9 +341,9 @@ fun ChatScreen(
Text(
text =
if (state.connectionStatus == ConnectionStatus.RECONNECTING) {
"Reconnecting to Hermes…"
stringResource(R.string.chat_status_reconnecting)
} else {
"Disconnected from Hermes"
stringResource(R.string.chat_status_disconnected)
},
style = MaterialTheme.typography.bodyMedium,
)
@@ -349,7 +355,7 @@ fun ChatScreen(
contentColor = MaterialTheme.colorScheme.onErrorContainer,
),
) {
Text("Reconnect")
Text(stringResource(R.string.chat_action_reconnect))
}
}
}
@@ -393,8 +399,8 @@ fun ChatScreen(
) {
if (state.messages.isEmpty() && !state.isLoading) {
EmptyState(
title = "Ready to chat",
subtitle = "Send a message to start a conversation",
title = stringResource(R.string.chat_empty_title),
subtitle = stringResource(R.string.chat_empty_subtitle),
)
}
@@ -482,7 +488,7 @@ fun ChatScreen(
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Connecting to Hermes…",
text = stringResource(R.string.chat_status_connecting),
style = MaterialTheme.typography.bodyMedium,
)
}
@@ -512,7 +518,7 @@ fun ChatScreen(
) {
Icon(
imageVector = Icons.Filled.KeyboardArrowDown,
contentDescription = "Scroll to bottom",
contentDescription = stringResource(R.string.content_desc_scroll_to_bottom),
)
}
}
@@ -584,9 +590,9 @@ private fun ThinkingIndicator(thinkingText: String) {
text =
if (thinkingText.isNotBlank()) {
val display = thinkingText.takeLast(100)
"Thinking: $display…"
stringResource(R.string.chat_thinking_param, display)
} else {
"Thinking…"
stringResource(R.string.chat_thinking)
},
style =
MaterialTheme.typography.bodySmall.copy(
@@ -671,14 +677,14 @@ private fun ChatInputBar(
items(filteredCommands) { cmd ->
val description =
when (cmd) {
"/help" -> "Show help menu"
"/status" -> "Check gateway and platform status"
"/sessions" -> "List all chat sessions"
"/stats" -> "Check system resource usage (stats)"
"/system" -> "Check system resource usage (system)"
"/new" -> "Create a new chat session"
"/stop" -> "Interrupt the active run"
"/interrupt" -> "Interrupt the active run"
"/help" -> stringResource(R.string.command_help_desc)
"/status" -> stringResource(R.string.command_status_desc)
"/sessions" -> stringResource(R.string.command_sessions_desc)
"/stats" -> stringResource(R.string.command_stats_desc)
"/system" -> stringResource(R.string.command_system_desc)
"/new" -> stringResource(R.string.command_new_desc)
"/stop" -> stringResource(R.string.command_stop_desc)
"/interrupt" -> stringResource(R.string.command_stop_desc)
else -> ""
}
DropdownMenuItem(
@@ -725,11 +731,11 @@ private fun ChatInputBar(
placeholder = {
Text(
if (!isConnected) {
"Not connected…"
stringResource(R.string.chat_input_placeholder_not_connected)
} else if (isAgentTyping) {
"Waiting for response…"
stringResource(R.string.chat_input_placeholder_waiting)
} else {
"Type a message…"
stringResource(R.string.chat_input_placeholder_type_message)
},
)
},
@@ -759,7 +765,7 @@ private fun ChatInputBar(
) {
Icon(
imageVector = Icons.Filled.Stop,
contentDescription = "Interrupt",
contentDescription = stringResource(R.string.content_desc_interrupt),
tint = MaterialTheme.colorScheme.error,
)
}
@@ -250,7 +250,15 @@ fun FilterChipRow(
FilterChip(
selected = chip == selectedChip,
onClick = { onChipSelected(chip) },
label = { Text(chip) },
label = {
Text(
if (chip == "All") {
stringResource(R.string.skills_category_all)
} else {
chip
},
)
},
)
}
}
@@ -45,6 +45,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -53,6 +54,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
@Composable
fun ConnectScreen(
@@ -106,7 +108,7 @@ fun ConnectScreen(
}
Text(
text = "Hermes",
text = stringResource(R.string.notif_title),
style =
MaterialTheme.typography.displayMedium.copy(
fontWeight = FontWeight.Bold,
@@ -114,7 +116,7 @@ fun ConnectScreen(
),
)
Text(
text = "AI Agent Control",
text = stringResource(R.string.nav_drawer_subtitle),
style =
MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -134,9 +136,9 @@ fun ConnectScreen(
Text(
text =
if (state.selectedProfile != null) {
"Profile: ${state.selectedProfile!!.name}"
stringResource(R.string.connect_selected_profile, state.selectedProfile!!.name)
} else {
"Select Saved Profile"
stringResource(R.string.connect_action_select_profile)
},
)
}
@@ -146,7 +148,7 @@ fun ConnectScreen(
modifier = Modifier.fillMaxWidth(0.85f),
) {
DropdownMenuItem(
text = { Text("None (Standalone)") },
text = { Text(stringResource(R.string.connect_profile_none)) },
onClick = {
viewModel.selectProfile(null)
profilesExpanded = false
@@ -171,7 +173,7 @@ fun ConnectScreen(
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = "Pairing string",
text = stringResource(R.string.connect_pairing_string),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onBackground,
)
@@ -182,7 +184,7 @@ fun ConnectScreen(
pairingString = it
if (it.isNotBlank()) viewModel.onPairingString(it)
},
placeholder = { Text("Paste hermes://… or Base64 config") },
placeholder = { Text(stringResource(R.string.connect_placeholder_pairing)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
@@ -215,7 +217,7 @@ fun ConnectScreen(
onCheckedChange = viewModel::onSaveProfileChange,
)
Text(
text = "Save connection profile",
text = stringResource(R.string.connect_save_profile),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
)
@@ -225,8 +227,8 @@ fun ConnectScreen(
OutlinedTextField(
value = state.profileName,
onValueChange = viewModel::onProfileNameChange,
label = { Text("Profile Name") },
placeholder = { Text("e.g. Local Hermit, Remote Server") },
label = { Text(stringResource(R.string.connect_profile_name)) },
placeholder = { Text(stringResource(R.string.connect_placeholder_profile_name)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
@@ -247,7 +249,7 @@ fun ConnectScreen(
strokeWidth = 2.dp,
)
} else {
Text("Connect", style = MaterialTheme.typography.labelLarge)
Text(stringResource(R.string.action_connect), style = MaterialTheme.typography.labelLarge)
}
}
@@ -256,7 +258,12 @@ fun ConnectScreen(
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = if (showManualFields) "Hide manual entry" else "Enter manually instead",
text =
if (showManualFields) {
stringResource(R.string.connect_action_hide_manual)
} else {
stringResource(R.string.connect_action_show_manual)
},
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
)
@@ -271,8 +278,8 @@ fun ConnectScreen(
OutlinedTextField(
value = state.token,
onValueChange = viewModel::onTokenChange,
label = { Text("Auth Token") },
placeholder = { Text("API token") },
label = { Text(stringResource(R.string.settings_field_token)) },
placeholder = { Text(stringResource(R.string.connect_placeholder_token)) },
singleLine = true,
visualTransformation =
if (passwordVisible) {
@@ -290,7 +297,11 @@ fun ConnectScreen(
Icons.Filled.VisibilityOff
},
contentDescription =
if (passwordVisible) "Hide token" else "Show token",
if (passwordVisible) {
stringResource(R.string.content_desc_hide_token)
} else {
stringResource(R.string.content_desc_show_token)
},
)
}
},
@@ -299,16 +310,16 @@ fun ConnectScreen(
OutlinedTextField(
value = state.host,
onValueChange = viewModel::onHostChange,
label = { Text("Host") },
placeholder = { Text("127.0.0.1") },
label = { Text(stringResource(R.string.settings_field_host)) },
placeholder = { Text(stringResource(R.string.connect_placeholder_host)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = state.port,
onValueChange = viewModel::onPortChange,
label = { Text("Port") },
placeholder = { Text("9119") },
label = { Text(stringResource(R.string.settings_field_port)) },
placeholder = { Text(stringResource(R.string.connect_placeholder_port)) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
@@ -39,10 +39,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.model.KanbanTask
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -73,7 +75,7 @@ fun KanbanScreen(
}
HermesScaffold(
title = { Text("Kanban Board") },
title = { Text(stringResource(R.string.kanban_board_title)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadBoards() },
@@ -99,10 +101,12 @@ fun KanbanScreen(
Text(
text = state.errorMessage ?: "",
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(16.dp),
modifier = Modifier.padding(paddingValues),
)
} else {
Column(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize().padding(paddingValues),
) {
// Board selector tab row
if (state.boards.isNotEmpty()) {
PrimaryScrollableTabRow(
@@ -121,11 +125,11 @@ fun KanbanScreen(
if (state.selectedBoard == null) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No boards found.")
Text(stringResource(R.string.kanban_no_boards))
}
} else if (state.columns.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No columns found for this board.")
Text(stringResource(R.string.kanban_no_columns))
}
} else {
LazyRow(
@@ -219,7 +223,10 @@ fun TaskCard(
) {
if (onMoveLeft != null) {
IconButton(onClick = onMoveLeft) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Move Left")
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.content_desc_move_left),
)
}
} else {
Spacer(modifier = Modifier.size(48.dp))
@@ -227,7 +234,10 @@ fun TaskCard(
if (onMoveRight != null) {
IconButton(onClick = onMoveRight) {
Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = "Move Right")
Icon(
imageVector = Icons.Filled.ArrowForward,
contentDescription = stringResource(R.string.content_desc_move_right),
)
}
} else {
Spacer(modifier = Modifier.size(48.dp))
@@ -248,20 +258,20 @@ fun AddTaskDialog(
androidx.compose.material3.AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Add New Task") },
title = { Text(stringResource(R.string.kanban_add_new_task)) },
text = {
Column {
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("Task Title") },
label = { Text(stringResource(R.string.kanban_task_title)) },
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = desc,
onValueChange = { desc = it },
label = { Text("Task Description") },
label = { Text(stringResource(R.string.kanban_task_desc)) },
modifier = Modifier.fillMaxWidth(),
)
}
@@ -271,12 +281,12 @@ fun AddTaskDialog(
onClick = { if (title.isNotBlank()) onConfirm(title, desc.ifBlank { null }) },
enabled = title.isNotBlank(),
) {
Text("Add")
Text(stringResource(R.string.action_add))
}
},
dismissButton = {
Button(onClick = onDismiss) {
Text("Cancel")
Text(stringResource(R.string.action_cancel))
}
},
)
@@ -53,6 +53,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -61,6 +62,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.BuildConfig
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -77,7 +79,7 @@ fun SettingsScreen(
BackHandler(onBack = onBack)
HermesScaffold(
title = { Text("Settings") },
title = { Text(stringResource(R.string.screen_settings)) },
showBack = true,
onBack = onBack,
) {
@@ -90,12 +92,12 @@ fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// Connection section
SectionCard(title = "Connection") {
SectionCard(title = stringResource(R.string.settings_sec_connection)) {
// Connection Profiles management in Settings
if (state.profiles.isNotEmpty()) {
var profilesExpanded by remember { mutableStateOf(false) }
Text(
text = "Connection Profile",
text = stringResource(R.string.settings_item_profile),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -105,7 +107,7 @@ fun SettingsScreen(
modifier = Modifier.fillMaxWidth(),
) {
val activeProfile = state.profiles.firstOrNull { it.id == state.selectedProfileId }
Text(text = activeProfile?.name ?: "Standalone / Default")
Text(text = activeProfile?.name ?: stringResource(R.string.settings_profile_default))
}
DropdownMenu(
expanded = profilesExpanded,
@@ -113,7 +115,7 @@ fun SettingsScreen(
modifier = Modifier.fillMaxWidth(0.85f),
) {
DropdownMenuItem(
text = { Text("None (Standalone / Default)") },
text = { Text(stringResource(R.string.settings_profile_none)) },
onClick = {
viewModel.selectProfile(null)
profilesExpanded = false
@@ -136,7 +138,10 @@ fun SettingsScreen(
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Delete Profile",
contentDescription =
stringResource(
R.string.content_desc_delete_profile,
),
tint = MaterialTheme.colorScheme.error,
)
}
@@ -155,7 +160,7 @@ fun SettingsScreen(
OutlinedTextField(
value = state.renameProfileName,
onValueChange = viewModel::onRenameProfileNameChange,
label = { Text("Rename Profile") },
label = { Text(stringResource(R.string.settings_action_rename_profile)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
@@ -163,7 +168,7 @@ fun SettingsScreen(
onClick = viewModel::renameProfile,
modifier = Modifier.padding(end = 4.dp),
) {
Text("Rename")
Text(stringResource(R.string.action_rename))
}
},
)
@@ -174,7 +179,7 @@ fun SettingsScreen(
OutlinedTextField(
value = state.host,
onValueChange = viewModel::onHostChange,
label = { Text("Host") },
label = { Text(stringResource(R.string.settings_field_host)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
colors =
@@ -189,7 +194,7 @@ fun SettingsScreen(
OutlinedTextField(
value = state.port,
onValueChange = viewModel::onPortChange,
label = { Text("Port") },
label = { Text(stringResource(R.string.settings_field_port)) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
@@ -205,7 +210,7 @@ fun SettingsScreen(
OutlinedTextField(
value = state.token,
onValueChange = viewModel::onTokenChange,
label = { Text("Auth Token") },
label = { Text(stringResource(R.string.settings_field_token)) },
singleLine = true,
visualTransformation =
if (passwordVisible) {
@@ -224,9 +229,9 @@ fun SettingsScreen(
},
contentDescription =
if (passwordVisible) {
"Hide token"
stringResource(R.string.content_desc_hide_token)
} else {
"Show token"
stringResource(R.string.content_desc_show_token)
},
)
}
@@ -253,12 +258,12 @@ fun SettingsScreen(
},
modifier = Modifier.fillMaxWidth(),
) {
Text("Clear Token")
Text(stringResource(R.string.settings_action_clear_token))
}
}
// Behavior section
SectionCard(title = "Behavior") {
SectionCard(title = stringResource(R.string.settings_sec_behavior)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -266,11 +271,11 @@ fun SettingsScreen(
) {
Column {
Text(
text = "Auto-Reconnect",
text = stringResource(R.string.settings_item_auto_reconnect),
style = MaterialTheme.typography.bodyLarge,
)
Text(
text = "Automatically reconnect on disconnect",
text = stringResource(R.string.settings_desc_auto_reconnect),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -290,9 +295,9 @@ fun SettingsScreen(
}
// Appearance section
SectionCard(title = "Appearance") {
SectionCard(title = stringResource(R.string.settings_sec_appearance)) {
Text(
text = "Theme",
text = stringResource(R.string.settings_item_theme),
style = MaterialTheme.typography.bodyLarge,
)
Spacer(modifier = Modifier.height(8.dp))
@@ -308,9 +313,11 @@ fun SettingsScreen(
),
) {
Text(
pref.name
.lowercase()
.replaceFirstChar { it.uppercase() },
when (pref) {
ThemePreference.SYSTEM -> stringResource(R.string.theme_system)
ThemePreference.LIGHT -> stringResource(R.string.theme_light)
ThemePreference.DARK -> stringResource(R.string.theme_dark)
},
)
}
}
@@ -318,7 +325,7 @@ fun SettingsScreen(
}
// Chat section
SectionCard(title = "Chat") {
SectionCard(title = stringResource(R.string.settings_sec_chat)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -326,11 +333,11 @@ fun SettingsScreen(
) {
Column {
Text(
text = "Typing Effect",
text = stringResource(R.string.settings_item_typing_effect),
style = MaterialTheme.typography.bodyLarge,
)
Text(
text = "Animate new messages word by word",
text = stringResource(R.string.settings_desc_typing_effect),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -352,7 +359,7 @@ fun SettingsScreen(
AnimatedVisibility(visible = state.typingEffectEnabled) {
Column(modifier = Modifier.padding(top = 12.dp)) {
Text(
text = "Delay: ${state.typingEffectDelayMs} ms",
text = stringResource(R.string.settings_item_typing_delay, state.typingEffectDelayMs),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(modifier = Modifier.height(4.dp))
@@ -368,14 +375,14 @@ fun SettingsScreen(
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = "10 ms",
text = stringResource(R.string.settings_delay_10ms),
style =
MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
),
)
Text(
text = "100 ms",
text = stringResource(R.string.settings_delay_100ms),
style =
MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -387,15 +394,16 @@ fun SettingsScreen(
}
// Navigation Bar section
SectionCard(title = "Navigation Bar") {
SectionCard(title = stringResource(R.string.settings_sec_nav_bar)) {
Text(
text = "Bottom bar items (${state.selectedNavItems.size}/5)",
text = stringResource(R.string.settings_item_nav_items, state.selectedNavItems.size),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(modifier = Modifier.height(8.dp))
state.selectedNavItems.forEachIndexed { index, name ->
val label = viewModel.availableNavItems.firstOrNull { it.first == name }?.second ?: name
val labelRes = viewModel.availableNavItems.firstOrNull { it.first == name }?.second
val label = if (labelRes != null) stringResource(labelRes) else name
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -417,7 +425,10 @@ fun SettingsScreen(
},
enabled = index > 0,
) {
Icon(Icons.Filled.KeyboardArrowUp, contentDescription = "Move up")
Icon(
Icons.Filled.KeyboardArrowUp,
contentDescription = stringResource(R.string.content_desc_move_up),
)
}
IconButton(
onClick = {
@@ -429,10 +440,16 @@ fun SettingsScreen(
},
enabled = index < state.selectedNavItems.lastIndex,
) {
Icon(Icons.Filled.KeyboardArrowDown, contentDescription = "Move down")
Icon(
Icons.Filled.KeyboardArrowDown,
contentDescription = stringResource(R.string.content_desc_move_down),
)
}
IconButton(onClick = { viewModel.removeNavItem(name) }) {
Icon(Icons.Filled.Close, contentDescription = "Remove $label")
Icon(
Icons.Filled.Close,
contentDescription = stringResource(R.string.content_desc_remove_item, label),
)
}
}
}
@@ -446,12 +463,12 @@ fun SettingsScreen(
OutlinedButton(onClick = { expanded = true }) {
Icon(Icons.Filled.Add, contentDescription = null)
Spacer(Modifier.size(4.dp))
Text("Add Item")
Text(stringResource(R.string.settings_action_add_item))
}
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
available.forEach { (name, label) ->
available.forEach { (name, labelRes) ->
DropdownMenuItem(
text = { Text(label) },
text = { Text(stringResource(labelRes)) },
onClick = {
viewModel.addNavItem(name)
expanded = false
@@ -463,7 +480,7 @@ fun SettingsScreen(
}
} else {
Text(
text = "Maximum of 5 items",
text = stringResource(R.string.settings_nav_max_reached),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -505,7 +522,7 @@ fun SettingsScreen(
exit = fadeOut(),
) {
Text(
text = "✅ Settings saved",
text = stringResource(R.string.settings_save_success),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.primary,
@@ -529,7 +546,7 @@ fun SettingsScreen(
strokeWidth = 2.dp,
)
} else {
Text("Test Connection")
Text(stringResource(R.string.settings_action_test_connection))
}
}
@@ -541,17 +558,17 @@ fun SettingsScreen(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text("Save")
Text(stringResource(R.string.action_save))
}
}
Spacer(modifier = Modifier.height(16.dp))
// ── About section ─────────────────────────────────────────
SectionCard(title = "About") {
SectionCard(title = stringResource(R.string.settings_sec_about)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "⚡ Hermes Control",
text = stringResource(R.string.settings_about_app_name),
style =
MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
@@ -563,13 +580,21 @@ fun SettingsScreen(
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(8.dp))
InfoRow(label = "Version", value = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
InfoRow(
label = "Build",
value = if (BuildConfig.DEBUG) "Debug" else "Release",
label = stringResource(R.string.settings_about_version),
value = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
)
InfoRow(
label = stringResource(R.string.settings_about_build),
value =
if (BuildConfig.DEBUG) {
stringResource(R.string.settings_about_debug)
} else {
stringResource(R.string.settings_about_release)
},
)
if (BuildConfig.GIT_SHA.isNotBlank()) {
InfoRow(label = "Commit", value = BuildConfig.GIT_SHA)
InfoRow(label = stringResource(R.string.settings_about_commit), value = BuildConfig.GIT_SHA)
}
Spacer(modifier = Modifier.height(12.dp))
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.NetworkResult
@@ -129,28 +130,28 @@ class SettingsViewModel : ViewModel() {
}
/** All screens available for bottom-nav selection (name → display label). */
val availableNavItems: List<Pair<String, String>> =
val availableNavItems: List<Pair<String, Int>> =
listOf(
"ChatScreen" to "Chat",
"SkillsScreen" to "Skills",
"CronJobsScreen" to "Cron Jobs",
"SystemScreen" to "System",
"SettingsScreen" to "Settings",
"ProfilesScreen" to "Profiles",
"WebhooksScreen" to "Webhooks",
"GatewayScreen" to "Gateway",
"HistoryScreen" to "History",
"ToolsetsScreen" to "Toolsets",
"PluginsScreen" to "Plugins",
"ConfigScreen" to "Config",
"McpServersScreen" to "MCP Servers",
"ModelScreen" to "Models",
"PairingScreen" to "Pairing",
"KeysScreen" to "Keys",
"ChannelsScreen" to "Channels",
"LogsScreen" to "Logs",
"KanbanScreen" to "Kanban",
"AchievementsScreen" to "Achievements",
"ChatScreen" to R.string.screen_chat,
"SkillsScreen" to R.string.screen_skills,
"CronJobsScreen" to R.string.screen_cron,
"SystemScreen" to R.string.screen_system,
"SettingsScreen" to R.string.screen_settings,
"ProfilesScreen" to R.string.screen_profiles,
"WebhooksScreen" to R.string.screen_webhooks,
"GatewayScreen" to R.string.screen_gateway,
"HistoryScreen" to R.string.screen_history,
"ToolsetsScreen" to R.string.screen_toolsets,
"PluginsScreen" to R.string.screen_plugins,
"ConfigScreen" to R.string.screen_config,
"McpServersScreen" to R.string.screen_mcp_servers,
"ModelScreen" to R.string.screen_models,
"PairingScreen" to R.string.screen_pairing,
"KeysScreen" to R.string.screen_keys,
"ChannelsScreen" to R.string.screen_channels,
"LogsScreen" to R.string.screen_logs,
"KanbanScreen" to R.string.screen_kanban,
"AchievementsScreen" to R.string.screen_achievements,
)
/** Add a nav item to the bottom bar (max 5). Auto-saves. */
@@ -35,6 +35,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
@@ -43,6 +44,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
@@ -117,7 +119,7 @@ fun SkillsScreen(
}
HermesScaffold(
title = { Text("Skills") },
title = { Text(stringResource(R.string.screen_skills)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadSkills() },
@@ -129,15 +131,15 @@ fun SkillsScreen(
state.errorMessage != null -> {
ErrorState(
message = state.errorMessage ?: "Unknown error",
message = state.errorMessage ?: stringResource(R.string.error_unknown),
onRetry = { viewModel.loadSkills() },
)
}
state.skills.isEmpty() -> {
EmptyState(
title = "No skills found",
subtitle = "Skills loaded from Hermes will appear here.",
title = stringResource(R.string.skills_no_skills),
subtitle = stringResource(R.string.skills_no_skills_desc),
icon = Icons.Filled.Extension,
)
}
@@ -152,7 +154,7 @@ fun SkillsScreen(
SearchBar(
query = query,
onQueryChange = { query = it },
placeholder = "Search skills…",
placeholder = stringResource(R.string.skills_search_placeholder),
)
}
item {
@@ -164,7 +166,7 @@ fun SkillsScreen(
verticalArrangement = Arrangement.spacedBy(spacing.xs),
) {
Text(
text = "Status",
text = stringResource(R.string.skills_filter_status),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -175,7 +177,16 @@ fun SkillsScreen(
FilterChip(
selected = selectedStatus == status,
onClick = { selectedStatus = status },
label = { Text(status) },
label = {
Text(
when (status) {
"All Statuses" -> stringResource(R.string.skills_status_all)
"Enabled" -> stringResource(R.string.skills_status_enabled)
"Disabled" -> stringResource(R.string.skills_status_disabled)
else -> status
},
)
},
)
}
}
@@ -191,7 +202,7 @@ fun SkillsScreen(
verticalArrangement = Arrangement.spacedBy(spacing.xs),
) {
Text(
text = "Category",
text = stringResource(R.string.skills_filter_category),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = spacing.md),
@@ -218,14 +229,14 @@ fun SkillsScreen(
verticalArrangement = Arrangement.spacedBy(spacing.xs),
) {
Text(
text = "No matching skills",
text = stringResource(R.string.skills_no_matching),
style =
MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
),
)
Text(
text = "Try adjusting your search or filters.",
text = stringResource(R.string.skills_no_matching_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -293,7 +304,7 @@ fun SkillsScreen(
) {
Icon(
imageVector = Icons.Filled.Edit,
contentDescription = "Edit Skill File",
contentDescription = stringResource(R.string.content_desc_edit_skill),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
@@ -373,7 +384,7 @@ fun SkillEditorDialog(
),
) {
HermesScaffold(
title = { Text("Edit: $skillName") },
title = { Text(stringResource(R.string.skills_edit_title, skillName)) },
onBack = {
if (hasChanges) {
showDiscardConfirm = true
@@ -397,7 +408,7 @@ fun SkillEditorDialog(
} else {
Icon(
imageVector = Icons.Filled.Save,
contentDescription = "Save Changes",
contentDescription = stringResource(R.string.content_desc_save_changes),
)
}
}
@@ -429,7 +440,7 @@ fun SkillEditorDialog(
color = MaterialTheme.colorScheme.onSurface,
),
placeholder = {
Text("Enter skill content...")
Text(stringResource(R.string.skills_content_placeholder))
},
)
}
@@ -442,8 +453,8 @@ fun SkillEditorDialog(
if (showDiscardConfirm) {
AlertDialog(
onDismissRequest = { showDiscardConfirm = false },
title = { Text("Discard changes?") },
text = { Text("You have unsaved changes. Are you sure you want to discard them?") },
title = { Text(stringResource(R.string.skills_discard_title)) },
text = { Text(stringResource(R.string.skills_discard_message)) },
confirmButton = {
TextButton(
onClick = {
@@ -451,12 +462,12 @@ fun SkillEditorDialog(
onDismiss()
},
) {
Text("Discard")
Text(stringResource(R.string.action_discard))
}
},
dismissButton = {
TextButton(onClick = { showDiscardConfirm = false }) {
Text("Cancel")
Text(stringResource(R.string.action_cancel))
}
},
)
+125
View File
@@ -70,4 +70,129 @@
<string name="notif_channel_chat_name">Hermes Chat Replies</string>
<string name="notif_channel_chat_desc">Alerts for new replies from Hermes</string>
<string name="action_reply">Reply</string>
<!-- Chat Screen & Bubble -->
<string name="chat_action_search">Search</string>
<string name="chat_action_close_search">Close search</string>
<string name="chat_action_reconnect">Reconnect</string>
<string name="chat_empty_title">Ready to chat</string>
<string name="chat_empty_subtitle">Send a message to start a conversation</string>
<string name="chat_status_connecting">Connecting to Hermes\u2026</string>
<string name="chat_status_reconnecting">Reconnecting to Hermes\u2026</string>
<string name="chat_status_disconnected">Disconnected from Hermes</string>
<string name="chat_thinking">Thinking\u2026</string>
<string name="chat_thinking_param">Thinking: %1$s\u2026</string>
<string name="chat_input_placeholder_not_connected">Not connected\u2026</string>
<string name="chat_input_placeholder_waiting">Waiting for response\u2026</string>
<string name="chat_input_placeholder_type_message">Type a message\u2026</string>
<string name="chat_tool_stderr">Error Output (stderr):\n%1$s</string>
<string name="chat_tool_execution_error">Execution Error: %1$s</string>
<string name="chat_tool_exit_code">Exit Code: %1$d</string>
<string name="chat_tool_fallback">Tool</string>
<string name="chat_tool_show_raw">Show Raw JSON</string>
<string name="chat_tool_show_parsed">Show Parsed Output</string>
<string name="command_help_desc">Show help menu</string>
<string name="command_status_desc">Check gateway and platform status</string>
<string name="command_sessions_desc">List all chat sessions</string>
<string name="command_stats_desc">Check system resource usage (stats)</string>
<string name="command_system_desc">Check system resource usage (system)</string>
<string name="command_new_desc">Create a new chat session</string>
<string name="command_stop_desc">Interrupt the active run</string>
<string name="content_desc_new_chat">New Chat</string>
<string name="content_desc_scroll_to_bottom">Scroll to bottom</string>
<string name="content_desc_interrupt">Interrupt</string>
<string name="content_desc_copy">Copy</string>
<!-- Settings Screen -->
<string name="settings_sec_connection">Connection</string>
<string name="settings_item_profile">Connection Profile</string>
<string name="settings_profile_default">Standalone / Default</string>
<string name="settings_profile_none">None (Standalone / Default)</string>
<string name="settings_action_rename_profile">Rename Profile</string>
<string name="settings_field_host">Host</string>
<string name="settings_field_port">Port</string>
<string name="settings_field_token">Auth Token</string>
<string name="settings_action_clear_token">Clear Token</string>
<string name="settings_sec_behavior">Behavior</string>
<string name="settings_item_auto_reconnect">Auto-Reconnect</string>
<string name="settings_desc_auto_reconnect">Automatically reconnect on disconnect</string>
<string name="settings_sec_appearance">Appearance</string>
<string name="settings_item_theme">Theme</string>
<string name="settings_sec_chat">Chat</string>
<string name="settings_item_typing_effect">Typing Effect</string>
<string name="settings_desc_typing_effect">Animate new messages word by word</string>
<string name="settings_item_typing_delay">Delay: %1$d ms</string>
<string name="settings_delay_10ms">10 ms</string>
<string name="settings_delay_100ms">100 ms</string>
<string name="settings_sec_nav_bar">Navigation Bar</string>
<string name="settings_item_nav_items">Bottom bar items (%1$d/5)</string>
<string name="settings_action_add_item">Add Item</string>
<string name="settings_nav_max_reached">Maximum of 5 items</string>
<string name="settings_save_success">✅ Settings saved</string>
<string name="settings_action_test_connection">Test Connection</string>
<string name="settings_sec_about">About</string>
<string name="settings_about_app_name">⚡ Hermes Control</string>
<string name="settings_about_version">Version</string>
<string name="settings_about_build">Build</string>
<string name="settings_about_debug">Debug</string>
<string name="settings_about_release">Release</string>
<string name="settings_about_commit">Commit</string>
<string name="action_rename">Rename</string>
<string name="action_save">Save</string>
<string name="content_desc_delete_profile">Delete Profile</string>
<string name="content_desc_hide_token">Hide token</string>
<string name="content_desc_show_token">Show token</string>
<string name="content_desc_move_up">Move up</string>
<string name="content_desc_move_down">Move down</string>
<string name="content_desc_remove_item">Remove %1$s</string>
<string name="theme_system">System</string>
<string name="theme_light">Light</string>
<string name="theme_dark">Dark</string>
<!-- Connect Screen -->
<string name="connect_selected_profile">Profile: %1$s</string>
<string name="connect_action_select_profile">Select Saved Profile</string>
<string name="connect_profile_none">None (Standalone)</string>
<string name="connect_pairing_string">Pairing string</string>
<string name="connect_placeholder_pairing">Paste hermes://\u2026 or Base64 config</string>
<string name="connect_save_profile">Save connection profile</string>
<string name="connect_profile_name">Profile Name</string>
<string name="connect_placeholder_profile_name">e.g. Local Hermit, Remote Server</string>
<string name="connect_action_hide_manual">Hide manual entry</string>
<string name="connect_action_show_manual">Enter manually instead</string>
<string name="connect_placeholder_token">API token</string>
<string name="connect_placeholder_host">127.0.0.1</string>
<string name="connect_placeholder_port">9119</string>
<string name="action_connect">Connect</string>
<!-- Kanban Screen -->
<string name="kanban_board_title">Kanban Board</string>
<string name="kanban_no_boards">No boards found.</string>
<string name="kanban_no_columns">No columns found for this board.</string>
<string name="kanban_add_new_task">Add New Task</string>
<string name="kanban_task_title">Task Title</string>
<string name="kanban_task_desc">Task Description</string>
<string name="action_add">Add</string>
<string name="content_desc_move_left">Move Left</string>
<string name="content_desc_move_right">Move Right</string>
<!-- Skills Screen -->
<string name="skills_status_all">All Statuses</string>
<string name="skills_status_enabled">Enabled</string>
<string name="skills_status_disabled">Disabled</string>
<string name="skills_category_all">All</string>
<string name="skills_no_skills">No skills found</string>
<string name="skills_no_skills_desc">Skills loaded from Hermes will appear here.</string>
<string name="skills_search_placeholder">Search skills\u2026</string>
<string name="skills_filter_status">Status</string>
<string name="skills_filter_category">Category</string>
<string name="skills_no_matching">No matching skills</string>
<string name="skills_no_matching_desc">Try adjusting your search or filters.</string>
<string name="skills_edit_title">Edit: %1$s</string>
<string name="skills_content_placeholder">Enter skill content\u2026</string>
<string name="skills_discard_title">Discard changes?</string>
<string name="skills_discard_message">You have unsaved changes. Are you sure you want to discard them?</string>
<string name="action_discard">Discard</string>
<string name="content_desc_edit_skill">Edit Skill File</string>
<string name="content_desc_save_changes">Save Changes</string>
</resources>