added updater and fixed fonts
Some checks failed
Build and Release / build (push) Failing after 1m37s

This commit is contained in:
2026-06-12 21:21:45 -05:00
parent ba7a22e0fd
commit 285ba5b3af
24 changed files with 760 additions and 84 deletions

View File

@@ -25,6 +25,8 @@ image = { version = "0.25", features = ["dds"] }
once_cell = "1"
tokio = { version = "1", features = ["full"] }
walkdir = "2"
reqwest = { version = "0.12", features = ["json", "stream"] }
futures-util = "0.3"
[features]
custom-protocol = ["tauri/custom-protocol"]

View File

@@ -2,3 +2,4 @@ pub mod asset_commands;
pub mod config_commands;
pub mod gump_commands;
pub mod script_commands;
pub mod update_commands;

View File

@@ -0,0 +1,192 @@
use futures_util::StreamExt;
use serde::{Deserialize, Serialize};
use std::io::Write;
use tauri::{AppHandle, Emitter};
const GITEA_API: &str =
"https://gitea.whitlocktech.com/api/v1/repos/whitlocktech/Artificers-Scrollwork/releases";
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
// ── Gitea API types ───────────────────────────────────────────────────────────
#[derive(Debug, Deserialize)]
struct GiteaRelease {
tag_name: String,
body: String,
assets: Vec<GiteaAsset>,
#[serde(default)]
prerelease: bool,
#[serde(default)]
draft: bool,
}
#[derive(Debug, Deserialize)]
struct GiteaAsset {
name: String,
size: u64,
browser_download_url: String,
}
// ── Public types returned to the frontend ────────────────────────────────────
#[derive(Debug, Serialize)]
pub struct UpdateInfo {
pub available: bool,
pub latest_version: String,
pub current_version: String,
pub release_notes: String,
pub download_url: String,
pub asset_name: String,
/// File size in bytes (0 if unknown)
pub asset_size: u64,
}
// ── Version helpers ───────────────────────────────────────────────────────────
fn parse_semver(v: &str) -> (u32, u32, u32) {
let v = v.trim_start_matches('v');
let mut parts = v.split('.').filter_map(|p| p.parse::<u32>().ok());
(
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
)
}
fn is_newer(tag: &str, current: &str) -> bool {
parse_semver(tag) > parse_semver(current)
}
// ── Commands ─────────────────────────────────────────────────────────────────
/// Check the Gitea releases API for a newer version.
/// Returns `UpdateInfo` with `available: true` when a newer MSI release exists.
#[tauri::command]
pub async fn check_for_update() -> Result<UpdateInfo, String> {
let client = reqwest::Client::builder()
.user_agent(format!(
"ArtificersScrollwork/{CURRENT_VERSION} (updater)"
))
.timeout(std::time::Duration::from_secs(15))
.build()
.map_err(|e| e.to_string())?;
let releases: Vec<GiteaRelease> = client
.get(format!("{GITEA_API}?limit=10&page=1"))
.send()
.await
.map_err(|e| format!("Network error: {e}"))?
.json()
.await
.map_err(|e| format!("Failed to parse release data: {e}"))?;
// Pick the latest non-draft, non-prerelease that ships an MSI
let release = releases
.iter()
.find(|r| !r.draft && !r.prerelease && r.assets.iter().any(|a| a.name.ends_with(".msi")));
let Some(release) = release else {
return Ok(UpdateInfo {
available: false,
latest_version: CURRENT_VERSION.to_string(),
current_version: CURRENT_VERSION.to_string(),
release_notes: String::new(),
download_url: String::new(),
asset_name: String::new(),
asset_size: 0,
});
};
let msi = release
.assets
.iter()
.find(|a| a.name.ends_with(".msi"))
.unwrap(); // safe: filtered above
Ok(UpdateInfo {
available: is_newer(&release.tag_name, CURRENT_VERSION),
latest_version: release.tag_name.trim_start_matches('v').to_string(),
current_version: CURRENT_VERSION.to_string(),
release_notes: release.body.clone(),
download_url: msi.browser_download_url.clone(),
asset_name: msi.name.clone(),
asset_size: msi.size,
})
}
/// Download the MSI to the system temp directory, emitting `update-progress`
/// events as chunks arrive, then launch the installer via msiexec.
///
/// The frontend should listen for `update-progress` events:
/// `{ downloaded: number, total: number }`
///
/// After this returns Ok(()), the installer will be running and the user
/// can follow the wizard. The app does NOT auto-close; the installer handles it.
#[tauri::command]
pub async fn download_and_install_update(
app: AppHandle,
download_url: String,
asset_name: String,
) -> Result<(), String> {
let client = reqwest::Client::builder()
.user_agent(format!(
"ArtificersScrollwork/{CURRENT_VERSION} (updater)"
))
.timeout(std::time::Duration::from_secs(300))
.build()
.map_err(|e| e.to_string())?;
let response = client
.get(&download_url)
.send()
.await
.map_err(|e| format!("Download request failed: {e}"))?;
if !response.status().is_success() {
return Err(format!(
"Download failed with status {}",
response.status()
));
}
let total = response.content_length().unwrap_or(0);
// Write to temp dir
let installer_path = std::env::temp_dir().join(&asset_name);
let mut file =
std::fs::File::create(&installer_path).map_err(|e| format!("Cannot create temp file: {e}"))?;
let mut downloaded = 0u64;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| format!("Download interrupted: {e}"))?;
file.write_all(&chunk)
.map_err(|e| format!("Write error: {e}"))?;
downloaded += chunk.len() as u64;
let _ = app.emit(
"update-progress",
serde_json::json!({ "downloaded": downloaded, "total": total }),
);
}
drop(file); // ensure file handle is closed before launching
// Launch installer — Windows opens .msi files with msiexec automatically
#[cfg(target_os = "windows")]
{
std::process::Command::new("msiexec")
.args(["/i", &installer_path.to_string_lossy().to_string()])
.spawn()
.map_err(|e| format!("Failed to launch installer: {e}"))?;
}
#[cfg(not(target_os = "windows"))]
{
// Fallback for non-Windows (future proofing)
opener::open(&installer_path).map_err(|e| format!("Failed to open installer: {e}"))?;
}
Ok(())
}

View File

@@ -19,6 +19,9 @@ pub fn run() {
Ok(())
})
.invoke_handler(tauri::generate_handler![
// Updater
commands::update_commands::check_for_update,
commands::update_commands::download_and_install_update,
// Config
commands::config_commands::get_config,
commands::config_commands::set_config,