From de9b9e82bc277976aaeb7cd522aecf1f7c363a2d Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 5 Oct 2026 05:29:18 -0400 Subject: [PATCH] Updates: check only when asked (no check at start) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By the owner's decision there is no update check at start and no in-place install for the foreseeable future. The startup option, its setting and the first-frame trigger are gone; Help ▸ Check for updates is the only request. Settings written by a build that had the option are ignored. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/commands.rs | 2 +- crates/ui-egui/src/lib.rs | 7 ----- crates/ui-egui/src/updates.rs | 50 ++++++++------------------------- crates/ui-egui/tests/updates.rs | 48 +++++++++++++++---------------- parity/acrobat-features.toml | 6 ++-- 5 files changed, 38 insertions(+), 75 deletions(-) diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 724a51a7..519cdbb3 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -460,7 +460,7 @@ impl PrintCraftApp { "page.split" => self.dialog = Some(Dialog::Split), "help.shortcuts" => self.dialog = Some(Dialog::Shortcuts), "help.about" => self.dialog = Some(Dialog::About), - "help.check_updates" => self.check_for_updates(true), + "help.check_updates" => self.check_for_updates(), _ => return false, } true diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index f5164ce8..f404f211 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -283,8 +283,6 @@ pub struct PrintCraftApp { pub dialog: Option, /// How to ask for the latest release (the desktop app sets it; see `updates`). pub update_source: Option, - /// Check for updates when PrintCraft starts (opt-in; persisted). - pub check_updates_at_start: bool, pub(crate) updates: updates::Updates, pub palette_open: bool, pub palette_query: String, @@ -456,7 +454,6 @@ impl PrintCraftApp { follow_system_theme: false, dialog: None, update_source: None, - check_updates_at_start: false, updates: updates::Updates::default(), palette_open: false, palette_query: String::new(), @@ -833,7 +830,6 @@ impl PrintCraftApp { "custom_stamps": stamps_ui::encode(&self.custom_stamps), "javascript": self.session.javascript(), "actions": actions_ui::encode(&self.custom_actions), - "check_updates_at_start": self.check_updates_at_start, }) .to_string() } @@ -850,9 +846,6 @@ impl PrintCraftApp { if let Ok(t) = serde_json::from_value::(v["theme"].clone()) { self.theme = t; } - if let Some(b) = v["check_updates_at_start"].as_bool() { - self.check_updates_at_start = b; - } if let Ok(s) = serde_json::from_value::>>(v["signature"].clone()) && s.iter().all(|st| st.iter().all(|p| p.iter().all(|x| x.is_finite()))) { diff --git a/crates/ui-egui/src/updates.rs b/crates/ui-egui/src/updates.rs index e24bbb9d..e19a2487 100644 --- a/crates/ui-egui/src/updates.rs +++ b/crates/ui-egui/src/updates.rs @@ -3,8 +3,7 @@ //! The desktop app supplies how to ask ([`PrintCraftApp::update_source`]), so this crate has no //! network code; without a source (the web build, tests) the command opens the releases page. //! PrintCraft never downloads or installs anything itself: the user downloads the new version. -//! A check when PrintCraft starts is opt-in (it contacts GitHub without being asked) and stays -//! silent unless a newer version exists. +//! It asks only when the user does: there is no check at start (the owner's decision). use std::sync::Arc; @@ -52,10 +51,7 @@ pub(crate) enum Check { #[default] Idle, #[cfg(not(target_arch = "wasm32"))] - Running { - rx: std::sync::mpsc::Receiver>, - manual: bool, - }, + Running(std::sync::mpsc::Receiver>), Done(Result), } @@ -64,24 +60,19 @@ pub(crate) struct Updates { pub(crate) check: Check, /// The Updates dialog is showing. pub(crate) open: bool, - /// The check at start has been considered (first frame, once the context is known). - started: bool, } impl PrintCraftApp { - /// Check for a newer release. `manual` (Help ▸ Check for updates) shows the outcome, whatever - /// it is; otherwise (at start) the dialog only appears when a newer version exists. - pub fn check_for_updates(&mut self, manual: bool) { + /// Help ▸ Check for updates: ask for the latest release and show the outcome. + pub fn check_for_updates(&mut self) { let Some(source) = self.update_source.clone() else { - if manual { - self.open_url(RELEASES_PAGE); - } + self.open_url(RELEASES_PAGE); return; }; #[cfg(not(target_arch = "wasm32"))] { - if matches!(self.updates.check, Check::Running { .. }) { - self.updates.open |= manual; + self.updates.open = true; + if matches!(self.updates.check, Check::Running(_)) { return; } let (tx, rx) = std::sync::mpsc::channel(); @@ -93,36 +84,24 @@ impl PrintCraftApp { ctx.request_repaint(); } }); - self.updates.check = Check::Running { rx, manual }; - self.updates.open = manual; + self.updates.check = Check::Running(rx); } #[cfg(target_arch = "wasm32")] { let _ = source; - if manual { - self.open_url(RELEASES_PAGE); - } + self.open_url(RELEASES_PAGE); } } - /// Start the opt-in check at start (first frame), and pick up a finished check (each frame). + /// Pick up a finished check (each frame). pub(crate) fn poll_updates(&mut self) { - if !self.updates.started { - self.updates.started = true; - if self.check_updates_at_start { - self.check_for_updates(false); - } - } #[cfg(not(target_arch = "wasm32"))] - if let Check::Running { rx, manual } = &self.updates.check { - let manual = *manual; + if let Check::Running(rx) = &self.updates.check { let result = match rx.try_recv() { Ok(r) => r, Err(std::sync::mpsc::TryRecvError::Empty) => return, Err(std::sync::mpsc::TryRecvError::Disconnected) => Err("the update check stopped unexpectedly".into()), }; - let newer = result.as_ref().is_ok_and(|r| is_newer(&r.version, env!("CARGO_PKG_VERSION"))); - self.updates.open |= manual || newer; self.updates.check = Check::Done(result); } } @@ -149,7 +128,7 @@ pub(crate) fn dialog(app: &mut PrintCraftApp, ctx: &egui::Context) { ui.label("No check has run yet."); } #[cfg(not(target_arch = "wasm32"))] - Check::Running { .. } => { + Check::Running(_) => { ui.horizontal(|ui| { ui.spinner(); ui.label("Checking for a newer version…"); @@ -172,11 +151,6 @@ pub(crate) fn dialog(app: &mut PrintCraftApp, ctx: &egui::Context) { } } ui.add_space(10.0); - ui.scope(|ui| { - // The light theme's field colour is the dialog's own white: outline the box. - ui.visuals_mut().widgets.inactive.bg_stroke = egui::Stroke::new(1.0, t.text_muted); - ui.checkbox(&mut app.check_updates_at_start, "Check for updates when PrintCraft starts"); - }); ui.label( egui::RichText::new("Asks GitHub for the latest release. Nothing is downloaded or installed automatically.").color(t.text_muted).small(), ); diff --git a/crates/ui-egui/tests/updates.rs b/crates/ui-egui/tests/updates.rs index fb6d79a8..816fb4d4 100644 --- a/crates/ui-egui/tests/updates.rs +++ b/crates/ui-egui/tests/updates.rs @@ -12,11 +12,10 @@ fn source(answer: Result<&str, &str>) -> UpdateSource { Arc::new(move || answer.clone().map(|v| Release { url: format!("https://github.com/storytold/printcraft/releases/tag/{v}"), version: v })) } -fn harness(answer: Result<&str, &str>, at_start: bool) -> Harness<'static, PrintCraftApp> { +fn harness(answer: Result<&str, &str>) -> Harness<'static, PrintCraftApp> { Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |_cc| { let mut app = PrintCraftApp::new(); app.update_source = Some(source(answer)); - app.check_updates_at_start = at_start; app }) } @@ -48,7 +47,7 @@ fn versions_compare_by_number() { #[test] fn a_newer_release_is_offered_for_download() { - let mut h = harness(Ok("v99.0.0"), false); + let mut h = harness(Ok("v99.0.0")); h.state_mut().execute("help.check_updates"); settle(&mut h); h.get_by_label_contains("PrintCraft 99.0.0 is available"); @@ -60,42 +59,39 @@ fn a_newer_release_is_offered_for_download() { #[test] fn an_up_to_date_or_failed_check_says_so() { - let mut h = harness(Ok(concat!("v", env!("CARGO_PKG_VERSION"))), false); + let mut h = harness(Ok(concat!("v", env!("CARGO_PKG_VERSION")))); h.state_mut().execute("help.check_updates"); settle(&mut h); h.get_by_label_contains("is up to date"); assert!(h.query_by_label("Download").is_none()); - let mut h = harness(Err("couldn't reach GitHub"), false); + let mut h = harness(Err("couldn't reach GitHub")); h.state_mut().execute("help.check_updates"); settle(&mut h); h.get_by_label_contains("Couldn't check for updates: couldn't reach GitHub"); } #[test] -fn the_check_at_start_is_opt_in_and_quiet_unless_there_is_news() { - // Off (the default): nothing is asked. - let mut h = harness(Ok("v99.0.0"), false); +fn nothing_is_asked_until_the_user_checks() { + // No check at start (the owner's decision): the source is only called from the command. + let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let counted = calls.clone(); + let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |_cc| { + let mut app = PrintCraftApp::new(); + // Settings from a build that had the startup option are ignored. + app.restore(r#"{"check_updates_at_start": true}"#); + let counted = counted.clone(); + app.update_source = Some(Arc::new(move || { + counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Ok(Release { version: "v99.0.0".into(), url: "https://github.com/storytold/printcraft/releases/tag/v99.0.0".into() }) + })); + app + }); settle(&mut h); + assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 0); assert!(h.query_by_label_contains("is available").is_none()); - // On, with a newer release: the dialog appears by itself. - let mut h = harness(Ok("v99.0.0"), true); + h.state_mut().execute("help.check_updates"); settle(&mut h); + assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1); h.get_by_label_contains("PrintCraft 99.0.0 is available"); - // On, up to date or failing: silent. - for answer in [Ok(concat!("v", env!("CARGO_PKG_VERSION"))), Err("offline")] { - let mut h = harness(answer, true); - settle(&mut h); - for message in ["is up to date", "Couldn't check for updates", "is available"] { - assert!(h.query_by_label_contains(message).is_none(), "{answer:?}: {message}"); - } - } - // The preference is remembered. - let mut app = PrintCraftApp::new(); - app.check_updates_at_start = true; - let mut restored = PrintCraftApp::new(); - restored.restore(&app.persist()); - assert!(restored.check_updates_at_start); - restored.restore("{}"); - assert!(restored.check_updates_at_start, "missing keys change nothing"); } diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 8880ca92..5f38026d 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1687,15 +1687,15 @@ notes = "" [[feature]] id = "view.check-for-updates" -title = "Help ▸ Check for updates (and an optional check at start)" +title = "Help ▸ Check for updates" area = "B" tier = "P2" milestone = "M3" status = "partial" commands = ["help.check_updates"] tools = [] -evidence = ["crates/ui-egui/tests/updates.rs::a_newer_release_is_offered_for_download", "crates/ui-egui/tests/updates.rs::the_check_at_start_is_opt_in_and_quiet_unless_there_is_news", "apps/printcraft/src/updates.rs::answers_are_read_and_only_our_release_pages_are_offered"] -notes = "asks GitHub Releases and offers the release page; the check at start is opt-in. Missing: installing the update in place (needs signed release artifacts)" +evidence = ["crates/ui-egui/tests/updates.rs::a_newer_release_is_offered_for_download", "crates/ui-egui/tests/updates.rs::nothing_is_asked_until_the_user_checks", "apps/printcraft/src/updates.rs::answers_are_read_and_only_our_release_pages_are_offered"] +notes = "asks GitHub Releases on request and offers the release page. By the owner's decision there is no check at start and no in-place install (Acrobat has both)" [[feature]] id = "view.single-key-accelerators"