Merge pull request #46 from Hy4ri/fix/issue-43-bottom-navbar
fix: bottom navbar updates dynamically (closes #43)
This commit is contained in:
25 files changed
+154
-47
No files matched your search
@@ -128,7 +128,9 @@ private fun resolveBottomNavItems(names: List<String>): List<BottomNavItem> =
|
|||||||
|
|
||||||
// ── Drawer sections for secondary screens ──────────────────────────────
|
// ── Drawer sections for secondary screens ──────────────────────────────
|
||||||
|
|
||||||
private enum class DrawerSection(val title: String) {
|
private enum class DrawerSection(
|
||||||
|
val title: String,
|
||||||
|
) {
|
||||||
CONVERSE("Converse"),
|
CONVERSE("Converse"),
|
||||||
AUTOMATE("Automate"),
|
AUTOMATE("Automate"),
|
||||||
CONFIGURE("Configure"),
|
CONFIGURE("Configure"),
|
||||||
@@ -179,12 +181,11 @@ fun MainNavigation() {
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
// Read dynamic bottom-nav config.
|
// Read dynamic bottom-nav config.
|
||||||
// NB: deliberately NOT wrapped in remember { … } — AuthManager values change
|
// AuthManager.bottomNavItemsFlow is a StateFlow that emits updates when the user
|
||||||
// when the user customises items in Settings (saved to SharedPreferences).
|
// customises items in Settings. Using collectAsState() ensures the NavigationBar
|
||||||
// Without a key that tracks prefs changes, remember would cache the stale
|
// recomposes and reflects choices instantly.
|
||||||
// defaults and the NavigationBar would never reflect the user's picks.
|
val bottomNavItemsState by AuthManager.bottomNavItemsFlow.collectAsState()
|
||||||
// Reading fresh on every recomposition is negligible (pure map over 18 items).
|
val bottomNavItems = resolveBottomNavItems(bottomNavItemsState)
|
||||||
val bottomNavItems = resolveBottomNavItems(AuthManager.getBottomNavItems())
|
|
||||||
val bottomNavKeys = remember(bottomNavItems) { bottomNavItems.map { it.key }.toSet() }
|
val bottomNavKeys = remember(bottomNavItems) { bottomNavItems.map { it.key }.toSet() }
|
||||||
|
|
||||||
// Sync primary screens to NavigationController
|
// Sync primary screens to NavigationController
|
||||||
@@ -208,10 +209,14 @@ fun MainNavigation() {
|
|||||||
val connectionStatus by HermesWsClient.connectionStatus.collectAsState()
|
val connectionStatus by HermesWsClient.connectionStatus.collectAsState()
|
||||||
val statusColor =
|
val statusColor =
|
||||||
when (connectionStatus) {
|
when (connectionStatus) {
|
||||||
ConnectionStatus.CONNECTED -> Color(0xFF4CAF50) // green
|
ConnectionStatus.CONNECTED -> Color(0xFF4CAF50)
|
||||||
|
|
||||||
|
// green
|
||||||
ConnectionStatus.CONNECTING,
|
ConnectionStatus.CONNECTING,
|
||||||
ConnectionStatus.RECONNECTING,
|
ConnectionStatus.RECONNECTING,
|
||||||
-> Color(0xFFFFC107) // yellow
|
-> Color(0xFFFFC107)
|
||||||
|
|
||||||
|
// yellow
|
||||||
ConnectionStatus.DISCONNECTED -> Color(0xFFF44336) // red
|
ConnectionStatus.DISCONNECTED -> Color(0xFFF44336) // red
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import android.content.SharedPreferences
|
|||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKey
|
import androidx.security.crypto.MasterKey
|
||||||
import com.m57.hermescontrol.theme.ThemePreference
|
import com.m57.hermescontrol.theme.ThemePreference
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton that manages encrypted storage of the Hermes dashboard token
|
* Singleton that manages encrypted storage of the Hermes dashboard token
|
||||||
@@ -29,6 +32,9 @@ object AuthManager {
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var prefs: SharedPreferences? = null
|
private var prefs: SharedPreferences? = null
|
||||||
|
|
||||||
|
private val _bottomNavItemsFlow = MutableStateFlow<List<String>>(emptyList())
|
||||||
|
val bottomNavItemsFlow: StateFlow<List<String>> = _bottomNavItemsFlow.asStateFlow()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialise the encrypted preferences.
|
* Initialise the encrypted preferences.
|
||||||
* Call this once from Application.onCreate() or MainActivity.onCreate().
|
* Call this once from Application.onCreate() or MainActivity.onCreate().
|
||||||
@@ -51,6 +57,8 @@ object AuthManager {
|
|||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_bottomNavItemsFlow.value = getBottomNavItems()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,5 +142,6 @@ object AuthManager {
|
|||||||
/** Persist the ordered list of bottom-nav item keys (max 5, data-object names). */
|
/** Persist the ordered list of bottom-nav item keys (max 5, data-object names). */
|
||||||
fun setBottomNavItems(items: List<String>) {
|
fun setBottomNavItems(items: List<String>) {
|
||||||
requirePrefs().edit().putString(KEY_BOTTOM_NAV_ITEMS, items.joinToString(",")).apply()
|
requirePrefs().edit().putString(KEY_BOTTOM_NAV_ITEMS, items.joinToString(",")).apply()
|
||||||
|
_bottomNavItemsFlow.value = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,9 +76,11 @@ class ChatNotificationService : Service() {
|
|||||||
.ifBlank { "New message" }
|
.ifBlank { "New message" }
|
||||||
showReplyNotification(preview)
|
showReplyNotification(preview)
|
||||||
}
|
}
|
||||||
|
|
||||||
is WsEvent.ClarifyRequest -> {
|
is WsEvent.ClarifyRequest -> {
|
||||||
showReplyNotification("Hermes needs a clarification")
|
showReplyNotification("Hermes needs a clarification")
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ fun AchievementsScreen(
|
|||||||
state.isLoading && state.achievements.isEmpty() -> {
|
state.isLoading && state.achievements.isEmpty() -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null && state.achievements.isEmpty() -> {
|
state.errorMessage != null && state.achievements.isEmpty() -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -61,6 +62,7 @@ fun AchievementsScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.achievements.isEmpty() -> {
|
state.achievements.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No achievements yet",
|
title = "No achievements yet",
|
||||||
@@ -69,6 +71,7 @@ fun AchievementsScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ fun ChannelsScreen(
|
|||||||
state.isLoading && state.platforms.isEmpty() -> {
|
state.isLoading && state.platforms.isEmpty() -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -75,6 +76,7 @@ fun ChannelsScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.platforms.isEmpty() -> {
|
state.platforms.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No channels configured",
|
title = "No channels configured",
|
||||||
@@ -82,6 +84,7 @@ fun ChannelsScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
||||||
|
|||||||
@@ -201,8 +201,7 @@ private fun AssistantBubble(
|
|||||||
bottomStart = 16.dp,
|
bottomStart = 16.dp,
|
||||||
bottomEnd = 16.dp,
|
bottomEnd = 16.dp,
|
||||||
),
|
),
|
||||||
)
|
).pointerInput(message.content) {
|
||||||
.pointerInput(message.content) {
|
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onLongPress = {
|
onLongPress = {
|
||||||
clipboardManager.setText(AnnotatedString(message.content))
|
clipboardManager.setText(AnnotatedString(message.content))
|
||||||
@@ -459,8 +458,10 @@ private fun RichText(
|
|||||||
text = annotated,
|
text = annotated,
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(color = textColor),
|
style = MaterialTheme.typography.bodyMedium.copy(color = textColor),
|
||||||
onClick = { offset: Int ->
|
onClick = { offset: Int ->
|
||||||
annotated.getStringAnnotations("URL", offset, offset)
|
annotated
|
||||||
.firstOrNull()?.let { uriHandler.openUri(it.item) }
|
.getStringAnnotations("URL", offset, offset)
|
||||||
|
.firstOrNull()
|
||||||
|
?.let { uriHandler.openUri(it.item) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,10 +116,12 @@ fun ChatScreen(
|
|||||||
NotificationHelper.setAppForeground(context, true)
|
NotificationHelper.setAppForeground(context, true)
|
||||||
NotificationHelper.stop(context)
|
NotificationHelper.stop(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
androidx.lifecycle.Lifecycle.Event.ON_STOP -> {
|
androidx.lifecycle.Lifecycle.Event.ON_STOP -> {
|
||||||
NotificationHelper.setAppForeground(context, false)
|
NotificationHelper.setAppForeground(context, false)
|
||||||
NotificationHelper.start(context)
|
NotificationHelper.start(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ data class ClarifyUi(
|
|||||||
val options: List<String>,
|
val options: List<String>,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
class ChatViewModel(
|
||||||
|
application: Application,
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
private val _uiState = MutableStateFlow(ChatUiState())
|
private val _uiState = MutableStateFlow(ChatUiState())
|
||||||
val uiState: StateFlow<ChatUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ChatUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ fun HermesScaffold(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpenDrawer != null -> {
|
onOpenDrawer != null -> {
|
||||||
IconButton(onClick = onOpenDrawer) {
|
IconButton(onClick = onOpenDrawer) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -75,13 +75,26 @@ fun StatusBadge(
|
|||||||
val statusColors = LocalHermesStatusColors.current
|
val statusColors = LocalHermesStatusColors.current
|
||||||
val (bgColor, fgColor) =
|
val (bgColor, fgColor) =
|
||||||
when (status) {
|
when (status) {
|
||||||
StatusBadgeType.SUCCESS -> statusColors.successContainer to statusColors.success
|
StatusBadgeType.SUCCESS -> {
|
||||||
StatusBadgeType.WARNING -> statusColors.warningContainer to statusColors.warning
|
statusColors.successContainer to statusColors.success
|
||||||
StatusBadgeType.ERROR -> statusColors.errorContainer to statusColors.error
|
}
|
||||||
StatusBadgeType.INFO -> statusColors.infoContainer to statusColors.info
|
|
||||||
StatusBadgeType.NEUTRAL ->
|
StatusBadgeType.WARNING -> {
|
||||||
|
statusColors.warningContainer to statusColors.warning
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusBadgeType.ERROR -> {
|
||||||
|
statusColors.errorContainer to statusColors.error
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusBadgeType.INFO -> {
|
||||||
|
statusColors.infoContainer to statusColors.info
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusBadgeType.NEUTRAL -> {
|
||||||
MaterialTheme.colorScheme.surfaceContainerHigh to
|
MaterialTheme.colorScheme.surfaceContainerHigh to
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ fun ConfigScreen(
|
|||||||
state.isLoading && state.yamlText == null -> {
|
state.isLoading && state.yamlText == null -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -73,7 +74,8 @@ fun ConfigScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
|
||||||
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading && state.yamlText == null) {
|
if (state.isLoading && state.yamlText == null) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -143,6 +145,7 @@ fun ConfigScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,19 +65,26 @@ fun CronJobsScreen(
|
|||||||
onRefresh = { viewModel.loadCronJobs() },
|
onRefresh = { viewModel.loadCronJobs() },
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading && state.jobs.isEmpty() -> LoadingState()
|
state.isLoading && state.jobs.isEmpty() -> {
|
||||||
state.errorMessage != null ->
|
LoadingState()
|
||||||
|
}
|
||||||
|
|
||||||
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "Unknown error",
|
message = state.errorMessage ?: "Unknown error",
|
||||||
onRetry = { viewModel.loadCronJobs() },
|
onRetry = { viewModel.loadCronJobs() },
|
||||||
)
|
)
|
||||||
state.jobs.isEmpty() ->
|
}
|
||||||
|
|
||||||
|
state.jobs.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No cron jobs",
|
title = "No cron jobs",
|
||||||
subtitle = "Scheduled tasks from Hermes will appear here.",
|
subtitle = "Scheduled tasks from Hermes will appear here.",
|
||||||
icon = Icons.Filled.Schedule,
|
icon = Icons.Filled.Schedule,
|
||||||
)
|
)
|
||||||
else ->
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentPadding = listContentPadding,
|
contentPadding = listContentPadding,
|
||||||
@@ -160,6 +167,7 @@ fun CronJobsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +71,7 @@ fun GatewayScreen(
|
|||||||
state.isLoading && state.status == null -> {
|
state.isLoading && state.status == null -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -78,7 +79,8 @@ fun GatewayScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
|
||||||
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading && state.status == null) {
|
if (state.isLoading && state.status == null) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -285,6 +287,7 @@ fun GatewayScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +80,7 @@ fun KanbanScreen(
|
|||||||
state.isLoading && state.boards.isEmpty() -> {
|
state.isLoading && state.boards.isEmpty() -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -87,7 +88,8 @@ fun KanbanScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
|
||||||
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -189,6 +191,7 @@ fun KanbanScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ fun KeysScreen(
|
|||||||
state.isLoading && state.envVars.isEmpty() -> {
|
state.isLoading && state.envVars.isEmpty() -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -82,7 +83,8 @@ fun KeysScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
|
||||||
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -205,6 +207,7 @@ fun KeysScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,13 +65,18 @@ fun LogsScreen(
|
|||||||
onRefresh = { viewModel.loadLogs() },
|
onRefresh = { viewModel.loadLogs() },
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading && state.logs.isEmpty() -> LoadingState()
|
state.isLoading && state.logs.isEmpty() -> {
|
||||||
state.errorMessage != null ->
|
LoadingState()
|
||||||
|
}
|
||||||
|
|
||||||
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "Unknown error",
|
message = state.errorMessage ?: "Unknown error",
|
||||||
onRetry = { viewModel.loadLogs() },
|
onRetry = { viewModel.loadLogs() },
|
||||||
)
|
)
|
||||||
state.logs.isEmpty() ->
|
}
|
||||||
|
|
||||||
|
state.logs.isEmpty() -> {
|
||||||
// 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(
|
||||||
@@ -79,7 +84,9 @@ fun LogsScreen(
|
|||||||
subtitle = "Log output from Hermes will appear here.",
|
subtitle = "Log output from Hermes will appear here.",
|
||||||
icon = Icons.Filled.HistoryEdu,
|
icon = Icons.Filled.HistoryEdu,
|
||||||
)
|
)
|
||||||
else ->
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier =
|
modifier =
|
||||||
@@ -105,6 +112,7 @@ fun LogsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,6 +68,7 @@ fun McpServersScreen(
|
|||||||
state.isLoading && state.servers.isEmpty() -> {
|
state.isLoading && state.servers.isEmpty() -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -75,7 +76,8 @@ fun McpServersScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
|
||||||
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading && state.servers.isEmpty()) {
|
if (state.isLoading && state.servers.isEmpty()) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -181,6 +183,7 @@ fun McpServersScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,6 +75,7 @@ fun ModelScreen(
|
|||||||
state.isLoading && state.providers.isEmpty() -> {
|
state.isLoading && state.providers.isEmpty() -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -82,7 +83,8 @@ fun ModelScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
|
||||||
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading && state.providers.isEmpty()) {
|
if (state.isLoading && state.providers.isEmpty()) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -201,7 +203,8 @@ fun ModelScreen(
|
|||||||
containerColor =
|
containerColor =
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
MaterialTheme.colorScheme
|
MaterialTheme.colorScheme
|
||||||
.primaryContainer.copy(
|
.primaryContainer
|
||||||
|
.copy(
|
||||||
alpha = 0.3f,
|
alpha = 0.3f,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -264,6 +267,7 @@ fun ModelScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,6 +67,7 @@ fun PairingScreen(
|
|||||||
state.isLoading && state.pairing == null -> {
|
state.isLoading && state.pairing == null -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -74,7 +75,8 @@ fun PairingScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
|
||||||
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading && state.pairing == null) {
|
if (state.isLoading && state.pairing == null) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -253,6 +255,7 @@ fun PairingScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +66,7 @@ fun PluginsScreen(
|
|||||||
state.isLoading && state.plugins.isEmpty() -> {
|
state.isLoading && state.plugins.isEmpty() -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -73,7 +74,8 @@ fun PluginsScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
|
||||||
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -150,6 +152,7 @@ fun PluginsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,6 +79,7 @@ fun ProfilesScreen(
|
|||||||
state.isLoading && state.profiles.isEmpty() -> {
|
state.isLoading && state.profiles.isEmpty() -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -86,7 +87,8 @@ fun ProfilesScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
|
||||||
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading && state.profiles.isEmpty()) {
|
if (state.isLoading && state.profiles.isEmpty()) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -222,6 +224,7 @@ fun ProfilesScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,19 +78,26 @@ fun SkillsScreen(
|
|||||||
onRefresh = { viewModel.loadSkills() },
|
onRefresh = { viewModel.loadSkills() },
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading && state.skills.isEmpty() -> LoadingState()
|
state.isLoading && state.skills.isEmpty() -> {
|
||||||
state.errorMessage != null ->
|
LoadingState()
|
||||||
|
}
|
||||||
|
|
||||||
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "Unknown error",
|
message = state.errorMessage ?: "Unknown error",
|
||||||
onRetry = { viewModel.loadSkills() },
|
onRetry = { viewModel.loadSkills() },
|
||||||
)
|
)
|
||||||
state.skills.isEmpty() ->
|
}
|
||||||
|
|
||||||
|
state.skills.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No skills found",
|
title = "No skills found",
|
||||||
subtitle = "Skills loaded from Hermes will appear here.",
|
subtitle = "Skills loaded from Hermes will appear here.",
|
||||||
icon = Icons.Filled.Extension,
|
icon = Icons.Filled.Extension,
|
||||||
)
|
)
|
||||||
else ->
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentPadding = listContentPadding,
|
contentPadding = listContentPadding,
|
||||||
@@ -162,6 +169,7 @@ fun SkillsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,18 +71,25 @@ fun SystemScreen(
|
|||||||
onRefresh = { viewModel.loadSystemData() },
|
onRefresh = { viewModel.loadSystemData() },
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading && state.stats == null && state.doctorReport == null -> LoadingState()
|
state.isLoading && state.stats == null && state.doctorReport == null -> {
|
||||||
state.errorMessage != null ->
|
LoadingState()
|
||||||
|
}
|
||||||
|
|
||||||
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "Unknown error",
|
message = state.errorMessage ?: "Unknown error",
|
||||||
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 = "No system data",
|
||||||
subtitle = "Tap refresh to load system diagnostics.",
|
subtitle = "Tap refresh to load system diagnostics.",
|
||||||
)
|
)
|
||||||
else ->
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentPadding =
|
contentPadding =
|
||||||
@@ -200,6 +207,7 @@ fun SystemScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +71,7 @@ fun ToolsetsScreen(
|
|||||||
state.isLoading && state.toolsets.isEmpty() -> {
|
state.isLoading && state.toolsets.isEmpty() -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -78,7 +79,8 @@ fun ToolsetsScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
|
||||||
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading && state.toolsets.isEmpty()) {
|
if (state.isLoading && state.toolsets.isEmpty()) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -171,6 +173,7 @@ fun ToolsetsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,6 +69,7 @@ fun WebhooksScreen(
|
|||||||
state.isLoading && state.baseUrl == null -> {
|
state.isLoading && state.baseUrl == null -> {
|
||||||
LoadingState(modifier = Modifier.padding(paddingValues))
|
LoadingState(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage != null -> {
|
state.errorMessage != null -> {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.errorMessage ?: "",
|
message = state.errorMessage ?: "",
|
||||||
@@ -76,7 +77,8 @@ fun WebhooksScreen(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
|
||||||
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading && state.baseUrl == null) {
|
if (state.isLoading && state.baseUrl == null) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -234,6 +236,7 @@ fun WebhooksScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user