From f7f14efe075cf3130670309362d7bf6513d4877e Mon Sep 17 00:00:00 2001 From: root Date: Sun, 26 Apr 2026 23:59:45 +0000 Subject: [PATCH] feat(engine): live sync status in Settings panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: " (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 --- solitaire_engine/src/settings_plugin.rs | 65 +++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/solitaire_engine/src/settings_plugin.rs b/solitaire_engine/src/settings_plugin.rs index b354f26..dbbe75f 100644 --- a/solitaire_engine/src/settings_plugin.rs +++ b/solitaire_engine/src/settings_plugin.rs @@ -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>, mut commands: Commands, settings: Res, + sync_status: Option>, ) { 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>, + mut text_nodes: Query<&mut Text, With>, +) { + 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,