feat: integrate StarLight updates and improve font settings and skin editor loading
Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled

This commit is contained in:
2026-09-19 18:14:41 +08:00
parent a47d309103
commit a477b1fb9a
24 changed files with 665 additions and 74 deletions

View File

@ -59,7 +59,7 @@
"url": "https://api.purpurmc.org/*"
},
{
"url": "https://update.axlmc.org/*"
"url": "https://skin.starlight.cool/*"
},
{
"url": "http://localhost:8000/*"

View File

@ -77,9 +77,10 @@ fn blockbench_skin_response(
} else {
relative_path.to_path_buf()
});
let contents = match fs::read(file_path) {
let contents = match fs::read(&file_path) {
Ok(contents) => contents,
Err(_) => {
Err(error) => {
tracing::warn!(path = %file_path.display(), %error, "Skin editor resource could not be read");
return Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Vec::new())
@ -88,10 +89,10 @@ fn blockbench_skin_response(
};
let contents = if is_compressed_bundle {
let mut decompressed = Vec::new();
if flate2::read::GzDecoder::new(contents.as_slice())
if let Err(error) = flate2::read::GzDecoder::new(contents.as_slice())
.read_to_end(&mut decompressed)
.is_err()
{
tracing::warn!(path = %file_path.display(), %error, "Skin editor bundle could not be decoded");
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Vec::new())
@ -125,6 +126,91 @@ fn blockbench_skin_response(
.expect("failed to build Blockbench skin response")
}
fn skin_editor_resource_errors(resource_dir: &Path) -> Vec<String> {
["index.html", "css/setup.css", "dist/skin.bundle.js"]
.into_iter()
.filter(|path| {
let response = blockbench_skin_response(path, resource_dir);
!response.status().is_success() || response.body().is_empty()
})
.map(str::to_owned)
.collect()
}
#[cfg(test)]
mod skin_editor_tests {
use super::*;
use std::io::Write;
fn editor_resources() -> tempfile::TempDir {
let directory = tempfile::tempdir().unwrap();
fs::create_dir(directory.path().join("css")).unwrap();
fs::create_dir(directory.path().join("dist")).unwrap();
fs::write(directory.path().join("index.html"), "<!doctype html>")
.unwrap();
fs::write(directory.path().join("css/setup.css"), "body {}").unwrap();
let mut bundle = flate2::write::GzEncoder::new(
Vec::new(),
flate2::Compression::default(),
);
bundle.write_all(b"window.editor = true;").unwrap();
fs::write(
directory.path().join("dist/skin.bundle.js.gz"),
bundle.finish().unwrap(),
)
.unwrap();
directory
}
#[test]
fn packaged_skin_editor_bundle_is_readable() {
let directory = editor_resources();
assert!(skin_editor_resource_errors(directory.path()).is_empty());
let response =
blockbench_skin_response("/dist/skin.bundle.js", directory.path());
assert_eq!(response.body(), b"window.editor = true;");
assert_eq!(
response.headers()[header::CONTENT_TYPE],
"text/javascript; charset=utf-8"
);
}
#[test]
fn missing_skin_editor_resources_are_reported() {
let directory = tempfile::tempdir().unwrap();
assert_eq!(
skin_editor_resource_errors(directory.path()),
["index.html", "css/setup.css", "dist/skin.bundle.js"]
);
}
#[test]
fn corrupt_skin_editor_bundle_is_reported() {
let directory = editor_resources();
fs::write(
directory.path().join("dist/skin.bundle.js.gz"),
"invalid gzip",
)
.unwrap();
assert_eq!(
skin_editor_resource_errors(directory.path()),
["dist/skin.bundle.js"]
);
}
}
#[tauri::command]
fn get_skin_editor_resource_errors(
app: tauri::AppHandle,
) -> Result<Vec<String>, String> {
let resource_dir = app
.path()
.resource_dir()
.map_err(|error| error.to_string())?
.join(BLOCKBENCH_SKIN_RESOURCE_DIR);
Ok(skin_editor_resource_errors(&resource_dir))
}
fn is_allowed_blockbench_skin_request(
request: &tauri::http::Request<Vec<u8>>,
) -> bool {
@ -685,6 +771,10 @@ fn main() {
"axolotl-skin",
move |context, request| {
if !is_allowed_blockbench_skin_request(&request) {
tracing::warn!(
path = request.uri().path(),
"Skin editor resource request was rejected"
);
return Response::builder()
.status(StatusCode::FORBIDDEN)
.body(Vec::new())
@ -703,6 +793,13 @@ fn main() {
);
builder = builder
.plugin(
tauri::plugin::Builder::<tauri::Wry>::new("skin-editor-errors")
.js_init_script_on_all_frames(include_str!(
"skin_editor_bridge.js"
))
.build(),
)
.plugin(
tauri::plugin::Builder::<tauri::Wry>::new("skin-site-session")
.js_init_script_on_all_frames(include_str!(
@ -853,6 +950,7 @@ fn main() {
.manage(PendingUpdateData::default())
.invoke_handler(tauri::generate_handler![
initialize_state,
get_skin_editor_resource_errors,
get_launcher_root_dir,
get_update_channel,
get_current_app_database_path,

View File

@ -0,0 +1,32 @@
;(() => {
const isEditor =
location.origin === 'http://axolotl-skin.localhost' ||
(location.protocol === 'axolotl-skin:' && location.host === 'localhost') ||
(location.origin === 'http://localhost:5201' &&
location.pathname === '/__blockbench_skin__/index.html')
if (
!isEditor ||
window.parent === window ||
new URLSearchParams(location.search).get('embed') !== 'skin'
)
return
function report(error) {
window.parent.postMessage(
{ type: 'axolotl-skin-load-error', error: String(error).slice(0, 1000) },
'*',
)
}
window.addEventListener('error', (event) => {
if (event.message) {
if (event.message.startsWith('ResizeObserver loop')) return
report(event.message)
}
})
window.addEventListener('unhandledrejection', (event) => {
report(event.reason?.message || event.reason)
})
window.addEventListener('DOMContentLoaded', () => {
window.blockbenchBundleReady?.catch((error) => report(error?.message || error))
})
})()

View File

@ -3,21 +3,23 @@ use futures::StreamExt;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::sync::{Arc, Mutex};
use tauri::http::HeaderValue;
use tauri::http::header::ACCEPT;
use tauri::http::HeaderValue;
use tauri::{Manager, ResourceId, Runtime, Webview};
use tauri_plugin_http::reqwest;
use tauri_plugin_http::reqwest::ClientBuilder;
use tauri_plugin_updater::{Error, Update, UpdaterExt};
use theseus::{
LoadingBarType, emit_loading, init_loading, launcher_user_agent,
emit_loading, init_loading, launcher_user_agent, LoadingBarType,
};
use tokio::time::Instant;
use url::Url;
const UPDATE_SERVER_LATEST_URL: &str = "https://update.axlmc.org/latest";
const UPDATE_SERVER_API: &str = "https://update.axlmc.org/api/versions";
const UPDATE_SERVER_BASE: &str = "https://update.axlmc.org/";
const STARLIGHT_UPDATE_LATEST_URL: &str =
"https://skin.starlight.cool/starlight/launcher/latest";
const STARLIGHT_UPDATE_VERSIONS_URL: &str =
"https://skin.starlight.cool/starlight/launcher/versions";
const STARLIGHT_UPDATE_BASE_URL: &str = "https://skin.starlight.cool/";
// The updater plugin builds `Update` with no request timeout, so a stalled
// connection would hang the download forever. Bound the whole download.
@ -62,7 +64,10 @@ struct ArtifactEntry {
variant: Option<String>,
platform: String,
architecture: String,
relative_path: String,
#[serde(default)]
relative_path: Option<String>,
#[serde(default)]
download_url: Option<String>,
#[serde(default)]
sha256: Option<String>,
#[serde(default)]
@ -94,7 +99,7 @@ async fn fetch_apt_deb_asset(version: &str) -> Result<AptDebAsset> {
.user_agent(launcher_user_agent())
.timeout(UPDATE_DOWNLOAD_TIMEOUT)
.build()?
.get(UPDATE_SERVER_API)
.get(STARLIGHT_UPDATE_VERSIONS_URL)
.send()
.await?;
@ -139,13 +144,7 @@ async fn fetch_apt_deb_asset(version: &str) -> Result<AptDebAsset> {
)))
})?;
let url =
Url::parse(&format!("{UPDATE_SERVER_BASE}{}", artifact.relative_path))
.map_err(|error| {
theseus::Error::from(theseus::ErrorKind::OtherError(
error.to_string(),
))
})?;
let url = artifact_download_url(artifact)?;
Ok(AptDebAsset {
url,
@ -154,6 +153,31 @@ async fn fetch_apt_deb_asset(version: &str) -> Result<AptDebAsset> {
})
}
fn artifact_download_url(artifact: &ArtifactEntry) -> Result<Url> {
if let Some(download_url) = artifact.download_url.as_deref() {
return Url::parse(download_url).map_err(|error| {
theseus::Error::from(theseus::ErrorKind::OtherError(
error.to_string(),
))
.into()
});
}
let relative_path = artifact.relative_path.as_deref().ok_or_else(|| {
theseus::Error::from(theseus::ErrorKind::OtherError(
"Update catalog artifact has no download URL".to_string(),
))
})?;
Url::parse(STARLIGHT_UPDATE_BASE_URL)
.and_then(|base| base.join(relative_path))
.map_err(|error| {
theseus::Error::from(theseus::ErrorKind::OtherError(
error.to_string(),
))
.into()
})
}
// ── Updater plugin helpers ───────────────────────────────────────
fn update_channel(channel: &str) -> Result<&str> {
@ -183,7 +207,7 @@ fn update_platform() -> Result<&'static str> {
}
fn update_endpoint() -> Result<Url> {
Url::parse(UPDATE_SERVER_LATEST_URL).map_err(|error| {
Url::parse(STARLIGHT_UPDATE_LATEST_URL).map_err(|error| {
theseus::Error::from(theseus::ErrorKind::OtherError(error.to_string()))
.into()
})

View File

@ -13,7 +13,7 @@
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNGNEE1MkMxOTI0MDA4NzYKUldSMkNFQ1N3VkpLUC9kcEtlZFRWak5FRXJsWUw3YllxWGh6bkg3ZEh3K1ZQa1VNZHl6Y0IvQysK",
"endpoints": ["https://update.axlmc.org/latest"],
"endpoints": ["https://skin.starlight.cool/starlight/launcher/latest"],
"windows": {
"installMode": "quiet"
}

View File

@ -111,7 +111,7 @@
"capabilities": ["core", "plugins"],
"devCsp": {
"default-src": "'self' customprotocol: asset:",
"connect-src": "ipc: http://ipc.localhost http://localhost:5201 ws://localhost:5201 https://modrinth.com https://*.modrinth.com https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://fill.papermc.io https://api.papermc.io https://piston-meta.mojang.com https://launchermeta.mojang.com https://meta.fabricmc.net https://files.minecraftforge.net https://maven.minecraftforge.net https://api.purpurmc.org https://mod.mcimirror.top https://mod.tianpao.top https://admin.axlmc.org 'self' data: blob:",
"connect-src": "ipc: http://ipc.localhost http://localhost:5201 ws://localhost:5201 https://modrinth.com https://*.modrinth.com https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://fill.papermc.io https://api.papermc.io https://piston-meta.mojang.com https://launchermeta.mojang.com https://meta.fabricmc.net https://files.minecraftforge.net https://maven.minecraftforge.net https://api.purpurmc.org https://mod.mcimirror.top https://mod.tianpao.top https://admin.axlmc.org https://skin.starlight.cool 'self' data: blob:",
"font-src": ["'self'", "data:", "https://cdn-raw.modrinth.com/fonts/"],
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
"style-src": "'unsafe-inline' 'self'",
@ -123,7 +123,7 @@
},
"csp": {
"default-src": "'self' customprotocol: asset:",
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://fill.papermc.io https://api.papermc.io https://piston-meta.mojang.com https://launchermeta.mojang.com https://meta.fabricmc.net https://files.minecraftforge.net https://maven.minecraftforge.net https://api.purpurmc.org https://mod.mcimirror.top https://mod.tianpao.top https://admin.axlmc.org 'self' data: blob:",
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://fill.papermc.io https://api.papermc.io https://piston-meta.mojang.com https://launchermeta.mojang.com https://meta.fabricmc.net https://files.minecraftforge.net https://maven.minecraftforge.net https://api.purpurmc.org https://mod.mcimirror.top https://mod.tianpao.top https://admin.axlmc.org https://skin.starlight.cool 'self' data: blob:",
"font-src": ["'self'", "data:", "https://cdn-raw.modrinth.com/fonts/"],
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
"style-src": "'unsafe-inline' 'self'",