Add Tauri launcher with Svelte UI

- Accounts: panel login/sign-up, Microsoft device-code sign-in, offline
  play; tokens encrypted with a key kept in the OS credential store
- Home: instance rail, live server status, news feed, social links,
  Play button with download progress, speed and ETA
- Full settings: accounts, memory/resolution/after-launch behaviour,
  Java and GC presets, Minecraft keybind editor applied to options.txt,
  theme overrides, storage manager, self-update via GitHub releases
- Game console, crash dialog with hints, per-instance overrides
- Live admin branding (colours, fonts, backgrounds, custom CSS)
- Online install tests for vanilla/Fabric/Quilt/Forge/NeoForge (CI)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
Claude committed 2026-09-28 05:00:30 +00:00
1 parent af9147ec75
commit 50eb1a0cac
60 files changed
+9424 -47

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
//! Self-update via GitHub Releases: find a newer `*-setup.exe`, download
//! it and hand over to the installer.
use crate::state::build;
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
pub struct UpdateInfo {
pub version: String,
pub notes: String,
pub url: String,
pub size: u64,
}
#[derive(Deserialize)]
struct Release {
tag_name: String,
#[serde(default)]
body: String,
#[serde(default)]
draft: bool,
#[serde(default)]
prerelease: bool,
assets: Vec<Asset>,
}
#[derive(Deserialize)]
struct Asset {
name: String,
browser_download_url: String,
size: u64,
}
fn parse(v: &str) -> Vec<u64> {
v.trim_start_matches('v').split(['.', '-', '+']).map_while(|p| p.parse().ok()).collect()
}
pub fn is_newer(candidate: &str, current: &str) -> bool {
parse(candidate) > parse(current)
}
pub async fn check(http: &reqwest::Client) -> Result<Option<UpdateInfo>> {
let Some(repo) = build::REPO.filter(|r| !r.is_empty()) else { return Ok(None) };
let release: Release = http
.get(format!("https://api.github.com/repos/{repo}/releases/latest"))
.header("Accept", "application/vnd.github+json")
.send()
.await?
.error_for_status()?
.json()
.await?;
if release.draft || release.prerelease || !is_newer(&release.tag_name, env!("CARGO_PKG_VERSION")) {
return Ok(None);
}
let asset = release
.assets
.iter()
.find(|a| a.name.to_lowercase().ends_with("-setup.exe"))
.ok_or_else(|| anyhow!("release {} has no installer", release.tag_name))?;
Ok(Some(UpdateInfo {
version: release.tag_name.trim_start_matches('v').to_string(),
notes: release.body,
url: asset.browser_download_url.clone(),
size: asset.size,
}))
}
/// Download the installer and start it. The caller exits the app afterwards.
pub async fn download_and_run(http: &reqwest::Client, url: &str) -> Result<()> {
if !url.starts_with("https://github.com/") && !url.starts_with("https://objects.githubusercontent.com/") {
anyhow::bail!("refusing to download an update from an unexpected host");
}
let dest = std::env::temp_dir().join("scopenet-launcher-setup.exe");
let dl = scopenet_core::http::Download::new(url, dest.clone(), None, None);
scopenet_core::http::download_one(http, &dl, &|_| {}).await.context("downloading update")?;
// /P = passive (progress only), /R = relaunch when done (Tauri NSIS).
std::process::Command::new(&dest).args(["/P", "/R"]).spawn().context("starting the installer")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn versions() {
assert!(is_newer("v0.2.0", "0.1.9"));
assert!(is_newer("1.0.0", "0.9.12"));
assert!(!is_newer("v0.1.0", "0.1.0"));
assert!(!is_newer("0.1.0-beta", "0.1.0"));
}
}