some tweaks

This commit is contained in:
LynithDev 2026-07-06 20:10:14 +02:00
parent 7416a3407f
commit 0af7c7455b
10 changed files with 245 additions and 47 deletions

View file

@ -250,6 +250,10 @@ impl CoreBridgeRuntime {
};
refresh_settings(snapshots)?;
bump_profiles_generation(snapshots);
let auto_update = LauncherState::get()?.settings.read().auto_update;
crate::updater::spawn_update_check(auto_update);
Ok(())
}
@ -640,6 +644,7 @@ fn sync_notifications(
let pending_view = pending.as_ref().map(|prompt| PendingPromptView {
title: prompt.title.clone(),
question: prompt.question.clone(),
kind: prompt.kind,
});
snapshots.notifications = engine.snapshot(inbox, center_open, pending_view);
}

View file

@ -26,6 +26,7 @@ mod tab_bar;
mod text_input;
mod toasts;
mod toggle;
mod update_prompt;
pub use active_cluster_panel::ActiveClusterPanel;
pub use avatar::Avatar;
@ -57,3 +58,4 @@ pub use tab_bar::{TabBar, TabItem};
pub use text_input::{TextInput, validate_number};
pub use toasts::Toasts;
pub use toggle::{toggle, toggle_controlled};
pub use update_prompt::UpdatePromptOverlay;

View file

