Updates: check only when asked (no check at start)

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 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-05 05:29:18 -04:00
parent 2ea5846a16
commit de9b9e82bc
5 changed files with 38 additions and 75 deletions

View File

@ -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

View File

@ -283,8 +283,6 @@ pub struct PrintCraftApp {
pub dialog: Option<Dialog>,
/// How to ask for the latest release (the desktop app sets it; see `updates`).
pub update_source: Option<updates::UpdateSource>,
/// 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::<ThemeKind>(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::<Vec<Vec<[f32; 2]>>>(v["signature"].clone())
&& s.iter().all(|st| st.iter().all(|p| p.iter().all(|x| x.is_finite())))
{

View File

@ -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<Result<Release, String>>,
manual: bool,
},
Running(std::sync::mpsc::Receiver<Result<Release, String>>),
Done(Result<Release, String>),
}
@ -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(),
);

View File

@ -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");
}

View File

@ -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"