M14: Preferences can turn off the update check at start (#5737, #5460)

The desktop app asks for a newer release on its first frame, and nothing
could stop it, though the README says PdfCraft works offline. A preference,
on by default so nothing changes for anyone who leaves it, keeps the app off
the network until Help ▸ Check for updates is chosen. It has its own settings
key: check_updates_at_start was an opt-in, off unless chosen, so settings
written then would turn this off unasked.
This commit is contained in:
Hasan committed 2026-11-04 12:17:00 +05:00
1 parent 1f8cb45353
commit 36d24eae6e
10 files changed
+100 -4

No files matched your search

+2
View File
@@ -231,6 +231,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
- **2026-11-02 (M10, #5743):** macOS print jobs name their title and owner (`com.apple.print.JobInfo.PMJobName`, `PMJobOwner`), as the system print dialog does, because a Kyocera filter crashes on a job without them. Checked by a test and through `doc_print` against a stand-in `lp`; not yet on a physical printer.
- **2026-11-02 (M14, #5737, #5460):** Preferences can turn off the update check at start. It stays on by default, and Help ▸ Check for updates still asks.
- **2026-11-01 (M7, #5657):** Header/footer, watermark and background additions and replacements stage the complete call before committing. Synthetic later-page failures preserve exact incremental bytes, object count, dirty state and allocation cursor; success controls retain other marks and shared/inherited resources. Estimates unchanged.
- **2026-11-01 (M14, #5572):** Preferences ▸ Documents and view sets how many recent files to keep, from 0 to 50 with 12 by default. At 0 Home has no Recent section and File ▸ Open Recent is disabled. The list is cut when Preferences closes, so the number can pass through 0 on its way to another.
+1
View File
@@ -128,6 +128,7 @@ impl PdfCraftApp {
last_session: Default::default(),
quit_session: None,
reload_changed_files: true,
update_check_at_start: true,
show_secured_notice: true,
always_default_view: false,
#[cfg(not(target_arch = "wasm32"))]
+3
View File
@@ -47,6 +47,9 @@ impl PdfCraftApp {
("show_page_edge_shadows", _) => {
self.show_page_edge_shadows = value.parse::<bool>().map_err(|_| "show_page_edge_shadows must be true or false")?;
}
("update_check_at_start", _) => {
self.update_check_at_start = value.parse::<bool>().map_err(|_| "update_check_at_start must be true or false")?;
}
("scroll_speed", _) => {
self.scroll_speed = scroll_speed::ScrollSpeed::parse(value).ok_or("scroll_speed must be slow, normal, or fast")?;
}
+6
View File
@@ -147,6 +147,9 @@ impl PdfCraftApp {
// Set outside json! above, which is at serde_json's macro recursion limit.
v["hide_empty_highlight_hover"] = serde_json::Value::Bool(self.comment_prefs.hide_empty_highlight_hover);
v["show_page_edge_shadows"] = self.show_page_edge_shadows.into();
// Not `check_updates_at_start`: that key was an opt-in of its own, off unless chosen, and
// settings written then would turn this off unasked.
v["update_check_at_start"] = self.update_check_at_start.into();
v["hotbar"] = serde_json::to_value(&self.hotbar).unwrap_or_default();
v["comment_tool_pinned"] = serde_json::Value::Bool(self.comment_prefs.pinned);
v["show_secured_notice"] = serde_json::Value::Bool(self.show_secured_notice);
@@ -219,6 +222,9 @@ impl PdfCraftApp {
if let Some(on) = v["reload_changed_files"].as_bool() {
self.reload_changed_files = on;
}
if let Some(on) = v["update_check_at_start"].as_bool() {
self.update_check_at_start = on;
}
if let Some(on) = v["show_secured_notice"].as_bool() {
self.show_secured_notice = on;
}
@@ -659,6 +659,8 @@ fn card_general(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, ctx: &mut
ctx.checkbox(ui, &mut app.restore_last_view, tl!("Restore the last view (page, zoom and page display) when a document is opened again"), t);
#[cfg(not(target_arch = "wasm32"))]
ctx.checkbox(ui, &mut app.reload_changed_files, tl!("Reload documents when their files change on disk"), t);
#[cfg(not(target_arch = "wasm32"))]
ctx.checkbox(ui, &mut app.update_check_at_start, tl!("Check for updates when PdfCraft starts"), t);
ctx.checkbox(ui, &mut app.show_secured_notice, tl!("Show a notice when a document is secured"), t);
#[cfg(not(target_arch = "wasm32"))]
+3
View File
@@ -370,6 +370,9 @@ pub struct PdfCraftApp {
quit_session: Option<last_session::LastSession>,
/// Preferences: reload a document when its file changes on disk (#431).
pub reload_changed_files: bool,
/// Preferences: ask for a newer release when the desktop app starts (#5737). Off, the app
/// asks only from Help ▸ Check for updates.
pub update_check_at_start: bool,
/// Preferences: tell the user, above the pages, that a document is secured (its security
/// settings restrict changes). Off only hides that notice (#4035); kept in the settings.
pub show_secured_notice: bool,
+4 -2
View File
@@ -21,7 +21,8 @@ pub(crate) struct Updates {
updater: Updater,
/// The Updates dialog is showing.
pub(crate) open: bool,
/// Ask for a newer release at start; the desktop app turns it on, so tests never reach the network.
/// Ask for a newer release at start, unless Preferences says not to; the desktop app turns
/// it on, so tests never reach the network.
#[cfg(not(target_arch = "wasm32"))]
startup_check: bool,
#[cfg(not(target_arch = "wasm32"))]
@@ -73,10 +74,11 @@ impl PdfCraftApp {
#[cfg(not(target_arch = "wasm32"))]
{
let Some(ctx) = self.ctx.clone() else { return };
let at_start = self.update_check_at_start;
let updates = &mut self.updates;
if std::mem::take(&mut updates.check_requested) {
updates.updater.check_now(&ctx);
} else if updates.startup_check {
} else if updates.startup_check && at_start {
updates.updater.check_at_start(&ctx);
}
updates.updater.tick(&ctx);
+76
View File
@@ -100,6 +100,82 @@ fn nothing_is_asked_until_the_user_checks() {
h.get_by_label_contains("PdfCraft 99.0.0 is available");
}
/// The desktop app's start, with `settings` restored, and the count of what the source was asked.
fn started_with(settings: &'static str) -> (Harness<'static, PdfCraftApp>, Arc<std::sync::atomic::AtomicUsize>) {
let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let counted = calls.clone();
let h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.set_startup_update_check(true);
app.restore(settings);
let counted = counted.clone();
app.set_update_source(Arc::new(move || {
counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(Some(release("v99.0.0")))
}));
app
});
(h, calls)
}
/// Run frames for `time`, or until the source has been asked. The check at start shows nothing
/// while it runs, so `settle` can't wait for it.
fn run_until_asked(h: &mut Harness<'static, PdfCraftApp>, calls: &std::sync::atomic::AtomicUsize, time: std::time::Duration) {
let deadline = std::time::Instant::now() + time;
while calls.load(std::sync::atomic::Ordering::SeqCst) == 0 && std::time::Instant::now() < deadline {
h.run_steps(2);
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
#[test]
fn the_preference_keeps_the_check_at_start_from_reaching_the_network() {
use std::sync::atomic::Ordering::SeqCst;
assert!(PdfCraftApp::new().update_check_at_start, "on by default");
// The same start with the preference left on does ask, so a count of none below means
// the preference held the check back and not that it hadn't run yet.
let (mut h, calls) = started_with("{}");
run_until_asked(&mut h, &calls, std::time::Duration::from_secs(30));
assert_eq!(calls.load(SeqCst), 1, "on, the app asks at start");
let (mut h, calls) = started_with(r#"{"update_check_at_start": false}"#);
run_until_asked(&mut h, &calls, std::time::Duration::from_millis(500));
assert_eq!(calls.load(SeqCst), 0, "nothing is asked at start");
let mut restarted = PdfCraftApp::new();
restarted.restore(&h.state().persist());
assert!(!restarted.update_check_at_start, "the choice is kept");
// Help ▸ Check for updates still asks.
h.state_mut().execute("help.check_updates");
settle(&mut h);
assert_eq!(calls.load(SeqCst), 1);
h.get_by_label_contains("PdfCraft 99.0.0 is available");
}
#[test]
fn preferences_turn_the_check_at_start_off() {
let (mut h, _calls) = started_with(r#"{"update_check_at_start": true}"#);
h.state_mut().execute("app.preferences");
h.run_steps(3);
h.get_by_label("Check for updates when PdfCraft starts").click();
h.run_steps(3);
assert!(!h.state().update_check_at_start);
// The control channel's option, as an agent sets it.
h.state_mut().set_option("update_check_at_start", "true").unwrap();
assert!(h.state().update_check_at_start);
assert!(h.state_mut().set_option("update_check_at_start", "maybe").is_err());
}
#[test]
fn settings_from_the_old_opt_in_leave_the_check_at_start_on() {
let mut app = PdfCraftApp::new();
app.restore(r#"{"check_updates_at_start": false}"#);
assert!(app.update_check_at_start, "a key of the removed option decides nothing");
}
#[test]
fn the_check_at_start_offers_the_prompt_and_later_dismisses_it() {
let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(|_cc| {
+1
View File
@@ -307,6 +307,7 @@ Estimates unchanged.
| 2026-11-03 | patch | M5 #5850 review: indirect typewriter intent and composer edits preserve outer whitespace; movement regression commits by outside click; estimates unchanged. |
| 2026-11-03 | patch | M5 #5850: Enter adds Fill & Sign line breaks; outside clicks commit, Escape cancels, and saved multiline appearances retain their text and height; estimates unchanged. |
| 2026-11-02 | patch | M10 #5743: macOS print jobs carry the job name and owner some vendors' filters need; not yet checked on a physical printer |
| 2026-11-02 | patch | M14 #5737, #5460: Preferences turns off the update check at start, on by default |
| 2026-11-01 | patch | M7 #5657: header/footer, watermark and background operations commit all selected pages together; later-page errors preserve marks, content, shared/inherited resources and pending edits; estimates unchanged. |
| 2026-10-31 | patch | #1577: reconcile optional page shadows with current viewer/organizer and settings modules; CI validation pending. |
| 2026-10-31 | patch | M3 #1354: reconcile the launch override with split app/file modules and the structured open result; preserve headless help/version exits. CI validation pending. |
+2 -2
View File
@@ -1778,8 +1778,8 @@ milestone = "M3"
status = "partial"
commands = ["help.check_updates"]
tools = []
evidence = ["crates/ui-egui/tests/it/updates.rs::a_newer_release_is_offered_for_download", "crates/ui-egui/tests/it/updates.rs::an_up_to_date_or_failed_check_says_so", "crates/ui-egui/tests/it/updates.rs::the_check_at_start_offers_the_prompt_and_later_dismisses_it", "crates/ui-egui/tests/it/updates.rs::nothing_is_asked_until_the_user_checks"]
notes = "asks GitHub Releases through craft-update at start (desktop app only) and on request, offers the release page, and updates and restarts in place where the install type allows it; an install waits while documents are unsaved"
evidence = ["crates/ui-egui/tests/it/updates.rs::a_newer_release_is_offered_for_download", "crates/ui-egui/tests/it/updates.rs::an_up_to_date_or_failed_check_says_so", "crates/ui-egui/tests/it/updates.rs::the_check_at_start_offers_the_prompt_and_later_dismisses_it", "crates/ui-egui/tests/it/updates.rs::nothing_is_asked_until_the_user_checks", "crates/ui-egui/tests/it/updates.rs::the_preference_keeps_the_check_at_start_from_reaching_the_network", "crates/ui-egui/tests/it/updates.rs::settings_from_the_old_opt_in_leave_the_check_at_start_on"]
notes = "asks GitHub Releases through craft-update at start (desktop app only) and on request, offers the release page, and updates and restarts in place where the install type allows it; an install waits while documents are unsaved; Preferences ▸ Check for updates when PdfCraft starts (update_check_at_start, on by default) turns the check at start off, leaving only the request from the Help menu"
[[feature]]
id = "view.single-key-accelerators"