@ -18,24 +18,27 @@ impl Component for Navbar {
rect()
.width(Size::fill())
.height(Size::px(theme::NAVBAR_HEIGHT_PX))
.horizontal()
.content(Content::Flex)
.cross_align(Alignment::Center)
.padding(Gaps::new_symmetric(0.0, 40.0))
.child(navbar_left())
.child(navbar_center())
.child(NavbarRight)
.position(Position::new_absolute().top(0.).left(0.))
.layer(Layer::RelativeOverlay(2))
// TODO: Window Drag?
// .child(
// rect()
// .window_drag()
// .width(Size::flex(1.))
// .height(Size::fill())
// .position(Position::new_absolute())
// .layer(Layer::RelativeOverlay(0))
// )
.child(
rect()
.width(Size::fill())
.height(Size::fill())
.horizontal()
.content(Content::Flex)
.cross_align(Alignment::Center)
.padding(Gaps::new_symmetric(0.0, 40.0))
.child(navbar_left())
.child(navbar_center())
.child(NavbarRight),
)
.child(
rect()
.window_drag()
.width(Size::window_percent(100.))
.height(Size::px(theme::NAVBAR_HEIGHT_PX))
.position(Position::new_absolute().top(0.).left(0.).right(0.))
)
}
}
@ -92,6 +95,8 @@ struct NavLink {
impl Component for NavLink {
fn render(&self) -> impl IntoElement {
let mut hovering = use_state(|| false);
let a11y_id = use_a11y();
let focused = use_focus(a11y_id);
let active = self.active;
let target = self.target.clone();
@ -101,7 +106,7 @@ impl Component for NavLink {
Cursor::set(CursorIcon::default());
});
let color = if active || *hovering.peek() {
let color = if active || *hovering.peek() || focused.peek().is_focused() {
theme::colors::fg_primary()
} else {
theme::colors::fg_secondary()
@ -109,7 +114,7 @@ impl Component for NavLink {
let underline_width = if active {
27.
} else if *hovering.peek() {
} else if *hovering.peek() || focused.peek().is_focused() {
18.
} else {
0.
@ -120,7 +125,13 @@ impl Component for NavLink {
.cross_align(Alignment::Center)
.spacing(2.)
.width(Size::px(nav_label.len() as f32 * 10. + 10.))
.on_press(move |e: Event<PressEventData>| {
// TODO: Hacky workaround for weird freya measurement bug, where if a rectangle has a transparent background (so in dec = 0), it will be measured weirdly.
// This basically fools freya into thinking it indeed has a box and properly handles pointer events
.background(Color::RED.with_a(0))
.a11y_id(a11y_id)
.a11y_focusable(true)
.a11y_role(AccessibilityRole::Button)
.on_all_press(move |e: Event<PressEventData>| {
e.prevent_default();
let _ = RouterContext::get().push(target.clone());
})

View file

@ -0,0 +1,102 @@
use freya::prelude::*;
use oneclient_core::notification::{PromptKind, UserChoice};
use crate::components::{Button, Icon, IconType, OverlayPopup};
use crate::hooks::{use_dispatch, use_notifications_snapshot};
use crate::theme::colors;
use crate::ui::border_all_color;
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
#[derive(PartialEq)]
pub struct UpdatePromptOverlay;
impl Component for UpdatePromptOverlay {
fn render(&self) -> impl IntoElement {
let snapshot = use_notifications_snapshot();
let dispatch = use_dispatch();
let Some(prompt) = snapshot.pending_prompt.clone() else {
return rect().into_element();
};
if prompt.kind != PromptKind::Update {
return rect().into_element();
}
let close = dispatch.clone();
let cancel = dispatch.clone();
let accept = dispatch.clone();
OverlayPopup::new()
.on_close(move |_| close.answer_prompt(UserChoice::Cancel))
.child(
rect()
.width(Size::window_percent(100.))
.height(Size::window_percent(100.))
.center()
.child(
rect()
.vertical()
.width(Size::px(440.))
.max_width(Size::window_percent(90.))
.spacing(14.)
.padding(Gaps::new_all(20.))
.corner_radius(CornerRadius::new_all(14.))
.background(CARD_BG)
.border(border_all_color(1., colors::component_border()))
.child(
rect()
.horizontal()
.cross_align(Alignment::Center)
.spacing(10.)
.child(
Icon::new(IconType::DownloadCloud02)
.size(20.)
.color(colors::brand()),
)
.child(
label()
.text(prompt.title.clone())
.font_size(16.)
.font_weight(FontWeight::SEMI_BOLD)
.color(colors::fg_primary()),
),
)
.child(
label()
.text(prompt.question.clone())
.font_size(12.)
.max_lines(4)
.width(Size::fill())
.color(colors::fg_secondary()),
)
.child(
rect()
.horizontal()
.width(Size::fill())
.main_align(Alignment::End)
.spacing(8.)
.child(
Button::new()
.secondary()
.on_press(move |_| {
cancel.answer_prompt(UserChoice::Cancel)
})
.text("Not now"),
)
.child(
Button::new()
.primary()
.on_press(move |_| {
accept.answer_prompt(UserChoice::Accept)
})
.child(Icon::new(IconType::DownloadCloud02).size(14.))
.text("Download"),
),
),
),
)
.into_element()
}
}

View file

@ -1,7 +1,7 @@
use freya::prelude::*;
use freya::router::*;
use crate::components::{NotificationCenter, StatusBar, Toasts};
use crate::components::{NotificationCenter, StatusBar, Toasts, UpdatePromptOverlay};
use crate::routes::Route;
use crate::theme;
use crate::theme::colors;
@ -27,6 +27,7 @@ impl Component for RootLayout {
)
.child(NotificationCenter)
.child(Toasts)
.child(UpdatePromptOverlay)
.child(StatusBar)
}
}

View file

@ -46,8 +46,6 @@ fn main() {
oneclient_core::logger::init()
}.expect("Failed to initialize logger");
oneclient_app::updater::spawn_update_check(settings.auto_update);
let (bridge, handle): (OneClientBridge, CoreBridgeHandle) = OneClientBridge::new();
handle.spawn_runtime();

View file

@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::time::{Duration, Instant};
use oneclient_core::notification::{
GroupedProgressEvent, Notification, NotificationLevel, UserChoice,
GroupedProgressEvent, Notification, NotificationLevel, PromptKind, UserChoice,
};
use tokio::sync::oneshot;
use uuid::Uuid;
@ -65,6 +65,7 @@ pub struct ActiveToast {
pub struct PendingPrompt {
pub title: String,
pub question: String,
pub kind: PromptKind,
pub reply_tx: Option<oneshot::Sender<UserChoice>>,
}
@ -72,6 +73,7 @@ pub struct PendingPrompt {
pub struct PendingPromptView {
pub title: String,
pub question: String,
pub kind: PromptKind,
}
#[derive(Clone, Debug)]
@ -187,7 +189,7 @@ impl NotificationState {
Notification::Prompt {
title,
question,
kind: _,
kind,
reply_tx,
} => {
let entry_id = self.push_inbox(
@ -202,9 +204,14 @@ impl NotificationState {
let pending_prompt = Some(PendingPrompt {
title: title.clone(),
question: question.clone(),
kind,
reply_tx: Some(reply_tx),
});
let pending_view = Some(PendingPromptView { title, question });
let pending_view = Some(PendingPromptView {
title,
question,
kind,
});
let timers = std::mem::take(&mut self.pending_timers);
return (
self.snapshot(inbox, false, pending_view),

View file

@ -1,24 +1,50 @@
//! In-app self-update via [`cargo-packager-updater`].
use std::cell::Cell;
use cargo_packager_updater::{Config, check_update};
use cargo_packager_updater::{Config, Update, check_update};
use oneclient_core::LauncherState;
use oneclient_core::notification::{NotificationService, UserChoice};
use uuid::Uuid;
pub const UPDATER_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFGODk3MkMyMjg0MjFDMDUKUldRRkhFSW93bktKSHpkWjNEMXNzaDVINVpCTU8xSnhuK2RnV0dTZ2FkcFJWbG1zUkhGYTNjaUkK";
pub const UPDATER_ENDPOINT: &str =
"https://github.com/Polyfrost/OneLauncher/releases/latest/download/latest.json";
const PROGRESS_STEP: u64 = 256 * 1024;
pub fn spawn_update_check(auto_install: bool) {
std::thread::Builder::new()
.name("oneclient-updater".into())
.spawn(move || {
if let Err(err) = run_check(auto_install) {
tracing::warn!("update check failed: {err}");
}
})
.ok();
tokio::spawn(async move {
if let Err(err) = run_check(auto_install).await {
tracing::warn!("update check failed: {err:#}");
}
});
}
fn run_check(auto_install: bool) -> anyhow::Result<()> {
async fn run_check(auto_install: bool) -> anyhow::Result<()> {
let notifier = LauncherState::get()?.services.notifier.clone();
// `check_update` performs a blocking HTTP request, so offload it to a thread pool.
let Some(update) = tokio::task::spawn_blocking(check_for_update).await?? else {
tracing::info!("no update available");
return Ok(());
};
tracing::info!("update available: {}", update.version);
if !auto_install {
match notifier.prompt_update(&update.version).await? {
UserChoice::Accept => {}
_ => {
tracing::info!("user declined update {}", update.version);
return Ok(());
}
}
}
download_and_install(update, notifier).await
}
fn check_for_update() -> anyhow::Result<Option<Update>> {
let current = env!("CARGO_PKG_VERSION").parse()?;
let config = Config {
endpoints: vec![UPDATER_ENDPOINT.parse()?],
@ -26,17 +52,53 @@ fn run_check(auto_install: bool) -> anyhow::Result<()> {
..Default::default()
};
match check_update(current, config)? {
Some(update) => {
tracing::info!("update available: {}", update.version);
if auto_install {
tracing::info!("downloading and installing update…");
update.download_and_install()?;
tracing::info!("update installed; restart to apply");
}
}
None => tracing::info!("no update available"),
}
Ok(check_update(current, config)?)
}
async fn download_and_install(
update: Update,
notifier: NotificationService,
) -> anyhow::Result<()> {
let progress_id = Uuid::new_v4();
let version = update.version.clone();
let label = format!("Downloading OneClient {version}");
notifier.send_progress(&progress_id, &label, 0, 0);
tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let downloaded = Cell::new(0u64);
let last_sent = Cell::new(0u64);
let bytes = update.download_extended(
|chunk, total| {
let now = downloaded.get() + chunk as u64;
downloaded.set(now);
let total = total.unwrap_or(0);
if now == chunk as u64
|| (total > 0 && now >= total)
|| now - last_sent.get() >= PROGRESS_STEP
{
last_sent.set(now);
notifier.send_progress(&progress_id, &label, now, total);
}
},
|| {},
)?;
let total = downloaded.get().max(1);
notifier.send_progress(&progress_id, &label, total, total);
update.install(bytes)?;
notifier.send_info(
"Update ready",
&format!("OneClient {version} installed. Restart to apply."),
);
Ok(())
})
.await??;
tracing::info!("update installed; restart to apply");
Ok(())
}

View file

@ -119,6 +119,7 @@ impl LaunchStage {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PromptKind {
JavaInstall { major: u32 },
Update,
}
#[derive(Debug)]

View file

@ -51,6 +51,15 @@ impl NotificationService {
.await
}
pub async fn prompt_update(&self, version: &str) -> Result<UserChoice, NotificationError> {
self.prompt(
"Update available",
&format!("OneClient {version} is ready to install. Download and install it now?"),
PromptKind::Update,
)
.await
}
pub async fn prompt(
&self,
title: &str,