From 1a36d9db075604fe8e48a07c0275444efd276d75 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 15:37:52 -0400 Subject: [PATCH] feat: startup update prompt modal and direct self-updating (#2566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- Cargo.lock | 225 +++++++++++++++++++++++++++--- Cargo.toml | 1 + apps/gridcraft/Cargo.toml | 1 + apps/gridcraft/src/main.rs | 9 ++ apps/gridcraft/src/native_menu.rs | 11 ++ crates/ui-egui/Cargo.toml | 1 + crates/ui-egui/src/control.rs | 1 + crates/ui-egui/src/lib.rs | 32 +++++ crates/ui-egui/src/ribbon.rs | 5 + crates/ui-egui/src/updates_ui.rs | 69 +++++++++ 10 files changed, 339 insertions(+), 16 deletions(-) create mode 100644 crates/ui-egui/src/updates_ui.rs diff --git a/Cargo.lock b/Cargo.lock index eaaa24559..c9ef5965c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -898,6 +898,31 @@ dependencies = [ "egui", ] +[[package]] +name = "craft-update" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=db19c373c8d59a545972ca340987aecfb3611a4b#db19c373c8d59a545972ca340987aecfb3611a4b" +dependencies = [ + "chrono", + "dunce", + "egui", + "flate2", + "getrandom 0.3.4", + "log", + "plist", + "semver", + "serde", + "serde_json", + "sha2 0.10.9", + "tar", + "tempfile", + "ureq", + "verifysign", + "winreg", + "winsafe", + "zip 9.1.0", +] + [[package]] name = "craft-winprint-gdi" version = "0.1.0" @@ -1016,6 +1041,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + [[package]] name = "derive_arbitrary" version = "1.5.0" @@ -1364,7 +1395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1838,6 +1869,7 @@ dependencies = [ "arboard", "chrono", "craft-print", + "craft-update", "eframe", "egui", "fmv-macos-events", @@ -1884,9 +1916,9 @@ dependencies = [ "gridcraft-engine", "gridcraft-mcp", "log", - "quick-xml", + "quick-xml 0.41.0", "serde_json", - "zip", + "zip 3.0.0", ] [[package]] @@ -1918,7 +1950,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.21", - "zip", + "zip 3.0.0", ] [[package]] @@ -1975,6 +2007,7 @@ name = "gridcraft-ui-egui" version = "0.12.1" dependencies = [ "craft-ui", + "craft-update", "egui", "egui_kittest", "epaint_default_fonts", @@ -2017,11 +2050,11 @@ dependencies = [ "gridcraft-core", "gridcraft-formula", "gridcraft-model", - "quick-xml", + "quick-xml 0.41.0", "sha1 0.11.0", "sha2 0.11.0", "thiserror 2.0.21", - "zip", + "zip 3.0.0", ] [[package]] @@ -2741,7 +2774,7 @@ dependencies = [ "objc2-foundation 0.3.2", "png", "thiserror 2.0.21", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2818,6 +2851,18 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -2833,6 +2878,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-traits" version = "0.2.19" @@ -3234,7 +3285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3407,6 +3458,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plist" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327a4cb2ad49dc48adfdc9da89e22c64d488b508c67a044d71ba729e4521a285" +dependencies = [ + "base64 0.23.1", + "indexmap", + "quick-xml 0.42.0", + "serde", + "time", +] + [[package]] name = "png" version = "0.18.1" @@ -3479,6 +3543,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + [[package]] name = "presser" version = "0.3.1" @@ -3554,6 +3624,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.12" @@ -3608,7 +3687,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3915,7 +3994,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3925,7 +4004,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -3972,7 +4053,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4075,6 +4156,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "send_wrapper" @@ -4535,7 +4620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4699,7 +4784,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4765,6 +4850,36 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.4" @@ -4994,6 +5109,12 @@ dependencies = [ "rustc-hash 2.1.3", ] +[[package]] +name = "typed-path" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e" + [[package]] name = "typenum" version = "1.20.1" @@ -5008,7 +5129,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5059,6 +5180,35 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde93ff05758a36a851a398a772b0b79a82fbe3c186a46f2696baaeaf84c966b" +dependencies = [ + "base64 0.23.1", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "ureq-proto", + "utf8-zero", +] + +[[package]] +name = "ureq-proto" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922" +dependencies = [ + "base64 0.23.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -5071,6 +5221,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5115,6 +5271,16 @@ dependencies = [ "vello_common", ] +[[package]] +name = "verifysign" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6acfcc42a8d44f340d001193d97ef56bf33210b51ad760ae8308c97d5241ca51" +dependencies = [ + "core-foundation 0.10.1", + "windows-sys 0.61.2", +] + [[package]] name = "version_check" version = "0.9.5" @@ -5330,7 +5496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.41.0", "quote", ] @@ -5570,7 +5736,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5939,6 +6105,16 @@ dependencies = [ "xml", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "winresource" version = "0.1.31" @@ -6334,6 +6510,23 @@ dependencies = [ "zopfli", ] +[[package]] +name = "zip" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886b00003a72c4ccee3f42cfc34908d33b2508a74d827dc2a4244706475c704" +dependencies = [ + "crc32fast", + "flate2", + "getrandom 0.4.3", + "indexmap", + "memchr", + "nix", + "typed-path", + "windows-sys 0.61.2", + "zopfli", +] + [[package]] name = "zlib-rs" version = "0.6.8" diff --git a/Cargo.toml b/Cargo.toml index 56d3e504b..6ba91fbf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ gridcraft-engine = { path = "crates/engine" } gridcraft-ui-egui = { path = "crates/ui-egui" } gridcraft-mcp = { path = "crates/mcp" } gridcraft-pdf = { path = "crates/pdf" } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "db19c373c8d59a545972ca340987aecfb3611a4b", features = ["egui"] } # foundation thiserror = "2" diff --git a/apps/gridcraft/Cargo.toml b/apps/gridcraft/Cargo.toml index 85481b731..cb4e1347d 100644 --- a/apps/gridcraft/Cargo.toml +++ b/apps/gridcraft/Cargo.toml @@ -26,6 +26,7 @@ winresource = "0.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] craft-print = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f" } +craft-update = { workspace = true } rfd = { workspace = true } arboard = { workspace = true } diff --git a/apps/gridcraft/src/main.rs b/apps/gridcraft/src/main.rs index 29dac2f89..55a34f49d 100644 --- a/apps/gridcraft/src/main.rs +++ b/apps/gridcraft/src/main.rs @@ -251,6 +251,14 @@ fn main() -> eframe::Result<()> { } })); let args: Vec = std::env::args().skip(1).collect(); + // The restart helper a self-update spawns: it finishes the swap and relaunches, then exits. + if let Some(done) = craft_update::self_update::finish(&craft_update::suite::GRIDCRAFT, &std::env::args_os().skip(1).collect::>()) { + if let Err(e) = done { + eprintln!("GridCraft update helper failed: {e}"); + std::process::exit(1); + } + return Ok(()); + } if args.iter().any(|a| a == "--version" || a == "-V") { println!("GridCraft {}", env!("CARGO_PKG_VERSION")); return Ok(()); @@ -299,6 +307,7 @@ fn main() -> eframe::Result<()> { } let mut app = SheetApp::new(session, services()); app.watch_desktop_theme(); + app.start_update_check(); app.after_engine(); // Show warnings from the sample. let control_port = control_port; load_prefs(&mut app); diff --git a/apps/gridcraft/src/native_menu.rs b/apps/gridcraft/src/native_menu.rs index 7dc549cb7..076070cd5 100644 --- a/apps/gridcraft/src/native_menu.rs +++ b/apps/gridcraft/src/native_menu.rs @@ -172,6 +172,7 @@ fn tree() -> Vec<(&'static str, Vec)> { ( "Help", vec![ + ("Check for Updates…", "app.checkForUpdates", None), ("Agent Control (MCP)…", "dialog:agents", None), ("Contributors…", "dialog:about:Contributors", None), ("GridCraft on getartcraft.com", "url:https://getartcraft.com/apps/gridcraft", None), @@ -337,6 +338,16 @@ mod tests { assert!(gridcraft_engine::find_command("file.exportPdf").is_some()); } + #[test] + fn help_menu_checks_for_updates_through_the_registered_command() { + let tree = tree(); + let (_, help) = tree.iter().find(|(title, _)| *title == "Help").expect("Help menu"); + let item = help.iter().find(|e| e.0 == "Check for Updates…").expect("Check for Updates… in the Help menu"); + assert_eq!(item.1, "app.checkForUpdates"); + let mut app = SheetApp::new(gridcraft_engine::Session::new(), Default::default()); + assert!(app.run(item.1, serde_json::json!({})).is_ok()); + } + #[test] fn theme_choices_use_the_shared_preference_command() { let mut app = SheetApp::new(gridcraft_engine::Session::new(), Default::default()); diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 3adb3d7c1..c223e258b 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -36,6 +36,7 @@ epaint_default_fonts = { workspace = true } gridcraft-xlsx = { workspace = true, features = ["test-support"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +craft-update = { workspace = true } # The desktop's preferred language for the first-run interface language. sys-locale = { workspace = true } diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 9d093e913..9b969d34d 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -595,6 +595,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) - let language_params = format!("{{language: {}}} (alias: code)", crate::i18n::Language::code_list("|")); for (id, label, params) in [ ("ui.ribbonTab", "Ribbon Tab", ""), + ("app.checkForUpdates", "Check for Updates…", ""), ("view.collapseRibbon", "Collapse Ribbon", ""), ("view.expandFormulaBar", "Expand Formula Bar", "{on?: bool} (toggles without it) → {on}"), ("view.darkMode", "Dark Mode", ""), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 712cce16a..6386530c2 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -35,6 +35,8 @@ pub mod selection_glide; pub mod tabs; pub mod text_box; pub mod theme; +#[cfg(not(target_arch = "wasm32"))] +mod updates_ui; pub mod widgets; use std::collections::HashMap; @@ -326,6 +328,9 @@ pub struct SheetApp { /// Every unsaved workbook has been answered for quitting (or `app.quit` asked): the next /// window close goes through. pub allow_close: bool, + /// The update check, the startup prompt and a running self-update (desktop only). + #[cfg(not(target_arch = "wasm32"))] + updates_rt: updates_ui::Runtime, } impl SheetApp { @@ -379,6 +384,8 @@ impl SheetApp { tab_run: None, started: now_ms(), allow_close: false, + #[cfg(not(target_arch = "wasm32"))] + updates_rt: Default::default(), } } @@ -666,6 +673,10 @@ impl SheetApp { /// UI-only commands (dialogs, view toggles that aren't saved in the file). fn run_ui_command(&mut self, id: &str, p: &Json) -> Option> { let r = match id { + "app.checkForUpdates" | "help.checkForUpdates" => { + self.check_for_updates(); + Ok(Json::Null) + } "ui.printSettings" => printing::settings(&mut self.printing, p, self.services.printers.as_ref()), "ui.printSubmit" => self.submit_print(p.clone()), "ui.ribbonTab" => { @@ -1234,6 +1245,8 @@ impl SheetApp { // frames still see worker completion. `ui` and the desktop app no longer poll it. self.poll_printing(ctx); self.guard_window_close(ctx); + #[cfg(not(target_arch = "wasm32"))] + updates_ui::tick(self, ctx); // Files read asynchronously (web file picker, dropped files). self.poll_async_opens(); self.autosave(ctx); @@ -1395,6 +1408,23 @@ impl SheetApp { } } + /// Look for a newer release once, in the background, as the desktop app starts (a no-op on the + /// web). A found release opens the update prompt; nothing is shown when GridCraft is current. + pub fn start_update_check(&mut self) { + #[cfg(not(target_arch = "wasm32"))] + updates_ui::start(self, false); + } + + /// Help ▸ Check for Updates…: look now, and say so when GridCraft is up to date or the check fails. + pub fn check_for_updates(&mut self) { + #[cfg(not(target_arch = "wasm32"))] + updates_ui::start(self, true); + #[cfg(target_arch = "wasm32")] + { + self.message = Some(("GridCraft".into(), "The web version is always the latest release.".into())); + } + } + /// Window title, next to the close/autosave state: `ViewportCommand::Title` is valid outside a /// pass and a control-channel rename while hidden updates it on return. fn update_title(&mut self, ctx: &egui::Context) { @@ -1473,6 +1503,8 @@ impl SheetApp { text_box::before_ui(self, &ctx); // The language the widgets translate with this frame (see `i18n::current`). i18n::set_current(&ctx, self.ui.language); + #[cfg(not(target_arch = "wasm32"))] + updates_ui::show(self, &ctx); let t = theme::Tokens::get(&ctx); // A host can turn the File tab off while Backstage is already open: close it. if !self.chrome.file_tab { diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 8ac5ae778..0305884ed 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -2614,6 +2614,11 @@ fn automate(app: &mut SheetApp, ui: &mut Ui) { if __k.clicked() { app.open_dialog("journal", json!({})); } + let __k = big_button(ui, Icon::Script, "Check for\nUpdates…", "Look for a newer GridCraft release", false); + control_keytip(app, ui, &__k, "checkForUpdates"); + if __k.clicked() { + app.run_or_alert("app.checkForUpdates", json!({})); + } let __k = big_button(ui, Icon::Script, "About\nGridCraft", "Version, contributors and the AI models that helped", false); control_keytip(app, ui, &__k, "about"); if __k.clicked() { diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs new file mode 100644 index 000000000..3de56ed87 --- /dev/null +++ b/crates/ui-egui/src/updates_ui.rs @@ -0,0 +1,69 @@ +//! 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 => {} + } +}