feat(updater): implement cross-platform in-app automatic software update
This commit is contained in:
parent
b2d18297da
commit
91f603918d
2 changed files with 444 additions and 85 deletions
|
|
@ -2625,10 +2625,34 @@ impl ScytheOverlayApp {
|
||||||
.strong(),
|
.strong(),
|
||||||
);
|
);
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
if squared_button_sized(ui, "VIEW RELEASE / DOWNLOAD", true, accent, Some(egui::vec2(180.0, 26.0))) {
|
if squared_button_sized(ui, "UPDATE NOW", true, accent, Some(egui::vec2(110.0, 26.0))) {
|
||||||
|
crate::updater::spawn_auto_update(info.clone(), self.update_status.clone());
|
||||||
|
}
|
||||||
|
ui.add_space(6.0);
|
||||||
|
if squared_button_sized(ui, "Release Notes", false, accent, Some(egui::vec2(110.0, 26.0))) {
|
||||||
crate::updater::open_browser_url(&info.html_url);
|
crate::updater::open_browser_url(&info.html_url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
crate::updater::UpdateStatus::Updating { version, progress } => {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(format!("Updating to v{}: {}", version, progress))
|
||||||
|
.size(11.5)
|
||||||
|
.color(Color32::from_rgb(96, 165, 250))
|
||||||
|
.strong(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
crate::updater::UpdateStatus::Updated { version, message: _ } => {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(format!("Updated to v{}! Restart required.", version))
|
||||||
|
.size(11.5)
|
||||||
|
.color(Color32::from_rgb(34, 197, 94))
|
||||||
|
.strong(),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
if squared_button_sized(ui, "Restart Scythe", true, accent, Some(egui::vec2(120.0, 26.0))) {
|
||||||
|
crate::updater::restart_application();
|
||||||
|
}
|
||||||
|
}
|
||||||
crate::updater::UpdateStatus::Failed(err) => {
|
crate::updater::UpdateStatus::Failed(err) => {
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new(format!("Offline or error: {}", err))
|
egui::RichText::new(format!("Offline or error: {}", err))
|
||||||
|
|
@ -3026,25 +3050,58 @@ impl ScytheOverlayApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_update_popup(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
fn render_update_popup(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
let (has_update, rel_info) = {
|
let cur = self.update_status.lock().ok().map(|g| g.clone()).unwrap_or_default();
|
||||||
let cur = self.update_status.lock().ok().map(|g| g.clone()).unwrap_or_default();
|
let (should_render, title, version_badge, body_lines, show_do_not_ask) = match &cur {
|
||||||
match cur {
|
crate::updater::UpdateStatus::Available(info) => {
|
||||||
crate::updater::UpdateStatus::Available(info) => (true, Some(info)),
|
if self.update_dismissed || self.config.do_not_ask_updates {
|
||||||
_ => (false, None),
|
(false, "", "", vec![], false)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
true,
|
||||||
|
"Update Available",
|
||||||
|
info.version.as_str(),
|
||||||
|
vec![
|
||||||
|
format!("A new version of Scythe is available (v{}). Installed: v{}.", info.version, crate::updater::CURRENT_VERSION),
|
||||||
|
"Would you like to download and install the update locally?".to_string(),
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
crate::updater::UpdateStatus::Updating { version, progress } => {
|
||||||
|
(
|
||||||
|
true,
|
||||||
|
"Updating Scythe...",
|
||||||
|
version.as_str(),
|
||||||
|
vec![
|
||||||
|
format!("Downloading and applying update v{}...", version),
|
||||||
|
progress.clone(),
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
crate::updater::UpdateStatus::Updated { version, message } => {
|
||||||
|
(
|
||||||
|
true,
|
||||||
|
"Update Complete!",
|
||||||
|
version.as_str(),
|
||||||
|
vec![
|
||||||
|
format!("Scythe has been updated to v{}.", version),
|
||||||
|
message.clone(),
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => (false, "", "", vec![], false),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !has_update || self.update_dismissed || self.config.do_not_ask_updates {
|
if !should_render {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let info = match rel_info {
|
|
||||||
Some(i) => i,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let screen_rect = ctx.screen_rect();
|
let screen_rect = ctx.screen_rect();
|
||||||
let modal_w = 460.0_f32;
|
let modal_w = 480.0_f32;
|
||||||
let modal_h = 210.0_f32;
|
let modal_h = 220.0_f32;
|
||||||
let modal_x = ((screen_rect.width() - modal_w) * 0.5).floor();
|
let modal_x = ((screen_rect.width() - modal_w) * 0.5).floor();
|
||||||
let modal_y = ((screen_rect.height() - modal_h) * 0.40).floor();
|
let modal_y = ((screen_rect.height() - modal_h) * 0.40).floor();
|
||||||
let modal_rect = egui::Rect::from_min_size(egui::pos2(modal_x, modal_y), Vec2::new(modal_w, modal_h));
|
let modal_rect = egui::Rect::from_min_size(egui::pos2(modal_x, modal_y), Vec2::new(modal_w, modal_h));
|
||||||
|
|
@ -3074,92 +3131,114 @@ impl ScytheOverlayApp {
|
||||||
let inner_rect = modal_rect.shrink(18.0);
|
let inner_rect = modal_rect.shrink(18.0);
|
||||||
let mut child_ui = ui.new_child(egui::UiBuilder::new().max_rect(inner_rect));
|
let mut child_ui = ui.new_child(egui::UiBuilder::new().max_rect(inner_rect));
|
||||||
|
|
||||||
// Header: Scythe Icon + "Update Available" + version badge
|
// Header: Scythe Icon + Title + version badge
|
||||||
child_ui.horizontal(|ui| {
|
child_ui.horizontal(|ui| {
|
||||||
let icon_center = egui::pos2(ui.cursor().min.x + 14.0, ui.cursor().min.y + 12.0);
|
let icon_center = egui::pos2(ui.cursor().min.x + 14.0, ui.cursor().min.y + 12.0);
|
||||||
draw_scythe_icon(ui.painter(), icon_center, 18.0, accent);
|
draw_scythe_icon(ui.painter(), icon_center, 18.0, accent);
|
||||||
ui.add_space(32.0);
|
ui.add_space(32.0);
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new("Update Available")
|
egui::RichText::new(title)
|
||||||
.size(15.0)
|
.size(15.0)
|
||||||
.strong()
|
.strong()
|
||||||
.color(Color32::WHITE),
|
.color(Color32::WHITE),
|
||||||
);
|
);
|
||||||
ui.add_space(8.0);
|
if !version_badge.is_empty() {
|
||||||
let badge_text = format!("v{}", info.version);
|
ui.add_space(8.0);
|
||||||
let badge_bg = Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 35);
|
let badge_text = format!("v{}", version_badge);
|
||||||
let badge_stroke = Stroke::new(1.0_f32, accent);
|
let badge_bg = Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 35);
|
||||||
let galley = ui.painter().layout_no_wrap(
|
let badge_stroke = Stroke::new(1.0_f32, accent);
|
||||||
badge_text.clone(),
|
let galley = ui.painter().layout_no_wrap(
|
||||||
FontId::proportional(11.0),
|
badge_text.clone(),
|
||||||
accent,
|
FontId::proportional(11.0),
|
||||||
);
|
accent,
|
||||||
let badge_rect = egui::Rect::from_min_size(
|
);
|
||||||
ui.cursor().min + egui::vec2(0.0, 3.0),
|
let badge_rect = egui::Rect::from_min_size(
|
||||||
galley.size() + egui::vec2(12.0, 6.0),
|
ui.cursor().min + egui::vec2(0.0, 3.0),
|
||||||
);
|
galley.size() + egui::vec2(12.0, 6.0),
|
||||||
ui.painter().rect(badge_rect, CornerRadius::ZERO, badge_bg, badge_stroke, egui::StrokeKind::Inside);
|
);
|
||||||
ui.painter().text(
|
ui.painter().rect(badge_rect, CornerRadius::ZERO, badge_bg, badge_stroke, egui::StrokeKind::Inside);
|
||||||
badge_rect.center(),
|
ui.painter().text(
|
||||||
egui::Align2::CENTER_CENTER,
|
badge_rect.center(),
|
||||||
badge_text,
|
egui::Align2::CENTER_CENTER,
|
||||||
FontId::proportional(11.0),
|
badge_text,
|
||||||
accent,
|
FontId::proportional(11.0),
|
||||||
);
|
accent,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child_ui.add_space(14.0);
|
child_ui.add_space(14.0);
|
||||||
|
|
||||||
// Body message
|
// Body lines
|
||||||
child_ui.label(
|
if let Some(first) = body_lines.first() {
|
||||||
egui::RichText::new(format!(
|
child_ui.label(
|
||||||
"A new version of Scythe is available (v{}). Installed: v{}.",
|
egui::RichText::new(first)
|
||||||
info.version,
|
.size(12.0)
|
||||||
crate::updater::CURRENT_VERSION
|
.color(Color32::from_rgb(220, 225, 235)),
|
||||||
))
|
);
|
||||||
.size(12.0)
|
}
|
||||||
.color(Color32::from_rgb(220, 225, 235)),
|
if let Some(second) = body_lines.get(1) {
|
||||||
);
|
child_ui.add_space(2.0);
|
||||||
child_ui.add_space(2.0);
|
child_ui.label(
|
||||||
child_ui.label(
|
egui::RichText::new(second)
|
||||||
egui::RichText::new("Would you like to open the release page to download and update now?")
|
.size(11.5)
|
||||||
.size(11.5)
|
.color(Color32::from_rgb(155, 160, 170)),
|
||||||
.color(Color32::from_rgb(155, 160, 170)),
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
child_ui.add_space(14.0);
|
child_ui.add_space(12.0);
|
||||||
|
|
||||||
// Smaller toggle option: "Do not ask next time"
|
if show_do_not_ask {
|
||||||
child_ui.checkbox(
|
child_ui.checkbox(
|
||||||
&mut self.popup_do_not_ask,
|
&mut self.popup_do_not_ask,
|
||||||
egui::RichText::new("Do not ask next time")
|
egui::RichText::new("Do not ask next time")
|
||||||
.size(10.5)
|
.size(10.5)
|
||||||
.color(Color32::from_rgb(175, 180, 190)),
|
.color(Color32::from_rgb(175, 180, 190)),
|
||||||
);
|
);
|
||||||
|
child_ui.add_space(12.0);
|
||||||
|
}
|
||||||
|
|
||||||
child_ui.add_space(14.0);
|
// Action buttons based on current state
|
||||||
|
|
||||||
// Action buttons: "Update Now" and "Later"
|
|
||||||
child_ui.horizontal(|ui| {
|
child_ui.horizontal(|ui| {
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
if squared_button(ui, "Update Now", true, accent) {
|
match &cur {
|
||||||
if self.popup_do_not_ask {
|
crate::updater::UpdateStatus::Available(info) => {
|
||||||
self.config.do_not_ask_updates = true;
|
if squared_button_sized(ui, "Update Now", true, accent, Some(egui::vec2(100.0, 28.0))) {
|
||||||
let _ = self.config.save();
|
if self.popup_do_not_ask {
|
||||||
}
|
self.config.do_not_ask_updates = true;
|
||||||
self.update_dismissed = true;
|
let _ = self.config.save();
|
||||||
crate::updater::open_browser_url(&info.html_url);
|
}
|
||||||
self.show_hud_notification("UPDATER", "Opening release in browser...", crate::overlay::ToastIcon::Info);
|
crate::updater::spawn_auto_update(info.clone(), self.update_status.clone());
|
||||||
}
|
self.show_hud_notification("UPDATER", "Downloading and applying update locally...", crate::overlay::ToastIcon::Info);
|
||||||
|
}
|
||||||
|
|
||||||
ui.add_space(10.0);
|
ui.add_space(10.0);
|
||||||
|
|
||||||
if squared_button(ui, "Later", false, accent) {
|
if squared_button_sized(ui, "Later", false, accent, Some(egui::vec2(70.0, 28.0))) {
|
||||||
if self.popup_do_not_ask {
|
if self.popup_do_not_ask {
|
||||||
self.config.do_not_ask_updates = true;
|
self.config.do_not_ask_updates = true;
|
||||||
let _ = self.config.save();
|
let _ = self.config.save();
|
||||||
|
}
|
||||||
|
self.update_dismissed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.update_dismissed = true;
|
crate::updater::UpdateStatus::Updating { .. } => {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new("Please wait while update completes...")
|
||||||
|
.size(11.5)
|
||||||
|
.color(Color32::from_rgb(96, 165, 250)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
crate::updater::UpdateStatus::Updated { .. } => {
|
||||||
|
if squared_button_sized(ui, "Restart Now", true, accent, Some(egui::vec2(110.0, 28.0))) {
|
||||||
|
crate::updater::restart_application();
|
||||||
|
}
|
||||||
|
ui.add_space(10.0);
|
||||||
|
if squared_button_sized(ui, "Close", false, accent, Some(egui::vec2(70.0, 28.0))) {
|
||||||
|
self.update_dismissed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -3454,8 +3533,12 @@ impl eframe::App for ScytheOverlayApp {
|
||||||
} else {
|
} else {
|
||||||
let modal_active = {
|
let modal_active = {
|
||||||
let cur = self.update_status.lock().ok().map(|g| g.clone()).unwrap_or_default();
|
let cur = self.update_status.lock().ok().map(|g| g.clone()).unwrap_or_default();
|
||||||
matches!(cur, crate::updater::UpdateStatus::Available(_))
|
matches!(
|
||||||
&& !self.update_dismissed
|
cur,
|
||||||
|
crate::updater::UpdateStatus::Available(_)
|
||||||
|
| crate::updater::UpdateStatus::Updating { .. }
|
||||||
|
| crate::updater::UpdateStatus::Updated { .. }
|
||||||
|
) && !self.update_dismissed
|
||||||
&& !self.config.do_not_ask_updates
|
&& !self.config.do_not_ask_updates
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
286
src/updater.rs
286
src/updater.rs
|
|
@ -4,6 +4,14 @@ use serde::Deserialize;
|
||||||
pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
pub const GITHUB_REPO: &str = "oiupoyt/scythe";
|
pub const GITHUB_REPO: &str = "oiupoyt/scythe";
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct ReleaseAsset {
|
||||||
|
pub name: String,
|
||||||
|
pub browser_download_url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub size: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub struct ReleaseInfo {
|
pub struct ReleaseInfo {
|
||||||
pub tag_name: String,
|
pub tag_name: String,
|
||||||
|
|
@ -12,6 +20,7 @@ pub struct ReleaseInfo {
|
||||||
pub html_url: String,
|
pub html_url: String,
|
||||||
pub release_notes: String,
|
pub release_notes: String,
|
||||||
pub has_update: bool,
|
pub has_update: bool,
|
||||||
|
pub assets: Vec<ReleaseAsset>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
|
@ -20,6 +29,8 @@ struct GitHubRelease {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
html_url: String,
|
html_url: String,
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
assets: Vec<ReleaseAsset>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_newer(latest: &str, current: &str) -> bool {
|
pub fn is_newer(latest: &str, current: &str) -> bool {
|
||||||
|
|
@ -45,13 +56,13 @@ pub fn is_newer(latest: &str, current: &str) -> bool {
|
||||||
pub fn check_for_updates() -> Option<ReleaseInfo> {
|
pub fn check_for_updates() -> Option<ReleaseInfo> {
|
||||||
let url = format!("https://api.github.com/repos/{}/releases/latest", GITHUB_REPO);
|
let url = format!("https://api.github.com/repos/{}/releases/latest", GITHUB_REPO);
|
||||||
|
|
||||||
// Run curl with a 4-second timeout to avoid blocking
|
// Run curl with a 6-second timeout to avoid blocking
|
||||||
let mut cmd = std::process::Command::new("curl");
|
let mut cmd = std::process::Command::new("curl");
|
||||||
cmd.args([
|
cmd.args([
|
||||||
"-s",
|
"-s",
|
||||||
"-H", "User-Agent: scythe-updater",
|
"-H", "User-Agent: scythe-updater",
|
||||||
"-H", "Accept: application/vnd.github.v3+json",
|
"-H", "Accept: application/vnd.github.v3+json",
|
||||||
"--max-time", "4",
|
"--max-time", "6",
|
||||||
&url,
|
&url,
|
||||||
]);
|
]);
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
|
@ -76,21 +87,22 @@ pub fn check_for_updates() -> Option<ReleaseInfo> {
|
||||||
html_url: release.html_url,
|
html_url: release.html_url,
|
||||||
release_notes: release.body.unwrap_or_default(),
|
release_notes: release.body.unwrap_or_default(),
|
||||||
has_update,
|
has_update,
|
||||||
|
assets: release.assets,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
#[derive(Default)]
|
|
||||||
pub enum UpdateStatus {
|
pub enum UpdateStatus {
|
||||||
#[default]
|
#[default]
|
||||||
Idle,
|
Idle,
|
||||||
Checking,
|
Checking,
|
||||||
UpToDate { version: String },
|
UpToDate { version: String },
|
||||||
Available(ReleaseInfo),
|
Available(ReleaseInfo),
|
||||||
|
Updating { version: String, progress: String },
|
||||||
|
Updated { version: String, message: String },
|
||||||
Failed(String),
|
Failed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn spawn_update_check(status: Arc<Mutex<UpdateStatus>>) {
|
pub fn spawn_update_check(status: Arc<Mutex<UpdateStatus>>) {
|
||||||
if let Ok(mut lock) = status.lock() {
|
if let Ok(mut lock) = status.lock() {
|
||||||
*lock = UpdateStatus::Checking;
|
*lock = UpdateStatus::Checking;
|
||||||
|
|
@ -115,6 +127,270 @@ pub fn spawn_update_check(status: Arc<Mutex<UpdateStatus>>) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn download_file(url: &str, dest: &std::path::Path) -> Result<(), String> {
|
||||||
|
let dest_str = dest.to_str().ok_or_else(|| "Invalid destination path".to_string())?;
|
||||||
|
let mut cmd = std::process::Command::new("curl");
|
||||||
|
cmd.args([
|
||||||
|
"-L",
|
||||||
|
"--fail",
|
||||||
|
"--silent",
|
||||||
|
"--show-error",
|
||||||
|
"-H", "User-Agent: scythe-updater",
|
||||||
|
"-o", dest_str,
|
||||||
|
url,
|
||||||
|
]);
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
cmd.creation_flags(0x08000000);
|
||||||
|
}
|
||||||
|
let output = cmd.output().map_err(|e| format!("Failed to run curl: {}", e))?;
|
||||||
|
if !output.status.success() {
|
||||||
|
let err = String::from_utf8_lossy(&output.stderr);
|
||||||
|
return Err(format!("Download failed: {}", err.trim()));
|
||||||
|
}
|
||||||
|
if !dest.exists() || std::fs::metadata(dest).map(|m| m.len() == 0).unwrap_or(true) {
|
||||||
|
return Err("Downloaded file is empty or missing".to_string());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_auto_update(info: ReleaseInfo, status: Arc<Mutex<UpdateStatus>>) {
|
||||||
|
if let Ok(mut lock) = status.lock() {
|
||||||
|
*lock = UpdateStatus::Updating {
|
||||||
|
version: info.version.clone(),
|
||||||
|
progress: "Preparing update...".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let res = run_auto_update(&info, &status);
|
||||||
|
if let Ok(mut lock) = status.lock() {
|
||||||
|
match res {
|
||||||
|
Ok(msg) => {
|
||||||
|
*lock = UpdateStatus::Updated {
|
||||||
|
version: info.version.clone(),
|
||||||
|
message: msg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
*lock = UpdateStatus::Failed(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_auto_update(info: &ReleaseInfo, status: &Arc<Mutex<UpdateStatus>>) -> Result<String, String> {
|
||||||
|
let set_progress = |msg: &str| {
|
||||||
|
if let Ok(mut lock) = status.lock() {
|
||||||
|
*lock = UpdateStatus::Updating {
|
||||||
|
version: info.version.clone(),
|
||||||
|
progress: msg.to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// 1. Check if running from AppImage
|
||||||
|
if let Ok(appimage_path) = std::env::var("APPIMAGE") {
|
||||||
|
let appimage_file = std::path::PathBuf::from(&appimage_path);
|
||||||
|
if appimage_file.exists() {
|
||||||
|
set_progress("Locating AppImage package...");
|
||||||
|
let asset = info
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name.ends_with(".AppImage"))
|
||||||
|
.ok_or_else(|| "Could not find AppImage in release assets".to_string())?;
|
||||||
|
|
||||||
|
set_progress(&format!("Downloading AppImage ({})...", asset.name));
|
||||||
|
let temp_download = appimage_file.with_extension("download");
|
||||||
|
download_file(&asset.browser_download_url, &temp_download)?;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let _ = std::fs::set_permissions(&temp_download, std::fs::Permissions::from_mode(0o755));
|
||||||
|
}
|
||||||
|
|
||||||
|
set_progress("Applying update...");
|
||||||
|
std::fs::rename(&temp_download, &appimage_file)
|
||||||
|
.map_err(|e| format!("Failed to replace AppImage: {}", e))?;
|
||||||
|
|
||||||
|
return Ok(format!("Updated AppImage to v{}", info.version));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Standard binary update (.tar.gz containing scythe-ui, scythe-daemon)
|
||||||
|
set_progress("Locating Linux package...");
|
||||||
|
let asset = info
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name == "scythe-linux-x86_64.tar.gz")
|
||||||
|
.or_else(|| info.assets.iter().find(|a| a.name.contains("linux") && a.name.ends_with(".tar.gz")))
|
||||||
|
.ok_or_else(|| "Could not find scythe-linux-x86_64.tar.gz in release assets".to_string())?;
|
||||||
|
|
||||||
|
let temp_dir = std::env::temp_dir().join(format!("scythe_update_{}", std::process::id()));
|
||||||
|
let _ = std::fs::create_dir_all(&temp_dir);
|
||||||
|
let archive_path = temp_dir.join("scythe-linux-x86_64.tar.gz");
|
||||||
|
|
||||||
|
set_progress(&format!("Downloading {}...", asset.name));
|
||||||
|
download_file(&asset.browser_download_url, &archive_path)?;
|
||||||
|
|
||||||
|
set_progress("Extracting binaries...");
|
||||||
|
let extract_status = std::process::Command::new("tar")
|
||||||
|
.args(["-xzf", archive_path.to_str().unwrap(), "-C", temp_dir.to_str().unwrap()])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to run tar: {}", e))?;
|
||||||
|
|
||||||
|
if !extract_status.status.success() {
|
||||||
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||||
|
return Err(format!("tar extraction failed: {}", String::from_utf8_lossy(&extract_status.stderr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let extracted_dir = temp_dir.join("scythe-linux-x86_64");
|
||||||
|
if !extracted_dir.exists() {
|
||||||
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||||
|
return Err("Extracted directory not found".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve current executable's directory
|
||||||
|
let current_exe = std::env::current_exe().map_err(|e| format!("Cannot locate current executable: {}", e))?;
|
||||||
|
let canonical_exe = std::fs::canonicalize(¤t_exe).unwrap_or_else(|_| current_exe.clone());
|
||||||
|
let exe_dir = canonical_exe.parent().ok_or_else(|| "Invalid executable path".to_string())?;
|
||||||
|
|
||||||
|
set_progress(&format!("Installing binaries into {}...", exe_dir.display()));
|
||||||
|
|
||||||
|
for bin_name in ["scythe-ui", "scythe-daemon"] {
|
||||||
|
let src = extracted_dir.join(bin_name);
|
||||||
|
if src.exists() {
|
||||||
|
let dest = exe_dir.join(bin_name);
|
||||||
|
let dest_new = exe_dir.join(format!("{}.new", bin_name));
|
||||||
|
|
||||||
|
std::fs::copy(&src, &dest_new)
|
||||||
|
.map_err(|e| format!("Failed to write {}: {}. Check write permissions in {}", bin_name, e, exe_dir.display()))?;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let _ = std::fs::set_permissions(&dest_new, std::fs::Permissions::from_mode(0o755));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::rename(&dest_new, &dest)
|
||||||
|
.map_err(|e| format!("Failed to atomically replace {}: {}", bin_name, e))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintain aliases in ~/.local/bin if installed there
|
||||||
|
if exe_dir.ends_with(".local/bin") {
|
||||||
|
for link in ["scythe", "vrec", "vrec-ui"] {
|
||||||
|
let link_path = exe_dir.join(link);
|
||||||
|
let target_path = exe_dir.join("scythe-ui");
|
||||||
|
if !link_path.exists() {
|
||||||
|
#[cfg(unix)]
|
||||||
|
let _ = std::os::unix::fs::symlink(&target_path, &link_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let daemon_link = exe_dir.join("vrec-daemon");
|
||||||
|
if !daemon_link.exists() {
|
||||||
|
#[cfg(unix)]
|
||||||
|
let _ = std::os::unix::fs::symlink(exe_dir.join("scythe-daemon"), &daemon_link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||||
|
Ok(format!("Installed v{} to {}", info.version, exe_dir.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// 1. Check for official NSIS installer (scythe-setup.exe)
|
||||||
|
if let Some(asset) = info.assets.iter().find(|a| a.name == "scythe-setup.exe") {
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let installer_path = temp_dir.join(format!("scythe-setup-v{}.exe", info.version));
|
||||||
|
|
||||||
|
set_progress(&format!("Downloading Windows installer ({})...", asset.name));
|
||||||
|
download_file(&asset.browser_download_url, &installer_path)?;
|
||||||
|
|
||||||
|
set_progress("Launching installer...");
|
||||||
|
let mut cmd = std::process::Command::new(&installer_path);
|
||||||
|
cmd.arg("/S");
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
cmd.creation_flags(0x08000000);
|
||||||
|
let _ = cmd.spawn().map_err(|e| format!("Failed to launch installer: {}", e))?;
|
||||||
|
|
||||||
|
return Ok(format!("Update installer launched for v{}.", info.version));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Portable zip update
|
||||||
|
if let Some(asset) = info.assets.iter().find(|a| a.name == "scythe-windows-x86_64.zip") {
|
||||||
|
let temp_dir = std::env::temp_dir().join(format!("scythe_update_{}", std::process::id()));
|
||||||
|
let _ = std::fs::create_dir_all(&temp_dir);
|
||||||
|
let zip_path = temp_dir.join("scythe-windows-x86_64.zip");
|
||||||
|
|
||||||
|
set_progress(&format!("Downloading {}...", asset.name));
|
||||||
|
download_file(&asset.browser_download_url, &zip_path)?;
|
||||||
|
|
||||||
|
set_progress("Extracting archive...");
|
||||||
|
let tar_out = std::process::Command::new("tar.exe")
|
||||||
|
.args(["-xf", zip_path.to_str().unwrap(), "-C", temp_dir.to_str().unwrap()])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
let extracted_ok = tar_out.map(|o| o.status.success()).unwrap_or(false);
|
||||||
|
if !extracted_ok {
|
||||||
|
let ps_script = format!(
|
||||||
|
"Expand-Archive -Path '{}' -DestinationPath '{}' -Force",
|
||||||
|
zip_path.display(),
|
||||||
|
temp_dir.display()
|
||||||
|
);
|
||||||
|
let _ = std::process::Command::new("powershell")
|
||||||
|
.args(["-NoProfile", "-Command", &ps_script])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Extraction failed: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_exe = std::env::current_exe().map_err(|e| format!("Cannot locate current executable: {}", e))?;
|
||||||
|
let exe_dir = current_exe.parent().ok_or_else(|| "Invalid executable path".to_string())?;
|
||||||
|
|
||||||
|
set_progress("Replacing binaries...");
|
||||||
|
for bin_name in ["scythe-ui.exe", "scythe-daemon.exe"] {
|
||||||
|
let src = temp_dir.join(bin_name);
|
||||||
|
if src.exists() {
|
||||||
|
let dest = exe_dir.join(bin_name);
|
||||||
|
let dest_old = exe_dir.join(format!("{}.old", bin_name));
|
||||||
|
let _ = std::fs::remove_file(&dest_old);
|
||||||
|
let _ = std::fs::rename(&dest, &dest_old);
|
||||||
|
let _ = std::fs::copy(&src, &dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||||
|
return Ok(format!("Updated binaries to v{} in {}", info.version, exe_dir.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("No compatible Windows package found in release assets".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||||
|
{
|
||||||
|
Err("Automatic update is not supported on this operating system".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restart_application() {
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
let mut cmd = std::process::Command::new(exe);
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
cmd.creation_flags(0x08000000);
|
||||||
|
}
|
||||||
|
let _ = cmd.spawn();
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn open_browser_url(url: &str) {
|
pub fn open_browser_url(url: &str) {
|
||||||
let u = url.to_string();
|
let u = url.to_string();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue