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

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