feat(engine): live sync status in Settings panel

The Settings panel now shows a "Sync" section with a live status line:
- "Status: idle" (no SyncPlugin installed)
- "Status: syncing…" (pull in progress)
- "Last synced: Xs ago" (successful pull)
- "Sync error: <msg>" (failed pull)

The text is snapshotted from SyncStatusResource when the panel opens and
updated reactively via update_sync_status_text whenever SyncStatusResource
changes while the panel is visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-04-26 23:59:45 +00:00
parent 303c78aa4c
commit f7f14efe07
+62 -3
View File
@@ -15,6 +15,8 @@ use bevy::prelude::*;
use solitaire_core::game_state::DrawMode;
use solitaire_data::{load_settings_from, save_settings_to, settings_file_path, settings::Theme, Settings};
use crate::resources::{SyncStatus, SyncStatusResource};
/// Volume adjustment step applied by the `[` / `]` hotkeys.
pub const SFX_STEP: f32 = 0.1;
@@ -54,6 +56,10 @@ struct DrawModeText;
#[derive(Component, Debug)]
struct ThemeText;
/// Marks the `Text` node showing the live sync status.
#[derive(Component, Debug)]
struct SyncStatusText;
/// Tags interactive buttons inside the Settings panel.
#[derive(Component, Debug)]
enum SettingsButton {
@@ -109,7 +115,11 @@ impl Plugin for SettingsPlugin {
if self.ui_enabled {
app.add_systems(
Update,
(sync_settings_panel_visibility, handle_settings_buttons),
(
sync_settings_panel_visibility,
handle_settings_buttons,
update_sync_status_text,
),
);
}
}
@@ -172,13 +182,17 @@ fn sync_settings_panel_visibility(
panels: Query<Entity, With<SettingsPanel>>,
mut commands: Commands,
settings: Res<SettingsResource>,
sync_status: Option<Res<SyncStatusResource>>,
) {
if !screen.is_changed() {
return;
}
if screen.0 {
if panels.is_empty() {
spawn_settings_panel(&mut commands, &settings.0);
let status_label = sync_status
.map(|s| sync_status_label(&s.0))
.unwrap_or_else(|| "Status: not configured".to_string());
spawn_settings_panel(&mut commands, &settings.0, &status_label);
}
} else {
for entity in &panels {
@@ -187,6 +201,42 @@ fn sync_settings_panel_visibility(
}
}
/// Keeps the sync-status text node current while the panel is open.
fn update_sync_status_text(
sync_status: Option<Res<SyncStatusResource>>,
mut text_nodes: Query<&mut Text, With<SyncStatusText>>,
) {
let Some(status) = sync_status else {
return;
};
if !status.is_changed() {
return;
}
let label = sync_status_label(&status.0);
for mut text in &mut text_nodes {
**text = label.clone();
}
}
fn sync_status_label(status: &SyncStatus) -> String {
match status {
SyncStatus::Idle => "Status: idle".to_string(),
SyncStatus::Syncing => "Status: syncing…".to_string(),
SyncStatus::LastSynced(t) => {
let secs = chrono::Utc::now()
.signed_duration_since(*t)
.num_seconds()
.max(0);
if secs < 60 {
format!("Last synced: {secs}s ago")
} else {
format!("Last synced: {}m ago", secs / 60)
}
}
SyncStatus::Error(e) => format!("Sync error: {e}"),
}
}
/// Reacts to button presses inside the Settings panel.
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
fn handle_settings_buttons(
@@ -298,7 +348,7 @@ fn theme_label(theme: &Theme) -> String {
// UI construction
// ---------------------------------------------------------------------------
fn spawn_settings_panel(commands: &mut Commands, settings: &Settings) {
fn spawn_settings_panel(commands: &mut Commands, settings: &Settings, sync_status: &str) {
commands
.spawn((
SettingsPanel,
@@ -401,6 +451,15 @@ fn spawn_settings_panel(commands: &mut Commands, settings: &Settings) {
icon_button(row, "", SettingsButton::ToggleTheme);
});
// --- Sync section ---
section_label(card, "Sync");
card.spawn((
SyncStatusText,
Text::new(sync_status.to_string()),
TextFont { font_size: 16.0, ..default() },
TextColor(Color::srgb(0.65, 0.65, 0.70)),
));
// Done button
card.spawn((
SettingsButton::Done,