OneLauncher-Cracked/packages/oneclient_app/src/components/status_bar.rs

166 lines
5.1 KiB
Rust

use std::collections::HashSet;
use freya::animation::{AnimNum, Ease, Function, OnCreation, use_animation};
use freya::prelude::*;
use oneclient_net::status::{self, ServiceStatus};
use crate::components::{Icon, IconType};
use crate::theme::colors;
const BAR_HEIGHT: f32 = 34.;
const AMBER: Color = Color::from_rgb(191, 122, 26);
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum Issue {
NoInternet,
McAuthDown,
PolyfrostDown,
}
impl Issue {
fn is_active(self, s: &ServiceStatus) -> bool {
match self {
Self::NoInternet => !s.online,
Self::McAuthDown => s.online && !s.mc_auth_up,
Self::PolyfrostDown => s.online && !s.polyfrost_up,
}
}
fn message(self) -> &'static str {
match self {
Self::NoInternet => "No internet connection.",
Self::McAuthDown => {
"Minecraft authentication servers are unreachable. Logging in may fail."
}
Self::PolyfrostDown => "Polyfrost services are experiencing issues.",
}
}
fn icon(self) -> IconType {
match self {
Self::NoInternet => IconType::Globe01,
Self::McAuthDown => IconType::AlertTriangle,
Self::PolyfrostDown => IconType::AlertCircle,
}
}
fn background(self) -> Color {
match self {
Self::NoInternet => colors::danger(),
Self::McAuthDown | Self::PolyfrostDown => AMBER,
}
}
fn closeable(self) -> bool {
!matches!(self, Self::NoInternet)
}
}
fn active_issues(s: &ServiceStatus) -> Vec<Issue> {
[Issue::NoInternet, Issue::McAuthDown, Issue::PolyfrostDown]
.into_iter()
.filter(|i| i.is_active(s))
.collect()
}
#[derive(PartialEq)]
pub struct StatusBar;
impl Component for StatusBar {
fn render(&self) -> impl IntoElement {
let mut status = use_state(status::current);
let mut dismissed = use_state(HashSet::<Issue>::new);
use_hook(move || {
status::request_recheck();
let mut rx = status::subscribe();
spawn(async move {
while rx.changed().await.is_ok() {
status.set(*rx.borrow());
}
});
});
use_side_effect(move || {
let s = *status.read();
let cur = dismissed.peek().clone();
let next: HashSet<Issue> = cur.iter().copied().filter(|i| i.is_active(&s)).collect();
if next != cur {
dismissed.set(next);
}
});
let s = *status.read();
let dset = dismissed.read();
let visible = active_issues(&s).into_iter().find(|i| !dset.contains(i));
match visible {
Some(issue) => StatusBanner { issue, dismissed }.into_element(),
None => rect().into_element(),
}
}
}
#[derive(PartialEq)]
struct StatusBanner {
issue: Issue,
dismissed: State<HashSet<Issue>>,
}
impl Component for StatusBanner {
fn render(&self) -> impl IntoElement {
let issue = self.issue;
let mut dismissed = self.dismissed;
let intro = use_animation(|conf| {
conf.on_creation(OnCreation::Run);
AnimNum::new(0., 1.)
.time(260)
.ease(Ease::Out)
.function(Function::Cubic)
});
let p = intro.get().value();
let close = issue.closeable().then(|| {
rect()
.position(Position::new_absolute().right(10.))
.center()
.width(Size::px(20.))
.height(Size::px(20.))
.corner_radius(CornerRadius::new_all(6.))
.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
.on_press(move |_| {
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))
.position(Position::new_global().bottom(0.).left(0.))
.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(
label()
.text(issue.message())
.font_size(12.)
.font_weight(FontWeight::MEDIUM)
.max_lines(1)
.color(Color::WHITE),
)
.maybe_child(close)
}
}