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
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:
@ -12,6 +12,15 @@ import SettingsSection from './SettingsSection.vue'
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
forceUnicodeFont: {
|
||||
id: 'app.settings.defaults.force-unicode-font',
|
||||
defaultMessage: 'Force Unicode font',
|
||||
},
|
||||
forceUnicodeFontDescription: {
|
||||
id: 'app.settings.defaults.force-unicode-font-description',
|
||||
defaultMessage:
|
||||
'Use the Unicode font when initializing a new instance. Off by default; existing font settings are preserved.',
|
||||
},
|
||||
fullscreen: { id: 'app.settings.defaults.fullscreen', defaultMessage: 'Fullscreen' },
|
||||
fullscreenDescription: {
|
||||
id: 'app.settings.defaults.fullscreen-description',
|
||||
@ -205,6 +214,24 @@ watch(
|
||||
</SettingsRow>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsRow>
|
||||
<template #label>
|
||||
<span id="settings-target-defaults-unicode-font" tabindex="-1">
|
||||
{{ formatMessage(messages.forceUnicodeFont) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #description>{{ formatMessage(messages.forceUnicodeFontDescription) }}</template>
|
||||
<template #control>
|
||||
<Toggle
|
||||
id="force-unicode-font"
|
||||
v-model="settings.force_unicode_font"
|
||||
:aria-label="formatMessage(messages.forceUnicodeFont)"
|
||||
/>
|
||||
</template>
|
||||
</SettingsRow>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsRow stacked>
|
||||
<template #label>
|
||||
|
||||
@ -294,7 +294,9 @@ async function loadLatestChannelVersions() {
|
||||
const versions = await Promise.all(
|
||||
(['release', 'beta'] as const).map(async (channel) => {
|
||||
try {
|
||||
const response = await tauriFetch(`https://update.axlmc.org/latest?channel=${channel}`)
|
||||
const response = await tauriFetch(
|
||||
`https://skin.starlight.cool/starlight/launcher/latest?channel=${channel}`,
|
||||
)
|
||||
if (!response.ok) return [channel, undefined] as const
|
||||
const payload = (await response.json()) as { version?: string }
|
||||
return [channel, payload.version] as const
|
||||
|
||||
@ -254,6 +254,16 @@ export const settingsSearchEntries: SettingsSearchEntry[] = [
|
||||
targetId: 'settings-target-defaults-environment',
|
||||
label: message('app.settings.defaults.environment-variables', 'Environment variables'),
|
||||
},
|
||||
{
|
||||
id: 'defaults-unicode-font',
|
||||
categoryId: 'launch-defaults',
|
||||
targetId: 'settings-target-defaults-unicode-font',
|
||||
label: message('app.settings.defaults.force-unicode-font', 'Force Unicode font'),
|
||||
description: message(
|
||||
'app.settings.defaults.force-unicode-font-description',
|
||||
'Use the Unicode font when initializing a new instance. Off by default; existing font settings are preserved.',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'defaults-launch-hooks',
|
||||
categoryId: 'launch-defaults',
|
||||
|
||||
@ -104,6 +104,28 @@ test('settings search index has unique entries with categories', () => {
|
||||
assert.deepEqual(validateSettingsSearchEntries(), [])
|
||||
})
|
||||
|
||||
test('Unicode font is searchable in Chinese and English', () => {
|
||||
const entry = settingsSearchEntries.find((entry) => entry.id === 'defaults-unicode-font')!
|
||||
assert.equal(entry.categoryId, 'launch-defaults')
|
||||
assert.ok(
|
||||
readFileSync(new URL('./DefaultInstanceSettings.vue', import.meta.url), 'utf8').includes(
|
||||
`id="${getSettingsSearchTargetId(entry)}"`,
|
||||
),
|
||||
)
|
||||
for (const translated of [false, true]) {
|
||||
const documents = settingsSearchEntries.map((entry) => ({
|
||||
item: entry,
|
||||
text: translated
|
||||
? (chineseLocale[entry.label.id]?.message ?? entry.label.defaultMessage ?? '')
|
||||
: (entry.label.defaultMessage ?? ''),
|
||||
}))
|
||||
for (const query of translated ? ['字体', 'Unicode'] : ['font', 'Unicode']) {
|
||||
const matches = filterSettingsSearchDocuments(query, documents)
|
||||
assert.ok(matches.some(({ item }) => item.id === 'defaults-unicode-font'))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('settings search keywords are valid message descriptors', () => {
|
||||
for (const entry of settingsSearchEntries) {
|
||||
for (const keyword of entry.keywords ?? []) {
|
||||
|
||||
@ -183,6 +183,7 @@ export type AppSettings = {
|
||||
custom_env_vars: [string, string][]
|
||||
memory: MemorySettings
|
||||
force_fullscreen: boolean
|
||||
force_unicode_font: boolean
|
||||
maximize_window: boolean
|
||||
game_resolution: [number, number]
|
||||
hide_on_process_start: boolean
|
||||
@ -227,6 +228,7 @@ function normalizeDownloadSettings(settings: AppSettings & LegacyMirrorSettings)
|
||||
settings.auto_concurrent_downloads ??= true
|
||||
settings.download_engine ??= 'legacy'
|
||||
settings.auto_set_java_high_performance_mode ??= true
|
||||
settings.force_unicode_font ??= false
|
||||
settings.minecraft_metadata_source ??=
|
||||
usesLegacyDefaults || !hasLegacySettings ? 'auto' : legacySource(settings.use_minecraft_mirror)
|
||||
settings.minecraft_file_source ??=
|
||||
|
||||
@ -5066,6 +5066,9 @@
|
||||
"app.lab.skin-editor.retry": {
|
||||
"message": "Try again"
|
||||
},
|
||||
"app.lab.skin-editor.resource-error": {
|
||||
"message": "Skin editor files are missing or damaged. Reinstall the launcher or extract the complete portable archive."
|
||||
},
|
||||
"app.lab.skin-editor.title": {
|
||||
"message": "Skin editor"
|
||||
},
|
||||
@ -6836,6 +6839,12 @@
|
||||
"app.settings.defaults.environment-variables-placeholder": {
|
||||
"message": "Enter environment variables..."
|
||||
},
|
||||
"app.settings.defaults.force-unicode-font": {
|
||||
"message": "Force Unicode font"
|
||||
},
|
||||
"app.settings.defaults.force-unicode-font-description": {
|
||||
"message": "Use the Unicode font when initializing a new instance. Off by default; existing font settings are preserved."
|
||||
},
|
||||
"app.settings.defaults.fullscreen": {
|
||||
"message": "Fullscreen"
|
||||
},
|
||||
|
||||
@ -5152,6 +5152,9 @@
|
||||
"app.lab.skin-editor.retry": {
|
||||
"message": "重试"
|
||||
},
|
||||
"app.lab.skin-editor.resource-error": {
|
||||
"message": "皮肤编辑器文件缺失或损坏,请重新安装启动器,或完整解压便携版。"
|
||||
},
|
||||
"app.lab.skin-editor.title": {
|
||||
"message": "皮肤编辑器"
|
||||
},
|
||||
@ -6895,6 +6898,12 @@
|
||||
"app.settings.defaults.environment-variables-placeholder": {
|
||||
"message": "输入环境变量……"
|
||||
},
|
||||
"app.settings.defaults.force-unicode-font": {
|
||||
"message": "强制使用 Unicode 字体"
|
||||
},
|
||||
"app.settings.defaults.force-unicode-font-description": {
|
||||
"message": "初始化新实例时使用 Unicode 字体。默认关闭;实例已有的字体设置会保留。"
|
||||
},
|
||||
"app.settings.defaults.fullscreen": {
|
||||
"message": "全屏"
|
||||
},
|
||||
|
||||
@ -6575,6 +6575,12 @@
|
||||
"app.settings.defaults.environment-variables-placeholder": {
|
||||
"message": "輸入環境變數..."
|
||||
},
|
||||
"app.settings.defaults.force-unicode-font": {
|
||||
"message": "強制使用 Unicode 字型"
|
||||
},
|
||||
"app.settings.defaults.force-unicode-font-description": {
|
||||
"message": "初始化新例項時使用 Unicode 字型。預設關閉;例項既有的字型設定會保留。"
|
||||
},
|
||||
"app.settings.defaults.fullscreen": {
|
||||
"message": "全螢幕"
|
||||
},
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
LoadingIndicator,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { save } from '@tauri-apps/plugin-dialog'
|
||||
import { writeFile } from '@tauri-apps/plugin-fs'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
@ -27,6 +28,11 @@ const messages = defineMessages({
|
||||
id: 'app.lab.skin-editor.load-error-description',
|
||||
defaultMessage: 'The embedded editor did not finish loading. Try again.',
|
||||
},
|
||||
resourceError: {
|
||||
id: 'app.lab.skin-editor.resource-error',
|
||||
defaultMessage:
|
||||
'Skin editor files are missing or damaged. Reinstall the launcher or extract the complete portable archive.',
|
||||
},
|
||||
retry: { id: 'app.lab.skin-editor.retry', defaultMessage: 'Try again' },
|
||||
exportSkin: { id: 'app.lab.skin-editor.export-skin', defaultMessage: 'Minecraft skin PNG' },
|
||||
})
|
||||
@ -40,7 +46,10 @@ const blockbenchLocale = computed(() => {
|
||||
})
|
||||
|
||||
const editorState = ref<'loading' | 'ready' | 'error'>('loading')
|
||||
const errorDetail = ref('')
|
||||
const resourceError = ref(false)
|
||||
const frameKey = ref(0)
|
||||
let loadAttempt = 0
|
||||
let loadTimeout: number | undefined
|
||||
|
||||
const editorUrl = computed(() => {
|
||||
@ -60,6 +69,8 @@ function clearLoadTimeout() {
|
||||
|
||||
function beginEditorLoad() {
|
||||
clearLoadTimeout()
|
||||
errorDetail.value = ''
|
||||
resourceError.value = false
|
||||
editorState.value = 'loading'
|
||||
loadTimeout = window.setTimeout(() => {
|
||||
editorState.value = 'error'
|
||||
@ -77,11 +88,22 @@ function markEditorError() {
|
||||
}
|
||||
|
||||
async function reloadEditor() {
|
||||
const attempt = ++loadAttempt
|
||||
beginEditorLoad()
|
||||
if (!editorUrl.value) {
|
||||
if (!import.meta.env.DEV) {
|
||||
platformName.value = undefined
|
||||
try {
|
||||
platformName.value = await platform()
|
||||
const errors = await invoke<string[]>('get_skin_editor_resource_errors')
|
||||
if (attempt !== loadAttempt) return
|
||||
if (errors.length) {
|
||||
resourceError.value = true
|
||||
errorDetail.value = errors.join(', ')
|
||||
markEditorError()
|
||||
return
|
||||
}
|
||||
platformName.value = platform()
|
||||
} catch (error) {
|
||||
if (attempt !== loadAttempt) return
|
||||
markEditorError()
|
||||
handleError(error)
|
||||
return
|
||||
@ -104,7 +126,18 @@ function handleFrameLoad() {
|
||||
async function handleEditorMessage(event: MessageEvent<unknown>) {
|
||||
if (event.source !== frame.value?.contentWindow) return
|
||||
if (!event.data || typeof event.data !== 'object') return
|
||||
const message = event.data as { type?: unknown; name?: unknown; dataUrl?: unknown }
|
||||
const message = event.data as {
|
||||
type?: unknown
|
||||
name?: unknown
|
||||
dataUrl?: unknown
|
||||
error?: unknown
|
||||
}
|
||||
if (message.type === 'axolotl-skin-load-error' && typeof message.error === 'string') {
|
||||
if (editorState.value === 'ready') return
|
||||
errorDetail.value = message.error.slice(0, 1000)
|
||||
markEditorError()
|
||||
return
|
||||
}
|
||||
if (message.type === 'axolotl-skin-theme-ready') {
|
||||
sendThemeToEditor()
|
||||
markEditorReady()
|
||||
@ -149,15 +182,11 @@ onMounted(async () => {
|
||||
attributeFilter: ['class', 'style'],
|
||||
})
|
||||
if (!import.meta.env.DEV) {
|
||||
try {
|
||||
platformName.value = await platform()
|
||||
} catch (error) {
|
||||
markEditorError()
|
||||
handleError(error)
|
||||
}
|
||||
await reloadEditor()
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
loadAttempt += 1
|
||||
clearLoadTimeout()
|
||||
window.removeEventListener('message', handleEditorMessage)
|
||||
themeObserver?.disconnect()
|
||||
@ -184,7 +213,12 @@ onUnmounted(() => {
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.loadErrorTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 max-w-md text-secondary">{{ formatMessage(messages.loadErrorDescription) }}</p>
|
||||
<p class="m-0 max-w-md text-secondary">
|
||||
{{ formatMessage(resourceError ? messages.resourceError : messages.loadErrorDescription) }}
|
||||
</p>
|
||||
<p v-if="errorDetail" class="m-0 max-w-xl break-words text-sm text-secondary">
|
||||
{{ errorDetail }}
|
||||
</p>
|
||||
<ButtonStyled color="brand" @click="reloadEditor">
|
||||
{{ formatMessage(messages.retry) }}
|
||||
</ButtonStyled>
|
||||
@ -195,8 +229,8 @@ onUnmounted(() => {
|
||||
ref="frame"
|
||||
:title="formatMessage(messages.title)"
|
||||
:src="editorUrl"
|
||||
class="h-full min-h-0 w-full flex-1 border-0 transition-opacity duration-150"
|
||||
:class="editorState === 'ready' ? 'opacity-100' : 'pointer-events-none opacity-0'"
|
||||
class="h-full min-h-0 w-full flex-1 border-0"
|
||||
:class="{ 'pointer-events-none': editorState !== 'ready' }"
|
||||
:aria-label="formatMessage(messages.title)"
|
||||
:aria-hidden="editorState !== 'ready'"
|
||||
@load="handleFrameLoad"
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
"url": "https://api.purpurmc.org/*"
|
||||
},
|
||||
{
|
||||
"url": "https://update.axlmc.org/*"
|
||||
"url": "https://skin.starlight.cool/*"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/*"
|
||||
|
||||
@ -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,
|
||||
|
||||
32
apps/app/src/skin_editor_bridge.js
Normal file
32
apps/app/src/skin_editor_bridge.js
Normal 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))
|
||||
})
|
||||
})()
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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'",
|
||||
|
||||
Reference in New Issue
Block a user