- src/tray.rs: system tray with show/hide/quit menu, left-click
toggles main window visibility (minimize-to-tray parity with v1).
- src/shutdown.rs: 60s countdown with per-second 'shutdown-countdown'
event; sleep/shutdown/restart via rundll32/shutdown on Windows,
cancel-aware.
- cancel_shutdown + set_shutdown_after_finish commands now drive the
scheduler so the renderer's existing countdown UI works unchanged.
- Cargo features + tray-icon + image-png added.
- Updater pointed at new Gitea repo Administrator/Multi-Hoster-Upload-2.
121 lines
4.3 KiB
Rust
121 lines
4.3 KiB
Rust
//! Auto-updater — checks the Gitea releases endpoint for a newer version
|
|
//! than the one currently running. Downloads are opt-in (user clicks
|
|
//! "Install Update"), not automatic.
|
|
//!
|
|
//! Port of the v1 `lib/updater.js` semver-compare + Gitea polling flow.
|
|
|
|
use semver::Version;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::time::Duration;
|
|
|
|
const REPO: &str = "Administrator/Multi-Hoster-Upload-2";
|
|
const BASE: &str = "https://git.24-music.de/api/v1/repos";
|
|
|
|
#[derive(Serialize, Default, Clone, Debug)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct UpdateCheck {
|
|
pub available: bool,
|
|
pub current_version: String,
|
|
pub latest_version: Option<String>,
|
|
pub download_url: Option<String>,
|
|
pub release_notes: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GiteaRelease {
|
|
tag_name: String,
|
|
body: Option<String>,
|
|
assets: Option<Vec<GiteaAsset>>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GiteaAsset {
|
|
name: String,
|
|
browser_download_url: String,
|
|
}
|
|
|
|
pub async fn download_and_launch(app: tauri::AppHandle) -> Result<(), String> {
|
|
let info = check().await;
|
|
if !info.available {
|
|
return Err("Kein Update verfügbar".into());
|
|
}
|
|
let url = info.download_url.ok_or("Kein Download-URL im Release")?;
|
|
let client = reqwest::Client::builder()
|
|
.timeout(Duration::from_secs(5 * 60))
|
|
.user_agent("multi-hoster-upload/2.0")
|
|
.build().map_err(|e| e.to_string())?;
|
|
let resp = client.get(&url).send().await.map_err(|e| e.to_string())?;
|
|
if !resp.status().is_success() {
|
|
return Err(format!("Download HTTP {}", resp.status().as_u16()));
|
|
}
|
|
let bytes = resp.bytes().await.map_err(|e| e.to_string())?;
|
|
let tmp = std::env::temp_dir();
|
|
let file_name = url.rsplit('/').next().unwrap_or("Multi-Hoster-Upload-Setup.exe");
|
|
let target = tmp.join(format!("multi-hoster-update-{}-{file_name}",
|
|
chrono::Utc::now().timestamp()));
|
|
std::fs::write(&target, &bytes).map_err(|e| e.to_string())?;
|
|
|
|
// Notify UI and launch the installer detached. We then exit so the NSIS
|
|
// installer can replace the running exe.
|
|
let _ = tauri::Emitter::emit(&app, "app:update-installing",
|
|
&serde_json::json!({ "path": target.display().to_string() }));
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
use std::os::windows::process::CommandExt;
|
|
const DETACHED_PROCESS: u32 = 0x00000008;
|
|
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
|
std::process::Command::new(&target)
|
|
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP)
|
|
.spawn()
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
std::process::Command::new(&target).spawn().map_err(|e| e.to_string())?;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
app.exit(0);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn check() -> UpdateCheck {
|
|
let current = env!("CARGO_PKG_VERSION").to_string();
|
|
let mut out = UpdateCheck {
|
|
available: false,
|
|
current_version: current.clone(),
|
|
..Default::default()
|
|
};
|
|
|
|
let Ok(client) = reqwest::Client::builder()
|
|
.timeout(Duration::from_secs(15))
|
|
.user_agent("multi-hoster-upload/2.0")
|
|
.build()
|
|
else { return out };
|
|
|
|
let url = format!("{BASE}/{REPO}/releases?limit=10");
|
|
let Ok(resp) = client.get(&url).send().await else { return out };
|
|
if !resp.status().is_success() { return out; }
|
|
let Ok(list) = resp.json::<Vec<GiteaRelease>>().await else { return out };
|
|
|
|
let Ok(current_v) = Version::parse(current.trim_start_matches('v')) else { return out };
|
|
|
|
for r in list {
|
|
let raw = r.tag_name.trim_start_matches('v');
|
|
let Ok(v) = Version::parse(raw) else { continue };
|
|
if v > current_v {
|
|
out.available = true;
|
|
out.latest_version = Some(r.tag_name.clone());
|
|
out.release_notes = r.body.clone();
|
|
// Prefer MSI > NSIS Setup > fallback.
|
|
let assets = r.assets.unwrap_or_default();
|
|
let url = assets.iter().find(|a| a.name.ends_with(".msi"))
|
|
.or_else(|| assets.iter().find(|a| a.name.to_lowercase().contains("setup.exe")))
|
|
.or_else(|| assets.iter().find(|a| a.name.ends_with(".exe")));
|
|
out.download_url = url.map(|a| a.browser_download_url.clone());
|
|
break;
|
|
}
|
|
}
|
|
out
|
|
}
|