feat(#13): add pull-to-refresh to HermesScaffold (#14)

- HermesScaffold: added isRefreshing param, wraps content in PullToRefreshBox when onRefresh is set
- All 16 data screens: pass isRefreshing = state.isLoading
- Initial loading only shows full-screen LoadingState when data is empty (no spinner over existing content)
- AchievementsScreen already had the correct pattern, just needed isRefreshing param

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-06-18 18:58:08 +03:00
1 parent d37de1d223
commit 4892572bcc
18 files changed
+56 -23

No files matched your search

@@ -47,6 +47,7 @@ fun AchievementsScreen(
HermesScaffold( HermesScaffold(
title = { Text("Agent Achievements") }, title = { Text("Agent Achievements") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadAchievements() }, onRefresh = { viewModel.loadAchievements() },
) { paddingValues -> ) { paddingValues ->
when { when {
@@ -61,10 +61,11 @@ fun ChannelsScreen(
HermesScaffold( HermesScaffold(
title = { Text("Messaging Channels") }, title = { Text("Messaging Channels") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadPlatforms() }, onRefresh = { viewModel.loadPlatforms() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.platforms.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -15,6 +15,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable 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
@@ -38,6 +39,7 @@ fun HermesScaffold(
title: @Composable () -> Unit, title: @Composable () -> Unit,
onOpenDrawer: (() -> Unit)? = null, onOpenDrawer: (() -> Unit)? = null,
onRefresh: (() -> Unit)? = null, onRefresh: (() -> Unit)? = null,
isRefreshing: Boolean = false,
onBack: (() -> Unit)? = null, onBack: (() -> Unit)? = null,
showBack: Boolean = false, showBack: Boolean = false,
snackbarHost: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {},
@@ -95,13 +97,27 @@ fun HermesScaffold(
}, },
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
) { paddingValues -> ) { paddingValues ->
Box( val refreshContent: @Composable () -> Unit = {
modifier = Box(
Modifier modifier =
.fillMaxSize() Modifier
.padding(paddingValues), .fillMaxSize()
) { .padding(paddingValues),
content(paddingValues) ) {
content(paddingValues)
}
}
if (onRefresh != null) {
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = onRefresh,
modifier = Modifier.fillMaxSize(),
) {
refreshContent()
}
} else {
refreshContent()
} }
} }
} }
@@ -59,10 +59,11 @@ fun ConfigScreen(
HermesScaffold( HermesScaffold(
title = { Text("Configuration") }, title = { Text("Configuration") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadRawConfig() }, onRefresh = { viewModel.loadRawConfig() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.yamlText == null -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -61,10 +61,11 @@ fun CronJobsScreen(
HermesScaffold( HermesScaffold(
title = { Text("Cron Jobs") }, title = { Text("Cron Jobs") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadCronJobs() }, onRefresh = { viewModel.loadCronJobs() },
) { ) {
when { when {
state.isLoading -> LoadingState() state.isLoading && state.jobs.isEmpty() -> LoadingState()
state.errorMessage != null -> state.errorMessage != null ->
ErrorState( ErrorState(
message = state.errorMessage ?: "Unknown error", message = state.errorMessage ?: "Unknown error",
@@ -64,10 +64,11 @@ fun GatewayScreen(
HermesScaffold( HermesScaffold(
title = { Text("Gateway Control") }, title = { Text("Gateway Control") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadStatus() }, onRefresh = { viewModel.loadStatus() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.status == null -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -73,10 +73,11 @@ fun KanbanScreen(
HermesScaffold( HermesScaffold(
title = { Text("Kanban Board") }, title = { Text("Kanban Board") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadBoards() }, onRefresh = { viewModel.loadBoards() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.boards.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -68,10 +68,11 @@ fun KeysScreen(
HermesScaffold( HermesScaffold(
title = { Text("Keys & Credentials") }, title = { Text("Keys & Credentials") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadKeys() }, onRefresh = { viewModel.loadKeys() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.envVars.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -61,10 +61,11 @@ fun LogsScreen(
HermesScaffold( HermesScaffold(
title = { Text("Logs") }, title = { Text("Logs") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadLogs() }, onRefresh = { viewModel.loadLogs() },
) { ) {
when { when {
state.isLoading -> LoadingState() state.isLoading && state.logs.isEmpty() -> LoadingState()
state.errorMessage != null -> state.errorMessage != null ->
ErrorState( ErrorState(
message = state.errorMessage ?: "Unknown error", message = state.errorMessage ?: "Unknown error",
@@ -61,10 +61,11 @@ fun McpServersScreen(
HermesScaffold( HermesScaffold(
title = { Text("MCP Servers") }, title = { Text("MCP Servers") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadServers() }, onRefresh = { viewModel.loadServers() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.servers.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -68,10 +68,11 @@ fun ModelScreen(
HermesScaffold( HermesScaffold(
title = { Text("Models") }, title = { Text("Models") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadModelOptions() }, onRefresh = { viewModel.loadModelOptions() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.providers.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -60,10 +60,11 @@ fun PairingScreen(
HermesScaffold( HermesScaffold(
title = { Text("Device Pairing") }, title = { Text("Device Pairing") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadPairing() }, onRefresh = { viewModel.loadPairing() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.pairing == null -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -59,10 +59,11 @@ fun PluginsScreen(
HermesScaffold( HermesScaffold(
title = { Text("Plugins") }, title = { Text("Plugins") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadPlugins() }, onRefresh = { viewModel.loadPlugins() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.plugins.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -72,10 +72,11 @@ fun ProfilesScreen(
HermesScaffold( HermesScaffold(
title = { Text("Profiles") }, title = { Text("Profiles") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadProfiles() }, onRefresh = { viewModel.loadProfiles() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.profiles.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -74,10 +74,11 @@ fun SkillsScreen(
HermesScaffold( HermesScaffold(
title = { Text("Skills") }, title = { Text("Skills") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadSkills() }, onRefresh = { viewModel.loadSkills() },
) { ) {
when { when {
state.isLoading -> LoadingState() state.isLoading && state.skills.isEmpty() -> LoadingState()
state.errorMessage != null -> state.errorMessage != null ->
ErrorState( ErrorState(
message = state.errorMessage ?: "Unknown error", message = state.errorMessage ?: "Unknown error",
@@ -67,10 +67,11 @@ fun SystemScreen(
HermesScaffold( HermesScaffold(
title = { Text("System") }, title = { Text("System") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadSystemData() }, onRefresh = { viewModel.loadSystemData() },
) { ) {
when { when {
state.isLoading -> LoadingState() state.isLoading && state.stats == null && state.doctorReport == null -> LoadingState()
state.errorMessage != null -> state.errorMessage != null ->
ErrorState( ErrorState(
message = state.errorMessage ?: "Unknown error", message = state.errorMessage ?: "Unknown error",
@@ -64,10 +64,11 @@ fun ToolsetsScreen(
HermesScaffold( HermesScaffold(
title = { Text("Toolsets") }, title = { Text("Toolsets") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadToolsets() }, onRefresh = { viewModel.loadToolsets() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.toolsets.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -62,10 +62,11 @@ fun WebhooksScreen(
HermesScaffold( HermesScaffold(
title = { Text("Webhooks") }, title = { Text("Webhooks") },
onOpenDrawer = onOpenDrawer, onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadWebhooks() }, onRefresh = { viewModel.loadWebhooks() },
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading -> { state.isLoading && state.baseUrl == null -> {
LoadingState(modifier = Modifier.padding(paddingValues)) LoadingState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {