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

Ref: storytold/craft-libs#319

* build: pin craft-update to craft-libs main (b9ba47f) and refresh the lock

* refactor(updates): drive the flow with craft-update's Updater

Results are read independent of the prompt, a dropped worker is an error, failures return to the prompt, and no update starts over unsaved work. A manual check during the startup check is honored.
2026-10-25 14:48:33 -04:00

79 lines
2.8 KiB
Rust

//! One background check at start-up (and on demand from Help) for a newer release, and the modal
//! prompt that offers to update and restart. The flow itself is `craft_update::updater::Updater`.
use craft_update::updater::{Action, Status, Updater};
use crate::SlideApp;
const VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) struct Runtime {
updater: Updater,
/// The desktop host turns the check at start on; tests and the web build leave it off.
enabled: bool,
check_requested: bool,
}
impl Default for Runtime {
fn default() -> Self {
Self { updater: Updater::new(&craft_update::suite::DECKCRAFT, VERSION), enabled: false, check_requested: false }
}
}
pub(crate) fn enable(app: &mut SlideApp) {
app.updates_rt.enabled = true;
}
/// Help > Check for Updates…: runs a check and reports the outcome either way.
pub(crate) fn request_check(app: &mut SlideApp) {
if !matches!(app.updates_rt.updater.status(), Status::Checking) {
app.updates_rt.check_requested = true;
app.notify("Checking for updates…");
}
}
pub(crate) fn tick(app: &mut SlideApp, ctx: &egui::Context) {
let rt = &mut app.updates_rt;
if std::mem::take(&mut rt.check_requested) {
rt.updater.check_now(ctx);
} else if rt.enabled {
rt.updater.check_at_start(ctx);
}
rt.updater.tick(ctx);
let outcome = match rt.updater.status() {
Status::UpToDate => Some(format!("DeckCraft {VERSION} is up to date.")),
Status::Failed(e) => Some(format!("Could not check for updates: {e}")),
Status::Idle | Status::Checking => None,
};
if let Some(message) = outcome {
rt.updater.dismiss_status();
app.notify(message);
}
}
pub(crate) fn show(app: &mut SlideApp, 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) => ctx.open_url(egui::OpenUrl::new_tab(url)),
// The app's close path asks about unsaved work; the restart helper relaunches once the process exits.
Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close),
Action::None => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_offered_release_opens_the_prompt() {
let mut app = SlideApp::new(deckcraft_engine::Session::new(), crate::Services::default());
let ctx = egui::Context::default();
assert!(!app.updates_rt.updater.prompt_open());
app.updates_rt.updater.offer(craft_update::Release { tag: "v99.0.0".into(), ..Default::default() });
ctx.run_ui(egui::RawInput::default(), |ui| show(&mut app, ui.ctx())).textures_delta.clear();
assert!(app.updates_rt.updater.prompt_open());
}
}