Refactor: Consolidate navigation items into ScreenRegistry (#310)
* Refactor: Consolidate navigation items into ScreenRegistry * fix(#310): remove unused imports, fix ktlint import ordering * feat(#289): add ScreenRegistry for centralized screen configuration * fix(#310): fix ktlint when-condition blank lines in Navigation.kt * fix(#289): derive availableNavItems from ScreenRegistry * fix(#310): use addEntryProvider instead of reified entry for dynamic screen registration * fix(#310): remove double top padding from kanban, channels, achievements screens --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
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
|
||||||
@@ -16,25 +15,6 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ListAlt
|
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
|
||||||
import androidx.compose.material.icons.filled.Bolt
|
|
||||||
import androidx.compose.material.icons.filled.Build
|
|
||||||
import androidx.compose.material.icons.filled.Code
|
|
||||||
import androidx.compose.material.icons.filled.Dashboard
|
|
||||||
import androidx.compose.material.icons.filled.Devices
|
|
||||||
import androidx.compose.material.icons.filled.Extension
|
|
||||||
import androidx.compose.material.icons.filled.History
|
|
||||||
import androidx.compose.material.icons.filled.HistoryEdu
|
|
||||||
import androidx.compose.material.icons.filled.Info
|
|
||||||
import androidx.compose.material.icons.filled.Key
|
|
||||||
import androidx.compose.material.icons.filled.Memory
|
|
||||||
import androidx.compose.material.icons.filled.Psychology
|
|
||||||
import androidx.compose.material.icons.filled.Schedule
|
|
||||||
import androidx.compose.material.icons.filled.Settings
|
|
||||||
import androidx.compose.material.icons.filled.Webhook
|
|
||||||
import androidx.compose.material3.DrawerValue
|
import androidx.compose.material3.DrawerValue
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -58,7 +38,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Alignment
|
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.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -72,122 +51,13 @@ import com.m57.hermescontrol.data.ws.ConnectionStatus
|
|||||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||||
import com.m57.hermescontrol.theme.BottomNavDisplayMode
|
import com.m57.hermescontrol.theme.BottomNavDisplayMode
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.m57.hermescontrol.ui.achievements.AchievementsScreen as AchievementsScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.authlogin.AuthLoginScreen as AuthLoginScreenContent
|
import com.m57.hermescontrol.ui.authlogin.AuthLoginScreen as AuthLoginScreenContent
|
||||||
import com.m57.hermescontrol.ui.channels.ChannelsScreen as ChannelsScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.chat.ChatScreen as ChatScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.config.ConfigScreen as ConfigScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.cron.CronJobsScreen as CronJobsScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.gateway.GatewayScreen as GatewayScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.kanban.KanbanScreen as KanbanScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.keys.KeysScreen as KeysScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.landing.LandingScreen as LandingScreenContent
|
import com.m57.hermescontrol.ui.landing.LandingScreen as LandingScreenContent
|
||||||
import com.m57.hermescontrol.ui.logs.LogsScreen as LogsScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.mcp.McpServersScreen as McpServersScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.model.ModelScreen as ModelScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.pairing.PairingScreen as PairingScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.plugins.PluginsScreen as PluginsScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.profiles.ProfilesScreen as ProfilesScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.sessions.SessionsScreen as HistoryScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.settings.SettingsScreen as SettingsScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.skills.SkillsScreen as SkillsScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.system.SystemScreen as SystemScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.toolsets.ToolsetsScreen as ToolsetsScreenContent
|
|
||||||
import com.m57.hermescontrol.ui.webhooks.WebhooksScreen as WebhooksScreenContent
|
|
||||||
|
|
||||||
// ── Bottom-nav item model ──────────────────────────────────────────────
|
private fun resolveBottomNavItems(names: List<String>): List<ScreenDefinition> =
|
||||||
|
names.mapNotNull { name -> ScreenRegistry.ALL_SCREENS.firstOrNull { it.key::class.simpleName == name } }
|
||||||
|
|
||||||
private data class BottomNavItem(
|
private val DRAWER_GESTURE_SCREENS: Set<NavKey> = ScreenRegistry.ALL_SCREENS.mapTo(mutableSetOf()) { it.key }
|
||||||
val key: NavKey,
|
|
||||||
@param:StringRes val labelRes: Int,
|
|
||||||
val icon: ImageVector,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Master list of ALL screens available for bottom-nav selection. */
|
|
||||||
private val ALL_NAV_ITEMS: List<BottomNavItem> =
|
|
||||||
listOf(
|
|
||||||
BottomNavItem(ChatScreen, R.string.screen_chat, Icons.AutoMirrored.Filled.Chat),
|
|
||||||
BottomNavItem(SkillsScreen, R.string.screen_skills, Icons.Filled.Extension),
|
|
||||||
BottomNavItem(CronJobsScreen, R.string.screen_cron, Icons.Filled.Schedule),
|
|
||||||
BottomNavItem(SystemScreen, R.string.screen_system, Icons.Filled.Info),
|
|
||||||
BottomNavItem(SettingsScreen, R.string.screen_settings, Icons.Filled.Settings),
|
|
||||||
BottomNavItem(ProfilesScreen, R.string.screen_profiles, Icons.Filled.AccountCircle),
|
|
||||||
BottomNavItem(WebhooksScreen, R.string.screen_webhooks, Icons.Filled.Webhook),
|
|
||||||
BottomNavItem(GatewayScreen, R.string.screen_gateway, Icons.Filled.Bolt),
|
|
||||||
BottomNavItem(ToolsetsScreen, R.string.screen_toolsets, Icons.Filled.Build),
|
|
||||||
BottomNavItem(PluginsScreen, R.string.screen_plugins, Icons.Filled.Memory),
|
|
||||||
BottomNavItem(ConfigScreen, R.string.screen_config, Icons.Filled.Code),
|
|
||||||
BottomNavItem(McpServersScreen, R.string.screen_mcp_servers, Icons.Filled.Dashboard),
|
|
||||||
BottomNavItem(ModelScreen, R.string.screen_models, Icons.Filled.Psychology),
|
|
||||||
BottomNavItem(PairingScreen, R.string.screen_pairing, Icons.Filled.Devices),
|
|
||||||
BottomNavItem(KeysScreen, R.string.screen_keys, Icons.Filled.Key),
|
|
||||||
BottomNavItem(ChannelsScreen, R.string.screen_channels, Icons.AutoMirrored.Filled.ListAlt),
|
|
||||||
BottomNavItem(LogsScreen, R.string.screen_logs, Icons.Filled.HistoryEdu),
|
|
||||||
BottomNavItem(KanbanScreen, R.string.screen_kanban, Icons.Filled.Dashboard),
|
|
||||||
BottomNavItem(AchievementsScreen, R.string.screen_achievements, Icons.Filled.Info),
|
|
||||||
BottomNavItem(HistoryScreen, R.string.screen_history, Icons.Filled.History),
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Lookup: data-object simple name → NavKey (used by bottom-nav config). */
|
|
||||||
private val NAV_KEY_BY_NAME: Map<String, NavKey> =
|
|
||||||
ALL_NAV_ITEMS.mapNotNull { it.key::class.simpleName?.let { name -> name to it.key } }.toMap()
|
|
||||||
|
|
||||||
/** Resolve a persisted list of nav-item names to BottomNavItem instances. */
|
|
||||||
private fun resolveBottomNavItems(names: List<String>): List<BottomNavItem> =
|
|
||||||
names.mapNotNull { name -> NAV_KEY_BY_NAME[name]?.let { key -> ALL_NAV_ITEMS.first { it.key == key } } }
|
|
||||||
|
|
||||||
// ── Drawer sections ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private enum class DrawerSection(
|
|
||||||
@param:StringRes val titleRes: Int,
|
|
||||||
) {
|
|
||||||
CONVERSE(R.string.nav_drawer_section_converse),
|
|
||||||
AUTOMATE(R.string.nav_drawer_section_automate),
|
|
||||||
CONFIGURE(R.string.nav_drawer_section_configure),
|
|
||||||
INSPECT(R.string.nav_drawer_section_inspect),
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class DrawerEntry(
|
|
||||||
val key: NavKey,
|
|
||||||
@param:StringRes val labelRes: Int,
|
|
||||||
val icon: ImageVector,
|
|
||||||
val section: DrawerSection,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val DRAWER_ENTRIES =
|
|
||||||
listOf(
|
|
||||||
// Converse
|
|
||||||
DrawerEntry(ChatScreen, R.string.screen_chat, Icons.AutoMirrored.Filled.Chat, DrawerSection.CONVERSE),
|
|
||||||
DrawerEntry(HistoryScreen, R.string.screen_history, Icons.Filled.History, DrawerSection.CONVERSE),
|
|
||||||
DrawerEntry(ProfilesScreen, R.string.screen_profiles, Icons.Filled.AccountCircle, DrawerSection.CONVERSE),
|
|
||||||
// Automate
|
|
||||||
DrawerEntry(CronJobsScreen, R.string.screen_cron, Icons.Filled.Schedule, DrawerSection.AUTOMATE),
|
|
||||||
DrawerEntry(WebhooksScreen, R.string.screen_webhooks, Icons.Filled.Webhook, DrawerSection.AUTOMATE),
|
|
||||||
DrawerEntry(GatewayScreen, R.string.screen_gateway, Icons.Filled.Bolt, DrawerSection.AUTOMATE),
|
|
||||||
// Configure
|
|
||||||
DrawerEntry(SkillsScreen, R.string.screen_skills, Icons.Filled.Extension, DrawerSection.CONFIGURE),
|
|
||||||
DrawerEntry(ToolsetsScreen, R.string.screen_toolsets, Icons.Filled.Build, DrawerSection.CONFIGURE),
|
|
||||||
DrawerEntry(PluginsScreen, R.string.screen_plugins, Icons.Filled.Memory, DrawerSection.CONFIGURE),
|
|
||||||
DrawerEntry(ConfigScreen, R.string.screen_config, Icons.Filled.Code, DrawerSection.CONFIGURE),
|
|
||||||
DrawerEntry(McpServersScreen, R.string.screen_mcp_servers, Icons.Filled.Dashboard, DrawerSection.CONFIGURE),
|
|
||||||
DrawerEntry(ModelScreen, R.string.screen_models, Icons.Filled.Psychology, DrawerSection.CONFIGURE),
|
|
||||||
DrawerEntry(PairingScreen, R.string.screen_pairing, Icons.Filled.Devices, DrawerSection.CONFIGURE),
|
|
||||||
DrawerEntry(KeysScreen, R.string.screen_keys, Icons.Filled.Key, DrawerSection.CONFIGURE),
|
|
||||||
DrawerEntry(
|
|
||||||
ChannelsScreen,
|
|
||||||
R.string.screen_channels,
|
|
||||||
Icons.AutoMirrored.Filled.ListAlt,
|
|
||||||
DrawerSection.CONFIGURE,
|
|
||||||
),
|
|
||||||
// Inspect
|
|
||||||
DrawerEntry(SystemScreen, R.string.screen_system, Icons.Filled.Info, DrawerSection.INSPECT),
|
|
||||||
DrawerEntry(LogsScreen, R.string.screen_logs, Icons.Filled.HistoryEdu, DrawerSection.INSPECT),
|
|
||||||
DrawerEntry(KanbanScreen, R.string.screen_kanban, Icons.Filled.Dashboard, DrawerSection.INSPECT),
|
|
||||||
DrawerEntry(AchievementsScreen, R.string.screen_achievements, Icons.Filled.Info, DrawerSection.INSPECT),
|
|
||||||
DrawerEntry(SettingsScreen, R.string.screen_settings, Icons.Filled.Settings, DrawerSection.INSPECT),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val DRAWER_GESTURE_SCREENS: Set<NavKey> = ALL_NAV_ITEMS.mapTo(mutableSetOf()) { it.key }
|
|
||||||
|
|
||||||
private fun appEntryProvider(
|
private fun appEntryProvider(
|
||||||
sessionId: String?,
|
sessionId: String?,
|
||||||
@@ -215,133 +85,10 @@ private fun appEntryProvider(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry<ChatScreen> {
|
ScreenRegistry.ALL_SCREENS.forEach { screen ->
|
||||||
ChatScreenContent(
|
addEntryProvider(clazz = screen.key::class) {
|
||||||
onOpenDrawer = openDrawer,
|
screen.content(sessionId, openDrawer)
|
||||||
sessionId = sessionId,
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<HistoryScreen> {
|
|
||||||
HistoryScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<SettingsScreen> {
|
|
||||||
SettingsScreenContent(
|
|
||||||
onBack = { NavigationController.goBack() },
|
|
||||||
onLogout = {
|
|
||||||
// First clear auth state to trigger token flow
|
|
||||||
// then reset navigation to landing screen
|
|
||||||
NavigationController.backStack?.let { stack ->
|
|
||||||
stack.clear()
|
|
||||||
stack.add(LandingScreen)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<SkillsScreen> {
|
|
||||||
SkillsScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<CronJobsScreen> {
|
|
||||||
CronJobsScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<ProfilesScreen> {
|
|
||||||
ProfilesScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<ToolsetsScreen> {
|
|
||||||
ToolsetsScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<AchievementsScreen> {
|
|
||||||
AchievementsScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<PairingScreen> {
|
|
||||||
PairingScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<ConfigScreen> {
|
|
||||||
ConfigScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<McpServersScreen> {
|
|
||||||
McpServersScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<WebhooksScreen> {
|
|
||||||
WebhooksScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<ModelScreen> {
|
|
||||||
ModelScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<GatewayScreen> {
|
|
||||||
GatewayScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<LogsScreen> {
|
|
||||||
LogsScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<PluginsScreen> {
|
|
||||||
PluginsScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<ChannelsScreen> {
|
|
||||||
ChannelsScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<KeysScreen> {
|
|
||||||
KeysScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<SystemScreen> {
|
|
||||||
SystemScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<KanbanScreen> {
|
|
||||||
KanbanScreenContent(
|
|
||||||
onOpenDrawer = openDrawer,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,8 +100,6 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
|
|
||||||
val backStack = remember { NavBackStack(startScreen) }
|
val backStack = remember { NavBackStack(startScreen) }
|
||||||
LaunchedEffect(startScreen) {
|
LaunchedEffect(startScreen) {
|
||||||
// When start screen changes (e.g., logout → LandingScreen, or
|
|
||||||
// fresh login → ChatScreen), reset the backstack atomically.
|
|
||||||
if (backStack.lastOrNull() != startScreen) {
|
if (backStack.lastOrNull() != startScreen) {
|
||||||
backStack.clear()
|
backStack.clear()
|
||||||
backStack.add(startScreen)
|
backStack.add(startScreen)
|
||||||
@@ -366,18 +111,11 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
// Read dynamic bottom-nav config.
|
|
||||||
// AuthManager.bottomNavItemsFlow is a StateFlow that emits updates when the user
|
|
||||||
// customises items in Settings. Using collectAsState() ensures the NavigationBar
|
|
||||||
// recomposes and reflects choices instantly.
|
|
||||||
val bottomNavItemsState by AuthManager.bottomNavItemsFlow.collectAsState()
|
val bottomNavItemsState by AuthManager.bottomNavItemsFlow.collectAsState()
|
||||||
val bottomNavDisplayMode by AuthManager.bottomNavDisplayModeFlow.collectAsState()
|
val bottomNavDisplayMode by AuthManager.bottomNavDisplayModeFlow.collectAsState()
|
||||||
val bottomNavItems = resolveBottomNavItems(bottomNavItemsState)
|
val bottomNavItems = resolveBottomNavItems(bottomNavItemsState)
|
||||||
val bottomNavKeys = remember(bottomNavItems) { bottomNavItems.mapTo(mutableSetOf()) { it.key } }
|
val bottomNavKeys = remember(bottomNavItems) { bottomNavItems.mapTo(mutableSetOf()) { it.key } }
|
||||||
|
|
||||||
val drawerEntriesBySection = remember { DRAWER_ENTRIES.groupBy { it.section } }
|
|
||||||
|
|
||||||
// Sync primary screens to NavigationController
|
|
||||||
LaunchedEffect(bottomNavKeys) {
|
LaunchedEffect(bottomNavKeys) {
|
||||||
NavigationController.updatePrimaryScreens(bottomNavKeys)
|
NavigationController.updatePrimaryScreens(bottomNavKeys)
|
||||||
}
|
}
|
||||||
@@ -396,19 +134,16 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
ModalDrawerSheet(
|
ModalDrawerSheet(
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
// Brand header with connection status
|
|
||||||
val connectionStatus by HermesWsClient.connectionStatus.collectAsState()
|
val connectionStatus by HermesWsClient.connectionStatus.collectAsState()
|
||||||
val statusColor =
|
val statusColor =
|
||||||
when (connectionStatus) {
|
when (connectionStatus) {
|
||||||
ConnectionStatus.CONNECTED -> Color(0xFF4CAF50)
|
ConnectionStatus.CONNECTED -> Color(0xFF4CAF50)
|
||||||
|
|
||||||
// green
|
|
||||||
ConnectionStatus.CONNECTING,
|
ConnectionStatus.CONNECTING,
|
||||||
ConnectionStatus.RECONNECTING,
|
ConnectionStatus.RECONNECTING,
|
||||||
-> Color(0xFFFFC107)
|
-> Color(0xFFFFC107)
|
||||||
|
|
||||||
// yellow
|
ConnectionStatus.DISCONNECTED -> Color(0xFFF44336)
|
||||||
ConnectionStatus.DISCONNECTED -> Color(0xFFF44336) // red
|
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -452,31 +187,25 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
drawerEntriesBySection[section]?.forEach { entry ->
|
ScreenRegistry.ALL_SCREENS
|
||||||
NavigationDrawerItem(
|
.filter { it.drawerSection == section }
|
||||||
icon = { Icon(entry.icon, contentDescription = null) },
|
.forEach { entry ->
|
||||||
label = { Text(stringResource(entry.labelRes)) },
|
NavigationDrawerItem(
|
||||||
selected = currentScreen == entry.key,
|
icon = { Icon(entry.icon, contentDescription = null) },
|
||||||
onClick = {
|
label = { Text(stringResource(entry.labelRes)) },
|
||||||
scope.launch { drawerState.close() }
|
selected = currentScreen == entry.key,
|
||||||
NavigationController.navigateTo(entry.key)
|
onClick = {
|
||||||
},
|
scope.launch { drawerState.close() }
|
||||||
colors =
|
NavigationController.navigateTo(entry.key)
|
||||||
NavigationDrawerItemDefaults.colors(
|
},
|
||||||
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
colors =
|
||||||
selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
NavigationDrawerItemDefaults.colors(
|
||||||
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
),
|
selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
modifier =
|
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
Modifier
|
|
||||||
.padding(horizontal = 8.dp, vertical = 1.dp)
|
|
||||||
.testTag(
|
|
||||||
"drawer_${entry.key::class.simpleName?.lowercase()?.removeSuffix(
|
|
||||||
"screen",
|
|
||||||
) ?: ""}",
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package com.m57.hermescontrol
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ListAlt
|
||||||
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
|
import androidx.compose.material.icons.filled.Bolt
|
||||||
|
import androidx.compose.material.icons.filled.Build
|
||||||
|
import androidx.compose.material.icons.filled.Code
|
||||||
|
import androidx.compose.material.icons.filled.Dashboard
|
||||||
|
import androidx.compose.material.icons.filled.Devices
|
||||||
|
import androidx.compose.material.icons.filled.Extension
|
||||||
|
import androidx.compose.material.icons.filled.History
|
||||||
|
import androidx.compose.material.icons.filled.HistoryEdu
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material.icons.filled.Key
|
||||||
|
import androidx.compose.material.icons.filled.Memory
|
||||||
|
import androidx.compose.material.icons.filled.Psychology
|
||||||
|
import androidx.compose.material.icons.filled.Schedule
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.filled.Webhook
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
import com.m57.hermescontrol.ui.achievements.AchievementsScreen as AchievementsScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.channels.ChannelsScreen as ChannelsScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.chat.ChatScreen as ChatScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.config.ConfigScreen as ConfigScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.cron.CronJobsScreen as CronJobsScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.gateway.GatewayScreen as GatewayScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.kanban.KanbanScreen as KanbanScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.keys.KeysScreen as KeysScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.logs.LogsScreen as LogsScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.mcp.McpServersScreen as McpServersScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.model.ModelScreen as ModelScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.pairing.PairingScreen as PairingScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.plugins.PluginsScreen as PluginsScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.profiles.ProfilesScreen as ProfilesScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.sessions.SessionsScreen as HistoryScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.settings.SettingsScreen as SettingsScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.skills.SkillsScreen as SkillsScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.system.SystemScreen as SystemScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.toolsets.ToolsetsScreen as ToolsetsScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.webhooks.WebhooksScreen as WebhooksScreenContent
|
||||||
|
|
||||||
|
enum class DrawerSection(
|
||||||
|
@param:StringRes val titleRes: Int,
|
||||||
|
) {
|
||||||
|
CONVERSE(R.string.nav_drawer_section_converse),
|
||||||
|
AUTOMATE(R.string.nav_drawer_section_automate),
|
||||||
|
CONFIGURE(R.string.nav_drawer_section_configure),
|
||||||
|
INSPECT(R.string.nav_drawer_section_inspect),
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ScreenDefinition(
|
||||||
|
val key: NavKey,
|
||||||
|
@param:StringRes val labelRes: Int,
|
||||||
|
val icon: ImageVector,
|
||||||
|
val drawerSection: DrawerSection?,
|
||||||
|
val content: @Composable (sessionId: String?, openDrawer: () -> Unit) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
object ScreenRegistry {
|
||||||
|
val ALL_SCREENS =
|
||||||
|
listOf(
|
||||||
|
ScreenDefinition(
|
||||||
|
ChatScreen,
|
||||||
|
R.string.screen_chat,
|
||||||
|
Icons.AutoMirrored.Filled.Chat,
|
||||||
|
DrawerSection.CONVERSE,
|
||||||
|
) { sessionId, openDrawer -> ChatScreenContent(onOpenDrawer = openDrawer, sessionId = sessionId) },
|
||||||
|
ScreenDefinition(
|
||||||
|
HistoryScreen,
|
||||||
|
R.string.screen_history,
|
||||||
|
Icons.Filled.History,
|
||||||
|
DrawerSection.CONVERSE,
|
||||||
|
) { sessionId, openDrawer -> HistoryScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
ProfilesScreen,
|
||||||
|
R.string.screen_profiles,
|
||||||
|
Icons.Filled.AccountCircle,
|
||||||
|
DrawerSection.CONVERSE,
|
||||||
|
) { sessionId, openDrawer -> ProfilesScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
CronJobsScreen,
|
||||||
|
R.string.screen_cron,
|
||||||
|
Icons.Filled.Schedule,
|
||||||
|
DrawerSection.AUTOMATE,
|
||||||
|
) { sessionId, openDrawer -> CronJobsScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
WebhooksScreen,
|
||||||
|
R.string.screen_webhooks,
|
||||||
|
Icons.Filled.Webhook,
|
||||||
|
DrawerSection.AUTOMATE,
|
||||||
|
) { sessionId, openDrawer -> WebhooksScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
GatewayScreen,
|
||||||
|
R.string.screen_gateway,
|
||||||
|
Icons.Filled.Bolt,
|
||||||
|
DrawerSection.AUTOMATE,
|
||||||
|
) { sessionId, openDrawer -> GatewayScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
SkillsScreen,
|
||||||
|
R.string.screen_skills,
|
||||||
|
Icons.Filled.Extension,
|
||||||
|
DrawerSection.CONFIGURE,
|
||||||
|
) { sessionId, openDrawer -> SkillsScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
ToolsetsScreen,
|
||||||
|
R.string.screen_toolsets,
|
||||||
|
Icons.Filled.Build,
|
||||||
|
DrawerSection.CONFIGURE,
|
||||||
|
) { sessionId, openDrawer -> ToolsetsScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
PluginsScreen,
|
||||||
|
R.string.screen_plugins,
|
||||||
|
Icons.Filled.Memory,
|
||||||
|
DrawerSection.CONFIGURE,
|
||||||
|
) { sessionId, openDrawer -> PluginsScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
ConfigScreen,
|
||||||
|
R.string.screen_config,
|
||||||
|
Icons.Filled.Code,
|
||||||
|
DrawerSection.CONFIGURE,
|
||||||
|
) { sessionId, openDrawer -> ConfigScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
McpServersScreen,
|
||||||
|
R.string.screen_mcp_servers,
|
||||||
|
Icons.Filled.Dashboard,
|
||||||
|
DrawerSection.CONFIGURE,
|
||||||
|
) { sessionId, openDrawer -> McpServersScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
ModelScreen,
|
||||||
|
R.string.screen_models,
|
||||||
|
Icons.Filled.Psychology,
|
||||||
|
DrawerSection.CONFIGURE,
|
||||||
|
) { sessionId, openDrawer -> ModelScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
PairingScreen,
|
||||||
|
R.string.screen_pairing,
|
||||||
|
Icons.Filled.Devices,
|
||||||
|
DrawerSection.CONFIGURE,
|
||||||
|
) { sessionId, openDrawer -> PairingScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
KeysScreen,
|
||||||
|
R.string.screen_keys,
|
||||||
|
Icons.Filled.Key,
|
||||||
|
DrawerSection.CONFIGURE,
|
||||||
|
) { sessionId, openDrawer -> KeysScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
ChannelsScreen,
|
||||||
|
R.string.screen_channels,
|
||||||
|
Icons.AutoMirrored.Filled.ListAlt,
|
||||||
|
DrawerSection.CONFIGURE,
|
||||||
|
) { sessionId, openDrawer -> ChannelsScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
SystemScreen,
|
||||||
|
R.string.screen_system,
|
||||||
|
Icons.Filled.Info,
|
||||||
|
DrawerSection.INSPECT,
|
||||||
|
) { sessionId, openDrawer -> SystemScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
LogsScreen,
|
||||||
|
R.string.screen_logs,
|
||||||
|
Icons.Filled.HistoryEdu,
|
||||||
|
DrawerSection.INSPECT,
|
||||||
|
) { sessionId, openDrawer -> LogsScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
KanbanScreen,
|
||||||
|
R.string.screen_kanban,
|
||||||
|
Icons.Filled.Dashboard,
|
||||||
|
DrawerSection.INSPECT,
|
||||||
|
) { sessionId, openDrawer -> KanbanScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
AchievementsScreen,
|
||||||
|
R.string.screen_achievements,
|
||||||
|
Icons.Filled.Info,
|
||||||
|
DrawerSection.INSPECT,
|
||||||
|
) { sessionId, openDrawer -> AchievementsScreenContent(onOpenDrawer = openDrawer) },
|
||||||
|
ScreenDefinition(
|
||||||
|
SettingsScreen,
|
||||||
|
R.string.screen_settings,
|
||||||
|
Icons.Filled.Settings,
|
||||||
|
DrawerSection.INSPECT,
|
||||||
|
) { sessionId, openDrawer ->
|
||||||
|
SettingsScreenContent(onBack = {
|
||||||
|
NavigationController.goBack()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -90,7 +90,7 @@ fun AchievementsScreen(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ fun ChannelsScreen(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = listContentPadding,
|
contentPadding = listContentPadding,
|
||||||
verticalArrangement = listItemSpacing,
|
verticalArrangement = listItemSpacing,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ fun KanbanScreen(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
SearchBar(
|
SearchBar(
|
||||||
query = query,
|
query = query,
|
||||||
|
|||||||
@@ -2,7 +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.ScreenRegistry
|
||||||
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
|
||||||
@@ -163,28 +163,9 @@ 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, Int>> =
|
val availableNavItems: List<Pair<String, Int>> =
|
||||||
listOf(
|
ScreenRegistry.ALL_SCREENS.map { screen ->
|
||||||
"ChatScreen" to R.string.screen_chat,
|
screen.key::class.simpleName!! to screen.labelRes
|
||||||
"SkillsScreen" to R.string.screen_skills,
|
}
|
||||||
"CronJobsScreen" to R.string.screen_cron,
|
|
||||||
"SystemScreen" to R.string.screen_system,
|
|
||||||
"SettingsScreen" to R.string.screen_settings,
|
|
||||||
"ProfilesScreen" to R.string.screen_profiles,
|
|
||||||
"WebhooksScreen" to R.string.screen_webhooks,
|
|
||||||
"GatewayScreen" to R.string.screen_gateway,
|
|
||||||
"HistoryScreen" to R.string.screen_history,
|
|
||||||
"ToolsetsScreen" to R.string.screen_toolsets,
|
|
||||||
"PluginsScreen" to R.string.screen_plugins,
|
|
||||||
"ConfigScreen" to R.string.screen_config,
|
|
||||||
"McpServersScreen" to R.string.screen_mcp_servers,
|
|
||||||
"ModelScreen" to R.string.screen_models,
|
|
||||||
"PairingScreen" to R.string.screen_pairing,
|
|
||||||
"KeysScreen" to R.string.screen_keys,
|
|
||||||
"ChannelsScreen" to R.string.screen_channels,
|
|
||||||
"LogsScreen" to R.string.screen_logs,
|
|
||||||
"KanbanScreen" to R.string.screen_kanban,
|
|
||||||
"AchievementsScreen" to R.string.screen_achievements,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Add a nav item to the bottom bar (max 5). Auto-saves. */
|
/** Add a nav item to the bottom bar (max 5). Auto-saves. */
|
||||||
fun addNavItem(name: String) {
|
fun addNavItem(name: String) {
|
||||||
|
|||||||
Reference in New Issue
Block a user