From 19d44c077b480dc8c1832e44a2a9f42d31395408 Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Sun, 21 Jun 2026 10:57:18 +0300 Subject: [PATCH 01/10] chore: format codebase with ktlint --- .../hermescontrol/data/local/AuthManager.kt | 4 +-- .../hermescontrol/ui/chat/ChatViewModel.kt | 34 +++++++++++++++---- .../ui/settings/SettingsViewModel.kt | 9 ++--- .../hermescontrol/ui/skills/SkillsScreen.kt | 6 +++- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt index 7624f819..0a39a7f1 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt @@ -95,9 +95,7 @@ object AuthManager { requirePrefs().edit().putString(KEY_CONNECTION_PROFILES, json).apply() } - fun getProfileToken(profileId: String): String? { - return requirePrefs().getString("token_$profileId", null) - } + fun getProfileToken(profileId: String): String? = requirePrefs().getString("token_$profileId", null) fun setProfileToken( profileId: String, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt index 61627288..853cd79c 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt @@ -150,13 +150,33 @@ class ChatViewModel( is WsEvent.SessionInfo -> {} - is WsEvent.MessageStart -> handleMessageStart(event) - is WsEvent.MessageToken -> handleMessageToken(event) - is WsEvent.ThinkingDelta -> handleThinkingDelta(event) - is WsEvent.MessageComplete -> handleMessageComplete(event) - is WsEvent.MessageDone -> handleMessageDone(event) - is WsEvent.ToolStart -> handleToolStart(event) - is WsEvent.ToolComplete -> handleToolComplete(event) + is WsEvent.MessageStart -> { + handleMessageStart(event) + } + + is WsEvent.MessageToken -> { + handleMessageToken(event) + } + + is WsEvent.ThinkingDelta -> { + handleThinkingDelta(event) + } + + is WsEvent.MessageComplete -> { + handleMessageComplete(event) + } + + is WsEvent.MessageDone -> { + handleMessageDone(event) + } + + is WsEvent.ToolStart -> { + handleToolStart(event) + } + + is WsEvent.ToolComplete -> { + handleToolComplete(event) + } is WsEvent.ClarifyRequest -> { if (!isCurrentSession(event.sessionId)) return diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt index e08f0ae1..362deb31 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt @@ -58,10 +58,11 @@ class SettingsViewModel : ViewModel() { profiles = AuthManager.getConnectionProfiles(), selectedProfileId = selectedId, renameProfileName = - AuthManager.getConnectionProfiles().firstOrNull { - p -> - p.id == selectedId - }?.name ?: "", + AuthManager + .getConnectionProfiles() + .firstOrNull { p -> + p.id == selectedId + }?.name ?: "", ) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt index dd030a66..a8b3ca54 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt @@ -67,7 +67,11 @@ fun SkillsScreen( var selectedStatus by remember { mutableStateOf("All Statuses") } val categories = remember(state.skills) { - listOf("All") + state.skills.mapNotNull { it.category }.distinct().sorted() + listOf("All") + + state.skills + .mapNotNull { it.category } + .distinct() + .sorted() } var selectedCategory by remember { mutableStateOf("All") } From 8efbe02c270fc28800bacab0356cfc82a2cd3e8c Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Sun, 21 Jun 2026 11:00:20 +0300 Subject: [PATCH 02/10] refactor(#78): extract shared components and common strings to resources --- .../m57/hermescontrol/ui/common/HermesScaffold.kt | 8 +++++--- .../hermescontrol/ui/common/SharedComponents.kt | 4 +++- .../com/m57/hermescontrol/ui/common/StateViews.kt | 4 +++- app/src/main/res/values/strings.xml | 15 +++++++++++++++ 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/common/HermesScaffold.kt b/app/src/main/java/com/m57/hermescontrol/ui/common/HermesScaffold.kt index 5a463193..41c2565b 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/common/HermesScaffold.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/common/HermesScaffold.kt @@ -21,6 +21,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import com.m57.hermescontrol.R /** * Shared Scaffold wrapper that standardizes the TopAppBar. @@ -69,7 +71,7 @@ fun HermesScaffold( IconButton(onClick = onBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.content_desc_back), modifier = Modifier.testTag("back_button"), ) } @@ -79,7 +81,7 @@ fun HermesScaffold( IconButton(onClick = onOpenDrawer) { Icon( imageVector = Icons.Filled.Menu, - contentDescription = "Open navigation drawer", + contentDescription = stringResource(R.string.content_desc_open_drawer), modifier = Modifier.testTag("menu_button"), ) } @@ -91,7 +93,7 @@ fun HermesScaffold( IconButton(onClick = onRefresh) { Icon( imageVector = Icons.Filled.Refresh, - contentDescription = "Refresh", + contentDescription = stringResource(R.string.content_desc_refresh), modifier = Modifier.testTag("refresh_button"), ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt b/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt index b481e37d..8f527582 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt @@ -31,8 +31,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.m57.hermescontrol.R import com.m57.hermescontrol.theme.LocalHermesStatusColors import com.m57.hermescontrol.theme.LocalSpacing @@ -215,7 +217,7 @@ fun SearchBar( IconButton(onClick = { onQueryChange("") }) { Icon( imageVector = Icons.Filled.Close, - contentDescription = "Clear search", + contentDescription = stringResource(R.string.content_desc_clear_search), ) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt b/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt index 7f85f11b..1c310c83 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt @@ -23,8 +23,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.m57.hermescontrol.R import com.m57.hermescontrol.theme.LocalSpacing // Unified loading / error / empty / skeleton placeholders so every screen @@ -94,7 +96,7 @@ fun ErrorState( modifier = Modifier.size(18.dp), ) Spacer(modifier = Modifier.size(spacing.sm)) - Text("Retry") + Text(stringResource(R.string.action_retry)) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a7124074..fb78a1fc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,4 +13,19 @@ Previous match Next match Close search + + + Retry + Cancel + OK + Delete + + + Back + Open navigation drawer + Refresh + Clear search + + + Unknown error \ No newline at end of file From c34f6f1e475174206598f76c69464c3223f34d7d Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Sun, 21 Jun 2026 11:09:44 +0300 Subject: [PATCH 03/10] refactor(#78): extract navigation and drawer labels to string resources --- .../java/com/m57/hermescontrol/Navigation.kt | 125 ++++++++++-------- app/src/main/res/values/strings.xml | 30 +++++ 2 files changed, 100 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/Navigation.kt b/app/src/main/java/com/m57/hermescontrol/Navigation.kt index 87d91847..5737f09e 100644 --- a/app/src/main/java/com/m57/hermescontrol/Navigation.kt +++ b/app/src/main/java/com/m57/hermescontrol/Navigation.kt @@ -1,5 +1,6 @@ package com.m57.hermescontrol +import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -58,12 +59,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay +import com.m57.hermescontrol.R import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.ws.ConnectionStatus import com.m57.hermescontrol.data.ws.HermesWsClient @@ -94,33 +97,33 @@ import com.m57.hermescontrol.ui.webhooks.WebhooksScreen as WebhooksScreenContent private data class BottomNavItem( val key: NavKey, - val label: String, + @param:StringRes val labelRes: Int, val icon: ImageVector, ) /** Master list of ALL screens available for bottom-nav selection. */ private val ALL_NAV_ITEMS: List = listOf( - BottomNavItem(ChatScreen, "Chat", Icons.AutoMirrored.Filled.Chat), - BottomNavItem(SkillsScreen, "Skills", Icons.Filled.Extension), - BottomNavItem(CronJobsScreen, "Cron", Icons.Filled.Schedule), - BottomNavItem(SystemScreen, "System", Icons.Filled.Info), - BottomNavItem(SettingsScreen, "Settings", Icons.Filled.Settings), - BottomNavItem(ProfilesScreen, "Profiles", Icons.Filled.AccountCircle), - BottomNavItem(WebhooksScreen, "Webhooks", Icons.Filled.Webhook), - BottomNavItem(GatewayScreen, "Gateway", Icons.Filled.Bolt), - BottomNavItem(ToolsetsScreen, "Toolsets", Icons.Filled.Build), - BottomNavItem(PluginsScreen, "Plugins", Icons.Filled.Memory), - BottomNavItem(ConfigScreen, "Config", Icons.Filled.Code), - BottomNavItem(McpServersScreen, "MCP Servers", Icons.Filled.Dashboard), - BottomNavItem(ModelScreen, "Models", Icons.Filled.Settings), - BottomNavItem(PairingScreen, "Pairing", Icons.Filled.Devices), - BottomNavItem(KeysScreen, "Keys", Icons.Filled.Key), - BottomNavItem(ChannelsScreen, "Channels", Icons.AutoMirrored.Filled.ListAlt), - BottomNavItem(LogsScreen, "Logs", Icons.Filled.HistoryEdu), - BottomNavItem(KanbanScreen, "Kanban", Icons.Filled.Dashboard), - BottomNavItem(AchievementsScreen, "Achievements", Icons.Filled.Info), - BottomNavItem(HistoryScreen, "History", Icons.Filled.History), + BottomNavItem(ChatScreen, R.string.screen_chat, Icons.AutoMirrored.Filled.Chat), + BottomNavItem(SkillsScreen, R.string.screen_skills, Icons.Filled.Extension), + BottomNavItem(CronJobsScreen, R.string.screen_cron, Icons.Filled.Schedule), + BottomNavItem(SystemScreen, R.string.screen_system, Icons.Filled.Info), + BottomNavItem(SettingsScreen, R.string.screen_settings, Icons.Filled.Settings), + BottomNavItem(ProfilesScreen, R.string.screen_profiles, Icons.Filled.AccountCircle), + BottomNavItem(WebhooksScreen, R.string.screen_webhooks, Icons.Filled.Webhook), + BottomNavItem(GatewayScreen, R.string.screen_gateway, Icons.Filled.Bolt), + BottomNavItem(ToolsetsScreen, R.string.screen_toolsets, Icons.Filled.Build), + BottomNavItem(PluginsScreen, R.string.screen_plugins, Icons.Filled.Memory), + BottomNavItem(ConfigScreen, R.string.screen_config, Icons.Filled.Code), + BottomNavItem(McpServersScreen, R.string.screen_mcp_servers, Icons.Filled.Dashboard), + BottomNavItem(ModelScreen, R.string.screen_models, Icons.Filled.Settings), + BottomNavItem(PairingScreen, R.string.screen_pairing, Icons.Filled.Devices), + BottomNavItem(KeysScreen, R.string.screen_keys, Icons.Filled.Key), + BottomNavItem(ChannelsScreen, R.string.screen_channels, Icons.AutoMirrored.Filled.ListAlt), + BottomNavItem(LogsScreen, R.string.screen_logs, Icons.Filled.HistoryEdu), + BottomNavItem(KanbanScreen, R.string.screen_kanban, Icons.Filled.Dashboard), + BottomNavItem(AchievementsScreen, R.string.screen_achievements, Icons.Filled.Info), + BottomNavItem(HistoryScreen, R.string.screen_history, Icons.Filled.History), ) /** Lookup: data-object simple name → NavKey (used by bottom-nav config). */ @@ -134,17 +137,17 @@ private fun resolveBottomNavItems(names: List): List = // ── Drawer sections ──────────────────────────────────────────────────── private enum class DrawerSection( - val title: String, + @param:StringRes val titleRes: Int, ) { - CONVERSE("Converse"), - AUTOMATE("Automate"), - CONFIGURE("Configure"), - INSPECT("Inspect"), + CONVERSE(R.string.nav_drawer_section_converse), + AUTOMATE(R.string.nav_drawer_section_automate), + CONFIGURE(R.string.nav_drawer_section_configure), + INSPECT(R.string.nav_drawer_section_inspect), } private data class DrawerEntry( val key: NavKey, - val label: String, + @param:StringRes val labelRes: Int, val icon: ImageVector, val section: DrawerSection, ) @@ -152,29 +155,34 @@ private data class DrawerEntry( private val DRAWER_ENTRIES = listOf( // Converse - DrawerEntry(ChatScreen, "Chat", Icons.AutoMirrored.Filled.Chat, DrawerSection.CONVERSE), - DrawerEntry(HistoryScreen, "History", Icons.Filled.History, DrawerSection.CONVERSE), - DrawerEntry(ProfilesScreen, "Profiles", Icons.Filled.AccountCircle, DrawerSection.CONVERSE), + DrawerEntry(ChatScreen, R.string.screen_chat, Icons.AutoMirrored.Filled.Chat, DrawerSection.CONVERSE), + DrawerEntry(HistoryScreen, R.string.screen_history, Icons.Filled.History, DrawerSection.CONVERSE), + DrawerEntry(ProfilesScreen, R.string.screen_profiles, Icons.Filled.AccountCircle, DrawerSection.CONVERSE), // Automate - DrawerEntry(CronJobsScreen, "Cron", Icons.Filled.Schedule, DrawerSection.AUTOMATE), - DrawerEntry(WebhooksScreen, "Webhooks", Icons.Filled.Webhook, DrawerSection.AUTOMATE), - DrawerEntry(GatewayScreen, "Gateway", Icons.Filled.Bolt, DrawerSection.AUTOMATE), + DrawerEntry(CronJobsScreen, R.string.screen_cron, Icons.Filled.Schedule, DrawerSection.AUTOMATE), + DrawerEntry(WebhooksScreen, R.string.screen_webhooks, Icons.Filled.Webhook, DrawerSection.AUTOMATE), + DrawerEntry(GatewayScreen, R.string.screen_gateway, Icons.Filled.Bolt, DrawerSection.AUTOMATE), // Configure - DrawerEntry(SkillsScreen, "Skills", Icons.Filled.Extension, DrawerSection.CONFIGURE), - DrawerEntry(SettingsScreen, "Settings", Icons.Filled.Settings, DrawerSection.CONFIGURE), - DrawerEntry(ToolsetsScreen, "Toolsets", Icons.Filled.Build, DrawerSection.CONFIGURE), - DrawerEntry(PluginsScreen, "Plugins", Icons.Filled.Memory, DrawerSection.CONFIGURE), - DrawerEntry(ConfigScreen, "Config", Icons.Filled.Code, DrawerSection.CONFIGURE), - DrawerEntry(McpServersScreen, "MCP Servers", Icons.Filled.Dashboard, DrawerSection.CONFIGURE), - DrawerEntry(ModelScreen, "Models", Icons.Filled.Settings, DrawerSection.CONFIGURE), - DrawerEntry(PairingScreen, "Pairing", Icons.Filled.Devices, DrawerSection.CONFIGURE), - DrawerEntry(KeysScreen, "Keys", Icons.Filled.Key, DrawerSection.CONFIGURE), - DrawerEntry(ChannelsScreen, "Channels", Icons.AutoMirrored.Filled.ListAlt, DrawerSection.CONFIGURE), + DrawerEntry(SkillsScreen, R.string.screen_skills, Icons.Filled.Extension, DrawerSection.CONFIGURE), + DrawerEntry(SettingsScreen, R.string.screen_settings, Icons.Filled.Settings, DrawerSection.CONFIGURE), + DrawerEntry(ToolsetsScreen, R.string.screen_toolsets, Icons.Filled.Build, DrawerSection.CONFIGURE), + DrawerEntry(PluginsScreen, R.string.screen_plugins, Icons.Filled.Memory, DrawerSection.CONFIGURE), + DrawerEntry(ConfigScreen, R.string.screen_config, Icons.Filled.Code, DrawerSection.CONFIGURE), + DrawerEntry(McpServersScreen, R.string.screen_mcp_servers, Icons.Filled.Dashboard, DrawerSection.CONFIGURE), + DrawerEntry(ModelScreen, R.string.screen_models, Icons.Filled.Settings, DrawerSection.CONFIGURE), + DrawerEntry(PairingScreen, R.string.screen_pairing, Icons.Filled.Devices, DrawerSection.CONFIGURE), + DrawerEntry(KeysScreen, R.string.screen_keys, Icons.Filled.Key, DrawerSection.CONFIGURE), + DrawerEntry( + ChannelsScreen, + R.string.screen_channels, + Icons.AutoMirrored.Filled.ListAlt, + DrawerSection.CONFIGURE, + ), // Inspect - DrawerEntry(SystemScreen, "System", Icons.Filled.Info, DrawerSection.INSPECT), - DrawerEntry(LogsScreen, "Logs", Icons.Filled.HistoryEdu, DrawerSection.INSPECT), - DrawerEntry(KanbanScreen, "Kanban", Icons.Filled.Dashboard, DrawerSection.INSPECT), - DrawerEntry(AchievementsScreen, "Achievements", Icons.Filled.Info, DrawerSection.INSPECT), + DrawerEntry(SystemScreen, R.string.screen_system, Icons.Filled.Info, DrawerSection.INSPECT), + DrawerEntry(LogsScreen, R.string.screen_logs, Icons.Filled.HistoryEdu, DrawerSection.INSPECT), + DrawerEntry(KanbanScreen, R.string.screen_kanban, Icons.Filled.Dashboard, DrawerSection.INSPECT), + DrawerEntry(AchievementsScreen, R.string.screen_achievements, Icons.Filled.Info, DrawerSection.INSPECT), ) private val DRAWER_GESTURE_SCREENS: Set = ALL_NAV_ITEMS.mapTo(mutableSetOf()) { it.key } @@ -370,7 +378,7 @@ fun MainNavigation(sessionId: String? = null) { modifier = Modifier.padding(start = 20.dp, top = 20.dp, end = 16.dp, bottom = 4.dp), ) { Text( - text = "⚡ Hermes", + text = stringResource(R.string.nav_drawer_title), style = MaterialTheme.typography.headlineSmall.copy( fontWeight = FontWeight.Bold, @@ -386,7 +394,7 @@ fun MainNavigation(sessionId: String? = null) { ) } Text( - text = "AI Agent Control", + text = stringResource(R.string.nav_drawer_subtitle), modifier = Modifier.padding(start = 20.dp, bottom = 12.dp, end = 16.dp), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -395,7 +403,7 @@ fun MainNavigation(sessionId: String? = null) { for (section in DrawerSection.entries) { Text( - text = section.title.uppercase(), + text = stringResource(section.titleRes).uppercase(), modifier = Modifier.padding( start = 20.dp, @@ -412,7 +420,7 @@ fun MainNavigation(sessionId: String? = null) { .forEach { entry -> NavigationDrawerItem( icon = { Icon(entry.icon, contentDescription = null) }, - label = { Text(entry.label) }, + label = { Text(stringResource(entry.labelRes)) }, selected = currentScreen == entry.key, onClick = { scope.launch { drawerState.close() } @@ -427,7 +435,11 @@ fun MainNavigation(sessionId: String? = null) { modifier = Modifier .padding(horizontal = 12.dp, vertical = 2.dp) - .testTag("drawer_${entry.label.lowercase().replace(' ', '_')}"), + .testTag( + "drawer_${entry.key::class.simpleName?.lowercase()?.removeSuffix( + "screen", + ) ?: ""}", + ), ) } } @@ -448,9 +460,12 @@ fun MainNavigation(sessionId: String? = null) { NavigationBarItem( selected = currentScreen == item.key, onClick = { NavigationController.navigateTo(item.key) }, - icon = { Icon(item.icon, contentDescription = item.label) }, - label = { Text(item.label) }, - modifier = Modifier.testTag("nav_${item.label.lowercase().replace(' ', '_')}"), + icon = { Icon(item.icon, contentDescription = stringResource(item.labelRes)) }, + label = { Text(stringResource(item.labelRes)) }, + modifier = + Modifier.testTag( + "nav_${item.key::class.simpleName?.lowercase()?.removeSuffix("screen") ?: ""}", + ), ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb78a1fc..5ecd8f52 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,4 +28,34 @@ Unknown error + + + ⚡ Hermes + AI Agent Control + Converse + Automate + Configure + Inspect + + + Chat + Skills + Cron + System + Settings + Profiles + Webhooks + Gateway + Toolsets + Plugins + Config + MCP Servers + Models + Pairing + Keys + Channels + Logs + Kanban + Achievements + History \ No newline at end of file From 7ca3399a01b564a42316d306c30c32f8b86fb295 Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Sun, 21 Jun 2026 11:21:29 +0300 Subject: [PATCH 04/10] chore: enable Android emulator and system images in Nix flake --- flake.nix | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 3ce41577..e680eb64 100644 --- a/flake.nix +++ b/flake.nix @@ -29,9 +29,11 @@ # Target platforms (36 required by AGP 9.0.1 / compileSdk 36) platformVersions = [ "36" "35" "34" ]; - # For the emulator (optional, remove if not needed) - includeEmulator = false; - includeSystemImages = false; + # For the emulator + includeEmulator = true; + includeSystemImages = true; + systemImageTypes = [ "google_apis" ]; + abiVersions = [ "x86_64" ]; # NDK (not needed for this project, but handy) includeNDK = false; From acd6cbf7510293fe49c35ed344390e9cb352f9ce Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Sun, 21 Jun 2026 11:31:09 +0300 Subject: [PATCH 05/10] refactor(#78): extract notification strings to resources --- .../notification/ChatNotificationService.kt | 22 +++++++++---------- app/src/main/res/values/strings.xml | 12 ++++++++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/notification/ChatNotificationService.kt b/app/src/main/java/com/m57/hermescontrol/notification/ChatNotificationService.kt index c4a0d8f8..24fa848f 100644 --- a/app/src/main/java/com/m57/hermescontrol/notification/ChatNotificationService.kt +++ b/app/src/main/java/com/m57/hermescontrol/notification/ChatNotificationService.kt @@ -70,7 +70,7 @@ class ChatNotificationService : Service() { override fun onCreate() { super.onCreate() createNotificationChannels() - startForeground(NOTIFICATION_ID, buildForegroundNotification("Waiting for Hermes replies")) + startForeground(NOTIFICATION_ID, buildForegroundNotification(getString(R.string.notif_waiting_replies))) startEventCollection() } @@ -85,12 +85,12 @@ class ChatNotificationService : Service() { event.text .take(100) .replace("\n", " ") - .ifBlank { "New message" } + .ifBlank { getString(R.string.notif_new_message) } showReplyNotification(preview, event.sessionId) } is WsEvent.ClarifyRequest -> { - showReplyNotification("Hermes needs a clarification", null) + showReplyNotification(getString(R.string.notif_clarification_needed), null) } else -> {} @@ -108,7 +108,7 @@ class ChatNotificationService : Service() { NotificationCompat .Builder(this, CHAT_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) - .setContentTitle("Hermes") + .setContentTitle(getString(R.string.notif_title)) .setContentText(text) .setStyle(NotificationCompat.BigTextStyle().bigText(text)) .setPriority(NotificationCompat.PRIORITY_HIGH) @@ -116,7 +116,7 @@ class ChatNotificationService : Service() { .setContentIntent(buildContentIntent(sessionId)) if (!sessionId.isNullOrBlank()) { - val replyLabel = "Type your reply..." + val replyLabel = getString(R.string.notif_reply_placeholder) val remoteInput = RemoteInput .Builder(NotificationReplyReceiver.KEY_TEXT_REPLY) @@ -140,7 +140,7 @@ class ChatNotificationService : Service() { NotificationCompat.Action .Builder( R.drawable.ic_notification, - "Reply", + getString(R.string.action_reply), replyPendingIntent, ).addRemoteInput(remoteInput) .build() @@ -169,7 +169,7 @@ class ChatNotificationService : Service() { NotificationCompat .Builder(this, SERVICE_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) - .setContentTitle("Hermes") + .setContentTitle(getString(R.string.notif_title)) .setContentText(text) .setPriority(NotificationCompat.PRIORITY_MIN) .setOngoing(true) @@ -180,19 +180,19 @@ class ChatNotificationService : Service() { val serviceChannel = NotificationChannel( SERVICE_CHANNEL_ID, - "Hermes Background Service", + getString(R.string.notif_channel_service_name), NotificationManager.IMPORTANCE_MIN, ).apply { - description = "Keeps connection alive in the background" + description = getString(R.string.notif_channel_service_desc) } val chatChannel = NotificationChannel( CHAT_CHANNEL_ID, - "Hermes Chat Replies", + getString(R.string.notif_channel_chat_name), NotificationManager.IMPORTANCE_HIGH, ).apply { - description = "Alerts for new replies from Hermes" + description = getString(R.string.notif_channel_chat_desc) } val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5ecd8f52..3f375975 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,4 +58,16 @@ Kanban Achievements History + + + Hermes + Waiting for Hermes replies + New message + Hermes needs a clarification + Type your reply\u2026 + Hermes Background Service + Keeps connection alive in the background + Hermes Chat Replies + Alerts for new replies from Hermes + Reply \ No newline at end of file From cdad31b555aa4c63019c3ae0a441d6b702078516 Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Sun, 21 Jun 2026 11:38:33 +0300 Subject: [PATCH 06/10] refactor(#78): extract UI strings for chat, settings, connect, kanban, and skills screens --- .../m57/hermescontrol/ui/chat/ChatBubble.kt | 18 +-- .../m57/hermescontrol/ui/chat/ChatScreen.kt | 52 ++++---- .../ui/common/SharedComponents.kt | 10 +- .../hermescontrol/ui/connect/ConnectScreen.kt | 49 ++++--- .../hermescontrol/ui/kanban/KanbanScreen.kt | 34 +++-- .../ui/settings/SettingsScreen.kt | 119 ++++++++++------- .../ui/settings/SettingsViewModel.kt | 43 +++--- .../hermescontrol/ui/skills/SkillsScreen.kt | 47 ++++--- app/src/main/res/values/strings.xml | 125 ++++++++++++++++++ 9 files changed, 348 insertions(+), 149 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt index 0d6c6654..036bda66 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt @@ -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, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt index e0de3b84..1ddf7929 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt @@ -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, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt b/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt index 8f527582..b8d19649 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt @@ -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 + }, + ) + }, ) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt index ce62929b..a14a3ec1 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt @@ -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(), diff --git a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt index b903be4c..da81522b 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt @@ -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)) } }, ) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt index 4696497a..06ab577f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt @@ -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)) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt index 362deb31..16244565 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt @@ -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> = + val availableNavItems: List> = 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. */ diff --git a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt index a8b3ca54..db6bea87 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt @@ -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)) } }, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3f375975..afa5e105 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,4 +70,129 @@ Hermes Chat Replies Alerts for new replies from Hermes Reply + + + Search + Close search + Reconnect + Ready to chat + Send a message to start a conversation + Connecting to Hermes\u2026 + Reconnecting to Hermes\u2026 + Disconnected from Hermes + Thinking\u2026 + Thinking: %1$s\u2026 + Not connected\u2026 + Waiting for response\u2026 + Type a message\u2026 + Error Output (stderr):\n%1$s + Execution Error: %1$s + Exit Code: %1$d + Tool + Show Raw JSON + Show Parsed Output + Show help menu + Check gateway and platform status + List all chat sessions + Check system resource usage (stats) + Check system resource usage (system) + Create a new chat session + Interrupt the active run + New Chat + Scroll to bottom + Interrupt + Copy + + + Connection + Connection Profile + Standalone / Default + None (Standalone / Default) + Rename Profile + Host + Port + Auth Token + Clear Token + Behavior + Auto-Reconnect + Automatically reconnect on disconnect + Appearance + Theme + Chat + Typing Effect + Animate new messages word by word + Delay: %1$d ms + 10 ms + 100 ms + Navigation Bar + Bottom bar items (%1$d/5) + Add Item + Maximum of 5 items + ✅ Settings saved + Test Connection + About + ⚡ Hermes Control + Version + Build + Debug + Release + Commit + Rename + Save + Delete Profile + Hide token + Show token + Move up + Move down + Remove %1$s + System + Light + Dark + + + Profile: %1$s + Select Saved Profile + None (Standalone) + Pairing string + Paste hermes://\u2026 or Base64 config + Save connection profile + Profile Name + e.g. Local Hermit, Remote Server + Hide manual entry + Enter manually instead + API token + 127.0.0.1 + 9119 + Connect + + + Kanban Board + No boards found. + No columns found for this board. + Add New Task + Task Title + Task Description + Add + Move Left + Move Right + + + All Statuses + Enabled + Disabled + All + No skills found + Skills loaded from Hermes will appear here. + Search skills\u2026 + Status + Category + No matching skills + Try adjusting your search or filters. + Edit: %1$s + Enter skill content\u2026 + Discard changes? + You have unsaved changes. Are you sure you want to discard them? + Discard + Edit Skill File + Save Changes \ No newline at end of file From dcb23ba0a32f32d258e38a9da2e322a2821bd595 Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Sun, 21 Jun 2026 11:41:41 +0300 Subject: [PATCH 07/10] refactor(#78): extract UI strings for logs, system, model, and sessions/history screens --- .../m57/hermescontrol/ui/logs/LogsScreen.kt | 10 +++--- .../m57/hermescontrol/ui/model/ModelScreen.kt | 28 +++++++++------ .../ui/sessions/SessionsScreen.kt | 18 ++++++---- .../hermescontrol/ui/system/SystemScreen.kt | 36 ++++++++++++------- app/src/main/res/values/strings.xml | 33 +++++++++++++++++ 5 files changed, 92 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt index 0c7b37e7..51ea9a49 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt @@ -18,11 +18,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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 @@ -59,7 +61,7 @@ fun LogsScreen( } HermesScaffold( - title = { Text("Logs") }, + title = { Text(stringResource(R.string.screen_logs)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadLogs() }, @@ -71,7 +73,7 @@ fun LogsScreen( state.errorMessage != null -> { ErrorState( - message = state.errorMessage ?: "Unknown error", + message = state.errorMessage ?: stringResource(R.string.error_unknown), onRetry = { viewModel.loadLogs() }, ) } @@ -80,8 +82,8 @@ fun LogsScreen( // B5 (Jun 18 2026, kanban t_2322818d): proper empty-state using // the shared EmptyState component instead of a bare Text. EmptyState( - title = "No logs available", - subtitle = "Log output from Hermes will appear here.", + title = stringResource(R.string.logs_empty_title), + subtitle = stringResource(R.string.logs_empty_desc), icon = Icons.Filled.HistoryEdu, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt index 8efb7d36..27036d4a 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt @@ -35,10 +35,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.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold @@ -67,7 +69,7 @@ fun ModelScreen( } HermesScaffold( - title = { Text("Models") }, + title = { Text(stringResource(R.string.screen_models)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadModelOptions() }, @@ -87,10 +89,10 @@ fun ModelScreen( state.providers.isEmpty() -> { EmptyState( - title = "No model providers reported", - subtitle = "Verify configuration or retry", + title = stringResource(R.string.model_empty_title), + subtitle = stringResource(R.string.model_empty_desc), onAction = { viewModel.loadModelOptions() }, - actionLabel = "Refresh", + actionLabel = stringResource(R.string.content_desc_refresh), modifier = Modifier.padding(paddingValues), ) } @@ -107,7 +109,10 @@ fun ModelScreen( Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error) Spacer(modifier = Modifier.height(16.dp)) IconButton(onClick = { viewModel.loadModelOptions() }) { - Icon(imageVector = Icons.Filled.Refresh, contentDescription = "Retry") + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = stringResource(R.string.action_retry), + ) } } } else { @@ -153,7 +158,7 @@ fun ModelScreen( fontWeight = FontWeight.Bold, ) Text( - text = "Slug: ${provider.slug}", + text = stringResource(R.string.model_label_slug, provider.slug), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -161,7 +166,7 @@ fun ModelScreen( if (isCurrent) { Text( - text = "CURRENT", + text = stringResource(R.string.model_status_current), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, @@ -183,7 +188,7 @@ fun ModelScreen( AnimatedVisibility(visible = isExpanded) { Column(modifier = Modifier.padding(top = 16.dp)) { Text( - text = "Available Models:", + text = stringResource(R.string.model_label_available), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(bottom = 8.dp), @@ -192,7 +197,7 @@ fun ModelScreen( val models = provider.models.orEmpty() if (models.isEmpty()) { Text( - text = "No models reported or provider needs authentication.", + text = stringResource(R.string.model_no_models), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -262,7 +267,10 @@ fun ModelScreen( if (isActive) { Icon( imageVector = Icons.Filled.Check, - contentDescription = "Active Model", + contentDescription = + stringResource( + R.string.content_desc_active_model, + ), tint = MaterialTheme.colorScheme.primary, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt index e5e6ad14..47662888 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt @@ -21,11 +21,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue 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.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.m57.hermescontrol.ChatScreen import com.m57.hermescontrol.NavigationController +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 @@ -48,7 +50,7 @@ fun SessionsScreen( } HermesScaffold( - title = { Text("History") }, + title = { Text(stringResource(R.string.screen_history)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadSessions() }, @@ -61,15 +63,15 @@ fun SessionsScreen( state.errorMessage != null -> { ErrorState( - message = state.errorMessage ?: "Unknown error", + message = state.errorMessage ?: stringResource(R.string.error_unknown), onRetry = { viewModel.loadSessions() }, ) } state.sessions.isEmpty() -> { EmptyState( - title = "No history", - subtitle = "Previous chat sessions from Hermes will appear here.", + title = stringResource(R.string.history_empty_title), + subtitle = stringResource(R.string.history_empty_desc), icon = Icons.Filled.History, ) } @@ -98,7 +100,7 @@ fun SessionsScreen( modifier = Modifier.padding(spacing.md), ) { Text( - text = session.title ?: "Untitled", + text = session.title ?: stringResource(R.string.history_untitled), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, ) @@ -107,7 +109,11 @@ fun SessionsScreen( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "${session.message_count ?: 0} messages", + text = + stringResource( + R.string.history_message_count, + session.message_count ?: 0, + ), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt index 2a307707..861c1bb5 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt @@ -27,8 +27,10 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.m57.hermescontrol.R import com.m57.hermescontrol.theme.LocalHermesStatusColors import com.m57.hermescontrol.theme.LocalSpacing import com.m57.hermescontrol.ui.common.EmptyState @@ -65,7 +67,7 @@ fun SystemScreen( } HermesScaffold( - title = { Text("System") }, + title = { Text(stringResource(R.string.screen_system)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadSystemData() }, @@ -77,15 +79,15 @@ fun SystemScreen( state.errorMessage != null -> { ErrorState( - message = state.errorMessage ?: "Unknown error", + message = state.errorMessage ?: stringResource(R.string.error_unknown), onRetry = { viewModel.loadSystemData() }, ) } state.stats == null && state.doctorReport == null -> { EmptyState( - title = "No system data", - subtitle = "Tap refresh to load system diagnostics.", + title = stringResource(R.string.system_empty_title), + subtitle = stringResource(R.string.system_empty_desc), ) } @@ -102,7 +104,7 @@ fun SystemScreen( // Stats row state.stats?.let { stats -> item { - SectionHeader(title = "Performance") + SectionHeader(title = stringResource(R.string.system_sec_performance)) } item { FlowRow( @@ -111,7 +113,7 @@ fun SystemScreen( ) { stats.cpuPercent?.let { cpu -> StatCard( - label = "CPU Usage", + label = stringResource(R.string.system_metric_cpu), value = "$cpu%", icon = Icons.Filled.Speed, modifier = Modifier.weight(1f), @@ -119,7 +121,7 @@ fun SystemScreen( } stats.memoryPercent?.let { mem -> StatCard( - label = "Memory", + label = stringResource(R.string.system_metric_memory), value = "$mem%", icon = Icons.Filled.Memory, modifier = Modifier.weight(1f), @@ -132,7 +134,7 @@ fun SystemScreen( // Doctor diagnostics state.doctorReport?.let { report -> item { - SectionHeader(title = "Doctor") + SectionHeader(title = stringResource(R.string.system_sec_doctor)) } item { Card( @@ -158,7 +160,12 @@ fun SystemScreen( }, ) StatusBadge( - text = if (report.ok) "Running" else "Failed", + text = + if (report.ok) { + stringResource(R.string.system_status_running) + } else { + stringResource(R.string.system_status_failed) + }, status = if (report.ok) { StatusBadgeType.SUCCESS @@ -169,10 +176,13 @@ fun SystemScreen( } Spacer(modifier = Modifier.height(spacing.sm)) report.pid?.let { pid -> - InfoRow(label = "Process PID", value = pid.toString()) + InfoRow( + label = stringResource(R.string.system_info_pid), + value = pid.toString(), + ) } report.name?.let { name -> - InfoRow(label = "Process Name", value = name) + InfoRow(label = stringResource(R.string.system_info_proc_name), value = name) } } } @@ -181,7 +191,7 @@ fun SystemScreen( // Admin actions item { - SectionHeader(title = "Administration") + SectionHeader(title = stringResource(R.string.system_sec_admin)) } item { Card( @@ -201,7 +211,7 @@ fun SystemScreen( contentDescription = null, modifier = Modifier.padding(end = spacing.xs), ) - Text("Trigger System Backup") + Text(stringResource(R.string.system_action_backup)) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index afa5e105..5c1f2527 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -195,4 +195,37 @@ Discard Edit Skill File Save Changes + + + No logs available + Log output from Hermes will appear here. + + + No system data + Tap refresh to load system diagnostics. + Performance + CPU Usage + Memory + Doctor + Running + Failed + Process PID + Process Name + Administration + Trigger System Backup + + + No model providers reported + Verify configuration or retry + Slug: %1$s + CURRENT + Available Models: + No models reported or provider needs authentication. + Active Model + + + No history + Previous chat sessions from Hermes will appear here. + Untitled + %1$d messages \ No newline at end of file From b1950ece34d5a9e9fff7ff3bdefd52d611564296 Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Sun, 21 Jun 2026 11:48:33 +0300 Subject: [PATCH 08/10] refactor(#78): extract UI strings for toolsets webhooks pairing and config screens --- .../hermescontrol/ui/config/ConfigScreen.kt | 12 +++++---- .../hermescontrol/ui/pairing/PairingScreen.kt | 24 ++++++++++------- .../ui/toolsets/ToolsetsScreen.kt | 15 +++++++---- .../ui/webhooks/WebhooksScreen.kt | 16 ++++++----- app/src/main/res/values/strings.xml | 27 +++++++++++++++++++ 5 files changed, 68 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt index 20000ee7..2a410214 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt @@ -26,10 +26,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.FontFamily 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.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState @@ -57,7 +59,7 @@ fun ConfigScreen( } HermesScaffold( - title = { Text("Configuration") }, + title = { Text(stringResource(R.string.config_screen_title)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadRawConfig() }, @@ -87,7 +89,7 @@ fun ConfigScreen( Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { viewModel.loadRawConfig() }) { - Text("Retry") + Text(stringResource(R.string.action_retry)) } } } else { @@ -113,7 +115,7 @@ fun ConfigScreen( Modifier .fillMaxWidth() .weight(1f), - placeholder = { Text("YAML Config...") }, + placeholder = { Text(stringResource(R.string.config_placeholder_yaml)) }, textStyle = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace), maxLines = Int.MAX_VALUE, ) @@ -125,7 +127,7 @@ fun ConfigScreen( onClick = { editingText = state.yamlText ?: "" }, modifier = Modifier.weight(1f), ) { - Text("Reset") + Text(stringResource(R.string.config_action_reset)) } Spacer(modifier = Modifier.width(16.dp)) @@ -138,7 +140,7 @@ fun ConfigScreen( if (state.isSaving) { CircularProgressIndicator(modifier = Modifier.width(16.dp)) } else { - Text("Save Config") + Text(stringResource(R.string.config_action_save)) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt index 367e51b5..dc606c42 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt @@ -28,10 +28,12 @@ import androidx.compose.runtime.getValue 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.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState @@ -58,7 +60,7 @@ fun PairingScreen( } HermesScaffold( - title = { Text("Device Pairing") }, + title = { Text(stringResource(R.string.pairing_screen_title)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadPairing() }, @@ -88,7 +90,7 @@ fun PairingScreen( Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { viewModel.loadPairing() }) { - Text("Retry") + Text(stringResource(R.string.action_retry)) } } } else { @@ -109,7 +111,7 @@ fun PairingScreen( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "Pending Approvals", + text = stringResource(R.string.pairing_sec_pending), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, ) @@ -121,7 +123,7 @@ fun PairingScreen( contentColor = MaterialTheme.colorScheme.error, ), ) { - Text("Clear All") + Text(stringResource(R.string.pairing_action_clear_all)) } } } @@ -170,7 +172,7 @@ fun PairingScreen( containerColor = MaterialTheme.colorScheme.primary, ), ) { - Text("Approve") + Text(stringResource(R.string.pairing_action_approve)) } } } @@ -179,7 +181,7 @@ fun PairingScreen( item { Text( - text = "Approved Devices", + text = stringResource(R.string.pairing_sec_approved), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, ) @@ -198,7 +200,7 @@ fun PairingScreen( contentAlignment = Alignment.Center, ) { Text( - text = "No approved devices paired yet.", + text = stringResource(R.string.pairing_empty_approved), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -225,7 +227,11 @@ fun PairingScreen( fontWeight = FontWeight.SemiBold, ) Text( - text = "User ID: ${item.user_id ?: "Unknown"}", + text = + stringResource( + R.string.pairing_label_user_id, + item.user_id ?: "Unknown", + ), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -246,7 +252,7 @@ fun PairingScreen( contentColor = MaterialTheme.colorScheme.error, ), ) { - Text("Revoke") + Text(stringResource(R.string.pairing_action_revoke)) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt index f0ca7232..028fa680 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt @@ -32,10 +32,12 @@ import androidx.compose.runtime.getValue 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.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold @@ -63,7 +65,7 @@ fun ToolsetsScreen( } HermesScaffold( - title = { Text("Toolsets") }, + title = { Text(stringResource(R.string.screen_toolsets)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadToolsets() }, @@ -83,10 +85,10 @@ fun ToolsetsScreen( state.toolsets.isEmpty() -> { EmptyState( - title = "No toolsets reported", - subtitle = "Verify configuration or retry", + title = stringResource(R.string.toolsets_empty_title), + subtitle = stringResource(R.string.toolsets_empty_desc), onAction = { viewModel.loadToolsets() }, - actionLabel = "Refresh", + actionLabel = stringResource(R.string.content_desc_refresh), modifier = Modifier.padding(paddingValues), ) } @@ -103,7 +105,10 @@ fun ToolsetsScreen( Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error) Spacer(modifier = Modifier.height(16.dp)) IconButton(onClick = { viewModel.loadToolsets() }) { - Icon(imageVector = Icons.Filled.Refresh, contentDescription = "Retry") + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = stringResource(R.string.action_retry), + ) } } } else { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt index b6b0c958..f08d2071 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt @@ -29,11 +29,13 @@ import androidx.compose.runtime.getValue 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.FontFamily 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.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState @@ -60,7 +62,7 @@ fun WebhooksScreen( } HermesScaffold( - title = { Text("Webhooks") }, + title = { Text(stringResource(R.string.screen_webhooks)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadWebhooks() }, @@ -90,7 +92,7 @@ fun WebhooksScreen( Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { viewModel.loadWebhooks() }) { - Text("Retry") + Text(stringResource(R.string.action_retry)) } } } else { @@ -123,7 +125,7 @@ fun WebhooksScreen( ) { Column(modifier = Modifier.weight(1f)) { Text( - text = "Global Webhooks Status", + text = stringResource(R.string.webhooks_sec_status), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, ) @@ -147,7 +149,7 @@ fun WebhooksScreen( // Subscriptions List Header item { Text( - text = "Subscriptions", + text = stringResource(R.string.webhooks_sec_subscriptions), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, ) @@ -166,7 +168,7 @@ fun WebhooksScreen( contentAlignment = Alignment.Center, ) { Text( - text = "No webhook subscriptions configured.", + text = stringResource(R.string.webhooks_empty_desc), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -191,7 +193,7 @@ fun WebhooksScreen( fontWeight = FontWeight.SemiBold, ) Text( - text = "Target URL:", + text = stringResource(R.string.webhooks_label_target), style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Bold, ) @@ -207,7 +209,7 @@ fun WebhooksScreen( if (events.isNotEmpty()) { Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Events Subscribed:", + text = stringResource(R.string.webhooks_label_events), style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Bold, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c1f2527..6006e65a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -228,4 +228,31 @@ Previous chat sessions from Hermes will appear here. Untitled %1$d messages + + + No toolsets reported + Verify configuration or retry + + + Global Webhooks Status + Subscriptions + No webhook subscriptions configured. + Target URL: + Events Subscribed: + + + Device Pairing + Pending Approvals + Clear All + Approve + Approved Devices + No approved devices paired yet. + User ID: %1$s + Revoke + + + Configuration + YAML Config... + Reset + Save Config \ No newline at end of file From 44e20064343483310126c88e9c7d7d85a4363842 Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Sun, 21 Jun 2026 11:55:18 +0300 Subject: [PATCH 09/10] refactor(#78): extract UI strings for remaining batch 2.3 screens --- .../ui/achievements/AchievementsScreen.kt | 15 +++-- .../ui/channels/ChannelsScreen.kt | 47 +++++++++++--- .../hermescontrol/ui/cron/CronJobsScreen.kt | 25 +++++--- .../hermescontrol/ui/gateway/GatewayScreen.kt | 27 +++++--- .../m57/hermescontrol/ui/keys/KeysScreen.kt | 26 +++++--- .../hermescontrol/ui/mcp/McpServersScreen.kt | 18 ++++-- .../hermescontrol/ui/plugins/PluginsScreen.kt | 18 +++--- .../ui/profiles/ProfilesScreen.kt | 33 +++++++--- app/src/main/res/values/strings.xml | 63 +++++++++++++++++++ 9 files changed, 207 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt index 9ad5c8ff..95a83025 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt @@ -23,10 +23,12 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue 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.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.m57.hermescontrol.R import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold @@ -45,7 +47,7 @@ fun AchievementsScreen( } HermesScaffold( - title = { Text("Agent Achievements") }, + title = { Text(stringResource(R.string.screen_achievements)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadAchievements() }, @@ -65,8 +67,8 @@ fun AchievementsScreen( state.achievements.isEmpty() -> { EmptyState( - title = "No achievements yet", - subtitle = "Complete tasks to unlock achievements.", + title = stringResource(R.string.achievements_empty_title), + subtitle = stringResource(R.string.achievements_empty_desc), icon = Icons.Filled.Refresh, modifier = Modifier.padding(paddingValues), ) @@ -156,8 +158,11 @@ fun AchievementsScreen( ) { Text( text = - "Progress: ${achievement.progress?.toInt() ?: 0} / " + - "${achievement.next_threshold?.toInt() ?: 0}", + stringResource( + R.string.achievements_label_progress, + achievement.progress?.toInt() ?: 0, + achievement.next_threshold?.toInt() ?: 0, + ), style = MaterialTheme.typography.bodySmall, ) Text( diff --git a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt index d3df7f06..f5ff2403 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt @@ -29,9 +29,11 @@ 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.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.m57.hermescontrol.R import com.m57.hermescontrol.data.model.MessagingPlatformUpdate import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState @@ -59,7 +61,7 @@ fun ChannelsScreen( } HermesScaffold( - title = { Text("Messaging Channels") }, + title = { Text(stringResource(R.string.screen_channels)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadPlatforms() }, @@ -79,8 +81,8 @@ fun ChannelsScreen( state.platforms.isEmpty() -> { EmptyState( - title = "No channels configured", - subtitle = "Add messaging platforms in Hermes to see them here.", + title = stringResource(R.string.channels_empty_title), + subtitle = stringResource(R.string.channels_empty_desc), modifier = Modifier.padding(paddingValues), ) } @@ -104,7 +106,14 @@ fun ChannelsScreen( Column { Text(text = platform.name, style = MaterialTheme.typography.titleLarge) Text( - text = if (platform.enabled) "Active" else "Disabled", + text = + if (platform.enabled) { + stringResource( + R.string.channels_status_active, + ) + } else { + stringResource(R.string.channels_status_disabled) + }, color = if (platform.enabled) { MaterialTheme.colorScheme.primary @@ -126,7 +135,15 @@ fun ChannelsScreen( ) Spacer(modifier = Modifier.width(8.dp)) Button(onClick = { showConfigureForm = !showConfigureForm }) { - Text(if (showConfigureForm) "Cancel" else "Configure") + Text( + if (showConfigureForm) { + stringResource( + R.string.action_cancel, + ) + } else { + stringResource(R.string.channels_action_configure) + }, + ) } } } @@ -134,7 +151,7 @@ fun ChannelsScreen( if (showConfigureForm) { Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Configuration Settings", + text = stringResource(R.string.channels_sec_settings), style = MaterialTheme.typography.titleMedium, ) @@ -148,7 +165,7 @@ fun ChannelsScreen( label = { Text(field.prompt ?: field.key) }, placeholder = { if (field.isSet) { - Text("******** (Already Configured)") + Text(stringResource(R.string.channels_placeholder_configured)) } }, modifier = Modifier.fillMaxWidth(), @@ -169,13 +186,16 @@ fun ChannelsScreen( }, modifier = Modifier.align(Alignment.End), ) { - Text("Save Settings") + Text(stringResource(R.string.channels_action_save_settings)) } } else { val envFields = platform.envVars.orEmpty() if (envFields.isNotEmpty()) { Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Configured Keys:", style = MaterialTheme.typography.titleSmall) + Text( + text = stringResource(R.string.channels_sec_keys), + style = MaterialTheme.typography.titleSmall, + ) envFields.forEach { field -> Row( modifier = Modifier.fillMaxWidth(), @@ -187,7 +207,14 @@ fun ChannelsScreen( color = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( - text = if (field.isSet) "Configured" else "Not set", + text = + if (field.isSet) { + stringResource( + R.string.channels_status_configured, + ) + } else { + stringResource(R.string.channels_status_not_set) + }, style = MaterialTheme.typography.bodyMedium, color = if (field.isSet) { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt index fbb4a9f3..0f168432 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt @@ -24,9 +24,11 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue 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.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 @@ -60,7 +62,7 @@ fun CronJobsScreen( } HermesScaffold( - title = { Text("Cron Jobs") }, + title = { Text(stringResource(R.string.screen_cron)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadCronJobs() }, @@ -79,8 +81,8 @@ fun CronJobsScreen( state.jobs.isEmpty() -> { EmptyState( - title = "No cron jobs", - subtitle = "Scheduled tasks from Hermes will appear here.", + title = stringResource(R.string.cron_empty_title), + subtitle = stringResource(R.string.cron_empty_desc), icon = Icons.Filled.Schedule, ) } @@ -112,7 +114,14 @@ fun CronJobsScreen( modifier = Modifier.weight(1f), ) StatusBadge( - text = if (job.state == "active") "Active" else "Paused", + text = + if (job.state == "active") { + stringResource( + R.string.cron_status_active, + ) + } else { + stringResource(R.string.cron_status_paused) + }, status = if (job.state == "active") { StatusBadgeType.SUCCESS @@ -143,21 +152,21 @@ fun CronJobsScreen( onClick = { viewModel.pauseCronJob(job.id) }, modifier = Modifier.weight(1f), ) { - Text("Pause") + Text(stringResource(R.string.cron_action_pause)) } } else { OutlinedButton( onClick = { viewModel.resumeCronJob(job.id) }, modifier = Modifier.weight(1f), ) { - Text("Resume") + Text(stringResource(R.string.cron_action_resume)) } } OutlinedButton( onClick = { viewModel.triggerCronJob(job.id) }, modifier = Modifier.weight(1f), ) { - Text("Run") + Text(stringResource(R.string.cron_action_run)) } Button( onClick = { viewModel.deleteCronJob(job.id) }, @@ -167,7 +176,7 @@ fun CronJobsScreen( ), modifier = Modifier.weight(1f), ) { - Text("Delete") + Text(stringResource(R.string.action_delete)) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt index 1bb314ca..5c9e6641 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt @@ -32,10 +32,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color 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.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState @@ -62,7 +64,7 @@ fun GatewayScreen( } HermesScaffold( - title = { Text("Gateway Control") }, + title = { Text(stringResource(R.string.screen_gateway)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadStatus() }, @@ -96,7 +98,7 @@ fun GatewayScreen( ) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { viewModel.loadStatus() }) { - Text("Retry") + Text(stringResource(R.string.action_retry)) } } } else { @@ -132,7 +134,14 @@ fun GatewayScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = if (isRunning) "GATEWAY RUNNING" else "GATEWAY STOPPED", + text = + if (isRunning) { + stringResource( + R.string.gateway_status_running, + ) + } else { + stringResource(R.string.gateway_status_stopped) + }, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = @@ -145,7 +154,7 @@ fun GatewayScreen( status?.version?.let { Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Version: $it", + text = stringResource(R.string.gateway_label_version, it), style = MaterialTheme.typography.bodySmall, color = if (isRunning) { @@ -170,7 +179,7 @@ fun GatewayScreen( verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( - text = "Controls", + text = stringResource(R.string.gateway_sec_controls), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, ) @@ -191,7 +200,7 @@ fun GatewayScreen( ) { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) Spacer(modifier = Modifier.width(4.dp)) - Text("Start") + Text(stringResource(R.string.gateway_action_start)) } Button( @@ -204,7 +213,7 @@ fun GatewayScreen( contentColor = Color.White, ), ) { - Text("Stop") + Text(stringResource(R.string.gateway_action_stop)) } } @@ -219,7 +228,7 @@ fun GatewayScreen( ) { Icon(imageVector = Icons.Filled.Refresh, contentDescription = null) Spacer(modifier = Modifier.width(4.dp)) - Text("Restart") + Text(stringResource(R.string.gateway_action_restart)) } if (state.isActionRunning) { @@ -245,7 +254,7 @@ fun GatewayScreen( verticalArrangement = Arrangement.spacedBy(8.dp), ) { Text( - text = "Active Platforms", + text = stringResource(R.string.gateway_sec_active_platforms), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, ) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt index e56ccf50..b0060401 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt @@ -36,10 +36,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.FontFamily 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.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold @@ -67,7 +69,7 @@ fun KeysScreen( } HermesScaffold( - title = { Text("Keys & Credentials") }, + title = { Text(stringResource(R.string.screen_keys)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadKeys() }, @@ -87,10 +89,10 @@ fun KeysScreen( state.envVars.isEmpty() -> { EmptyState( - title = "No environment keys reported", - subtitle = "Verify configuration or retry", + title = stringResource(R.string.keys_empty_title), + subtitle = stringResource(R.string.toolsets_empty_desc), onAction = { viewModel.loadKeys() }, - actionLabel = "Refresh", + actionLabel = stringResource(R.string.content_desc_refresh), modifier = Modifier.padding(paddingValues), ) } @@ -123,7 +125,7 @@ fun KeysScreen( } else if (config.isSet) { config.redactedValue ?: "********" } else { - "Not configured" + stringResource(R.string.keys_label_not_configured) } Card(modifier = Modifier.fillMaxWidth()) { @@ -158,13 +160,13 @@ fun KeysScreen( viewModel.updateKey(key, editedValue) isEditing = false }) { - Text("Save") + Text(stringResource(R.string.action_save)) } Spacer(modifier = Modifier.width(8.dp)) Button(onClick = { isEditing = false }) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } } } else { @@ -196,7 +198,10 @@ fun KeysScreen( } else { Icons.Filled.Visibility }, - contentDescription = "Toggle Visibility", + contentDescription = + stringResource( + R.string.keys_action_toggle_visibility, + ), ) } } @@ -206,7 +211,10 @@ fun KeysScreen( }) { Icon( imageVector = Icons.Filled.Edit, - contentDescription = "Edit", + contentDescription = + stringResource( + R.string.nav_drawer_section_converse, + ), ) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt index acb2dc14..39da6410 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt @@ -28,11 +28,13 @@ import androidx.compose.runtime.getValue 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.FontFamily 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.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState @@ -59,7 +61,7 @@ fun McpServersScreen( } HermesScaffold( - title = { Text("MCP Servers") }, + title = { Text(stringResource(R.string.screen_mcp_servers)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadServers() }, @@ -89,7 +91,7 @@ fun McpServersScreen( Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { viewModel.loadServers() }) { - Text("Retry") + Text(stringResource(R.string.action_retry)) } } } else { @@ -129,7 +131,11 @@ fun McpServersScreen( fontWeight = FontWeight.Bold, ) Text( - text = "Transport: ${server.transport ?: "stdio"}", + text = + stringResource( + R.string.mcp_servers_label_transport, + server.transport ?: "stdio", + ), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -145,7 +151,7 @@ fun McpServersScreen( Spacer(modifier = Modifier.height(8.dp)) val fullCmd = "${server.command} ${server.args.orEmpty().joinToString(" ")}" Text( - text = "Command:", + text = stringResource(R.string.mcp_servers_label_command), style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Bold, ) @@ -168,13 +174,13 @@ fun McpServersScreen( onClick = { viewModel.testServer(server.name) }, modifier = Modifier.padding(end = 8.dp), ) { - Text("Test") + Text(stringResource(R.string.mcp_servers_action_test)) } OutlinedButton( onClick = { viewModel.deleteServer(server.name) }, ) { - Text("Delete") + Text(stringResource(R.string.action_delete)) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt index bccbcb80..6b1d1db5 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt @@ -28,9 +28,11 @@ import androidx.compose.runtime.getValue 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.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.m57.hermescontrol.R import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold @@ -58,7 +60,7 @@ fun PluginsScreen( } HermesScaffold( - title = { Text("Plugins") }, + title = { Text(stringResource(R.string.screen_plugins)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadPlugins() }, @@ -78,10 +80,10 @@ fun PluginsScreen( state.plugins.isEmpty() -> { EmptyState( - title = "No plugins found", - subtitle = "Verify configuration or retry", + title = stringResource(R.string.plugins_empty_title), + subtitle = stringResource(R.string.plugins_empty_desc), onAction = { viewModel.loadPlugins() }, - actionLabel = "Refresh", + actionLabel = stringResource(R.string.content_desc_refresh), modifier = Modifier.padding(paddingValues), ) } @@ -129,7 +131,7 @@ fun PluginsScreen( } Spacer(modifier = Modifier.height(4.dp)) Text( - text = plugin.description ?: "No description provided.", + text = plugin.description ?: stringResource(R.string.plugins_no_desc), style = MaterialTheme.typography.bodyMedium, ) Spacer(modifier = Modifier.height(8.dp)) @@ -139,7 +141,7 @@ fun PluginsScreen( ) { if (plugin.installed) { OutlinedButton(onClick = { viewModel.updatePlugin(plugin.name) }) { - Text("Update") + Text(stringResource(R.string.plugins_action_update)) } Spacer(modifier = Modifier.width(8.dp)) OutlinedButton( @@ -149,11 +151,11 @@ fun PluginsScreen( contentColor = MaterialTheme.colorScheme.error, ), ) { - Text("Uninstall") + Text(stringResource(R.string.plugins_action_uninstall)) } } else { Button(onClick = { viewModel.installPlugin(plugin.name) }) { - Text("Install") + Text(stringResource(R.string.plugins_action_install)) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt index 00d8234b..52f846f8 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt @@ -35,10 +35,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.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold @@ -71,7 +73,7 @@ fun ProfilesScreen( } HermesScaffold( - title = { Text("Profiles") }, + title = { Text(stringResource(R.string.screen_profiles)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadProfiles() }, @@ -91,10 +93,10 @@ fun ProfilesScreen( state.profiles.isEmpty() -> { EmptyState( - title = "No profiles available", - subtitle = "Verify configuration or retry", + title = stringResource(R.string.profiles_empty_title), + subtitle = stringResource(R.string.toolsets_empty_desc), onAction = { viewModel.loadProfiles() }, - actionLabel = "Refresh", + actionLabel = stringResource(R.string.content_desc_refresh), modifier = Modifier.padding(paddingValues), ) } @@ -111,7 +113,7 @@ fun ProfilesScreen( Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { viewModel.loadProfiles() }) { - Text("Retry") + Text(stringResource(R.string.action_retry)) } } } else { @@ -170,7 +172,10 @@ fun ProfilesScreen( if (isActive) { Icon( imageVector = Icons.Filled.Check, - contentDescription = "Active Profile", + contentDescription = + stringResource( + R.string.profiles_content_desc_active, + ), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(start = 8.dp), ) @@ -186,11 +191,19 @@ fun ProfilesScreen( ) { Column { Text( - text = "Model: ${profile.model ?: "None"}", + text = + stringResource( + R.string.profiles_label_model, + profile.model ?: "None", + ), style = MaterialTheme.typography.bodyMedium, ) Text( - text = "Provider: ${profile.provider ?: "None"}", + text = + stringResource( + R.string.profiles_label_provider, + profile.provider ?: "None", + ), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -216,7 +229,7 @@ fun ProfilesScreen( modifier = Modifier.width(16.dp), ) Spacer(modifier = Modifier.width(4.dp)) - Text("Edit Soul") + Text(stringResource(R.string.profiles_action_edit_soul)) } Button( @@ -226,7 +239,7 @@ fun ProfilesScreen( tempModelName = profile.model ?: "" }, ) { - Text("Set Model") + Text(stringResource(R.string.profiles_action_set_model)) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6006e65a..bec1d6a7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -255,4 +255,67 @@ YAML Config... Reset Save Config + + + No profiles available + Model: %1$s + Provider: %1$s + Edit Soul + Set Model + Active Profile + + + No plugins found + Verify configuration or retry + No description provided. + Update + Uninstall + Install + + + Transport: %1$s + Command: + Test + + + GATEWAY RUNNING + GATEWAY STOPPED + Version: %1$s + Controls + Start + Stop + Restart + Active Platforms + + + No cron jobs + Scheduled tasks from Hermes will appear here. + Active + Paused + Pause + Resume + Run + + + No achievements yet + Complete tasks to unlock achievements. + Progress: %1$d / %2$d + + + No environment keys reported + Not configured + Toggle Visibility + + + No channels configured + Add messaging platforms in Hermes to see them here. + Active + Disabled + Configure + Configuration Settings + ******** (Already Configured) + Save Settings + Configured Keys: + Configured + Not set \ No newline at end of file From 69ff66b8defa882b753ffcdf338a2e884772a057 Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Sun, 21 Jun 2026 12:00:08 +0300 Subject: [PATCH 10/10] style: remove unused R import in Navigation.kt to pass ktlint --- app/src/main/java/com/m57/hermescontrol/Navigation.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/m57/hermescontrol/Navigation.kt b/app/src/main/java/com/m57/hermescontrol/Navigation.kt index 5737f09e..364cffd6 100644 --- a/app/src/main/java/com/m57/hermescontrol/Navigation.kt +++ b/app/src/main/java/com/m57/hermescontrol/Navigation.kt @@ -66,7 +66,6 @@ import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay -import com.m57.hermescontrol.R import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.ws.ConnectionStatus import com.m57.hermescontrol.data.ws.HermesWsClient