some tweaks
This commit is contained in:
parent
7416a3407f
commit
0af7c7455b
10 changed files with 245 additions and 47 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
})
|
||||
|
|
|
|||
102
packages/oneclient_app/src/components/update_prompt.rs
Normal file
102
packages/oneclient_app/src/components/update_prompt.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ impl LaunchStage {
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PromptKind {
|
||||
JavaInstall { major: u32 },
|
||||
Update,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue