OneLauncher-Cracked/packages/oneclient_app/src/components/toasts.rs
oiupoyt 163eb0ac0c
Some checks are pending
Release Multiplatform Binaries / build (deb,appimage, linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-gnu) (push) Waiting to run
Release Multiplatform Binaries / build (dmg,app, macos-arm64, macos-14, aarch64-apple-darwin) (push) Waiting to run
Release Multiplatform Binaries / build (dmg,app, macos-x86_64, macos-14, x86_64-apple-darwin) (push) Waiting to run
Release Multiplatform Binaries / build (nsis, windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Waiting to run
Release Multiplatform Binaries / create-release (push) Blocked by required conditions
feat: update base to official upstream 2.3.1 with all cracked features intact (2.3.1-c1)
- Rebased onto official Polyfrost OneLauncher v2.3.1 (commit 0e49c691)
- Preserved 100% offline authentication without requiring Microsoft accounts
- Integrated custom 3D offline skin changer with UUID character slot targeting
- Maintained CustomSkinLoader auto-configuration for Ely.by and SkinRestorer
- Maintained direct GitHub Releases auto-updater pointing to oiupoyt/OneLauncher-Cracked
- Maintained mods filter popover fix eliminating text ghosting/duplication
- Configured dev profile to prevent debug symbol bloat
2026-09-07 17:38:22 +05:30

482 lines
15 KiB
Rust

use std::{cell::Cell, rc::Rc, time::Instant};
use freya::{
animation::{
AnimNum, Ease, Function, OnChange, OnCreation, use_animation,
use_animation_with_dependencies,
},
prelude::*,
};
use oneclient_events::Level;
use crate::{
ui::{divider},
components::{Button, ButtonSize, Icon, IconType, progress_track},
hooks::{use_dispatch, use_notifications_snapshot},
motion::use_animations_enabled,
notifications::{InboxEntry, NotificationActionKind},
theme::colors,
};
const TOAST_W: f32 = 300.;
const TRAVEL: f32 = 320.;
const TTL_MS: f32 = crate::notifications::MESSAGE_TOAST_TTL.as_millis() as f32;
const TTL_STEP: std::time::Duration = std::time::Duration::from_millis(100);
/// Below the navbar a smaller offset let the startup progress toast cover the nav links
const TOP_PX: f32 = crate::theme::NAVBAR_HEIGHT_PX + 12.;
#[derive(Clone, Copy, PartialEq)]
enum Variant {
Progress,
Action,
Message,
}
impl Variant {
fn of(entry: &InboxEntry) -> Self {
if entry.is_loading && entry.progress.is_some() {
Self::Progress
} else if !entry.actions.is_empty() {
Self::Action
} else {
Self::Message
}
}
}
fn entry_icon(entry: &InboxEntry) -> (IconType, Color) {
if entry.is_loading && entry.progress.is_some() {
return (IconType::FolderDownload, colors::fg_primary());
}
match entry.level {
Level::Error => (IconType::AlertTriangle, colors::danger()),
Level::Info => (IconType::DownloadCloud02, colors::fg_primary()),
}
}
#[derive(PartialEq)]
pub struct Toasts;
impl Component for Toasts {
fn render(&self) -> impl IntoElement {
let snapshot = use_notifications_snapshot();
// While any toast is hovered every toast's timer pauses
let hovering = use_state(|| 0usize);
let entries: Vec<InboxEntry> = snapshot
.active_toast_entry_ids
.iter()
.filter_map(|id| snapshot.inbox.iter().find(|e| e.id == *id).cloned())
.collect();
if entries.is_empty() {
return rect().into_element();
}
let mut stack = rect()
.vertical()
.spacing(12.)
.position(Position::new_global().top(TOP_PX).right(24.))
.layer(Layer::OverlayLevel(3));
for entry in entries {
let id = entry.id;
stack = stack.child(
ToastCard {
entry,
hovering,
key: DiffKey::None,
}
.key(id),
);
}
stack.into_element()
}
}
#[derive(PartialEq)]
struct ToastCard {
entry: InboxEntry,
hovering: State<usize>,
key: DiffKey,
}
impl KeyExt for ToastCard {
fn write_key(&mut self) -> &mut DiffKey {
&mut self.key
}
}
impl Component for ToastCard {
fn render(&self) -> impl IntoElement {
let entry = self.entry.clone();
let id = entry.id;
let variant = Variant::of(&entry);
let dispatch = use_dispatch();
let slide = use_animation(|conf| {
conf.on_creation(OnCreation::Run);
AnimNum::new(TRAVEL, 0.)
.time(320)
.function(Function::Cubic)
.ease(Ease::Out)
});
let fade = use_animation(|conf| {
conf.on_creation(OnCreation::Run);
AnimNum::new(0., 1.).time(260).ease(Ease::Out)
});
let animations = use_animations_enabled();
let paused = *self.hovering.read() > 0;
let mut was_paused = use_state(|| false);
let mut from_pct = use_state(|| 100.);
let mut run_start = use_state(Instant::now);
let mut wall_pct = use_state(|| 100.);
if paused != *was_paused.peek() {
if paused {
let remaining = remaining_pct(*from_pct.peek(), *run_start.peek());
from_pct.set(remaining);
wall_pct.set(remaining);
} else {
run_start.set(Instant::now());
}
was_paused.set(paused);
}
let mut ticker = use_state(|| None::<OwnedTaskHandle>);
use_side_effect_with_deps(&(animations, paused), move |&(animations, paused)| {
if animations || paused {
ticker.set(None);
return;
}
let handle = spawn(async move {
let platform = Platform::get();
loop {
wall_pct.set(remaining_pct(*from_pct.peek(), *run_start.peek()));
platform.send(UserEvent::RequestRedraw);
tokio::time::sleep(TTL_STEP).await;
}
});
ticker.set(Some(handle.owned()));
});
let ttl_bar_anim =
use_animation_with_dependencies(&(paused, *from_pct.peek()), |conf, deps| {
let &(paused, from) = deps;
conf.on_creation(if paused {
OnCreation::Nothing
} else {
OnCreation::Run
});
conf.on_change(if paused {
OnChange::Reset
} else {
OnChange::Rerun
});
AnimNum::new(from, 0.).time(((TTL_MS * from / 100.) as u64).max(1))
});
let offset = slide.read().value();
let opacity = fade.read().value();
let ttl_pct = if animations {
ttl_bar_anim.read().value()
} else {
*wall_pct.read()
};
let mut hovering = self.hovering;
let hover_flag = use_hook(|| Rc::new(Cell::new(false)));
let (over_flag, out_flag, drop_flag) =
(hover_flag.clone(), hover_flag.clone(), hover_flag.clone());
let over_dispatch = dispatch.clone();
let out_dispatch = dispatch.clone();
let drop_dispatch = dispatch.clone();
use_drop(move || {
if drop_flag.replace(false) {
let count = hovering.peek().saturating_sub(1);
hovering.set(count);
if count == 0 {
drop_dispatch.resume_toasts();
}
}
});
let card = match variant {
Variant::Progress => progress_card(&entry),
Variant::Action => action_card(&entry, dispatch.clone(), id, ttl_pct),
Variant::Message => message_card(&entry, dispatch, id, ttl_pct),
};
rect()
.width(Size::px(TOAST_W))
.offset_x(offset)
.opacity(opacity)
// `enter`/`leave` are exclusive to the deepest node so the close button would
// read as leaving the toast
.on_pointer_over(move |_| {
if over_flag.replace(true) {
return;
}
let count = *hovering.peek() + 1;
hovering.set(count);
if count == 1 {
over_dispatch.pause_toasts();
}
})
.on_pointer_out(move |_| {
if !out_flag.replace(false) {
return;
}
let count = hovering.peek().saturating_sub(1);
hovering.set(count);
if count == 0 {
out_dispatch.resume_toasts();
}
})
.child(card)
}
}
fn remaining_pct(from_pct: f32, run_start: Instant) -> f32 {
let elapsed_pct = run_start.elapsed().as_secs_f32() * 1000. / TTL_MS * 100.;
(from_pct - elapsed_pct).clamp(0., 100.)
}
fn toast_shell(entry: &InboxEntry) -> Rect {
let border_color = match entry.level {
Level::Error => colors::danger(),
Level::Info => colors::component_border(),
};
rect()
.width(Size::px(TOAST_W))
.background(colors::component_bg())
.corner_radius(CornerRadius::new_all(12.))
.overflow(Overflow::Clip)
.border(
Border::new()
.fill(border_color)
.width(1.)
.alignment(BorderAlignment::Inner),
)
}
fn icon_badge(entry: &InboxEntry) -> impl IntoElement {
let (icon, color) = entry_icon(entry);
rect()
.width(Size::px(32.))
.height(Size::px(32.))
.corner_radius(CornerRadius::new_all(6.))
.center()
.child(Icon::new(entry.icon.unwrap_or(icon)).size(24.).color(color))
}
fn header(entry: &InboxEntry) -> impl IntoElement {
let title_color = match entry.level {
Level::Error => colors::danger(),
Level::Info => colors::fg_primary(),
};
let body_color = colors::fg_secondary();
rect()
.vertical()
.width(Size::flex(1.0))
.main_align(Alignment::Center)
.spacing(2.)
.child(
label()
.text(entry.title.clone())
.font_size(16.)
.font_weight(FontWeight::SEMI_BOLD)
.color(title_color),
)
.child(
label()
.text(entry.body.clone())
.font_size(12.)
.color(body_color),
)
}
fn ttl_bar(entry: &InboxEntry, pct: f32) -> impl IntoElement {
let color = match entry.level {
Level::Error => colors::danger(),
Level::Info => colors::brand(),
};
rect()
.width(Size::fill())
.height(Size::px(4.))
.background(color.with_a(60))
.child(
rect()
.width(Size::percent(pct.clamp(0.0, 100.0)))
.height(Size::px(4.))
.corner_radius(CornerRadius::new_all(60.))
.background(color),
)
}
fn close_button(dispatch: crate::Actions, id: u64) -> impl IntoElement {
rect()
.position(Position::new_absolute().top(8.).right(8.))
.child(
Button::new()
.ghost()
.size(ButtonSize::Icon)
.width(Size::px(28.))
.height(Size::px(28.))
.on_press(move |_| dispatch.dismiss_toast(id))
.child(
Icon::new(IconType::XClose)
.size(16.)
.color(colors::fg_secondary()),
),
)
}
fn message_card(
entry: &InboxEntry,
dispatch: crate::Actions,
id: u64,
ttl_pct: f32,
) -> Rect {
toast_shell(entry)
.child(
rect()
.horizontal()
.width(Size::fill())
.content(Content::Flex)
.padding(Gaps::new(15., 40., 17., 15.))
.spacing(16.)
.cross_align(Alignment::Center)
.child(icon_badge(entry))
.child(header(entry)),
)
.child(close_button(dispatch, id))
.child(ttl_bar(entry, ttl_pct))
}
fn action_card(entry: &InboxEntry, dispatch: crate::Actions, id: u64, ttl_pct: f32) -> Rect {
let action = entry.actions.first().cloned();
toast_shell(entry)
.child(
rect()
.vertical()
.width(Size::fill())
.padding(Gaps::new(15., 15., 12., 15.))
.spacing(10.)
.child(
rect()
.horizontal()
.width(Size::fill())
.content(Content::Flex)
.spacing(16.)
.cross_align(Alignment::Center)
.child(icon_badge(entry))
.child(header(entry)),
)
.child(divider())
.child(action_row(action, dispatch, id)),
)
.child(ttl_bar(entry, ttl_pct))
}
fn action_row(
action: Option<crate::notifications::NotificationAction>,
dispatch: crate::Actions,
id: u64,
) -> impl IntoElement {
let dismiss = dispatch.clone();
let review_label = action
.as_ref()
.map(|a| a.label.clone())
.unwrap_or_else(|| "Review".to_string());
let review_kind = action.map(|a| a.kind);
rect()
.horizontal()
.width(Size::fill())
.content(Content::Flex)
.cross_align(Alignment::Center)
.spacing(8.)
.child(
Button::new()
.small()
.secondary()
.width(Size::flex(1.0))
.on_press(move |_| dismiss.dismiss_toast(id))
.child(Icon::new(IconType::XClose).size(16.))
.text("Dismiss"),
)
.child(
Button::new()
.small()
.primary()
.width(Size::flex(1.0))
.on_press(move |_| match &review_kind {
Some(kind) => run_action(&dispatch, kind),
None => dispatch.mark_notification_read(id),
})
.child(Icon::new(IconType::Eye).size(16.))
.text(review_label),
)
}
fn run_action(dispatch: &crate::Actions, kind: &NotificationActionKind) {
match kind {
NotificationActionKind::OpenClusterUpdate(summaries) => {
dispatch.open_cluster_update(summaries.clone());
}
}
}
fn progress_card(entry: &InboxEntry) -> Rect {
let frac = entry
.progress
.map(|(cur, total)| cur as f32 / total.max(1) as f32)
.unwrap_or(0.0)
.clamp(0.0, 1.0);
toast_shell(entry).child(
rect()
.horizontal()
.width(Size::fill())
.content(Content::Flex)
.padding(Gaps::new(12., 14., 12., 14.))
.spacing(13.)
.cross_align(Alignment::Start)
.child(icon_badge(entry))
.child(
rect()
.vertical()
.width(Size::flex(1.0))
.spacing(2.)
.child(
label()
.text(entry.title.clone())
.font_size(14.)
.font_weight(FontWeight::SEMI_BOLD)
.color(colors::fg_primary()),
)
.child(
label()
.text(entry.body.clone())
.font_size(12.)
.color(colors::fg_secondary()),
)
.child(
progress_track(
frac * 100.0,
6.,
colors::brand(),
colors::brand().with_a(128),
)
.margin(Gaps::new(8., 0., 0., 0.)),
),
),
)
}