mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-10-14 14:26:48 -04:00
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:
6 files changed
+108
-1
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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)"));
|
||||
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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(),
|
||||
|
||||
@@ -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>| {
|
||||
|
||||
Reference in new issue
Block a user