fix(ui): render the connectivity status bar message properly

This commit is contained in:
LynithDev 2026-07-28 13:13:32 +02:00
parent 1fd587a840
commit 875c667313
3 changed files with 163 additions and 22 deletions

View file

@ -121,19 +121,6 @@ impl Component for StatusBanner {
});
let p = intro.get().value();
let content = rect()
.horizontal()
.cross_align(Alignment::Center)
.spacing(8.)
.child(Icon::new(issue.icon()).size(15.).color(Color::WHITE))
.child(
label()
.text(issue.message())
.font_size(12.)
.font_weight(FontWeight::MEDIUM)
.color(Color::WHITE),
);
let close = issue.closeable().then(|| {
rect()
.position(Position::new_absolute().right(10.))
@ -147,8 +134,12 @@ impl Component for StatusBanner {
dismissed.write().insert(issue);
})
.child(Icon::new(IconType::XClose).size(14.).color(Color::WHITE))
.into_element()
});
// The icon+message row is laid out directly in the bar rather than in a
// nested auto-sized wrapper: a transparent rect with no explicit size
// measures to nothing here, which is what was swallowing the message.
rect()
.width(Size::window_percent(100.))
.height(Size::px(BAR_HEIGHT))
@ -156,14 +147,20 @@ impl Component for StatusBanner {
.layer(Layer::OverlayLevel(u8::MAX - 20))
.background(issue.background())
.opacity(p)
.horizontal()
.main_align(Alignment::Center)
.cross_align(Alignment::Center)
.spacing(8.)
.padding(Gaps::new_symmetric(0., 40.))
.child(Icon::new(issue.icon()).size(15.).color(Color::WHITE))
.child(
rect()
.width(Size::fill())
.height(Size::fill())
.center()
.padding(Gaps::new_symmetric(0., 16.))
.child(content)
.maybe_child(close),
label()
.text(issue.message())
.font_size(12.)
.font_weight(FontWeight::MEDIUM)
.max_lines(1)
.color(Color::WHITE),
)
.maybe_child(close)
}
}

View file

@ -3,6 +3,7 @@ use freya::router::RouterContext;
use oneclient_db::console::{ConsoleQueryResult, run_console_query};
use oneclient_auth::preview_samples;
use oneclient_net::status::{self, ServiceStatus};
use crate::components::{Button, Icon, IconType, TextInput, login_dialog, toggle};
use crate::hooks::use_dispatch;
@ -76,6 +77,11 @@ impl Component for Debug {
vec![LauncherUpdateSimulator.into_element()],
))
.child(divider())
.child(section(
"Connectivity Status Bar",
vec![StatusBarSimulator.into_element()],
))
.child(divider())
.child(section(
"Auth Error Guidance",
vec![AuthGuidancePreview.into_element()],
@ -468,6 +474,114 @@ impl Component for LauncherUpdateSimulator {
}
}
/// Pins `oneclient_net::status` so the connectivity banner can be looked at
/// without unplugging anything. Each preset is one banner the bar knows how to
/// draw; the bar itself picks the first active issue.
#[derive(PartialEq)]
struct StatusBarSimulator;
const STATUS_PRESETS: [(&str, IconType, ServiceStatus); 4] = [
(
"No internet",
IconType::Globe01,
ServiceStatus {
online: false,
mc_auth_up: false,
polyfrost_up: false,
},
),
(
"Minecraft auth down",
IconType::AlertTriangle,
ServiceStatus {
online: true,
mc_auth_up: false,
polyfrost_up: true,
},
),
(
"Polyfrost down",
IconType::AlertCircle,
ServiceStatus {
online: true,
mc_auth_up: true,
polyfrost_up: false,
},
),
(
"Both services down",
IconType::AlertTriangle,
ServiceStatus {
online: true,
mc_auth_up: false,
polyfrost_up: false,
},
),
];
impl Component for StatusBarSimulator {
fn render(&self) -> impl IntoElement {
let mut pinned = use_state(status::forced);
let mut row = rect().horizontal().width(Size::fill()).spacing(12.);
for (text, icon, forced) in STATUS_PRESETS {
row = row.child(
Button::new()
.secondary()
.child(Icon::new(icon).size(16.))
.text(text)
.on_press(move |_| {
status::force(Some(forced));
pinned.set(Some(forced));
}),
);
}
let current = *pinned.read();
let state_text = match current {
Some(_) => "Pinned. Probing still runs, but its result is not published.".to_string(),
None => "Not pinned; showing the real probe result.".to_string(),
};
rect()
.vertical()
.width(Size::fill())
.spacing(10.)
.child(
label()
.text("Forces what the connectivity prober reports, so the real status bar renders its banner. Dismissing a banner is remembered until the issue clears.")
.font_size(13.)
.color(colors::fg_secondary()),
)
.child(row)
.child(
rect()
.horizontal()
.width(Size::fill())
.cross_align(Alignment::Center)
.spacing(12.)
.child(
Button::new()
.primary()
.child(Icon::new(IconType::RefreshCw01).size(16.))
.text("Clear + recheck")
.enabled(current.is_some())
.on_press(move |_| {
status::force(None);
pinned.set(None);
}),
)
.child(
label()
.text(state_text)
.font_size(13.)
.color(colors::fg_secondary()),
),
)
.into_element()
}
}
#[derive(PartialEq)]
struct AuthGuidancePreview;

View file

@ -1,4 +1,4 @@
use std::sync::OnceLock;
use std::sync::{OnceLock, RwLock};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
@ -34,6 +34,9 @@ struct Connectivity {
rx: watch::Receiver<ServiceStatus>,
notify: Notify,
started: AtomicBool,
/// Debug-only pin. While set, probe results are recorded but never
/// published, so the forced status survives a recheck.
forced: RwLock<Option<ServiceStatus>>,
}
fn connectivity() -> &'static Connectivity {
@ -45,10 +48,34 @@ fn connectivity() -> &'static Connectivity {
rx,
notify: Notify::new(),
started: AtomicBool::new(false),
forced: RwLock::new(None),
}
})
}
/// Pins the reported status to `status`, or releases the pin with `None` and
/// re-probes. Exposed for the debug page: the connectivity banner is otherwise
/// only reachable by actually taking a service down.
pub fn force(status: Option<ServiceStatus>) {
let conn = connectivity();
*conn.forced.write().unwrap_or_else(|e| e.into_inner()) = status;
match status {
Some(status) => {
let _ = conn.tx.send(status);
}
None => request_recheck(),
}
}
#[must_use]
pub fn forced() -> Option<ServiceStatus> {
*connectivity()
.forced
.read()
.unwrap_or_else(|e| e.into_inner())
}
pub fn subscribe() -> watch::Receiver<ServiceStatus> {
connectivity().rx.clone()
}
@ -78,7 +105,10 @@ pub fn start(client: RequestClient) {
tokio::spawn(async move {
loop {
let _ = conn.tx.send(check_service_status(&client).await);
let probed = check_service_status(&client).await;
if forced().is_none() {
let _ = conn.tx.send(probed);
}
conn.notify.notified().await;
}
});