Merge pull request #231 from Hy4ri/refactor/issue-78-strings-extraction
refactor(#78): extract hardcoded strings to strings.xml
This commit is contained in:
32 files changed
+906
-362
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
package com.m57.hermescontrol
|
package com.m57.hermescontrol
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -58,6 +59,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
@@ -94,33 +96,33 @@ import com.m57.hermescontrol.ui.webhooks.WebhooksScreen as WebhooksScreenContent
|
|||||||
|
|
||||||
private data class BottomNavItem(
|
private data class BottomNavItem(
|
||||||
val key: NavKey,
|
val key: NavKey,
|
||||||
val label: String,
|
@param:StringRes val labelRes: Int,
|
||||||
val icon: ImageVector,
|
val icon: ImageVector,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Master list of ALL screens available for bottom-nav selection. */
|
/** Master list of ALL screens available for bottom-nav selection. */
|
||||||
private val ALL_NAV_ITEMS: List<BottomNavItem> =
|
private val ALL_NAV_ITEMS: List<BottomNavItem> =
|
||||||
listOf(
|
listOf(
|
||||||
BottomNavItem(ChatScreen, "Chat", Icons.AutoMirrored.Filled.Chat),
|
BottomNavItem(ChatScreen, R.string.screen_chat, Icons.AutoMirrored.Filled.Chat),
|
||||||
BottomNavItem(SkillsScreen, "Skills", Icons.Filled.Extension),
|
BottomNavItem(SkillsScreen, R.string.screen_skills, Icons.Filled.Extension),
|
||||||
BottomNavItem(CronJobsScreen, "Cron", Icons.Filled.Schedule),
|
BottomNavItem(CronJobsScreen, R.string.screen_cron, Icons.Filled.Schedule),
|
||||||
BottomNavItem(SystemScreen, "System", Icons.Filled.Info),
|
BottomNavItem(SystemScreen, R.string.screen_system, Icons.Filled.Info),
|
||||||
BottomNavItem(SettingsScreen, "Settings", Icons.Filled.Settings),
|
BottomNavItem(SettingsScreen, R.string.screen_settings, Icons.Filled.Settings),
|
||||||
BottomNavItem(ProfilesScreen, "Profiles", Icons.Filled.AccountCircle),
|
BottomNavItem(ProfilesScreen, R.string.screen_profiles, Icons.Filled.AccountCircle),
|
||||||
BottomNavItem(WebhooksScreen, "Webhooks", Icons.Filled.Webhook),
|
BottomNavItem(WebhooksScreen, R.string.screen_webhooks, Icons.Filled.Webhook),
|
||||||
BottomNavItem(GatewayScreen, "Gateway", Icons.Filled.Bolt),
|
BottomNavItem(GatewayScreen, R.string.screen_gateway, Icons.Filled.Bolt),
|
||||||
BottomNavItem(ToolsetsScreen, "Toolsets", Icons.Filled.Build),
|
BottomNavItem(ToolsetsScreen, R.string.screen_toolsets, Icons.Filled.Build),
|
||||||
BottomNavItem(PluginsScreen, "Plugins", Icons.Filled.Memory),
|
BottomNavItem(PluginsScreen, R.string.screen_plugins, Icons.Filled.Memory),
|
||||||
BottomNavItem(ConfigScreen, "Config", Icons.Filled.Code),
|
BottomNavItem(ConfigScreen, R.string.screen_config, Icons.Filled.Code),
|
||||||
BottomNavItem(McpServersScreen, "MCP Servers", Icons.Filled.Dashboard),
|
BottomNavItem(McpServersScreen, R.string.screen_mcp_servers, Icons.Filled.Dashboard),
|
||||||
BottomNavItem(ModelScreen, "Models", Icons.Filled.Settings),
|
BottomNavItem(ModelScreen, R.string.screen_models, Icons.Filled.Settings),
|
||||||
BottomNavItem(PairingScreen, "Pairing", Icons.Filled.Devices),
|
BottomNavItem(PairingScreen, R.string.screen_pairing, Icons.Filled.Devices),
|
||||||
BottomNavItem(KeysScreen, "Keys", Icons.Filled.Key),
|
BottomNavItem(KeysScreen, R.string.screen_keys, Icons.Filled.Key),
|
||||||
BottomNavItem(ChannelsScreen, "Channels", Icons.AutoMirrored.Filled.ListAlt),
|
BottomNavItem(ChannelsScreen, R.string.screen_channels, Icons.AutoMirrored.Filled.ListAlt),
|
||||||
BottomNavItem(LogsScreen, "Logs", Icons.Filled.HistoryEdu),
|
BottomNavItem(LogsScreen, R.string.screen_logs, Icons.Filled.HistoryEdu),
|
||||||
BottomNavItem(KanbanScreen, "Kanban", Icons.Filled.Dashboard),
|
BottomNavItem(KanbanScreen, R.string.screen_kanban, Icons.Filled.Dashboard),
|
||||||
BottomNavItem(AchievementsScreen, "Achievements", Icons.Filled.Info),
|
BottomNavItem(AchievementsScreen, R.string.screen_achievements, Icons.Filled.Info),
|
||||||
BottomNavItem(HistoryScreen, "History", Icons.Filled.History),
|
BottomNavItem(HistoryScreen, R.string.screen_history, Icons.Filled.History),
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Lookup: data-object simple name → NavKey (used by bottom-nav config). */
|
/** Lookup: data-object simple name → NavKey (used by bottom-nav config). */
|
||||||
@@ -134,17 +136,17 @@ private fun resolveBottomNavItems(names: List<String>): List<BottomNavItem> =
|
|||||||
// ── Drawer sections ────────────────────────────────────────────────────
|
// ── Drawer sections ────────────────────────────────────────────────────
|
||||||
|
|
||||||
private enum class DrawerSection(
|
private enum class DrawerSection(
|
||||||
val title: String,
|
@param:StringRes val titleRes: Int,
|
||||||
) {
|
) {
|
||||||
CONVERSE("Converse"),
|
CONVERSE(R.string.nav_drawer_section_converse),
|
||||||
AUTOMATE("Automate"),
|
AUTOMATE(R.string.nav_drawer_section_automate),
|
||||||
CONFIGURE("Configure"),
|
CONFIGURE(R.string.nav_drawer_section_configure),
|
||||||
INSPECT("Inspect"),
|
INSPECT(R.string.nav_drawer_section_inspect),
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class DrawerEntry(
|
private data class DrawerEntry(
|
||||||
val key: NavKey,
|
val key: NavKey,
|
||||||
val label: String,
|
@param:StringRes val labelRes: Int,
|
||||||
val icon: ImageVector,
|
val icon: ImageVector,
|
||||||
val section: DrawerSection,
|
val section: DrawerSection,
|
||||||
)
|
)
|
||||||
@@ -152,29 +154,34 @@ private data class DrawerEntry(
|
|||||||
private val DRAWER_ENTRIES =
|
private val DRAWER_ENTRIES =
|
||||||
listOf(
|
listOf(
|
||||||
// Converse
|
// Converse
|
||||||
DrawerEntry(ChatScreen, "Chat", Icons.AutoMirrored.Filled.Chat, DrawerSection.CONVERSE),
|
DrawerEntry(ChatScreen, R.string.screen_chat, Icons.AutoMirrored.Filled.Chat, DrawerSection.CONVERSE),
|
||||||
DrawerEntry(HistoryScreen, "History", Icons.Filled.History, DrawerSection.CONVERSE),
|
DrawerEntry(HistoryScreen, R.string.screen_history, Icons.Filled.History, DrawerSection.CONVERSE),
|
||||||
DrawerEntry(ProfilesScreen, "Profiles", Icons.Filled.AccountCircle, DrawerSection.CONVERSE),
|
DrawerEntry(ProfilesScreen, R.string.screen_profiles, Icons.Filled.AccountCircle, DrawerSection.CONVERSE),
|
||||||
// Automate
|
// Automate
|
||||||
DrawerEntry(CronJobsScreen, "Cron", Icons.Filled.Schedule, DrawerSection.AUTOMATE),
|
DrawerEntry(CronJobsScreen, R.string.screen_cron, Icons.Filled.Schedule, DrawerSection.AUTOMATE),
|
||||||
DrawerEntry(WebhooksScreen, "Webhooks", Icons.Filled.Webhook, DrawerSection.AUTOMATE),
|
DrawerEntry(WebhooksScreen, R.string.screen_webhooks, Icons.Filled.Webhook, DrawerSection.AUTOMATE),
|
||||||
DrawerEntry(GatewayScreen, "Gateway", Icons.Filled.Bolt, DrawerSection.AUTOMATE),
|
DrawerEntry(GatewayScreen, R.string.screen_gateway, Icons.Filled.Bolt, DrawerSection.AUTOMATE),
|
||||||
// Configure
|
// Configure
|
||||||
DrawerEntry(SkillsScreen, "Skills", Icons.Filled.Extension, DrawerSection.CONFIGURE),
|
DrawerEntry(SkillsScreen, R.string.screen_skills, Icons.Filled.Extension, DrawerSection.CONFIGURE),
|
||||||
DrawerEntry(SettingsScreen, "Settings", Icons.Filled.Settings, DrawerSection.CONFIGURE),
|
DrawerEntry(SettingsScreen, R.string.screen_settings, Icons.Filled.Settings, DrawerSection.CONFIGURE),
|
||||||
DrawerEntry(ToolsetsScreen, "Toolsets", Icons.Filled.Build, DrawerSection.CONFIGURE),
|
DrawerEntry(ToolsetsScreen, R.string.screen_toolsets, Icons.Filled.Build, DrawerSection.CONFIGURE),
|
||||||
DrawerEntry(PluginsScreen, "Plugins", Icons.Filled.Memory, DrawerSection.CONFIGURE),
|
DrawerEntry(PluginsScreen, R.string.screen_plugins, Icons.Filled.Memory, DrawerSection.CONFIGURE),
|
||||||
DrawerEntry(ConfigScreen, "Config", Icons.Filled.Code, DrawerSection.CONFIGURE),
|
DrawerEntry(ConfigScreen, R.string.screen_config, Icons.Filled.Code, DrawerSection.CONFIGURE),
|
||||||
DrawerEntry(McpServersScreen, "MCP Servers", Icons.Filled.Dashboard, DrawerSection.CONFIGURE),
|
DrawerEntry(McpServersScreen, R.string.screen_mcp_servers, Icons.Filled.Dashboard, DrawerSection.CONFIGURE),
|
||||||
DrawerEntry(ModelScreen, "Models", Icons.Filled.Settings, DrawerSection.CONFIGURE),
|
DrawerEntry(ModelScreen, R.string.screen_models, Icons.Filled.Settings, DrawerSection.CONFIGURE),
|
||||||
DrawerEntry(PairingScreen, "Pairing", Icons.Filled.Devices, DrawerSection.CONFIGURE),
|
DrawerEntry(PairingScreen, R.string.screen_pairing, Icons.Filled.Devices, DrawerSection.CONFIGURE),
|
||||||
DrawerEntry(KeysScreen, "Keys", Icons.Filled.Key, DrawerSection.CONFIGURE),
|
DrawerEntry(KeysScreen, R.string.screen_keys, Icons.Filled.Key, DrawerSection.CONFIGURE),
|
||||||
DrawerEntry(ChannelsScreen, "Channels", Icons.AutoMirrored.Filled.ListAlt, DrawerSection.CONFIGURE),
|
DrawerEntry(
|
||||||
|
ChannelsScreen,
|
||||||
|
R.string.screen_channels,
|
||||||
|
Icons.AutoMirrored.Filled.ListAlt,
|
||||||
|
DrawerSection.CONFIGURE,
|
||||||
|
),
|
||||||
// Inspect
|
// Inspect
|
||||||
DrawerEntry(SystemScreen, "System", Icons.Filled.Info, DrawerSection.INSPECT),
|
DrawerEntry(SystemScreen, R.string.screen_system, Icons.Filled.Info, DrawerSection.INSPECT),
|
||||||
DrawerEntry(LogsScreen, "Logs", Icons.Filled.HistoryEdu, DrawerSection.INSPECT),
|
DrawerEntry(LogsScreen, R.string.screen_logs, Icons.Filled.HistoryEdu, DrawerSection.INSPECT),
|
||||||
DrawerEntry(KanbanScreen, "Kanban", Icons.Filled.Dashboard, DrawerSection.INSPECT),
|
DrawerEntry(KanbanScreen, R.string.screen_kanban, Icons.Filled.Dashboard, DrawerSection.INSPECT),
|
||||||
DrawerEntry(AchievementsScreen, "Achievements", Icons.Filled.Info, DrawerSection.INSPECT),
|
DrawerEntry(AchievementsScreen, R.string.screen_achievements, Icons.Filled.Info, DrawerSection.INSPECT),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DRAWER_GESTURE_SCREENS: Set<NavKey> = ALL_NAV_ITEMS.mapTo(mutableSetOf()) { it.key }
|
private val DRAWER_GESTURE_SCREENS: Set<NavKey> = ALL_NAV_ITEMS.mapTo(mutableSetOf()) { it.key }
|
||||||
@@ -370,7 +377,7 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
modifier = Modifier.padding(start = 20.dp, top = 20.dp, end = 16.dp, bottom = 4.dp),
|
modifier = Modifier.padding(start = 20.dp, top = 20.dp, end = 16.dp, bottom = 4.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "⚡ Hermes",
|
text = stringResource(R.string.nav_drawer_title),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.headlineSmall.copy(
|
MaterialTheme.typography.headlineSmall.copy(
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -386,7 +393,7 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "AI Agent Control",
|
text = stringResource(R.string.nav_drawer_subtitle),
|
||||||
modifier = Modifier.padding(start = 20.dp, bottom = 12.dp, end = 16.dp),
|
modifier = Modifier.padding(start = 20.dp, bottom = 12.dp, end = 16.dp),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -395,7 +402,7 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
|
|
||||||
for (section in DrawerSection.entries) {
|
for (section in DrawerSection.entries) {
|
||||||
Text(
|
Text(
|
||||||
text = section.title.uppercase(),
|
text = stringResource(section.titleRes).uppercase(),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.padding(
|
Modifier.padding(
|
||||||
start = 20.dp,
|
start = 20.dp,
|
||||||
@@ -412,7 +419,7 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
.forEach { entry ->
|
.forEach { entry ->
|
||||||
NavigationDrawerItem(
|
NavigationDrawerItem(
|
||||||
icon = { Icon(entry.icon, contentDescription = null) },
|
icon = { Icon(entry.icon, contentDescription = null) },
|
||||||
label = { Text(entry.label) },
|
label = { Text(stringResource(entry.labelRes)) },
|
||||||
selected = currentScreen == entry.key,
|
selected = currentScreen == entry.key,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch { drawerState.close() }
|
scope.launch { drawerState.close() }
|
||||||
@@ -427,7 +434,11 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(horizontal = 12.dp, vertical = 2.dp)
|
.padding(horizontal = 12.dp, vertical = 2.dp)
|
||||||
.testTag("drawer_${entry.label.lowercase().replace(' ', '_')}"),
|
.testTag(
|
||||||
|
"drawer_${entry.key::class.simpleName?.lowercase()?.removeSuffix(
|
||||||
|
"screen",
|
||||||
|
) ?: ""}",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,9 +459,12 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = currentScreen == item.key,
|
selected = currentScreen == item.key,
|
||||||
onClick = { NavigationController.navigateTo(item.key) },
|
onClick = { NavigationController.navigateTo(item.key) },
|
||||||
icon = { Icon(item.icon, contentDescription = item.label) },
|
icon = { Icon(item.icon, contentDescription = stringResource(item.labelRes)) },
|
||||||
label = { Text(item.label) },
|
label = { Text(stringResource(item.labelRes)) },
|
||||||
modifier = Modifier.testTag("nav_${item.label.lowercase().replace(' ', '_')}"),
|
modifier =
|
||||||
|
Modifier.testTag(
|
||||||
|
"nav_${item.key::class.simpleName?.lowercase()?.removeSuffix("screen") ?: ""}",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,9 +95,7 @@ object AuthManager {
|
|||||||
requirePrefs().edit().putString(KEY_CONNECTION_PROFILES, json).apply()
|
requirePrefs().edit().putString(KEY_CONNECTION_PROFILES, json).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getProfileToken(profileId: String): String? {
|
fun getProfileToken(profileId: String): String? = requirePrefs().getString("token_$profileId", null)
|
||||||
return requirePrefs().getString("token_$profileId", null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setProfileToken(
|
fun setProfileToken(
|
||||||
profileId: String,
|
profileId: String,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class ChatNotificationService : Service() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
createNotificationChannels()
|
createNotificationChannels()
|
||||||
startForeground(NOTIFICATION_ID, buildForegroundNotification("Waiting for Hermes replies"))
|
startForeground(NOTIFICATION_ID, buildForegroundNotification(getString(R.string.notif_waiting_replies)))
|
||||||
startEventCollection()
|
startEventCollection()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +85,12 @@ class ChatNotificationService : Service() {
|
|||||||
event.text
|
event.text
|
||||||
.take(100)
|
.take(100)
|
||||||
.replace("\n", " ")
|
.replace("\n", " ")
|
||||||
.ifBlank { "New message" }
|
.ifBlank { getString(R.string.notif_new_message) }
|
||||||
showReplyNotification(preview, event.sessionId)
|
showReplyNotification(preview, event.sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
is WsEvent.ClarifyRequest -> {
|
is WsEvent.ClarifyRequest -> {
|
||||||
showReplyNotification("Hermes needs a clarification", null)
|
showReplyNotification(getString(R.string.notif_clarification_needed), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
@@ -108,7 +108,7 @@ class ChatNotificationService : Service() {
|
|||||||
NotificationCompat
|
NotificationCompat
|
||||||
.Builder(this, CHAT_CHANNEL_ID)
|
.Builder(this, CHAT_CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle("Hermes")
|
.setContentTitle(getString(R.string.notif_title))
|
||||||
.setContentText(text)
|
.setContentText(text)
|
||||||
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
@@ -116,7 +116,7 @@ class ChatNotificationService : Service() {
|
|||||||
.setContentIntent(buildContentIntent(sessionId))
|
.setContentIntent(buildContentIntent(sessionId))
|
||||||
|
|
||||||
if (!sessionId.isNullOrBlank()) {
|
if (!sessionId.isNullOrBlank()) {
|
||||||
val replyLabel = "Type your reply..."
|
val replyLabel = getString(R.string.notif_reply_placeholder)
|
||||||
val remoteInput =
|
val remoteInput =
|
||||||
RemoteInput
|
RemoteInput
|
||||||
.Builder(NotificationReplyReceiver.KEY_TEXT_REPLY)
|
.Builder(NotificationReplyReceiver.KEY_TEXT_REPLY)
|
||||||
@@ -140,7 +140,7 @@ class ChatNotificationService : Service() {
|
|||||||
NotificationCompat.Action
|
NotificationCompat.Action
|
||||||
.Builder(
|
.Builder(
|
||||||
R.drawable.ic_notification,
|
R.drawable.ic_notification,
|
||||||
"Reply",
|
getString(R.string.action_reply),
|
||||||
replyPendingIntent,
|
replyPendingIntent,
|
||||||
).addRemoteInput(remoteInput)
|
).addRemoteInput(remoteInput)
|
||||||
.build()
|
.build()
|
||||||
@@ -169,7 +169,7 @@ class ChatNotificationService : Service() {
|
|||||||
NotificationCompat
|
NotificationCompat
|
||||||
.Builder(this, SERVICE_CHANNEL_ID)
|
.Builder(this, SERVICE_CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle("Hermes")
|
.setContentTitle(getString(R.string.notif_title))
|
||||||
.setContentText(text)
|
.setContentText(text)
|
||||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
@@ -180,19 +180,19 @@ class ChatNotificationService : Service() {
|
|||||||
val serviceChannel =
|
val serviceChannel =
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
SERVICE_CHANNEL_ID,
|
SERVICE_CHANNEL_ID,
|
||||||
"Hermes Background Service",
|
getString(R.string.notif_channel_service_name),
|
||||||
NotificationManager.IMPORTANCE_MIN,
|
NotificationManager.IMPORTANCE_MIN,
|
||||||
).apply {
|
).apply {
|
||||||
description = "Keeps connection alive in the background"
|
description = getString(R.string.notif_channel_service_desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
val chatChannel =
|
val chatChannel =
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHAT_CHANNEL_ID,
|
CHAT_CHANNEL_ID,
|
||||||
"Hermes Chat Replies",
|
getString(R.string.notif_channel_chat_name),
|
||||||
NotificationManager.IMPORTANCE_HIGH,
|
NotificationManager.IMPORTANCE_HIGH,
|
||||||
).apply {
|
).apply {
|
||||||
description = "Alerts for new replies from Hermes"
|
description = getString(R.string.notif_channel_chat_desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -45,7 +47,7 @@ fun AchievementsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Agent Achievements") },
|
title = { Text(stringResource(R.string.screen_achievements)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadAchievements() },
|
onRefresh = { viewModel.loadAchievements() },
|
||||||
@@ -65,8 +67,8 @@ fun AchievementsScreen(
|
|||||||
|
|
||||||
state.achievements.isEmpty() -> {
|
state.achievements.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No achievements yet",
|
title = stringResource(R.string.achievements_empty_title),
|
||||||
subtitle = "Complete tasks to unlock achievements.",
|
subtitle = stringResource(R.string.achievements_empty_desc),
|
||||||
icon = Icons.Filled.Refresh,
|
icon = Icons.Filled.Refresh,
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
@@ -156,8 +158,11 @@ fun AchievementsScreen(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
"Progress: ${achievement.progress?.toInt() ?: 0} / " +
|
stringResource(
|
||||||
"${achievement.next_threshold?.toInt() ?: 0}",
|
R.string.achievements_label_progress,
|
||||||
|
achievement.progress?.toInt() ?: 0,
|
||||||
|
achievement.next_threshold?.toInt() ?: 0,
|
||||||
|
),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -29,9 +29,11 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.data.model.MessagingPlatformUpdate
|
import com.m57.hermescontrol.data.model.MessagingPlatformUpdate
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
@@ -59,7 +61,7 @@ fun ChannelsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Messaging Channels") },
|
title = { Text(stringResource(R.string.screen_channels)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadPlatforms() },
|
onRefresh = { viewModel.loadPlatforms() },
|
||||||
@@ -79,8 +81,8 @@ fun ChannelsScreen(
|
|||||||
|
|
||||||
state.platforms.isEmpty() -> {
|
state.platforms.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No channels configured",
|
title = stringResource(R.string.channels_empty_title),
|
||||||
subtitle = "Add messaging platforms in Hermes to see them here.",
|
subtitle = stringResource(R.string.channels_empty_desc),
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,14 @@ fun ChannelsScreen(
|
|||||||
Column {
|
Column {
|
||||||
Text(text = platform.name, style = MaterialTheme.typography.titleLarge)
|
Text(text = platform.name, style = MaterialTheme.typography.titleLarge)
|
||||||
Text(
|
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 =
|
color =
|
||||||
if (platform.enabled) {
|
if (platform.enabled) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
@@ -126,7 +135,15 @@ fun ChannelsScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Button(onClick = { showConfigureForm = !showConfigureForm }) {
|
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) {
|
if (showConfigureForm) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Configuration Settings",
|
text = stringResource(R.string.channels_sec_settings),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -148,7 +165,7 @@ fun ChannelsScreen(
|
|||||||
label = { Text(field.prompt ?: field.key) },
|
label = { Text(field.prompt ?: field.key) },
|
||||||
placeholder = {
|
placeholder = {
|
||||||
if (field.isSet) {
|
if (field.isSet) {
|
||||||
Text("******** (Already Configured)")
|
Text(stringResource(R.string.channels_placeholder_configured))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -169,13 +186,16 @@ fun ChannelsScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.align(Alignment.End),
|
modifier = Modifier.align(Alignment.End),
|
||||||
) {
|
) {
|
||||||
Text("Save Settings")
|
Text(stringResource(R.string.channels_action_save_settings))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val envFields = platform.envVars.orEmpty()
|
val envFields = platform.envVars.orEmpty()
|
||||||
if (envFields.isNotEmpty()) {
|
if (envFields.isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
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 ->
|
envFields.forEach { field ->
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -187,7 +207,14 @@ fun ChannelsScreen(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
Text(
|
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,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color =
|
color =
|
||||||
if (field.isSet) {
|
if (field.isSet) {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import androidx.compose.ui.input.pointer.pointerInput
|
|||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
@@ -64,6 +65,7 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.AssistantBubble
|
import com.m57.hermescontrol.theme.AssistantBubble
|
||||||
import com.m57.hermescontrol.theme.AssistantBubbleLight
|
import com.m57.hermescontrol.theme.AssistantBubbleLight
|
||||||
import com.m57.hermescontrol.theme.StatusGreen
|
import com.m57.hermescontrol.theme.StatusGreen
|
||||||
@@ -232,7 +234,7 @@ private fun UserBubble(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.ContentCopy,
|
Icons.Filled.ContentCopy,
|
||||||
contentDescription = "Copy",
|
contentDescription = stringResource(R.string.content_desc_copy),
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
@@ -347,7 +349,7 @@ private fun AssistantBubble(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.ContentCopy,
|
Icons.Filled.ContentCopy,
|
||||||
contentDescription = "Copy",
|
contentDescription = stringResource(R.string.content_desc_copy),
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
@@ -489,7 +491,7 @@ private fun ParsedToolContent(
|
|||||||
}
|
}
|
||||||
parsed.stderr?.let {
|
parsed.stderr?.let {
|
||||||
Text(
|
Text(
|
||||||
text = "Error Output (stderr):\n$it",
|
text = stringResource(R.string.chat_tool_stderr, it),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.bodySmall.copy(
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
color = StatusRed.copy(alpha = 0.9f),
|
color = StatusRed.copy(alpha = 0.9f),
|
||||||
@@ -500,7 +502,7 @@ private fun ParsedToolContent(
|
|||||||
}
|
}
|
||||||
parsed.error?.let {
|
parsed.error?.let {
|
||||||
Text(
|
Text(
|
||||||
text = "Execution Error: $it",
|
text = stringResource(R.string.chat_tool_execution_error, it),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.bodySmall.copy(
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
color = StatusRed,
|
color = StatusRed,
|
||||||
@@ -513,7 +515,7 @@ private fun ParsedToolContent(
|
|||||||
parsed.exitCode?.let { code ->
|
parsed.exitCode?.let { code ->
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
Text(
|
Text(
|
||||||
text = "Exit Code: $code",
|
text = stringResource(R.string.chat_tool_exit_code, code),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.labelSmall.copy(
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
color = StatusRed,
|
color = StatusRed,
|
||||||
@@ -619,7 +621,7 @@ private fun ToolBubble(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = message.toolName ?: "Tool",
|
text = message.toolName ?: stringResource(R.string.chat_tool_fallback),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.labelMedium.copy(
|
MaterialTheme.typography.labelMedium.copy(
|
||||||
color = contentColor,
|
color = contentColor,
|
||||||
@@ -635,7 +637,7 @@ private fun ToolBubble(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Show Raw JSON",
|
text = stringResource(R.string.chat_tool_show_raw),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.labelSmall.copy(
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
@@ -660,7 +662,7 @@ private fun ToolBubble(
|
|||||||
if (parsedOutput != null) {
|
if (parsedOutput != null) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Show Parsed Output",
|
text = stringResource(R.string.chat_tool_show_parsed),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.labelSmall.copy(
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
|||||||
@@ -293,14 +293,20 @@ fun ChatScreen(
|
|||||||
imageVector =
|
imageVector =
|
||||||
if (state.isSearchActive) Icons.Filled.Close else Icons.Filled.Search,
|
if (state.isSearchActive) Icons.Filled.Close else Icons.Filled.Search,
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (state.isSearchActive) "Close search" else "Search",
|
if (state.isSearchActive) {
|
||||||
|
stringResource(
|
||||||
|
R.string.chat_action_close_search,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.chat_action_search)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(onClick = { viewModel.createNewSession() }) {
|
IconButton(onClick = { viewModel.createNewSession() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Add,
|
imageVector = Icons.Filled.Add,
|
||||||
contentDescription = "New Chat",
|
contentDescription = stringResource(R.string.content_desc_new_chat),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -335,9 +341,9 @@ fun ChatScreen(
|
|||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
if (state.connectionStatus == ConnectionStatus.RECONNECTING) {
|
if (state.connectionStatus == ConnectionStatus.RECONNECTING) {
|
||||||
"Reconnecting to Hermes…"
|
stringResource(R.string.chat_status_reconnecting)
|
||||||
} else {
|
} else {
|
||||||
"Disconnected from Hermes"
|
stringResource(R.string.chat_status_disconnected)
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
@@ -349,7 +355,7 @@ fun ChatScreen(
|
|||||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text("Reconnect")
|
Text(stringResource(R.string.chat_action_reconnect))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,8 +399,8 @@ fun ChatScreen(
|
|||||||
) {
|
) {
|
||||||
if (state.messages.isEmpty() && !state.isLoading) {
|
if (state.messages.isEmpty() && !state.isLoading) {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "Ready to chat",
|
title = stringResource(R.string.chat_empty_title),
|
||||||
subtitle = "Send a message to start a conversation",
|
subtitle = stringResource(R.string.chat_empty_subtitle),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,7 +488,7 @@ fun ChatScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Connecting to Hermes…",
|
text = stringResource(R.string.chat_status_connecting),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -512,7 +518,7 @@ fun ChatScreen(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.KeyboardArrowDown,
|
imageVector = Icons.Filled.KeyboardArrowDown,
|
||||||
contentDescription = "Scroll to bottom",
|
contentDescription = stringResource(R.string.content_desc_scroll_to_bottom),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,9 +590,9 @@ private fun ThinkingIndicator(thinkingText: String) {
|
|||||||
text =
|
text =
|
||||||
if (thinkingText.isNotBlank()) {
|
if (thinkingText.isNotBlank()) {
|
||||||
val display = thinkingText.takeLast(100)
|
val display = thinkingText.takeLast(100)
|
||||||
"Thinking: $display…"
|
stringResource(R.string.chat_thinking_param, display)
|
||||||
} else {
|
} else {
|
||||||
"Thinking…"
|
stringResource(R.string.chat_thinking)
|
||||||
},
|
},
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.bodySmall.copy(
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
@@ -671,14 +677,14 @@ private fun ChatInputBar(
|
|||||||
items(filteredCommands) { cmd ->
|
items(filteredCommands) { cmd ->
|
||||||
val description =
|
val description =
|
||||||
when (cmd) {
|
when (cmd) {
|
||||||
"/help" -> "Show help menu"
|
"/help" -> stringResource(R.string.command_help_desc)
|
||||||
"/status" -> "Check gateway and platform status"
|
"/status" -> stringResource(R.string.command_status_desc)
|
||||||
"/sessions" -> "List all chat sessions"
|
"/sessions" -> stringResource(R.string.command_sessions_desc)
|
||||||
"/stats" -> "Check system resource usage (stats)"
|
"/stats" -> stringResource(R.string.command_stats_desc)
|
||||||
"/system" -> "Check system resource usage (system)"
|
"/system" -> stringResource(R.string.command_system_desc)
|
||||||
"/new" -> "Create a new chat session"
|
"/new" -> stringResource(R.string.command_new_desc)
|
||||||
"/stop" -> "Interrupt the active run"
|
"/stop" -> stringResource(R.string.command_stop_desc)
|
||||||
"/interrupt" -> "Interrupt the active run"
|
"/interrupt" -> stringResource(R.string.command_stop_desc)
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
@@ -725,11 +731,11 @@ private fun ChatInputBar(
|
|||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
"Not connected…"
|
stringResource(R.string.chat_input_placeholder_not_connected)
|
||||||
} else if (isAgentTyping) {
|
} else if (isAgentTyping) {
|
||||||
"Waiting for response…"
|
stringResource(R.string.chat_input_placeholder_waiting)
|
||||||
} else {
|
} else {
|
||||||
"Type a message…"
|
stringResource(R.string.chat_input_placeholder_type_message)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -759,7 +765,7 @@ private fun ChatInputBar(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Stop,
|
imageVector = Icons.Filled.Stop,
|
||||||
contentDescription = "Interrupt",
|
contentDescription = stringResource(R.string.content_desc_interrupt),
|
||||||
tint = MaterialTheme.colorScheme.error,
|
tint = MaterialTheme.colorScheme.error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,13 +150,33 @@ class ChatViewModel(
|
|||||||
|
|
||||||
is WsEvent.SessionInfo -> {}
|
is WsEvent.SessionInfo -> {}
|
||||||
|
|
||||||
is WsEvent.MessageStart -> handleMessageStart(event)
|
is WsEvent.MessageStart -> {
|
||||||
is WsEvent.MessageToken -> handleMessageToken(event)
|
handleMessageStart(event)
|
||||||
is WsEvent.ThinkingDelta -> handleThinkingDelta(event)
|
}
|
||||||
is WsEvent.MessageComplete -> handleMessageComplete(event)
|
|
||||||
is WsEvent.MessageDone -> handleMessageDone(event)
|
is WsEvent.MessageToken -> {
|
||||||
is WsEvent.ToolStart -> handleToolStart(event)
|
handleMessageToken(event)
|
||||||
is WsEvent.ToolComplete -> handleToolComplete(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 -> {
|
is WsEvent.ClarifyRequest -> {
|
||||||
if (!isCurrentSession(event.sessionId)) return
|
if (!isCurrentSession(event.sessionId)) return
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared Scaffold wrapper that standardizes the TopAppBar.
|
* Shared Scaffold wrapper that standardizes the TopAppBar.
|
||||||
@@ -69,7 +71,7 @@ fun HermesScaffold(
|
|||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = "Back",
|
contentDescription = stringResource(R.string.content_desc_back),
|
||||||
modifier = Modifier.testTag("back_button"),
|
modifier = Modifier.testTag("back_button"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -79,7 +81,7 @@ fun HermesScaffold(
|
|||||||
IconButton(onClick = onOpenDrawer) {
|
IconButton(onClick = onOpenDrawer) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Menu,
|
imageVector = Icons.Filled.Menu,
|
||||||
contentDescription = "Open navigation drawer",
|
contentDescription = stringResource(R.string.content_desc_open_drawer),
|
||||||
modifier = Modifier.testTag("menu_button"),
|
modifier = Modifier.testTag("menu_button"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -91,7 +93,7 @@ fun HermesScaffold(
|
|||||||
IconButton(onClick = onRefresh) {
|
IconButton(onClick = onRefresh) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Refresh,
|
imageVector = Icons.Filled.Refresh,
|
||||||
contentDescription = "Refresh",
|
contentDescription = stringResource(R.string.content_desc_refresh),
|
||||||
modifier = Modifier.testTag("refresh_button"),
|
modifier = Modifier.testTag("refresh_button"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||||
import com.m57.hermescontrol.theme.LocalSpacing
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
|
|
||||||
@@ -215,7 +217,7 @@ fun SearchBar(
|
|||||||
IconButton(onClick = { onQueryChange("") }) {
|
IconButton(onClick = { onQueryChange("") }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Close,
|
imageVector = Icons.Filled.Close,
|
||||||
contentDescription = "Clear search",
|
contentDescription = stringResource(R.string.content_desc_clear_search),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,7 +250,15 @@ fun FilterChipRow(
|
|||||||
FilterChip(
|
FilterChip(
|
||||||
selected = chip == selectedChip,
|
selected = chip == selectedChip,
|
||||||
onClick = { onChipSelected(chip) },
|
onClick = { onChipSelected(chip) },
|
||||||
label = { Text(chip) },
|
label = {
|
||||||
|
Text(
|
||||||
|
if (chip == "All") {
|
||||||
|
stringResource(R.string.skills_category_all)
|
||||||
|
} else {
|
||||||
|
chip
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.LocalSpacing
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
|
|
||||||
// Unified loading / error / empty / skeleton placeholders so every screen
|
// Unified loading / error / empty / skeleton placeholders so every screen
|
||||||
@@ -94,7 +96,7 @@ fun ErrorState(
|
|||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(spacing.sm))
|
Spacer(modifier = Modifier.size(spacing.sm))
|
||||||
Text("Retry")
|
Text(stringResource(R.string.action_retry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
@@ -57,7 +59,7 @@ fun ConfigScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Configuration") },
|
title = { Text(stringResource(R.string.config_screen_title)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadRawConfig() },
|
onRefresh = { viewModel.loadRawConfig() },
|
||||||
@@ -87,7 +89,7 @@ fun ConfigScreen(
|
|||||||
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Button(onClick = { viewModel.loadRawConfig() }) {
|
Button(onClick = { viewModel.loadRawConfig() }) {
|
||||||
Text("Retry")
|
Text(stringResource(R.string.action_retry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -113,7 +115,7 @@ fun ConfigScreen(
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
placeholder = { Text("YAML Config...") },
|
placeholder = { Text(stringResource(R.string.config_placeholder_yaml)) },
|
||||||
textStyle = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
textStyle = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||||
maxLines = Int.MAX_VALUE,
|
maxLines = Int.MAX_VALUE,
|
||||||
)
|
)
|
||||||
@@ -125,7 +127,7 @@ fun ConfigScreen(
|
|||||||
onClick = { editingText = state.yamlText ?: "" },
|
onClick = { editingText = state.yamlText ?: "" },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
Text("Reset")
|
Text(stringResource(R.string.config_action_reset))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
@@ -138,7 +140,7 @@ fun ConfigScreen(
|
|||||||
if (state.isSaving) {
|
if (state.isSaving) {
|
||||||
CircularProgressIndicator(modifier = Modifier.width(16.dp))
|
CircularProgressIndicator(modifier = Modifier.width(16.dp))
|
||||||
} else {
|
} else {
|
||||||
Text("Save Config")
|
Text(stringResource(R.string.config_action_save))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
@@ -53,6 +54,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectScreen(
|
fun ConnectScreen(
|
||||||
@@ -106,7 +108,7 @@ fun ConnectScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Hermes",
|
text = stringResource(R.string.notif_title),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.displayMedium.copy(
|
MaterialTheme.typography.displayMedium.copy(
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -114,7 +116,7 @@ fun ConnectScreen(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "AI Agent Control",
|
text = stringResource(R.string.nav_drawer_subtitle),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.bodyLarge.copy(
|
MaterialTheme.typography.bodyLarge.copy(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -134,9 +136,9 @@ fun ConnectScreen(
|
|||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
if (state.selectedProfile != null) {
|
if (state.selectedProfile != null) {
|
||||||
"Profile: ${state.selectedProfile!!.name}"
|
stringResource(R.string.connect_selected_profile, state.selectedProfile!!.name)
|
||||||
} else {
|
} else {
|
||||||
"Select Saved Profile"
|
stringResource(R.string.connect_action_select_profile)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -146,7 +148,7 @@ fun ConnectScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(0.85f),
|
modifier = Modifier.fillMaxWidth(0.85f),
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("None (Standalone)") },
|
text = { Text(stringResource(R.string.connect_profile_none)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.selectProfile(null)
|
viewModel.selectProfile(null)
|
||||||
profilesExpanded = false
|
profilesExpanded = false
|
||||||
@@ -171,7 +173,7 @@ fun ConnectScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Pairing string",
|
text = stringResource(R.string.connect_pairing_string),
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
@@ -182,7 +184,7 @@ fun ConnectScreen(
|
|||||||
pairingString = it
|
pairingString = it
|
||||||
if (it.isNotBlank()) viewModel.onPairingString(it)
|
if (it.isNotBlank()) viewModel.onPairingString(it)
|
||||||
},
|
},
|
||||||
placeholder = { Text("Paste hermes://… or Base64 config") },
|
placeholder = { Text(stringResource(R.string.connect_placeholder_pairing)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
@@ -215,7 +217,7 @@ fun ConnectScreen(
|
|||||||
onCheckedChange = viewModel::onSaveProfileChange,
|
onCheckedChange = viewModel::onSaveProfileChange,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Save connection profile",
|
text = stringResource(R.string.connect_save_profile),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
@@ -225,8 +227,8 @@ fun ConnectScreen(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.profileName,
|
value = state.profileName,
|
||||||
onValueChange = viewModel::onProfileNameChange,
|
onValueChange = viewModel::onProfileNameChange,
|
||||||
label = { Text("Profile Name") },
|
label = { Text(stringResource(R.string.connect_profile_name)) },
|
||||||
placeholder = { Text("e.g. Local Hermit, Remote Server") },
|
placeholder = { Text(stringResource(R.string.connect_placeholder_profile_name)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
@@ -247,7 +249,7 @@ fun ConnectScreen(
|
|||||||
strokeWidth = 2.dp,
|
strokeWidth = 2.dp,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text("Connect", style = MaterialTheme.typography.labelLarge)
|
Text(stringResource(R.string.action_connect), style = MaterialTheme.typography.labelLarge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +258,12 @@ fun ConnectScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (showManualFields) "Hide manual entry" else "Enter manually instead",
|
text =
|
||||||
|
if (showManualFields) {
|
||||||
|
stringResource(R.string.connect_action_hide_manual)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.connect_action_show_manual)
|
||||||
|
},
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
@@ -271,8 +278,8 @@ fun ConnectScreen(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.token,
|
value = state.token,
|
||||||
onValueChange = viewModel::onTokenChange,
|
onValueChange = viewModel::onTokenChange,
|
||||||
label = { Text("Auth Token") },
|
label = { Text(stringResource(R.string.settings_field_token)) },
|
||||||
placeholder = { Text("API token") },
|
placeholder = { Text(stringResource(R.string.connect_placeholder_token)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
visualTransformation =
|
visualTransformation =
|
||||||
if (passwordVisible) {
|
if (passwordVisible) {
|
||||||
@@ -290,7 +297,11 @@ fun ConnectScreen(
|
|||||||
Icons.Filled.VisibilityOff
|
Icons.Filled.VisibilityOff
|
||||||
},
|
},
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (passwordVisible) "Hide token" else "Show token",
|
if (passwordVisible) {
|
||||||
|
stringResource(R.string.content_desc_hide_token)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.content_desc_show_token)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -299,16 +310,16 @@ fun ConnectScreen(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.host,
|
value = state.host,
|
||||||
onValueChange = viewModel::onHostChange,
|
onValueChange = viewModel::onHostChange,
|
||||||
label = { Text("Host") },
|
label = { Text(stringResource(R.string.settings_field_host)) },
|
||||||
placeholder = { Text("127.0.0.1") },
|
placeholder = { Text(stringResource(R.string.connect_placeholder_host)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.port,
|
value = state.port,
|
||||||
onValueChange = viewModel::onPortChange,
|
onValueChange = viewModel::onPortChange,
|
||||||
label = { Text("Port") },
|
label = { Text(stringResource(R.string.settings_field_port)) },
|
||||||
placeholder = { Text("9119") },
|
placeholder = { Text(stringResource(R.string.connect_placeholder_port)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.LocalSpacing
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
@@ -60,7 +62,7 @@ fun CronJobsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Cron Jobs") },
|
title = { Text(stringResource(R.string.screen_cron)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadCronJobs() },
|
onRefresh = { viewModel.loadCronJobs() },
|
||||||
@@ -79,8 +81,8 @@ fun CronJobsScreen(
|
|||||||
|
|
||||||
state.jobs.isEmpty() -> {
|
state.jobs.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No cron jobs",
|
title = stringResource(R.string.cron_empty_title),
|
||||||
subtitle = "Scheduled tasks from Hermes will appear here.",
|
subtitle = stringResource(R.string.cron_empty_desc),
|
||||||
icon = Icons.Filled.Schedule,
|
icon = Icons.Filled.Schedule,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -112,7 +114,14 @@ fun CronJobsScreen(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
StatusBadge(
|
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 =
|
status =
|
||||||
if (job.state == "active") {
|
if (job.state == "active") {
|
||||||
StatusBadgeType.SUCCESS
|
StatusBadgeType.SUCCESS
|
||||||
@@ -143,21 +152,21 @@ fun CronJobsScreen(
|
|||||||
onClick = { viewModel.pauseCronJob(job.id) },
|
onClick = { viewModel.pauseCronJob(job.id) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
Text("Pause")
|
Text(stringResource(R.string.cron_action_pause))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { viewModel.resumeCronJob(job.id) },
|
onClick = { viewModel.resumeCronJob(job.id) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
Text("Resume")
|
Text(stringResource(R.string.cron_action_resume))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { viewModel.triggerCronJob(job.id) },
|
onClick = { viewModel.triggerCronJob(job.id) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
Text("Run")
|
Text(stringResource(R.string.cron_action_run))
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
onClick = { viewModel.deleteCronJob(job.id) },
|
onClick = { viewModel.deleteCronJob(job.id) },
|
||||||
@@ -167,7 +176,7 @@ fun CronJobsScreen(
|
|||||||
),
|
),
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
Text("Delete")
|
Text(stringResource(R.string.action_delete))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,12 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
@@ -62,7 +64,7 @@ fun GatewayScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Gateway Control") },
|
title = { Text(stringResource(R.string.screen_gateway)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadStatus() },
|
onRefresh = { viewModel.loadStatus() },
|
||||||
@@ -96,7 +98,7 @@ fun GatewayScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Button(onClick = { viewModel.loadStatus() }) {
|
Button(onClick = { viewModel.loadStatus() }) {
|
||||||
Text("Retry")
|
Text(stringResource(R.string.action_retry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -132,7 +134,14 @@ fun GatewayScreen(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Text(
|
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,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color =
|
color =
|
||||||
@@ -145,7 +154,7 @@ fun GatewayScreen(
|
|||||||
status?.version?.let {
|
status?.version?.let {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Version: $it",
|
text = stringResource(R.string.gateway_label_version, it),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color =
|
color =
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
@@ -170,7 +179,7 @@ fun GatewayScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Controls",
|
text = stringResource(R.string.gateway_sec_controls),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
@@ -191,7 +200,7 @@ fun GatewayScreen(
|
|||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null)
|
Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text("Start")
|
Text(stringResource(R.string.gateway_action_start))
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
@@ -204,7 +213,7 @@ fun GatewayScreen(
|
|||||||
contentColor = Color.White,
|
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)
|
Icon(imageVector = Icons.Filled.Refresh, contentDescription = null)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text("Restart")
|
Text(stringResource(R.string.gateway_action_restart))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.isActionRunning) {
|
if (state.isActionRunning) {
|
||||||
@@ -245,7 +254,7 @@ fun GatewayScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Active Platforms",
|
text = stringResource(R.string.gateway_sec_active_platforms),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,10 +39,12 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.data.model.KanbanTask
|
import com.m57.hermescontrol.data.model.KanbanTask
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -73,7 +75,7 @@ fun KanbanScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Kanban Board") },
|
title = { Text(stringResource(R.string.kanban_board_title)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadBoards() },
|
onRefresh = { viewModel.loadBoards() },
|
||||||
@@ -99,10 +101,12 @@ fun KanbanScreen(
|
|||||||
Text(
|
Text(
|
||||||
text = state.errorMessage ?: "",
|
text = state.errorMessage ?: "",
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
||||||
|
) {
|
||||||
// Board selector tab row
|
// Board selector tab row
|
||||||
if (state.boards.isNotEmpty()) {
|
if (state.boards.isNotEmpty()) {
|
||||||
PrimaryScrollableTabRow(
|
PrimaryScrollableTabRow(
|
||||||
@@ -121,11 +125,11 @@ fun KanbanScreen(
|
|||||||
|
|
||||||
if (state.selectedBoard == null) {
|
if (state.selectedBoard == null) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text("No boards found.")
|
Text(stringResource(R.string.kanban_no_boards))
|
||||||
}
|
}
|
||||||
} else if (state.columns.isEmpty()) {
|
} else if (state.columns.isEmpty()) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text("No columns found for this board.")
|
Text(stringResource(R.string.kanban_no_columns))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
@@ -219,7 +223,10 @@ fun TaskCard(
|
|||||||
) {
|
) {
|
||||||
if (onMoveLeft != null) {
|
if (onMoveLeft != null) {
|
||||||
IconButton(onClick = onMoveLeft) {
|
IconButton(onClick = onMoveLeft) {
|
||||||
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Move Left")
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.content_desc_move_left),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Spacer(modifier = Modifier.size(48.dp))
|
Spacer(modifier = Modifier.size(48.dp))
|
||||||
@@ -227,7 +234,10 @@ fun TaskCard(
|
|||||||
|
|
||||||
if (onMoveRight != null) {
|
if (onMoveRight != null) {
|
||||||
IconButton(onClick = onMoveRight) {
|
IconButton(onClick = onMoveRight) {
|
||||||
Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = "Move Right")
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ArrowForward,
|
||||||
|
contentDescription = stringResource(R.string.content_desc_move_right),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Spacer(modifier = Modifier.size(48.dp))
|
Spacer(modifier = Modifier.size(48.dp))
|
||||||
@@ -248,20 +258,20 @@ fun AddTaskDialog(
|
|||||||
|
|
||||||
androidx.compose.material3.AlertDialog(
|
androidx.compose.material3.AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text("Add New Task") },
|
title = { Text(stringResource(R.string.kanban_add_new_task)) },
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = title,
|
value = title,
|
||||||
onValueChange = { title = it },
|
onValueChange = { title = it },
|
||||||
label = { Text("Task Title") },
|
label = { Text(stringResource(R.string.kanban_task_title)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = desc,
|
value = desc,
|
||||||
onValueChange = { desc = it },
|
onValueChange = { desc = it },
|
||||||
label = { Text("Task Description") },
|
label = { Text(stringResource(R.string.kanban_task_desc)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -271,12 +281,12 @@ fun AddTaskDialog(
|
|||||||
onClick = { if (title.isNotBlank()) onConfirm(title, desc.ifBlank { null }) },
|
onClick = { if (title.isNotBlank()) onConfirm(title, desc.ifBlank { null }) },
|
||||||
enabled = title.isNotBlank(),
|
enabled = title.isNotBlank(),
|
||||||
) {
|
) {
|
||||||
Text("Add")
|
Text(stringResource(R.string.action_add))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
Button(onClick = onDismiss) {
|
Button(onClick = onDismiss) {
|
||||||
Text("Cancel")
|
Text(stringResource(R.string.action_cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,10 +36,12 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -67,7 +69,7 @@ fun KeysScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Keys & Credentials") },
|
title = { Text(stringResource(R.string.screen_keys)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadKeys() },
|
onRefresh = { viewModel.loadKeys() },
|
||||||
@@ -87,10 +89,10 @@ fun KeysScreen(
|
|||||||
|
|
||||||
state.envVars.isEmpty() -> {
|
state.envVars.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No environment keys reported",
|
title = stringResource(R.string.keys_empty_title),
|
||||||
subtitle = "Verify configuration or retry",
|
subtitle = stringResource(R.string.toolsets_empty_desc),
|
||||||
onAction = { viewModel.loadKeys() },
|
onAction = { viewModel.loadKeys() },
|
||||||
actionLabel = "Refresh",
|
actionLabel = stringResource(R.string.content_desc_refresh),
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -123,7 +125,7 @@ fun KeysScreen(
|
|||||||
} else if (config.isSet) {
|
} else if (config.isSet) {
|
||||||
config.redactedValue ?: "********"
|
config.redactedValue ?: "********"
|
||||||
} else {
|
} else {
|
||||||
"Not configured"
|
stringResource(R.string.keys_label_not_configured)
|
||||||
}
|
}
|
||||||
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
@@ -158,13 +160,13 @@ fun KeysScreen(
|
|||||||
viewModel.updateKey(key, editedValue)
|
viewModel.updateKey(key, editedValue)
|
||||||
isEditing = false
|
isEditing = false
|
||||||
}) {
|
}) {
|
||||||
Text("Save")
|
Text(stringResource(R.string.action_save))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
isEditing = false
|
isEditing = false
|
||||||
}) {
|
}) {
|
||||||
Text("Cancel")
|
Text(stringResource(R.string.action_cancel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -196,7 +198,10 @@ fun KeysScreen(
|
|||||||
} else {
|
} else {
|
||||||
Icons.Filled.Visibility
|
Icons.Filled.Visibility
|
||||||
},
|
},
|
||||||
contentDescription = "Toggle Visibility",
|
contentDescription =
|
||||||
|
stringResource(
|
||||||
|
R.string.keys_action_toggle_visibility,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,7 +211,10 @@ fun KeysScreen(
|
|||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Edit,
|
imageVector = Icons.Filled.Edit,
|
||||||
contentDescription = "Edit",
|
contentDescription =
|
||||||
|
stringResource(
|
||||||
|
R.string.nav_drawer_section_converse,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.LocalSpacing
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
@@ -59,7 +61,7 @@ fun LogsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Logs") },
|
title = { Text(stringResource(R.string.screen_logs)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadLogs() },
|
onRefresh = { viewModel.loadLogs() },
|
||||||
@@ -71,7 +73,7 @@ fun LogsScreen(
|
|||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "Unknown error",
|
message = state.errorMessage ?: stringResource(R.string.error_unknown),
|
||||||
onRetry = { viewModel.loadLogs() },
|
onRetry = { viewModel.loadLogs() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -80,8 +82,8 @@ fun LogsScreen(
|
|||||||
// B5 (Jun 18 2026, kanban t_2322818d): proper empty-state using
|
// B5 (Jun 18 2026, kanban t_2322818d): proper empty-state using
|
||||||
// the shared EmptyState component instead of a bare Text.
|
// the shared EmptyState component instead of a bare Text.
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No logs available",
|
title = stringResource(R.string.logs_empty_title),
|
||||||
subtitle = "Log output from Hermes will appear here.",
|
subtitle = stringResource(R.string.logs_empty_desc),
|
||||||
icon = Icons.Filled.HistoryEdu,
|
icon = Icons.Filled.HistoryEdu,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
@@ -59,7 +61,7 @@ fun McpServersScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("MCP Servers") },
|
title = { Text(stringResource(R.string.screen_mcp_servers)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadServers() },
|
onRefresh = { viewModel.loadServers() },
|
||||||
@@ -89,7 +91,7 @@ fun McpServersScreen(
|
|||||||
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Button(onClick = { viewModel.loadServers() }) {
|
Button(onClick = { viewModel.loadServers() }) {
|
||||||
Text("Retry")
|
Text(stringResource(R.string.action_retry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -129,7 +131,11 @@ fun McpServersScreen(
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Transport: ${server.transport ?: "stdio"}",
|
text =
|
||||||
|
stringResource(
|
||||||
|
R.string.mcp_servers_label_transport,
|
||||||
|
server.transport ?: "stdio",
|
||||||
|
),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -145,7 +151,7 @@ fun McpServersScreen(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
val fullCmd = "${server.command} ${server.args.orEmpty().joinToString(" ")}"
|
val fullCmd = "${server.command} ${server.args.orEmpty().joinToString(" ")}"
|
||||||
Text(
|
Text(
|
||||||
text = "Command:",
|
text = stringResource(R.string.mcp_servers_label_command),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
@@ -168,13 +174,13 @@ fun McpServersScreen(
|
|||||||
onClick = { viewModel.testServer(server.name) },
|
onClick = { viewModel.testServer(server.name) },
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
) {
|
) {
|
||||||
Text("Test")
|
Text(stringResource(R.string.mcp_servers_action_test))
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { viewModel.deleteServer(server.name) },
|
onClick = { viewModel.deleteServer(server.name) },
|
||||||
) {
|
) {
|
||||||
Text("Delete")
|
Text(stringResource(R.string.action_delete))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,10 +35,12 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -67,7 +69,7 @@ fun ModelScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Models") },
|
title = { Text(stringResource(R.string.screen_models)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadModelOptions() },
|
onRefresh = { viewModel.loadModelOptions() },
|
||||||
@@ -87,10 +89,10 @@ fun ModelScreen(
|
|||||||
|
|
||||||
state.providers.isEmpty() -> {
|
state.providers.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No model providers reported",
|
title = stringResource(R.string.model_empty_title),
|
||||||
subtitle = "Verify configuration or retry",
|
subtitle = stringResource(R.string.model_empty_desc),
|
||||||
onAction = { viewModel.loadModelOptions() },
|
onAction = { viewModel.loadModelOptions() },
|
||||||
actionLabel = "Refresh",
|
actionLabel = stringResource(R.string.content_desc_refresh),
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -107,7 +109,10 @@ fun ModelScreen(
|
|||||||
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
IconButton(onClick = { viewModel.loadModelOptions() }) {
|
IconButton(onClick = { viewModel.loadModelOptions() }) {
|
||||||
Icon(imageVector = Icons.Filled.Refresh, contentDescription = "Retry")
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Refresh,
|
||||||
|
contentDescription = stringResource(R.string.action_retry),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -153,7 +158,7 @@ fun ModelScreen(
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Slug: ${provider.slug}",
|
text = stringResource(R.string.model_label_slug, provider.slug),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -161,7 +166,7 @@ fun ModelScreen(
|
|||||||
|
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
Text(
|
Text(
|
||||||
text = "CURRENT",
|
text = stringResource(R.string.model_status_current),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -183,7 +188,7 @@ fun ModelScreen(
|
|||||||
AnimatedVisibility(visible = isExpanded) {
|
AnimatedVisibility(visible = isExpanded) {
|
||||||
Column(modifier = Modifier.padding(top = 16.dp)) {
|
Column(modifier = Modifier.padding(top = 16.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Available Models:",
|
text = stringResource(R.string.model_label_available),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
@@ -192,7 +197,7 @@ fun ModelScreen(
|
|||||||
val models = provider.models.orEmpty()
|
val models = provider.models.orEmpty()
|
||||||
if (models.isEmpty()) {
|
if (models.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = "No models reported or provider needs authentication.",
|
text = stringResource(R.string.model_no_models),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -262,7 +267,10 @@ fun ModelScreen(
|
|||||||
if (isActive) {
|
if (isActive) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Check,
|
imageVector = Icons.Filled.Check,
|
||||||
contentDescription = "Active Model",
|
contentDescription =
|
||||||
|
stringResource(
|
||||||
|
R.string.content_desc_active_model,
|
||||||
|
),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,12 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
@@ -58,7 +60,7 @@ fun PairingScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Device Pairing") },
|
title = { Text(stringResource(R.string.pairing_screen_title)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadPairing() },
|
onRefresh = { viewModel.loadPairing() },
|
||||||
@@ -88,7 +90,7 @@ fun PairingScreen(
|
|||||||
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Button(onClick = { viewModel.loadPairing() }) {
|
Button(onClick = { viewModel.loadPairing() }) {
|
||||||
Text("Retry")
|
Text(stringResource(R.string.action_retry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -109,7 +111,7 @@ fun PairingScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Pending Approvals",
|
text = stringResource(R.string.pairing_sec_pending),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
@@ -121,7 +123,7 @@ fun PairingScreen(
|
|||||||
contentColor = MaterialTheme.colorScheme.error,
|
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,
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text("Approve")
|
Text(stringResource(R.string.pairing_action_approve))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +181,7 @@ fun PairingScreen(
|
|||||||
|
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = "Approved Devices",
|
text = stringResource(R.string.pairing_sec_approved),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
@@ -198,7 +200,7 @@ fun PairingScreen(
|
|||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "No approved devices paired yet.",
|
text = stringResource(R.string.pairing_empty_approved),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -225,7 +227,11 @@ fun PairingScreen(
|
|||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "User ID: ${item.user_id ?: "Unknown"}",
|
text =
|
||||||
|
stringResource(
|
||||||
|
R.string.pairing_label_user_id,
|
||||||
|
item.user_id ?: "Unknown",
|
||||||
|
),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -246,7 +252,7 @@ fun PairingScreen(
|
|||||||
contentColor = MaterialTheme.colorScheme.error,
|
contentColor = MaterialTheme.colorScheme.error,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text("Revoke")
|
Text(stringResource(R.string.pairing_action_revoke))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -58,7 +60,7 @@ fun PluginsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Plugins") },
|
title = { Text(stringResource(R.string.screen_plugins)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadPlugins() },
|
onRefresh = { viewModel.loadPlugins() },
|
||||||
@@ -78,10 +80,10 @@ fun PluginsScreen(
|
|||||||
|
|
||||||
state.plugins.isEmpty() -> {
|
state.plugins.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No plugins found",
|
title = stringResource(R.string.plugins_empty_title),
|
||||||
subtitle = "Verify configuration or retry",
|
subtitle = stringResource(R.string.plugins_empty_desc),
|
||||||
onAction = { viewModel.loadPlugins() },
|
onAction = { viewModel.loadPlugins() },
|
||||||
actionLabel = "Refresh",
|
actionLabel = stringResource(R.string.content_desc_refresh),
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -129,7 +131,7 @@ fun PluginsScreen(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = plugin.description ?: "No description provided.",
|
text = plugin.description ?: stringResource(R.string.plugins_no_desc),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -139,7 +141,7 @@ fun PluginsScreen(
|
|||||||
) {
|
) {
|
||||||
if (plugin.installed) {
|
if (plugin.installed) {
|
||||||
OutlinedButton(onClick = { viewModel.updatePlugin(plugin.name) }) {
|
OutlinedButton(onClick = { viewModel.updatePlugin(plugin.name) }) {
|
||||||
Text("Update")
|
Text(stringResource(R.string.plugins_action_update))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
@@ -149,11 +151,11 @@ fun PluginsScreen(
|
|||||||
contentColor = MaterialTheme.colorScheme.error,
|
contentColor = MaterialTheme.colorScheme.error,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text("Uninstall")
|
Text(stringResource(R.string.plugins_action_uninstall))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button(onClick = { viewModel.installPlugin(plugin.name) }) {
|
Button(onClick = { viewModel.installPlugin(plugin.name) }) {
|
||||||
Text("Install")
|
Text(stringResource(R.string.plugins_action_install))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,10 +35,12 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -71,7 +73,7 @@ fun ProfilesScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Profiles") },
|
title = { Text(stringResource(R.string.screen_profiles)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadProfiles() },
|
onRefresh = { viewModel.loadProfiles() },
|
||||||
@@ -91,10 +93,10 @@ fun ProfilesScreen(
|
|||||||
|
|
||||||
state.profiles.isEmpty() -> {
|
state.profiles.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No profiles available",
|
title = stringResource(R.string.profiles_empty_title),
|
||||||
subtitle = "Verify configuration or retry",
|
subtitle = stringResource(R.string.toolsets_empty_desc),
|
||||||
onAction = { viewModel.loadProfiles() },
|
onAction = { viewModel.loadProfiles() },
|
||||||
actionLabel = "Refresh",
|
actionLabel = stringResource(R.string.content_desc_refresh),
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -111,7 +113,7 @@ fun ProfilesScreen(
|
|||||||
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Button(onClick = { viewModel.loadProfiles() }) {
|
Button(onClick = { viewModel.loadProfiles() }) {
|
||||||
Text("Retry")
|
Text(stringResource(R.string.action_retry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -170,7 +172,10 @@ fun ProfilesScreen(
|
|||||||
if (isActive) {
|
if (isActive) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Check,
|
imageVector = Icons.Filled.Check,
|
||||||
contentDescription = "Active Profile",
|
contentDescription =
|
||||||
|
stringResource(
|
||||||
|
R.string.profiles_content_desc_active,
|
||||||
|
),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
)
|
)
|
||||||
@@ -186,11 +191,19 @@ fun ProfilesScreen(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = "Model: ${profile.model ?: "None"}",
|
text =
|
||||||
|
stringResource(
|
||||||
|
R.string.profiles_label_model,
|
||||||
|
profile.model ?: "None",
|
||||||
|
),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Provider: ${profile.provider ?: "None"}",
|
text =
|
||||||
|
stringResource(
|
||||||
|
R.string.profiles_label_provider,
|
||||||
|
profile.provider ?: "None",
|
||||||
|
),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -216,7 +229,7 @@ fun ProfilesScreen(
|
|||||||
modifier = Modifier.width(16.dp),
|
modifier = Modifier.width(16.dp),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text("Edit Soul")
|
Text(stringResource(R.string.profiles_action_edit_soul))
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
@@ -226,7 +239,7 @@ fun ProfilesScreen(
|
|||||||
tempModelName = profile.model ?: ""
|
tempModelName = profile.model ?: ""
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text("Set Model")
|
Text(stringResource(R.string.profiles_action_set_model))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.m57.hermescontrol.ChatScreen
|
import com.m57.hermescontrol.ChatScreen
|
||||||
import com.m57.hermescontrol.NavigationController
|
import com.m57.hermescontrol.NavigationController
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.LocalSpacing
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
@@ -48,7 +50,7 @@ fun SessionsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("History") },
|
title = { Text(stringResource(R.string.screen_history)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadSessions() },
|
onRefresh = { viewModel.loadSessions() },
|
||||||
@@ -61,15 +63,15 @@ fun SessionsScreen(
|
|||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "Unknown error",
|
message = state.errorMessage ?: stringResource(R.string.error_unknown),
|
||||||
onRetry = { viewModel.loadSessions() },
|
onRetry = { viewModel.loadSessions() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.sessions.isEmpty() -> {
|
state.sessions.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No history",
|
title = stringResource(R.string.history_empty_title),
|
||||||
subtitle = "Previous chat sessions from Hermes will appear here.",
|
subtitle = stringResource(R.string.history_empty_desc),
|
||||||
icon = Icons.Filled.History,
|
icon = Icons.Filled.History,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -98,7 +100,7 @@ fun SessionsScreen(
|
|||||||
modifier = Modifier.padding(spacing.md),
|
modifier = Modifier.padding(spacing.md),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = session.title ?: "Untitled",
|
text = session.title ?: stringResource(R.string.history_untitled),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
@@ -107,7 +109,11 @@ fun SessionsScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${session.message_count ?: 0} messages",
|
text =
|
||||||
|
stringResource(
|
||||||
|
R.string.history_message_count,
|
||||||
|
session.message_count ?: 0,
|
||||||
|
),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
@@ -61,6 +62,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.m57.hermescontrol.BuildConfig
|
import com.m57.hermescontrol.BuildConfig
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.ThemePreference
|
import com.m57.hermescontrol.theme.ThemePreference
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@ fun SettingsScreen(
|
|||||||
BackHandler(onBack = onBack)
|
BackHandler(onBack = onBack)
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Settings") },
|
title = { Text(stringResource(R.string.screen_settings)) },
|
||||||
showBack = true,
|
showBack = true,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
) {
|
) {
|
||||||
@@ -90,12 +92,12 @@ fun SettingsScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
// Connection section
|
// Connection section
|
||||||
SectionCard(title = "Connection") {
|
SectionCard(title = stringResource(R.string.settings_sec_connection)) {
|
||||||
// Connection Profiles management in Settings
|
// Connection Profiles management in Settings
|
||||||
if (state.profiles.isNotEmpty()) {
|
if (state.profiles.isNotEmpty()) {
|
||||||
var profilesExpanded by remember { mutableStateOf(false) }
|
var profilesExpanded by remember { mutableStateOf(false) }
|
||||||
Text(
|
Text(
|
||||||
text = "Connection Profile",
|
text = stringResource(R.string.settings_item_profile),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -105,7 +107,7 @@ fun SettingsScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
val activeProfile = state.profiles.firstOrNull { it.id == state.selectedProfileId }
|
val activeProfile = state.profiles.firstOrNull { it.id == state.selectedProfileId }
|
||||||
Text(text = activeProfile?.name ?: "Standalone / Default")
|
Text(text = activeProfile?.name ?: stringResource(R.string.settings_profile_default))
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = profilesExpanded,
|
expanded = profilesExpanded,
|
||||||
@@ -113,7 +115,7 @@ fun SettingsScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(0.85f),
|
modifier = Modifier.fillMaxWidth(0.85f),
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("None (Standalone / Default)") },
|
text = { Text(stringResource(R.string.settings_profile_none)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.selectProfile(null)
|
viewModel.selectProfile(null)
|
||||||
profilesExpanded = false
|
profilesExpanded = false
|
||||||
@@ -136,7 +138,10 @@ fun SettingsScreen(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Close,
|
imageVector = Icons.Filled.Close,
|
||||||
contentDescription = "Delete Profile",
|
contentDescription =
|
||||||
|
stringResource(
|
||||||
|
R.string.content_desc_delete_profile,
|
||||||
|
),
|
||||||
tint = MaterialTheme.colorScheme.error,
|
tint = MaterialTheme.colorScheme.error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -155,7 +160,7 @@ fun SettingsScreen(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.renameProfileName,
|
value = state.renameProfileName,
|
||||||
onValueChange = viewModel::onRenameProfileNameChange,
|
onValueChange = viewModel::onRenameProfileNameChange,
|
||||||
label = { Text("Rename Profile") },
|
label = { Text(stringResource(R.string.settings_action_rename_profile)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
@@ -163,7 +168,7 @@ fun SettingsScreen(
|
|||||||
onClick = viewModel::renameProfile,
|
onClick = viewModel::renameProfile,
|
||||||
modifier = Modifier.padding(end = 4.dp),
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
) {
|
) {
|
||||||
Text("Rename")
|
Text(stringResource(R.string.action_rename))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -174,7 +179,7 @@ fun SettingsScreen(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.host,
|
value = state.host,
|
||||||
onValueChange = viewModel::onHostChange,
|
onValueChange = viewModel::onHostChange,
|
||||||
label = { Text("Host") },
|
label = { Text(stringResource(R.string.settings_field_host)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors =
|
colors =
|
||||||
@@ -189,7 +194,7 @@ fun SettingsScreen(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.port,
|
value = state.port,
|
||||||
onValueChange = viewModel::onPortChange,
|
onValueChange = viewModel::onPortChange,
|
||||||
label = { Text("Port") },
|
label = { Text(stringResource(R.string.settings_field_port)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -205,7 +210,7 @@ fun SettingsScreen(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.token,
|
value = state.token,
|
||||||
onValueChange = viewModel::onTokenChange,
|
onValueChange = viewModel::onTokenChange,
|
||||||
label = { Text("Auth Token") },
|
label = { Text(stringResource(R.string.settings_field_token)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
visualTransformation =
|
visualTransformation =
|
||||||
if (passwordVisible) {
|
if (passwordVisible) {
|
||||||
@@ -224,9 +229,9 @@ fun SettingsScreen(
|
|||||||
},
|
},
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (passwordVisible) {
|
if (passwordVisible) {
|
||||||
"Hide token"
|
stringResource(R.string.content_desc_hide_token)
|
||||||
} else {
|
} else {
|
||||||
"Show token"
|
stringResource(R.string.content_desc_show_token)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -253,12 +258,12 @@ fun SettingsScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Text("Clear Token")
|
Text(stringResource(R.string.settings_action_clear_token))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Behavior section
|
// Behavior section
|
||||||
SectionCard(title = "Behavior") {
|
SectionCard(title = stringResource(R.string.settings_sec_behavior)) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -266,11 +271,11 @@ fun SettingsScreen(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = "Auto-Reconnect",
|
text = stringResource(R.string.settings_item_auto_reconnect),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Automatically reconnect on disconnect",
|
text = stringResource(R.string.settings_desc_auto_reconnect),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.bodySmall.copy(
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -290,9 +295,9 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Appearance section
|
// Appearance section
|
||||||
SectionCard(title = "Appearance") {
|
SectionCard(title = stringResource(R.string.settings_sec_appearance)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Theme",
|
text = stringResource(R.string.settings_item_theme),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -308,9 +313,11 @@ fun SettingsScreen(
|
|||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
pref.name
|
when (pref) {
|
||||||
.lowercase()
|
ThemePreference.SYSTEM -> stringResource(R.string.theme_system)
|
||||||
.replaceFirstChar { it.uppercase() },
|
ThemePreference.LIGHT -> stringResource(R.string.theme_light)
|
||||||
|
ThemePreference.DARK -> stringResource(R.string.theme_dark)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,7 +325,7 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Chat section
|
// Chat section
|
||||||
SectionCard(title = "Chat") {
|
SectionCard(title = stringResource(R.string.settings_sec_chat)) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -326,11 +333,11 @@ fun SettingsScreen(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = "Typing Effect",
|
text = stringResource(R.string.settings_item_typing_effect),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Animate new messages word by word",
|
text = stringResource(R.string.settings_desc_typing_effect),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.bodySmall.copy(
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -352,7 +359,7 @@ fun SettingsScreen(
|
|||||||
AnimatedVisibility(visible = state.typingEffectEnabled) {
|
AnimatedVisibility(visible = state.typingEffectEnabled) {
|
||||||
Column(modifier = Modifier.padding(top = 12.dp)) {
|
Column(modifier = Modifier.padding(top = 12.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Delay: ${state.typingEffectDelayMs} ms",
|
text = stringResource(R.string.settings_item_typing_delay, state.typingEffectDelayMs),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
@@ -368,14 +375,14 @@ fun SettingsScreen(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "10 ms",
|
text = stringResource(R.string.settings_delay_10ms),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.labelSmall.copy(
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "100 ms",
|
text = stringResource(R.string.settings_delay_100ms),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.labelSmall.copy(
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -387,15 +394,16 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Navigation Bar section
|
// Navigation Bar section
|
||||||
SectionCard(title = "Navigation Bar") {
|
SectionCard(title = stringResource(R.string.settings_sec_nav_bar)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Bottom bar items (${state.selectedNavItems.size}/5)",
|
text = stringResource(R.string.settings_item_nav_items, state.selectedNavItems.size),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
state.selectedNavItems.forEachIndexed { index, name ->
|
state.selectedNavItems.forEachIndexed { index, name ->
|
||||||
val label = viewModel.availableNavItems.firstOrNull { it.first == name }?.second ?: name
|
val labelRes = viewModel.availableNavItems.firstOrNull { it.first == name }?.second
|
||||||
|
val label = if (labelRes != null) stringResource(labelRes) else name
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -417,7 +425,10 @@ fun SettingsScreen(
|
|||||||
},
|
},
|
||||||
enabled = index > 0,
|
enabled = index > 0,
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.KeyboardArrowUp, contentDescription = "Move up")
|
Icon(
|
||||||
|
Icons.Filled.KeyboardArrowUp,
|
||||||
|
contentDescription = stringResource(R.string.content_desc_move_up),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -429,10 +440,16 @@ fun SettingsScreen(
|
|||||||
},
|
},
|
||||||
enabled = index < state.selectedNavItems.lastIndex,
|
enabled = index < state.selectedNavItems.lastIndex,
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.KeyboardArrowDown, contentDescription = "Move down")
|
Icon(
|
||||||
|
Icons.Filled.KeyboardArrowDown,
|
||||||
|
contentDescription = stringResource(R.string.content_desc_move_down),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = { viewModel.removeNavItem(name) }) {
|
IconButton(onClick = { viewModel.removeNavItem(name) }) {
|
||||||
Icon(Icons.Filled.Close, contentDescription = "Remove $label")
|
Icon(
|
||||||
|
Icons.Filled.Close,
|
||||||
|
contentDescription = stringResource(R.string.content_desc_remove_item, label),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,12 +463,12 @@ fun SettingsScreen(
|
|||||||
OutlinedButton(onClick = { expanded = true }) {
|
OutlinedButton(onClick = { expanded = true }) {
|
||||||
Icon(Icons.Filled.Add, contentDescription = null)
|
Icon(Icons.Filled.Add, contentDescription = null)
|
||||||
Spacer(Modifier.size(4.dp))
|
Spacer(Modifier.size(4.dp))
|
||||||
Text("Add Item")
|
Text(stringResource(R.string.settings_action_add_item))
|
||||||
}
|
}
|
||||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
available.forEach { (name, label) ->
|
available.forEach { (name, labelRes) ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(label) },
|
text = { Text(stringResource(labelRes)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.addNavItem(name)
|
viewModel.addNavItem(name)
|
||||||
expanded = false
|
expanded = false
|
||||||
@@ -463,7 +480,7 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = "Maximum of 5 items",
|
text = stringResource(R.string.settings_nav_max_reached),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -505,7 +522,7 @@ fun SettingsScreen(
|
|||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "✅ Settings saved",
|
text = stringResource(R.string.settings_save_success),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.bodyMedium.copy(
|
MaterialTheme.typography.bodyMedium.copy(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
@@ -529,7 +546,7 @@ fun SettingsScreen(
|
|||||||
strokeWidth = 2.dp,
|
strokeWidth = 2.dp,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text("Test Connection")
|
Text(stringResource(R.string.settings_action_test_connection))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,17 +558,17 @@ fun SettingsScreen(
|
|||||||
containerColor = MaterialTheme.colorScheme.primary,
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text("Save")
|
Text(stringResource(R.string.action_save))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// ── About section ─────────────────────────────────────────
|
// ── About section ─────────────────────────────────────────
|
||||||
SectionCard(title = "About") {
|
SectionCard(title = stringResource(R.string.settings_sec_about)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = "⚡ Hermes Control",
|
text = stringResource(R.string.settings_about_app_name),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.titleMedium.copy(
|
MaterialTheme.typography.titleMedium.copy(
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -563,13 +580,21 @@ fun SettingsScreen(
|
|||||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
InfoRow(label = "Version", value = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
|
||||||
InfoRow(
|
InfoRow(
|
||||||
label = "Build",
|
label = stringResource(R.string.settings_about_version),
|
||||||
value = if (BuildConfig.DEBUG) "Debug" else "Release",
|
value = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
||||||
|
)
|
||||||
|
InfoRow(
|
||||||
|
label = stringResource(R.string.settings_about_build),
|
||||||
|
value =
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
stringResource(R.string.settings_about_debug)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.settings_about_release)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if (BuildConfig.GIT_SHA.isNotBlank()) {
|
if (BuildConfig.GIT_SHA.isNotBlank()) {
|
||||||
InfoRow(label = "Commit", value = BuildConfig.GIT_SHA)
|
InfoRow(label = stringResource(R.string.settings_about_commit), value = BuildConfig.GIT_SHA)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.settings
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.data.local.AuthManager
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
import com.m57.hermescontrol.data.remote.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
@@ -58,10 +59,11 @@ class SettingsViewModel : ViewModel() {
|
|||||||
profiles = AuthManager.getConnectionProfiles(),
|
profiles = AuthManager.getConnectionProfiles(),
|
||||||
selectedProfileId = selectedId,
|
selectedProfileId = selectedId,
|
||||||
renameProfileName =
|
renameProfileName =
|
||||||
AuthManager.getConnectionProfiles().firstOrNull {
|
AuthManager
|
||||||
p ->
|
.getConnectionProfiles()
|
||||||
p.id == selectedId
|
.firstOrNull { p ->
|
||||||
}?.name ?: "",
|
p.id == selectedId
|
||||||
|
}?.name ?: "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,28 +130,28 @@ class SettingsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** All screens available for bottom-nav selection (name → display label). */
|
/** All screens available for bottom-nav selection (name → display label). */
|
||||||
val availableNavItems: List<Pair<String, String>> =
|
val availableNavItems: List<Pair<String, Int>> =
|
||||||
listOf(
|
listOf(
|
||||||
"ChatScreen" to "Chat",
|
"ChatScreen" to R.string.screen_chat,
|
||||||
"SkillsScreen" to "Skills",
|
"SkillsScreen" to R.string.screen_skills,
|
||||||
"CronJobsScreen" to "Cron Jobs",
|
"CronJobsScreen" to R.string.screen_cron,
|
||||||
"SystemScreen" to "System",
|
"SystemScreen" to R.string.screen_system,
|
||||||
"SettingsScreen" to "Settings",
|
"SettingsScreen" to R.string.screen_settings,
|
||||||
"ProfilesScreen" to "Profiles",
|
"ProfilesScreen" to R.string.screen_profiles,
|
||||||
"WebhooksScreen" to "Webhooks",
|
"WebhooksScreen" to R.string.screen_webhooks,
|
||||||
"GatewayScreen" to "Gateway",
|
"GatewayScreen" to R.string.screen_gateway,
|
||||||
"HistoryScreen" to "History",
|
"HistoryScreen" to R.string.screen_history,
|
||||||
"ToolsetsScreen" to "Toolsets",
|
"ToolsetsScreen" to R.string.screen_toolsets,
|
||||||
"PluginsScreen" to "Plugins",
|
"PluginsScreen" to R.string.screen_plugins,
|
||||||
"ConfigScreen" to "Config",
|
"ConfigScreen" to R.string.screen_config,
|
||||||
"McpServersScreen" to "MCP Servers",
|
"McpServersScreen" to R.string.screen_mcp_servers,
|
||||||
"ModelScreen" to "Models",
|
"ModelScreen" to R.string.screen_models,
|
||||||
"PairingScreen" to "Pairing",
|
"PairingScreen" to R.string.screen_pairing,
|
||||||
"KeysScreen" to "Keys",
|
"KeysScreen" to R.string.screen_keys,
|
||||||
"ChannelsScreen" to "Channels",
|
"ChannelsScreen" to R.string.screen_channels,
|
||||||
"LogsScreen" to "Logs",
|
"LogsScreen" to R.string.screen_logs,
|
||||||
"KanbanScreen" to "Kanban",
|
"KanbanScreen" to R.string.screen_kanban,
|
||||||
"AchievementsScreen" to "Achievements",
|
"AchievementsScreen" to R.string.screen_achievements,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Add a nav item to the bottom bar (max 5). Auto-saves. */
|
/** Add a nav item to the bottom bar (max 5). Auto-saves. */
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -43,6 +44,7 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.LocalSpacing
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
@@ -67,7 +69,11 @@ fun SkillsScreen(
|
|||||||
var selectedStatus by remember { mutableStateOf("All Statuses") }
|
var selectedStatus by remember { mutableStateOf("All Statuses") }
|
||||||
val categories =
|
val categories =
|
||||||
remember(state.skills) {
|
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") }
|
var selectedCategory by remember { mutableStateOf("All") }
|
||||||
|
|
||||||
@@ -113,7 +119,7 @@ fun SkillsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Skills") },
|
title = { Text(stringResource(R.string.screen_skills)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadSkills() },
|
onRefresh = { viewModel.loadSkills() },
|
||||||
@@ -125,15 +131,15 @@ fun SkillsScreen(
|
|||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "Unknown error",
|
message = state.errorMessage ?: stringResource(R.string.error_unknown),
|
||||||
onRetry = { viewModel.loadSkills() },
|
onRetry = { viewModel.loadSkills() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.skills.isEmpty() -> {
|
state.skills.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No skills found",
|
title = stringResource(R.string.skills_no_skills),
|
||||||
subtitle = "Skills loaded from Hermes will appear here.",
|
subtitle = stringResource(R.string.skills_no_skills_desc),
|
||||||
icon = Icons.Filled.Extension,
|
icon = Icons.Filled.Extension,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -148,7 +154,7 @@ fun SkillsScreen(
|
|||||||
SearchBar(
|
SearchBar(
|
||||||
query = query,
|
query = query,
|
||||||
onQueryChange = { query = it },
|
onQueryChange = { query = it },
|
||||||
placeholder = "Search skills…",
|
placeholder = stringResource(R.string.skills_search_placeholder),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
@@ -160,7 +166,7 @@ fun SkillsScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(spacing.xs),
|
verticalArrangement = Arrangement.spacedBy(spacing.xs),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Status",
|
text = stringResource(R.string.skills_filter_status),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -171,7 +177,16 @@ fun SkillsScreen(
|
|||||||
FilterChip(
|
FilterChip(
|
||||||
selected = selectedStatus == status,
|
selected = selectedStatus == status,
|
||||||
onClick = { selectedStatus = status },
|
onClick = { selectedStatus = status },
|
||||||
label = { Text(status) },
|
label = {
|
||||||
|
Text(
|
||||||
|
when (status) {
|
||||||
|
"All Statuses" -> stringResource(R.string.skills_status_all)
|
||||||
|
"Enabled" -> stringResource(R.string.skills_status_enabled)
|
||||||
|
"Disabled" -> stringResource(R.string.skills_status_disabled)
|
||||||
|
else -> status
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,7 +202,7 @@ fun SkillsScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(spacing.xs),
|
verticalArrangement = Arrangement.spacedBy(spacing.xs),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Category",
|
text = stringResource(R.string.skills_filter_category),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(horizontal = spacing.md),
|
modifier = Modifier.padding(horizontal = spacing.md),
|
||||||
@@ -214,14 +229,14 @@ fun SkillsScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(spacing.xs),
|
verticalArrangement = Arrangement.spacedBy(spacing.xs),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "No matching skills",
|
text = stringResource(R.string.skills_no_matching),
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.titleMedium.copy(
|
MaterialTheme.typography.titleMedium.copy(
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Try adjusting your search or filters.",
|
text = stringResource(R.string.skills_no_matching_desc),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -289,7 +304,7 @@ fun SkillsScreen(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Edit,
|
imageVector = Icons.Filled.Edit,
|
||||||
contentDescription = "Edit Skill File",
|
contentDescription = stringResource(R.string.content_desc_edit_skill),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -369,7 +384,7 @@ fun SkillEditorDialog(
|
|||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Edit: $skillName") },
|
title = { Text(stringResource(R.string.skills_edit_title, skillName)) },
|
||||||
onBack = {
|
onBack = {
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
showDiscardConfirm = true
|
showDiscardConfirm = true
|
||||||
@@ -393,7 +408,7 @@ fun SkillEditorDialog(
|
|||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Save,
|
imageVector = Icons.Filled.Save,
|
||||||
contentDescription = "Save Changes",
|
contentDescription = stringResource(R.string.content_desc_save_changes),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +440,7 @@ fun SkillEditorDialog(
|
|||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text("Enter skill content...")
|
Text(stringResource(R.string.skills_content_placeholder))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -438,8 +453,8 @@ fun SkillEditorDialog(
|
|||||||
if (showDiscardConfirm) {
|
if (showDiscardConfirm) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showDiscardConfirm = false },
|
onDismissRequest = { showDiscardConfirm = false },
|
||||||
title = { Text("Discard changes?") },
|
title = { Text(stringResource(R.string.skills_discard_title)) },
|
||||||
text = { Text("You have unsaved changes. Are you sure you want to discard them?") },
|
text = { Text(stringResource(R.string.skills_discard_message)) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -447,12 +462,12 @@ fun SkillEditorDialog(
|
|||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text("Discard")
|
Text(stringResource(R.string.action_discard))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showDiscardConfirm = false }) {
|
TextButton(onClick = { showDiscardConfirm = false }) {
|
||||||
Text("Cancel")
|
Text(stringResource(R.string.action_cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||||
import com.m57.hermescontrol.theme.LocalSpacing
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
@@ -65,7 +67,7 @@ fun SystemScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("System") },
|
title = { Text(stringResource(R.string.screen_system)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadSystemData() },
|
onRefresh = { viewModel.loadSystemData() },
|
||||||
@@ -77,15 +79,15 @@ fun SystemScreen(
|
|||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "Unknown error",
|
message = state.errorMessage ?: stringResource(R.string.error_unknown),
|
||||||
onRetry = { viewModel.loadSystemData() },
|
onRetry = { viewModel.loadSystemData() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.stats == null && state.doctorReport == null -> {
|
state.stats == null && state.doctorReport == null -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No system data",
|
title = stringResource(R.string.system_empty_title),
|
||||||
subtitle = "Tap refresh to load system diagnostics.",
|
subtitle = stringResource(R.string.system_empty_desc),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +104,7 @@ fun SystemScreen(
|
|||||||
// Stats row
|
// Stats row
|
||||||
state.stats?.let { stats ->
|
state.stats?.let { stats ->
|
||||||
item {
|
item {
|
||||||
SectionHeader(title = "Performance")
|
SectionHeader(title = stringResource(R.string.system_sec_performance))
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
@@ -111,7 +113,7 @@ fun SystemScreen(
|
|||||||
) {
|
) {
|
||||||
stats.cpuPercent?.let { cpu ->
|
stats.cpuPercent?.let { cpu ->
|
||||||
StatCard(
|
StatCard(
|
||||||
label = "CPU Usage",
|
label = stringResource(R.string.system_metric_cpu),
|
||||||
value = "$cpu%",
|
value = "$cpu%",
|
||||||
icon = Icons.Filled.Speed,
|
icon = Icons.Filled.Speed,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
@@ -119,7 +121,7 @@ fun SystemScreen(
|
|||||||
}
|
}
|
||||||
stats.memoryPercent?.let { mem ->
|
stats.memoryPercent?.let { mem ->
|
||||||
StatCard(
|
StatCard(
|
||||||
label = "Memory",
|
label = stringResource(R.string.system_metric_memory),
|
||||||
value = "$mem%",
|
value = "$mem%",
|
||||||
icon = Icons.Filled.Memory,
|
icon = Icons.Filled.Memory,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
@@ -132,7 +134,7 @@ fun SystemScreen(
|
|||||||
// Doctor diagnostics
|
// Doctor diagnostics
|
||||||
state.doctorReport?.let { report ->
|
state.doctorReport?.let { report ->
|
||||||
item {
|
item {
|
||||||
SectionHeader(title = "Doctor")
|
SectionHeader(title = stringResource(R.string.system_sec_doctor))
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
Card(
|
Card(
|
||||||
@@ -158,7 +160,12 @@ fun SystemScreen(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
StatusBadge(
|
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 =
|
status =
|
||||||
if (report.ok) {
|
if (report.ok) {
|
||||||
StatusBadgeType.SUCCESS
|
StatusBadgeType.SUCCESS
|
||||||
@@ -169,10 +176,13 @@ fun SystemScreen(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(spacing.sm))
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
report.pid?.let { pid ->
|
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 ->
|
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
|
// Admin actions
|
||||||
item {
|
item {
|
||||||
SectionHeader(title = "Administration")
|
SectionHeader(title = stringResource(R.string.system_sec_admin))
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
Card(
|
Card(
|
||||||
@@ -201,7 +211,7 @@ fun SystemScreen(
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.padding(end = spacing.xs),
|
modifier = Modifier.padding(end = spacing.xs),
|
||||||
)
|
)
|
||||||
Text("Trigger System Backup")
|
Text(stringResource(R.string.system_action_backup))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,12 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -63,7 +65,7 @@ fun ToolsetsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Toolsets") },
|
title = { Text(stringResource(R.string.screen_toolsets)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadToolsets() },
|
onRefresh = { viewModel.loadToolsets() },
|
||||||
@@ -83,10 +85,10 @@ fun ToolsetsScreen(
|
|||||||
|
|
||||||
state.toolsets.isEmpty() -> {
|
state.toolsets.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No toolsets reported",
|
title = stringResource(R.string.toolsets_empty_title),
|
||||||
subtitle = "Verify configuration or retry",
|
subtitle = stringResource(R.string.toolsets_empty_desc),
|
||||||
onAction = { viewModel.loadToolsets() },
|
onAction = { viewModel.loadToolsets() },
|
||||||
actionLabel = "Refresh",
|
actionLabel = stringResource(R.string.content_desc_refresh),
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -103,7 +105,10 @@ fun ToolsetsScreen(
|
|||||||
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
IconButton(onClick = { viewModel.loadToolsets() }) {
|
IconButton(onClick = { viewModel.loadToolsets() }) {
|
||||||
Icon(imageVector = Icons.Filled.Refresh, contentDescription = "Retry")
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Refresh,
|
||||||
|
contentDescription = stringResource(R.string.action_retry),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -29,11 +29,13 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
@@ -60,7 +62,7 @@ fun WebhooksScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text("Webhooks") },
|
title = { Text(stringResource(R.string.screen_webhooks)) },
|
||||||
onOpenDrawer = onOpenDrawer,
|
onOpenDrawer = onOpenDrawer,
|
||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadWebhooks() },
|
onRefresh = { viewModel.loadWebhooks() },
|
||||||
@@ -90,7 +92,7 @@ fun WebhooksScreen(
|
|||||||
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Button(onClick = { viewModel.loadWebhooks() }) {
|
Button(onClick = { viewModel.loadWebhooks() }) {
|
||||||
Text("Retry")
|
Text(stringResource(R.string.action_retry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -123,7 +125,7 @@ fun WebhooksScreen(
|
|||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Global Webhooks Status",
|
text = stringResource(R.string.webhooks_sec_status),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
@@ -147,7 +149,7 @@ fun WebhooksScreen(
|
|||||||
// Subscriptions List Header
|
// Subscriptions List Header
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = "Subscriptions",
|
text = stringResource(R.string.webhooks_sec_subscriptions),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
@@ -166,7 +168,7 @@ fun WebhooksScreen(
|
|||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "No webhook subscriptions configured.",
|
text = stringResource(R.string.webhooks_empty_desc),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -191,7 +193,7 @@ fun WebhooksScreen(
|
|||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Target URL:",
|
text = stringResource(R.string.webhooks_label_target),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
@@ -207,7 +209,7 @@ fun WebhooksScreen(
|
|||||||
if (events.isNotEmpty()) {
|
if (events.isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Events Subscribed:",
|
text = stringResource(R.string.webhooks_label_events),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,4 +13,309 @@
|
|||||||
<string name="chat_prev_match_desc">Previous match</string>
|
<string name="chat_prev_match_desc">Previous match</string>
|
||||||
<string name="chat_next_match_desc">Next match</string>
|
<string name="chat_next_match_desc">Next match</string>
|
||||||
<string name="chat_close_search_desc">Close search</string>
|
<string name="chat_close_search_desc">Close search</string>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<string name="action_retry">Retry</string>
|
||||||
|
<string name="action_cancel">Cancel</string>
|
||||||
|
<string name="action_ok">OK</string>
|
||||||
|
<string name="action_delete">Delete</string>
|
||||||
|
|
||||||
|
<!-- Content Descriptions -->
|
||||||
|
<string name="content_desc_back">Back</string>
|
||||||
|
<string name="content_desc_open_drawer">Open navigation drawer</string>
|
||||||
|
<string name="content_desc_refresh">Refresh</string>
|
||||||
|
<string name="content_desc_clear_search">Clear search</string>
|
||||||
|
|
||||||
|
<!-- General Errors -->
|
||||||
|
<string name="error_unknown">Unknown error</string>
|
||||||
|
|
||||||
|
<!-- Navigation & Drawer -->
|
||||||
|
<string name="nav_drawer_title">⚡ Hermes</string>
|
||||||
|
<string name="nav_drawer_subtitle">AI Agent Control</string>
|
||||||
|
<string name="nav_drawer_section_converse">Converse</string>
|
||||||
|
<string name="nav_drawer_section_automate">Automate</string>
|
||||||
|
<string name="nav_drawer_section_configure">Configure</string>
|
||||||
|
<string name="nav_drawer_section_inspect">Inspect</string>
|
||||||
|
|
||||||
|
<!-- Screen Names -->
|
||||||
|
<string name="screen_chat">Chat</string>
|
||||||
|
<string name="screen_skills">Skills</string>
|
||||||
|
<string name="screen_cron">Cron</string>
|
||||||
|
<string name="screen_system">System</string>
|
||||||
|
<string name="screen_settings">Settings</string>
|
||||||
|
<string name="screen_profiles">Profiles</string>
|
||||||
|
<string name="screen_webhooks">Webhooks</string>
|
||||||
|
<string name="screen_gateway">Gateway</string>
|
||||||
|
<string name="screen_toolsets">Toolsets</string>
|
||||||
|
<string name="screen_plugins">Plugins</string>
|
||||||
|
<string name="screen_config">Config</string>
|
||||||
|
<string name="screen_mcp_servers">MCP Servers</string>
|
||||||
|
<string name="screen_models">Models</string>
|
||||||
|
<string name="screen_pairing">Pairing</string>
|
||||||
|
<string name="screen_keys">Keys</string>
|
||||||
|
<string name="screen_channels">Channels</string>
|
||||||
|
<string name="screen_logs">Logs</string>
|
||||||
|
<string name="screen_kanban">Kanban</string>
|
||||||
|
<string name="screen_achievements">Achievements</string>
|
||||||
|
<string name="screen_history">History</string>
|
||||||
|
|
||||||
|
<!-- Notifications -->
|
||||||
|
<string name="notif_title">Hermes</string>
|
||||||
|
<string name="notif_waiting_replies">Waiting for Hermes replies</string>
|
||||||
|
<string name="notif_new_message">New message</string>
|
||||||
|
<string name="notif_clarification_needed">Hermes needs a clarification</string>
|
||||||
|
<string name="notif_reply_placeholder">Type your reply\u2026</string>
|
||||||
|
<string name="notif_channel_service_name">Hermes Background Service</string>
|
||||||
|
<string name="notif_channel_service_desc">Keeps connection alive in the background</string>
|
||||||
|
<string name="notif_channel_chat_name">Hermes Chat Replies</string>
|
||||||
|
<string name="notif_channel_chat_desc">Alerts for new replies from Hermes</string>
|
||||||
|
<string name="action_reply">Reply</string>
|
||||||
|
|
||||||
|
<!-- Chat Screen & Bubble -->
|
||||||
|
<string name="chat_action_search">Search</string>
|
||||||
|
<string name="chat_action_close_search">Close search</string>
|
||||||
|
<string name="chat_action_reconnect">Reconnect</string>
|
||||||
|
<string name="chat_empty_title">Ready to chat</string>
|
||||||
|
<string name="chat_empty_subtitle">Send a message to start a conversation</string>
|
||||||
|
<string name="chat_status_connecting">Connecting to Hermes\u2026</string>
|
||||||
|
<string name="chat_status_reconnecting">Reconnecting to Hermes\u2026</string>
|
||||||
|
<string name="chat_status_disconnected">Disconnected from Hermes</string>
|
||||||
|
<string name="chat_thinking">Thinking\u2026</string>
|
||||||
|
<string name="chat_thinking_param">Thinking: %1$s\u2026</string>
|
||||||
|
<string name="chat_input_placeholder_not_connected">Not connected\u2026</string>
|
||||||
|
<string name="chat_input_placeholder_waiting">Waiting for response\u2026</string>
|
||||||
|
<string name="chat_input_placeholder_type_message">Type a message\u2026</string>
|
||||||
|
<string name="chat_tool_stderr">Error Output (stderr):\n%1$s</string>
|
||||||
|
<string name="chat_tool_execution_error">Execution Error: %1$s</string>
|
||||||
|
<string name="chat_tool_exit_code">Exit Code: %1$d</string>
|
||||||
|
<string name="chat_tool_fallback">Tool</string>
|
||||||
|
<string name="chat_tool_show_raw">Show Raw JSON</string>
|
||||||
|
<string name="chat_tool_show_parsed">Show Parsed Output</string>
|
||||||
|
<string name="command_help_desc">Show help menu</string>
|
||||||
|
<string name="command_status_desc">Check gateway and platform status</string>
|
||||||
|
<string name="command_sessions_desc">List all chat sessions</string>
|
||||||
|
<string name="command_stats_desc">Check system resource usage (stats)</string>
|
||||||
|
<string name="command_system_desc">Check system resource usage (system)</string>
|
||||||
|
<string name="command_new_desc">Create a new chat session</string>
|
||||||
|
<string name="command_stop_desc">Interrupt the active run</string>
|
||||||
|
<string name="content_desc_new_chat">New Chat</string>
|
||||||
|
<string name="content_desc_scroll_to_bottom">Scroll to bottom</string>
|
||||||
|
<string name="content_desc_interrupt">Interrupt</string>
|
||||||
|
<string name="content_desc_copy">Copy</string>
|
||||||
|
|
||||||
|
<!-- Settings Screen -->
|
||||||
|
<string name="settings_sec_connection">Connection</string>
|
||||||
|
<string name="settings_item_profile">Connection Profile</string>
|
||||||
|
<string name="settings_profile_default">Standalone / Default</string>
|
||||||
|
<string name="settings_profile_none">None (Standalone / Default)</string>
|
||||||
|
<string name="settings_action_rename_profile">Rename Profile</string>
|
||||||
|
<string name="settings_field_host">Host</string>
|
||||||
|
<string name="settings_field_port">Port</string>
|
||||||
|
<string name="settings_field_token">Auth Token</string>
|
||||||
|
<string name="settings_action_clear_token">Clear Token</string>
|
||||||
|
<string name="settings_sec_behavior">Behavior</string>
|
||||||
|
<string name="settings_item_auto_reconnect">Auto-Reconnect</string>
|
||||||
|
<string name="settings_desc_auto_reconnect">Automatically reconnect on disconnect</string>
|
||||||
|
<string name="settings_sec_appearance">Appearance</string>
|
||||||
|
<string name="settings_item_theme">Theme</string>
|
||||||
|
<string name="settings_sec_chat">Chat</string>
|
||||||
|
<string name="settings_item_typing_effect">Typing Effect</string>
|
||||||
|
<string name="settings_desc_typing_effect">Animate new messages word by word</string>
|
||||||
|
<string name="settings_item_typing_delay">Delay: %1$d ms</string>
|
||||||
|
<string name="settings_delay_10ms">10 ms</string>
|
||||||
|
<string name="settings_delay_100ms">100 ms</string>
|
||||||
|
<string name="settings_sec_nav_bar">Navigation Bar</string>
|
||||||
|
<string name="settings_item_nav_items">Bottom bar items (%1$d/5)</string>
|
||||||
|
<string name="settings_action_add_item">Add Item</string>
|
||||||
|
<string name="settings_nav_max_reached">Maximum of 5 items</string>
|
||||||
|
<string name="settings_save_success">✅ Settings saved</string>
|
||||||
|
<string name="settings_action_test_connection">Test Connection</string>
|
||||||
|
<string name="settings_sec_about">About</string>
|
||||||
|
<string name="settings_about_app_name">⚡ Hermes Control</string>
|
||||||
|
<string name="settings_about_version">Version</string>
|
||||||
|
<string name="settings_about_build">Build</string>
|
||||||
|
<string name="settings_about_debug">Debug</string>
|
||||||
|
<string name="settings_about_release">Release</string>
|
||||||
|
<string name="settings_about_commit">Commit</string>
|
||||||
|
<string name="action_rename">Rename</string>
|
||||||
|
<string name="action_save">Save</string>
|
||||||
|
<string name="content_desc_delete_profile">Delete Profile</string>
|
||||||
|
<string name="content_desc_hide_token">Hide token</string>
|
||||||
|
<string name="content_desc_show_token">Show token</string>
|
||||||
|
<string name="content_desc_move_up">Move up</string>
|
||||||
|
<string name="content_desc_move_down">Move down</string>
|
||||||
|
<string name="content_desc_remove_item">Remove %1$s</string>
|
||||||
|
<string name="theme_system">System</string>
|
||||||
|
<string name="theme_light">Light</string>
|
||||||
|
<string name="theme_dark">Dark</string>
|
||||||
|
|
||||||
|
<!-- Connect Screen -->
|
||||||
|
<string name="connect_selected_profile">Profile: %1$s</string>
|
||||||
|
<string name="connect_action_select_profile">Select Saved Profile</string>
|
||||||
|
<string name="connect_profile_none">None (Standalone)</string>
|
||||||
|
<string name="connect_pairing_string">Pairing string</string>
|
||||||
|
<string name="connect_placeholder_pairing">Paste hermes://\u2026 or Base64 config</string>
|
||||||
|
<string name="connect_save_profile">Save connection profile</string>
|
||||||
|
<string name="connect_profile_name">Profile Name</string>
|
||||||
|
<string name="connect_placeholder_profile_name">e.g. Local Hermit, Remote Server</string>
|
||||||
|
<string name="connect_action_hide_manual">Hide manual entry</string>
|
||||||
|
<string name="connect_action_show_manual">Enter manually instead</string>
|
||||||
|
<string name="connect_placeholder_token">API token</string>
|
||||||
|
<string name="connect_placeholder_host">127.0.0.1</string>
|
||||||
|
<string name="connect_placeholder_port">9119</string>
|
||||||
|
<string name="action_connect">Connect</string>
|
||||||
|
|
||||||
|
<!-- Kanban Screen -->
|
||||||
|
<string name="kanban_board_title">Kanban Board</string>
|
||||||
|
<string name="kanban_no_boards">No boards found.</string>
|
||||||
|
<string name="kanban_no_columns">No columns found for this board.</string>
|
||||||
|
<string name="kanban_add_new_task">Add New Task</string>
|
||||||
|
<string name="kanban_task_title">Task Title</string>
|
||||||
|
<string name="kanban_task_desc">Task Description</string>
|
||||||
|
<string name="action_add">Add</string>
|
||||||
|
<string name="content_desc_move_left">Move Left</string>
|
||||||
|
<string name="content_desc_move_right">Move Right</string>
|
||||||
|
|
||||||
|
<!-- Skills Screen -->
|
||||||
|
<string name="skills_status_all">All Statuses</string>
|
||||||
|
<string name="skills_status_enabled">Enabled</string>
|
||||||
|
<string name="skills_status_disabled">Disabled</string>
|
||||||
|
<string name="skills_category_all">All</string>
|
||||||
|
<string name="skills_no_skills">No skills found</string>
|
||||||
|
<string name="skills_no_skills_desc">Skills loaded from Hermes will appear here.</string>
|
||||||
|
<string name="skills_search_placeholder">Search skills\u2026</string>
|
||||||
|
<string name="skills_filter_status">Status</string>
|
||||||
|
<string name="skills_filter_category">Category</string>
|
||||||
|
<string name="skills_no_matching">No matching skills</string>
|
||||||
|
<string name="skills_no_matching_desc">Try adjusting your search or filters.</string>
|
||||||
|
<string name="skills_edit_title">Edit: %1$s</string>
|
||||||
|
<string name="skills_content_placeholder">Enter skill content\u2026</string>
|
||||||
|
<string name="skills_discard_title">Discard changes?</string>
|
||||||
|
<string name="skills_discard_message">You have unsaved changes. Are you sure you want to discard them?</string>
|
||||||
|
<string name="action_discard">Discard</string>
|
||||||
|
<string name="content_desc_edit_skill">Edit Skill File</string>
|
||||||
|
<string name="content_desc_save_changes">Save Changes</string>
|
||||||
|
|
||||||
|
<!-- Logs Screen -->
|
||||||
|
<string name="logs_empty_title">No logs available</string>
|
||||||
|
<string name="logs_empty_desc">Log output from Hermes will appear here.</string>
|
||||||
|
|
||||||
|
<!-- System Screen -->
|
||||||
|
<string name="system_empty_title">No system data</string>
|
||||||
|
<string name="system_empty_desc">Tap refresh to load system diagnostics.</string>
|
||||||
|
<string name="system_sec_performance">Performance</string>
|
||||||
|
<string name="system_metric_cpu">CPU Usage</string>
|
||||||
|
<string name="system_metric_memory">Memory</string>
|
||||||
|
<string name="system_sec_doctor">Doctor</string>
|
||||||
|
<string name="system_status_running">Running</string>
|
||||||
|
<string name="system_status_failed">Failed</string>
|
||||||
|
<string name="system_info_pid">Process PID</string>
|
||||||
|
<string name="system_info_proc_name">Process Name</string>
|
||||||
|
<string name="system_sec_admin">Administration</string>
|
||||||
|
<string name="system_action_backup">Trigger System Backup</string>
|
||||||
|
|
||||||
|
<!-- Model Screen -->
|
||||||
|
<string name="model_empty_title">No model providers reported</string>
|
||||||
|
<string name="model_empty_desc">Verify configuration or retry</string>
|
||||||
|
<string name="model_label_slug">Slug: %1$s</string>
|
||||||
|
<string name="model_status_current">CURRENT</string>
|
||||||
|
<string name="model_label_available">Available Models:</string>
|
||||||
|
<string name="model_no_models">No models reported or provider needs authentication.</string>
|
||||||
|
<string name="content_desc_active_model">Active Model</string>
|
||||||
|
|
||||||
|
<!-- Sessions (History) Screen -->
|
||||||
|
<string name="history_empty_title">No history</string>
|
||||||
|
<string name="history_empty_desc">Previous chat sessions from Hermes will appear here.</string>
|
||||||
|
<string name="history_untitled">Untitled</string>
|
||||||
|
<string name="history_message_count">%1$d messages</string>
|
||||||
|
|
||||||
|
<!-- Toolsets Screen -->
|
||||||
|
<string name="toolsets_empty_title">No toolsets reported</string>
|
||||||
|
<string name="toolsets_empty_desc">Verify configuration or retry</string>
|
||||||
|
|
||||||
|
<!-- Webhooks Screen -->
|
||||||
|
<string name="webhooks_sec_status">Global Webhooks Status</string>
|
||||||
|
<string name="webhooks_sec_subscriptions">Subscriptions</string>
|
||||||
|
<string name="webhooks_empty_desc">No webhook subscriptions configured.</string>
|
||||||
|
<string name="webhooks_label_target">Target URL:</string>
|
||||||
|
<string name="webhooks_label_events">Events Subscribed:</string>
|
||||||
|
|
||||||
|
<!-- Pairing Screen -->
|
||||||
|
<string name="pairing_screen_title">Device Pairing</string>
|
||||||
|
<string name="pairing_sec_pending">Pending Approvals</string>
|
||||||
|
<string name="pairing_action_clear_all">Clear All</string>
|
||||||
|
<string name="pairing_action_approve">Approve</string>
|
||||||
|
<string name="pairing_sec_approved">Approved Devices</string>
|
||||||
|
<string name="pairing_empty_approved">No approved devices paired yet.</string>
|
||||||
|
<string name="pairing_label_user_id">User ID: %1$s</string>
|
||||||
|
<string name="pairing_action_revoke">Revoke</string>
|
||||||
|
|
||||||
|
<!-- Config Screen -->
|
||||||
|
<string name="config_screen_title">Configuration</string>
|
||||||
|
<string name="config_placeholder_yaml">YAML Config...</string>
|
||||||
|
<string name="config_action_reset">Reset</string>
|
||||||
|
<string name="config_action_save">Save Config</string>
|
||||||
|
|
||||||
|
<!-- Profiles Screen -->
|
||||||
|
<string name="profiles_empty_title">No profiles available</string>
|
||||||
|
<string name="profiles_label_model">Model: %1$s</string>
|
||||||
|
<string name="profiles_label_provider">Provider: %1$s</string>
|
||||||
|
<string name="profiles_action_edit_soul">Edit Soul</string>
|
||||||
|
<string name="profiles_action_set_model">Set Model</string>
|
||||||
|
<string name="profiles_content_desc_active">Active Profile</string>
|
||||||
|
|
||||||
|
<!-- Plugins Screen -->
|
||||||
|
<string name="plugins_empty_title">No plugins found</string>
|
||||||
|
<string name="plugins_empty_desc">Verify configuration or retry</string>
|
||||||
|
<string name="plugins_no_desc">No description provided.</string>
|
||||||
|
<string name="plugins_action_update">Update</string>
|
||||||
|
<string name="plugins_action_uninstall">Uninstall</string>
|
||||||
|
<string name="plugins_action_install">Install</string>
|
||||||
|
|
||||||
|
<!-- McpServers Screen -->
|
||||||
|
<string name="mcp_servers_label_transport">Transport: %1$s</string>
|
||||||
|
<string name="mcp_servers_label_command">Command:</string>
|
||||||
|
<string name="mcp_servers_action_test">Test</string>
|
||||||
|
|
||||||
|
<!-- Gateway Screen -->
|
||||||
|
<string name="gateway_status_running">GATEWAY RUNNING</string>
|
||||||
|
<string name="gateway_status_stopped">GATEWAY STOPPED</string>
|
||||||
|
<string name="gateway_label_version">Version: %1$s</string>
|
||||||
|
<string name="gateway_sec_controls">Controls</string>
|
||||||
|
<string name="gateway_action_start">Start</string>
|
||||||
|
<string name="gateway_action_stop">Stop</string>
|
||||||
|
<string name="gateway_action_restart">Restart</string>
|
||||||
|
<string name="gateway_sec_active_platforms">Active Platforms</string>
|
||||||
|
|
||||||
|
<!-- CronJobs Screen -->
|
||||||
|
<string name="cron_empty_title">No cron jobs</string>
|
||||||
|
<string name="cron_empty_desc">Scheduled tasks from Hermes will appear here.</string>
|
||||||
|
<string name="cron_status_active">Active</string>
|
||||||
|
<string name="cron_status_paused">Paused</string>
|
||||||
|
<string name="cron_action_pause">Pause</string>
|
||||||
|
<string name="cron_action_resume">Resume</string>
|
||||||
|
<string name="cron_action_run">Run</string>
|
||||||
|
|
||||||
|
<!-- Achievements Screen -->
|
||||||
|
<string name="achievements_empty_title">No achievements yet</string>
|
||||||
|
<string name="achievements_empty_desc">Complete tasks to unlock achievements.</string>
|
||||||
|
<string name="achievements_label_progress">Progress: %1$d / %2$d</string>
|
||||||
|
|
||||||
|
<!-- Keys Screen -->
|
||||||
|
<string name="keys_empty_title">No environment keys reported</string>
|
||||||
|
<string name="keys_label_not_configured">Not configured</string>
|
||||||
|
<string name="keys_action_toggle_visibility">Toggle Visibility</string>
|
||||||
|
|
||||||
|
<!-- Channels Screen -->
|
||||||
|
<string name="channels_empty_title">No channels configured</string>
|
||||||
|
<string name="channels_empty_desc">Add messaging platforms in Hermes to see them here.</string>
|
||||||
|
<string name="channels_status_active">Active</string>
|
||||||
|
<string name="channels_status_disabled">Disabled</string>
|
||||||
|
<string name="channels_action_configure">Configure</string>
|
||||||
|
<string name="channels_sec_settings">Configuration Settings</string>
|
||||||
|
<string name="channels_placeholder_configured">******** (Already Configured)</string>
|
||||||
|
<string name="channels_action_save_settings">Save Settings</string>
|
||||||
|
<string name="channels_sec_keys">Configured Keys:</string>
|
||||||
|
<string name="channels_status_configured">Configured</string>
|
||||||
|
<string name="channels_status_not_set">Not set</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -29,9 +29,11 @@
|
|||||||
# Target platforms (36 required by AGP 9.0.1 / compileSdk 36)
|
# Target platforms (36 required by AGP 9.0.1 / compileSdk 36)
|
||||||
platformVersions = [ "36" "35" "34" ];
|
platformVersions = [ "36" "35" "34" ];
|
||||||
|
|
||||||
# For the emulator (optional, remove if not needed)
|
# For the emulator
|
||||||
includeEmulator = false;
|
includeEmulator = true;
|
||||||
includeSystemImages = false;
|
includeSystemImages = true;
|
||||||
|
systemImageTypes = [ "google_apis" ];
|
||||||
|
abiVersions = [ "x86_64" ];
|
||||||
|
|
||||||
# NDK (not needed for this project, but handy)
|
# NDK (not needed for this project, but handy)
|
||||||
includeNDK = false;
|
includeNDK = false;
|
||||||
|
|||||||
Reference in new issue
Block a user