Optionally download and install updates automatically (#94)

* Automatically download and install updates when available

* Fix clippy (unnecessary_map_or, collapsible_if) and rustfmt

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-11 02:16:49 -04:00
1 parent e8614afa3f
commit 4003ecd809
6 files changed
+108 -1

No files matched your search

+41 -1
View File
@@ -103,6 +103,9 @@ pub struct LauncherUi {
last_detect: i64,
/// Whether the window had focus last frame, to re-check when the user comes back to it.
was_focused: bool,
/// Versions of apps or the launcher already queued for auto-update during this session,
/// preventing repeated install loops if an update fails or is in progress.
pub(crate) auto_update_attempted: std::collections::BTreeSet<(String, String)>,
}
/// The language to draw in: the preference, or English where it needs a Chinese font the system
@@ -150,6 +153,7 @@ impl LauncherUi {
sent_options: None,
last_detect: 0,
was_focused: true,
auto_update_attempted: std::collections::BTreeSet::new(),
}
}
@@ -170,6 +174,7 @@ impl LauncherUi {
/// Looks for updates now (the tray's "Check for Updates").
pub fn check_now(&mut self) {
self.auto_update_attempted.clear();
self.backend.send(Command::CheckForUpdates { force: true });
self.backend.send(Command::Detect);
self.last_detect = self.backend.now();
@@ -231,6 +236,9 @@ impl LauncherUi {
}
if changed {
self.announce_updates(host);
if self.settings.auto_update {
self.trigger_auto_updates();
}
}
if self.restart_requested {
// The helper is waiting for this process to exit before it starts the new launcher.
@@ -318,8 +326,9 @@ impl LauncherUi {
true
}
Event::LauncherUpdate(r) => {
let fresh = self.launcher_update.as_ref().is_none_or(|cur| cur.version != r.version);
self.launcher_update = Some(r);
false
fresh
}
Event::CheckStarted => {
self.checking = true;
@@ -368,6 +377,37 @@ impl LauncherUi {
}
}
/// When auto-updates are enabled, automatically downloads and installs pending updates.
pub(crate) fn trigger_auto_updates(&mut self) {
if !self.caps.manage_apps || !self.settings.auto_update {
return;
}
let to_update: Vec<(String, String)> = self
.entries
.iter()
.filter(|e| e.status(self.platform, true) == AppStatus::UpdateAvailable && e.job.is_none() && !e.ready)
.filter_map(|e| {
let v = e.latest_label()?;
if self.auto_update_attempted.contains(&(e.app.id.to_string(), v.clone())) { None } else { Some((e.app.id.to_string(), v)) }
})
.collect();
for (id, version) in to_update {
self.auto_update_attempted.insert((id.clone(), version));
self.send(Command::Install { app: id });
}
if self.caps.self_update
&& let Some(r) = &self.launcher_update
&& self.launcher_job.is_none()
{
let id = artcraft_launcher_core::catalog::LAUNCHER_ID.to_string();
let v = r.version_label();
if !self.auto_update_attempted.contains(&(id.clone(), v.clone())) {
self.auto_update_attempted.insert((id, v));
self.send(Command::UpdateLauncher);
}
}
}
/// Draws the launcher.
pub fn ui(&mut self, ui: &mut egui::Ui, host: &mut dyn Host) {
let ctx = ui.ctx().clone();
+6
View File
@@ -181,6 +181,12 @@ impl LauncherUi {
});
});
self.settings.check_every = every;
if self.caps.manage_apps
&& ui.checkbox(&mut self.settings.auto_update, tl!("Automatically download and install updates")).changed()
&& self.settings.auto_update
{
self.trigger_auto_updates();
}
ui.checkbox(&mut self.settings.notify, tl!("Notify me when an installed app has an update"));
ui.checkbox(&mut self.settings.prereleases, tl!("Include pre-releases (betas and release candidates)"));
+1
View File
@@ -82,6 +82,7 @@
Close Закрыть
ArtCraft Launcher {version} ArtCraft Launcher {version}
Only when I ask Только по запросу
Automatically download and install updates Автоматически скачивать и устанавливать обновления
Notify me when an installed app has an update Уведомлять, когда у установленного приложения есть обновление
Include pre-releases (betas and release candidates) Включать предварительные релизы (бета и RC)
Installing on Linux Установка в Linux
Can't render this file because it contains an unexpected character in line 13 and column 44.
+1
View File
@@ -66,6 +66,7 @@
Close 关闭
ArtCraft Launcher {version} ArtCraft Launcher {version}
Only when I ask 仅手动检查
Automatically download and install updates 自动下载并安装更新
Notify me when an installed app has an update 已安装应用有更新时通知我
Include pre-releases (betas and release candidates) 包含预发布版本(测试版和候选版)
Installing on Linux Linux 安装设置
Can't render this file because it contains an unexpected character in line 4 and column 3.
+4
View File
@@ -97,6 +97,9 @@ pub struct Settings {
/// the desktop it was on.
pub show_on_current_desktop: bool,
pub check_every: CheckEvery,
/// Automatically download and install updates when found.
#[serde(default)]
pub auto_update: bool,
/// Show a system notification when an installed app has an update.
pub notify: bool,
pub prereleases: bool,
@@ -141,6 +144,7 @@ impl Default for Settings {
close_to_tray: true,
show_on_current_desktop: true,
check_every: CheckEvery::Hour,
auto_update: false,
notify: true,
prereleases: false,
preferred_kinds: Vec::new(),
+55
View File
@@ -727,6 +727,61 @@ fn automatic_checks_run_every_five_minutes() {
assert_eq!(CheckEvery::Never.seconds(), None);
}
#[test]
fn automatic_updates_queue_installs_for_outdated_apps_and_launcher() {
let mut ev = events();
ev.push(Event::LauncherUpdate(release("artcraft-launcher", "0.2.0", NOW)));
let sent = Rc::new(RefCell::new(Vec::new()));
let sent2 = sent.clone();
let mut backend = Some(Fake { events: ev, sent: sent2, manage: true, clock: Rc::new(Cell::new(NOW)) });
let mut h = Harness::builder().with_size(egui::vec2(1180.0, 900.0)).build_ui_state(
move |ui, state: &mut Option<LauncherUi>| {
if state.is_none()
&& let Some(b) = backend.take()
{
*state = Some(LauncherUi::new(ui.ctx(), Box::new(b), Settings { auto_update: true, notify: false, ..Settings::default() }));
}
if let Some(l) = state.as_mut() {
l.logic(&ui.ctx().clone(), &mut NoHost);
l.ui(ui, &mut NoHost);
}
},
None,
);
h.run_steps(4);
let commands = sent.borrow().clone();
assert!(commands.contains(&Command::Install { app: "photocraft".into() }), "outdated photocraft should be queued for install: {commands:?}");
assert!(commands.contains(&Command::Install { app: "effectcraft".into() }), "outdated effectcraft should be queued for install: {commands:?}");
assert!(commands.contains(&Command::UpdateLauncher), "launcher update should be queued: {commands:?}");
}
#[test]
fn automatic_updates_are_disabled_by_default() {
let mut ev = events();
ev.push(Event::LauncherUpdate(release("artcraft-launcher", "0.2.0", NOW)));
let sent = Rc::new(RefCell::new(Vec::new()));
let sent2 = sent.clone();
let mut backend = Some(Fake { events: ev, sent: sent2, manage: true, clock: Rc::new(Cell::new(NOW)) });
let mut h = Harness::builder().with_size(egui::vec2(1180.0, 900.0)).build_ui_state(
move |ui, state: &mut Option<LauncherUi>| {
if state.is_none()
&& let Some(b) = backend.take()
{
*state = Some(LauncherUi::new(ui.ctx(), Box::new(b), Settings { auto_update: false, notify: false, ..Settings::default() }));
}
if let Some(l) = state.as_mut() {
l.logic(&ui.ctx().clone(), &mut NoHost);
l.ui(ui, &mut NoHost);
}
},
None,
);
h.run_steps(4);
let commands = sent.borrow().clone();
assert!(!commands.contains(&Command::Install { app: "photocraft".into() }), "photocraft should not be auto-installed when auto_update is false");
assert!(!commands.contains(&Command::UpdateLauncher), "launcher should not be auto-updated when auto_update is false");
}
#[test]
fn the_footer_names_the_services_behind_what_is_on_screen() {
let footer = |extra: Vec<Event>| {