Files
gridcraft/crates/ui-egui/src/updates_ui.rs
T
Dylan M. Taylor 1a36d9db07 feat: startup update prompt modal and direct self-updating (#2566)
* feat: startup update prompt modal and direct self-updating

The update flow (check at start or on request, the startup prompt, the self-update worker) is craft-update's Updater; the app only supplies the suite, its close path and its status message. An install holds back while the workbook is unsaved.

Ref: storytold/craft-libs#319

* build: refresh the lock

* build: pin craft-update to craft-libs main (db19c37)

* fix(updates): address review feedback

Wake the UI when a manual check is queued from a menu callback; the update prompt owns the keyboard while it is open (LightCraft); Download opens through the desktop URL hook (FilmCraft); the Help menu dispatches through menus::invoke (FigCraft); print jobs and comment drafts hold an install back (WordCraft); Check for Updates is a registered command (GridCraft); an install holds back while documents are unsaved.

* fix(updates): add Check for Updates… to the native macOS Help menu
2026-10-25 15:37:52 -04:00

70 lines
2.4 KiB
Rust

//! One background check at startup (and on Help ▸ Check for Updates…) and the modal prompt that
//! offers the new release: Update and Restart (direct self-update), Download or Later. The flow
//! itself is `craft_update::updater::Updater`.
use craft_update::suite::GRIDCRAFT;
use craft_update::updater::{Action, Status, Updater};
use crate::SheetApp;
const VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) struct Runtime {
updater: Updater,
/// The check at start was asked for; `tick` starts it with the context.
startup: bool,
/// Help ▸ Check for Updates… was chosen; the next `tick` starts it.
manual: bool,
}
impl Default for Runtime {
fn default() -> Self {
Self { updater: Updater::new(&GRIDCRAFT, VERSION), startup: false, manual: false }
}
}
/// Ask for a check; the next frame's `tick` starts it. A manual one reports every outcome.
pub(crate) fn start(app: &mut SheetApp, manual: bool) {
if manual {
app.updates_rt.manual = true;
} else {
app.updates_rt.startup = true;
}
}
pub(crate) fn tick(app: &mut SheetApp, ctx: &egui::Context) {
let rt = &mut app.updates_rt;
if std::mem::take(&mut rt.manual) {
rt.updater.check_now(ctx);
} else if rt.startup {
rt.updater.check_at_start(ctx);
}
rt.updater.tick(ctx);
let lang = app.ui.language;
let outcome = match rt.updater.status() {
Status::UpToDate => Some(lang.tr("GridCraft {version} is up to date.").replace("{version}", VERSION)),
Status::Failed(e) => Some(lang.tr("Could not check for updates: {error}").replace("{error}", e)),
Status::Idle | Status::Checking => None,
};
if let Some(message) = outcome {
rt.updater.dismiss_status();
app.message = Some(("GridCraft".into(), message));
}
}
pub(crate) fn show(app: &mut SheetApp, ctx: &egui::Context) {
// The restart helper is scheduled before the app closes, so no update starts over unsaved work.
let clean = !app.session.documents().iter().any(|d| d.is_dirty());
match app.updates_rt.updater.show_if(ctx, clean) {
Action::OpenUrl(url) => {
if let Some(open) = &app.services.open_url {
open(&url);
}
}
// The guarded close path asks about unsaved work; the restart helper relaunches once the
// process has exited.
Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close),
Action::None => {}
}
}