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

@ -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>

View File

@ -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

View File

@ -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',

View File

@ -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 ?? []) {

View File

@ -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 ??=

View File

@ -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"
},

View File

@ -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": "全屏"
},

View File

@ -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": "全螢幕"
},

View File

@ -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"