Compare commits

..
Author SHA1 Message Date
AxTps 15b6575824 chore: 发布 1.0.1-beta3 公告
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
Starlight release / Release preflight (push) Has been cancelled
Prepare pnpm cache / prepare (push) Has been cancelled
Starlight release / Build (Windows x64) (push) Has been cancelled
Starlight release / Build (Linux arm64) (push) Has been cancelled
Starlight release / Build (Linux x64) (push) Has been cancelled
Starlight release / Build (macOS universal) (push) Has been cancelled
Starlight release / Verify and publish (push) Has been cancelled
- catalog.ts 增量新增 1.0.1-beta3 公告(界面缩放、首次启动公告、
  浏览页与链接修复);
- 网站 catalog.json 重新生成。
2026-09-30 23:13:34 +08:00
AxTps 5bd785d780 chore(i18n): 补充界面缩放设置的中英文文案并清理多余键
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
- en-US / zh-CN 新增 app.appearance-settings.ui-scale.title 与
  .description;
- 移除未使用的 ui-scale.custom / ui-scale.percent 文案键。
2026-09-30 22:55:34 +08:00
AxTps 94cbcef324 feat: 新增启动器界面缩放设置
在高缩放/小屏下,内嵌网页(如皮肤站)顶栏会被裁掉。新增“界面
与外观”中的“启动器缩放”设置,可整体缩放启动器界面。

- 新增 helpers/ui-scale.ts:用 localStorage 保存缩放值(纯本地
  显示偏好,无需后端),并通过 CSS zoom 应用缩放(含内嵌网页)。
- App.vue 启动时调用 installUiScale() 应用已保存的缩放。
- AppearanceSettings.vue 新增缩放设置项:100/110/125/150 预设
  加 50–200 滑条自定义。
2026-09-30 22:40:24 +08:00
AxTps d8a6d44890 fix: 关于页许可证与更新日志链接指向 GitHub 仓库
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
- config.ts:repositoryUrl 修正为启动器仓库
  (github.com/ApTx2012/Starlight_Lancher),此前误指向服务端仓库;
  新增 releasesUrl。
- 关于页“项目许可证”“第三方许可证”随之指向正确的 GitHub 路径。
- 更新页“查看完整更新日志”:当前版本回退到 releasesUrl;历史版本
  回退到对应 tag 的 release 页面。
2026-09-30 21:53:16 +08:00
AxTps 9b75ca4f3f fix: 未配置 CurseForge API Key 时浏览页仍显示导航栏
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
此前在世界地图浏览(projectType=world)且未配置 CurseForge API Key
时,整个 BrowsePageLayout 被 v-if 隐藏,导致选择栏(整合包/模组/
资源包/…/服务器/收藏夹)与搜索栏全部空白。

现在始终渲染 BrowsePageLayout,仅在其结果区上方(#above-results)
于该场景下显示“地图不可用 / 请配置 CurseForge API Key”的提示,导航
与搜索栏保持可见。
2026-09-30 21:31:07 +08:00
AxTps 913e0071dd fix: 修复首次启动公告未弹出并添加侧栏公告入口
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
首次启动公告未弹出,原因有二:
- StartupAnnouncementModal 未 defineExpose({ show }),父组件调用
  show() 静默失败;
- 展示逻辑在调用 show() 之前就写入了 localStorage 的"已读"标记,
  一旦 show 失败便永久不再弹出。

修复:
- 组件补上 defineExpose({ show });
- 展示逻辑改为"确认能拿到组件实例并 show() 成功后再写标记",
  组件尚未挂载时于 nextTick 重试。

同时把首页侧栏的"每日小挑战"替换为可点击的"公告"入口
(HomeAnnouncementEntry),点击通过 provide/inject 的
showStartupAnnouncement 打开公告弹窗。
2026-09-30 21:11:40 +08:00
Disy bb6b8cb8d2 fix: 未登录时阻止下载StarLight实例
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
Sync LobeHub models / sync (push) Has been cancelled
2026-09-29 17:36:10 +08:00
18 changed files with 418 additions and 27 deletions

No files matched your search

+22 -4
View File
@@ -63,7 +63,7 @@ import { openUrl } from '@tauri-apps/plugin-opener'
import { type as getOsType } from '@tauri-apps/plugin-os'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
import { hideAllPoppers } from 'floating-vue'
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, provide, ref, watch } from 'vue'
import { type RouteLocationNormalizedLoaded, RouterView, useRoute, useRouter } from 'vue-router'
import InstancePlayerModal from '@/components/instance/InstancePlayerModal.vue'
@@ -141,6 +141,7 @@ import {
isNetworkMetered,
setRestartAfterPendingUpdate,
} from '@/helpers/utils.js'
import { installUiScale } from '@/helpers/ui-scale'
import { start_join_server, start_join_singleplayer_world } from '@/helpers/worlds.ts'
import i18n, { resolveInitialLocale } from '@/i18n.config'
import {
@@ -1271,6 +1272,9 @@ provide(
)
provide('previewMinecraftCrashModal', () => minecraftCrashModal.value?.showPreview())
provide('showLauncherPopup', (_request: unknown) => {})
provide('showStartupAnnouncement', () => startupAnnouncementModal.value?.show())
installUiScale()
const stateFailed = ref(false)
stateInitialization
@@ -1544,13 +1548,27 @@ const startupAnnouncementModal = ref<InstanceType<typeof StartupAnnouncementModa
const STARTUP_ANNOUNCEMENT_KEY = 'starlight-startup-announcement-seen-v1'
function maybeShowStartupAnnouncement() {
let alreadySeen = false
try {
alreadySeen = localStorage.getItem(STARTUP_ANNOUNCEMENT_KEY) !== null
} catch (error) {
console.warn('Failed to read startup announcement flag', error)
}
if (alreadySeen) return
const modal = startupAnnouncementModal.value
if (!modal) {
// The modal component has not mounted yet; try again on the next tick.
void nextTick(maybeShowStartupAnnouncement)
return
}
modal.show()
try {
if (localStorage.getItem(STARTUP_ANNOUNCEMENT_KEY)) return
localStorage.setItem(STARTUP_ANNOUNCEMENT_KEY, '1')
} catch (error) {
console.warn('Failed to read/write startup announcement flag', error)
console.warn('Failed to write startup announcement flag', error)
}
startupAnnouncementModal.value?.show()
}
// ── Drop import system ──────────────────────────────────────────────────
@@ -32,6 +32,41 @@ export const ANNOUNCEMENT_CHANGE_TYPES: readonly AnnouncementChangeType[] = [
]
export const launcherAnnouncements: readonly LauncherAnnouncement[] = [
{
id: 'launcher-1.0.1-beta3',
version: '1.0.1-beta3',
publishedAt: '2026-09-30',
title: {
'en-US': 'Starlight Launcher 1.0.1-beta3',
'zh-CN': 'Starlight Launcher 1.0.1-beta3',
},
changes: {
added: [
{
'en-US':
'Added a launcher UI scale setting under Appearance, which also helps when embedded pages are cut off.',
'zh-CN': '在“界面与外观”中新增启动器缩放设置,同时可缓解内嵌网页被裁切的问题。',
},
{
'en-US':
'Added a one-time welcome announcement on first launch, with an entry in the home sidebar.',
'zh-CN': '新增首次启动时的欢迎公告弹窗,并在主页侧栏提供入口。',
},
],
fixed: [
{
'en-US':
'Fixed the browse page going blank without a CurseForge API key; the navigation tabs now stay visible.',
'zh-CN': '修复未配置 CurseForge API Key 时浏览页整页空白的问题,导航栏现在保持可见。',
},
{
'en-US':
'Fixed the about page license links and the update changelog link to point at the correct repository.',
'zh-CN': '修复关于页许可证链接与更新日志链接,改为指向正确的仓库。',
},
],
},
},
{
id: 'launcher-1.0.1-beta2',
version: '1.0.1-beta2',
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { NewspaperIcon, RightArrowIcon } from '@modrinth/assets'
import { inject } from 'vue'
const showAnnouncement = inject<(() => void) | undefined>('showStartupAnnouncement')
function open() {
showAnnouncement?.()
}
</script>
<template>
<section
class="flex min-w-0 flex-col gap-3 border-0 border-b-[1px] border-solid border-[--brand-gradient-border] p-4"
>
<button
type="button"
class="group flex w-full cursor-pointer flex-col gap-2 border-0 bg-transparent p-0 text-left"
@click="open"
>
<div class="flex items-center gap-2">
<NewspaperIcon class="size-4 shrink-0 text-secondary" aria-hidden="true" />
<h2 class="m-0 truncate text-lg">公告</h2>
<RightArrowIcon
class="ml-auto size-4 shrink-0 text-secondary transition-transform group-hover:translate-x-0.5"
aria-hidden="true"
/>
</div>
<p class="m-0 text-sm leading-relaxed text-secondary">点击查看最新公告</p>
</button>
</section>
</template>
@@ -30,6 +30,8 @@ function hide() {
function openLink(url: string) {
void openUrl(url)
}
defineExpose({ show })
</script>
<template>
@@ -61,7 +61,7 @@ const historyAnnouncements = computed(() =>
<UpdateAnnouncementContent
:announcement="currentAnnouncement"
:version="currentVersion"
:external-url="currentAnnouncement?.externalUrl ?? AxolotlBrandConfig.website"
:external-url="currentAnnouncement?.externalUrl ?? AxolotlBrandConfig.releasesUrl"
/>
</div>
@@ -102,7 +102,7 @@ const historyAnnouncements = computed(() =>
<UpdateAnnouncementContent
:announcement="announcement"
:show-header="false"
:external-url="announcement.externalUrl"
:external-url="announcement.externalUrl ?? `${AxolotlBrandConfig.releasesUrl}/tag/v${announcement.version}`"
/>
</div>
</Accordion>
@@ -26,6 +26,7 @@ import { computed, ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.ts'
import { getOS } from '@/helpers/utils'
import { setUiScale, uiScale } from '@/helpers/ui-scale'
import { useTheming } from '@/store/state'
import {
type AccentColor,
@@ -209,6 +210,15 @@ const messages = defineMessages({
defaultMessage:
'Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering.',
},
uiScaleTitle: {
id: 'app.appearance-settings.ui-scale.title',
defaultMessage: 'Launcher scale',
},
uiScaleDescription: {
id: 'app.appearance-settings.ui-scale.description',
defaultMessage:
'Scale the whole launcher interface. A smaller scale fits more content, which helps when embedded pages are cut off.',
},
pageTransitionsTitle: {
id: 'app.appearance-settings.page-transitions.title',
defaultMessage: 'Page transition animations',
@@ -1020,6 +1030,43 @@ watch(
/>
</template>
</SettingsRow>
<SettingsRow stacked>
<template #label>
<span id="settings-target-appearance-ui-scale" tabindex="-1">
{{ formatMessage(messages.uiScaleTitle) }}
</span>
</template>
<template #description>{{ formatMessage(messages.uiScaleDescription) }}</template>
<template #control>
<div class="flex w-full flex-col gap-3">
<div class="flex flex-wrap gap-2">
<button
v-for="preset in [100, 110, 125, 150]"
:key="preset"
type="button"
class="min-w-16 rounded-lg border border-solid px-3 py-1.5 text-sm font-semibold transition-colors"
:class="
uiScale === preset
? 'border-brand bg-brand-highlight text-brand'
: 'border-surface-4 bg-surface-3 text-secondary hover:border-surface-5 hover:text-contrast'
"
@click="setUiScale(preset)"
>
{{ preset }}%
</button>
</div>
<Slider
id="ui-scale"
:model-value="uiScale"
:min="50"
:max="200"
:step="5"
unit="%"
@update:model-value="(value) => setUiScale(Number(value))"
/>
</div>
</template>
</SettingsRow>
<SettingsRow>
<template #label>
<span id="settings-target-appearance-page-transitions" tabindex="-1">
@@ -3,6 +3,7 @@ import test from 'node:test'
import {
clearHostedSession,
HOSTED_ACCOUNT_REQUIRED_MESSAGE,
hostedCreate,
hostedDefault,
hostedSync,
@@ -46,6 +47,10 @@ test('hosted installation sends the JWT to native commands while Local needs no
let createdId = 'instance'
let instanceExists = false
let tokenRequests = 0
let accounts: Array<{
account_type: string
yggdrasil?: { api_root: string }
}> = [{ account_type: 'microsoft' }]
const frame = {
postMessage(data: { type: string; requestId: string }) {
if (data.type !== 'starlight-pack-token-request') return
@@ -70,6 +75,14 @@ test('hosted installation sends the JWT to native commands while Local needs no
__TAURI_INTERNALS__: {
async invoke(command: string, args: Record<string, unknown>) {
calls.push({ command, args })
if (command === 'plugin:install|hosted_has_game_account')
return accounts.some(
(account) =>
account.account_type === 'microsoft' ||
(account.account_type === 'yggdrasil' &&
account.yggdrasil?.api_root.replace(/\/+$/, '') ===
'https://skin.starlight.cool/yggdrasil'),
)
if (command === 'plugin:install|hosted_sync' && syncResponse) return syncResponse()
if (command === 'plugin:instance|instance_get')
return instanceExists ? { id: args.instanceId } : null
@@ -99,7 +112,13 @@ test('hosted installation sends the JWT to native commands while Local needs no
await setInstanceMode('instance', 'starlight')
assert.equal(tokenRequests, 4)
assert.deepEqual(
calls.map(({ command }) => command),
calls
.map(({ command }) => command)
.filter(
(command) =>
command.startsWith('plugin:install|') &&
command !== 'plugin:install|hosted_has_game_account',
),
[
'hosted_set_session',
'hosted_default',
@@ -210,9 +229,35 @@ test('hosted installation sends the JWT to native commands while Local needs no
assert.equal(tokenRequests, requestsBeforeLogout)
const attempts: string[] = []
const stopListening = onHostedPackAttemptStarted((id) => attempts.push(id))
accounts = []
const installCallsBeforeAccountFailure = calls.filter(
({ command }) =>
command.startsWith('plugin:install|') &&
command !== 'plugin:install|hosted_has_game_account',
).length
await assert.rejects(hostedSync('failed-instance'), {
message: HOSTED_ACCOUNT_REQUIRED_MESSAGE,
})
await assert.rejects(hostedCreate(), {
message: HOSTED_ACCOUNT_REQUIRED_MESSAGE,
})
assert.deepEqual(attempts, [])
assert.equal(
calls.filter(
({ command }) =>
command.startsWith('plugin:install|') &&
command !== 'plugin:install|hosted_has_game_account',
).length,
installCallsBeforeAccountFailure,
)
const unauthenticatedCreation = useHostedCreation()
await unauthenticatedCreation.install()
assert.equal(unauthenticatedCreation.installError.value, HOSTED_ACCOUNT_REQUIRED_MESSAGE)
assert.equal(unauthenticatedCreation.createdInstance.value, undefined)
accounts = [{ account_type: 'microsoft' }]
const unauthenticatedRetry = hostedSync('failed-instance')
assert.deepEqual(attempts, ['failed-instance'])
await assert.rejects(unauthenticatedRetry)
assert.deepEqual(attempts, ['failed-instance'])
stopListening()
await assert.rejects(hostedSync('failed-instance'))
assert.equal(attempts.length, 1)
@@ -10,6 +10,10 @@ const createdInstance = ref<string>()
const completed = ref(false)
let generation = 0
function errorMessage(cause: unknown) {
return cause instanceof Error ? cause.message : String(cause)
}
export function forgetHostedCreation(instanceId: string) {
if (createdInstance.value !== instanceId) return
generation++
@@ -27,7 +31,7 @@ export function markHostedCreationCompleted(instanceId: string) {
export function markHostedCreationFailed(instanceId: string, cause: unknown) {
if (createdInstance.value !== instanceId) return
completed.value = false
installError.value = String(cause)
installError.value = errorMessage(cause)
}
export function useHostedCreation() {
@@ -63,7 +67,7 @@ export function useHostedCreation() {
} catch (cause) {
if (attempt === generation) {
if (createdInstance.value) markHostedCreationFailed(createdInstance.value, cause)
else installError.value = String(cause)
else installError.value = errorMessage(cause)
}
} finally {
installing.value = false
+2 -1
View File
@@ -4,7 +4,8 @@ export const AxolotlBrandConfig = Object.freeze({
productName: 'Starlight Launcher',
shortProductName: 'Starlight',
website: 'https://skin.starlight.cool/',
repositoryUrl: 'https://git.starlight.cool/Disy/StarLight-Yggdrasil-Server',
repositoryUrl: 'https://github.com/ApTx2012/Starlight_Lancher',
releasesUrl: 'https://github.com/ApTx2012/Starlight_Lancher/releases',
supportUrl: 'https://skin.starlight.cool/',
qqGroupNumber: '000000000',
qqChannelUrl: 'https://skin.starlight.cool/',
+15 -3
View File
@@ -7,6 +7,8 @@ import {
waitForSkinSiteSession,
} from '../composables/skin-site-session.ts'
export const HOSTED_ACCOUNT_REQUIRED_MESSAGE = '您需要登录正版或皮肤站账号后才能操作'
let sessionUpdate: Promise<void> = Promise.resolve()
const attemptListeners = new Set<(instanceId: string) => void>()
@@ -21,6 +23,13 @@ function startHostedPackAttempt(instanceId: string) {
for (const listener of attemptListeners) listener(instanceId)
}
export async function requireHostedGameAccount(): Promise<void> {
const available = await invokeHosted<boolean>('plugin:install|hosted_has_game_account')
if (!available) {
throw new Error(HOSTED_ACCOUNT_REQUIRED_MESSAGE)
}
}
export function clearHostedSession(): Promise<void> {
const update = sessionUpdate
.catch(() => {})
@@ -100,13 +109,16 @@ export interface HostedSyncResult {
}
export const hostedDefault = () =>
invokeWithSession<HostedPublication>('plugin:install|hosted_default')
export const hostedCreate = (gameDirRoot?: string | null) =>
invokeWithSession<string>('plugin:install|hosted_create', {
export const hostedCreate = async (gameDirRoot?: string | null) => {
await requireHostedGameAccount()
return invokeWithSession<string>('plugin:install|hosted_create', {
gameDirRoot: gameDirRoot ?? null,
})
}
export const hostedBinding = (instanceId: string) =>
invokeHosted<HostedBinding | null>('plugin:install|hosted_binding', { instanceId })
export const hostedSync = (instanceId: string) => {
export const hostedSync = async (instanceId: string) => {
await requireHostedGameAccount()
startHostedPackAttempt(instanceId)
return invokeWithSession<HostedSyncResult>('plugin:install|hosted_sync', { instanceId })
}
+60
View File
@@ -0,0 +1,60 @@
import { ref, watch } from 'vue'
/**
* 启动器界面缩放(百分比)。
* 这是纯本地的显示偏好,用 localStorage 保存即可,无需写入后端设置数据库。
*/
const UI_SCALE_STORAGE_KEY = 'starlight-ui-scale'
const MIN_UI_SCALE = 50
const MAX_UI_SCALE = 200
const DEFAULT_UI_SCALE = 100
/** 把缩放值限制在允许范围内。 */
function clampUiScale(value: number): number {
if (!Number.isFinite(value)) return DEFAULT_UI_SCALE
return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(value)))
}
/** 从 localStorage 读取已保存的缩放值。 */
function readStoredUiScale(): number {
try {
const raw = localStorage.getItem(UI_SCALE_STORAGE_KEY)
if (raw === null) return DEFAULT_UI_SCALE
return clampUiScale(Number(raw))
} catch {
return DEFAULT_UI_SCALE
}
}
export const uiScale = ref(readStoredUiScale())
export function setUiScale(value: number): void {
uiScale.value = clampUiScale(value)
}
export function resetUiScale(): void {
setUiScale(DEFAULT_UI_SCALE)
}
export { DEFAULT_UI_SCALE, MAX_UI_SCALE, MIN_UI_SCALE }
/** 把缩放应用到文档根节点,并在变化时持久化。 */
export function installUiScale(): void {
const apply = (value: number) => {
const factor = String(value / 100)
const root = document.documentElement
root.style.setProperty('--ui-scale', factor)
// 用 CSS zoom 整体缩放界面(含内嵌网页),在 WebView2/WebKit 上最可靠。
root.style.setProperty('zoom', factor)
}
apply(uiScale.value)
watch(uiScale, (value) => {
apply(value)
try {
localStorage.setItem(UI_SCALE_STORAGE_KEY, String(value))
} catch (error) {
console.warn('保存界面缩放失败', error)
}
})
}
@@ -773,6 +773,12 @@
"app.appearance-settings.transparent-background.title": {
"message": "Transparent background"
},
"app.appearance-settings.ui-scale.title": {
"message": "Launcher scale"
},
"app.appearance-settings.ui-scale.description": {
"message": "Scale the whole launcher interface. A smaller scale fits more content, which helps when embedded pages are cut off."
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it."
},
@@ -805,6 +805,12 @@
"app.appearance-settings.transparent-background.title": {
"message": "透明背景"
},
"app.appearance-settings.ui-scale.title": {
"message": "启动器缩放"
},
"app.appearance-settings.ui-scale.description": {
"message": "缩放整个启动器界面。缩小后能显示更多内容,适合内嵌网页被裁切的情况。"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "如果你尝试安装一个未托管在 Modrinth 上的 Modrinth 整合包文件(.mrpack),我们会确保你在安装之前了解相关风险。"
},
+7 -7
View File
@@ -3093,7 +3093,7 @@ provideBrowseManager({
<template>
<div data-onboarding-id="browse-content" class="flex flex-col gap-3 p-6">
<BrowsePageLayout v-if="!isWorldMapBrowse || curseForgeCapability.configured">
<BrowsePageLayout>
<template #nav-tabs-actions>
<ButtonStyled size="large" type="transparent">
<button :disabled="translationLoading" @click="toggleTranslation">
@@ -3193,14 +3193,14 @@ provideBrowseManager({
{{ formatMessage(messages.fuzzySearchSuffix) }}
</p>
</div>
<EmptyState
v-if="isWorldMapBrowse && !curseForgeCapability.configured"
type="empty-inbox"
:heading="formatMessage(messages.mapsUnavailable)"
:description="formatMessage(messages.mapsUnavailableDescription)"
/>
</template>
</BrowsePageLayout>
<EmptyState
v-else
type="empty-inbox"
:heading="formatMessage(messages.mapsUnavailable)"
:description="formatMessage(messages.mapsUnavailableDescription)"
/>
<CreationFlowModal
v-if="isServerContext && projectType === 'modpack'"
ref="serverSetupModalRef"
+2 -2
View File
@@ -10,7 +10,7 @@ import { computed, onUnmounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getActivePlayerName } from '@/components/home/home-utils'
import HomeDailyChallenge from '@/components/home/HomeDailyChallenge.vue'
import HomeAnnouncementEntry from '@/components/home/HomeAnnouncementEntry.vue'
import HomeInstancePickerModal from '@/components/home/HomeInstancePickerModal.vue'
import HomeLaunchProgress from '@/components/home/HomeLaunchProgress.vue'
import HomeMinecraftNews from '@/components/home/HomeMinecraftNews.vue'
@@ -202,7 +202,7 @@ onUnmounted(() => {
>
<HomeLaunchProgress />
<HomePlayInsights />
<HomeDailyChallenge />
<HomeAnnouncementEntry />
<HomeMinecraftNews />
</div>
</Teleport>
+6
View File
@@ -16,6 +16,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
.invoke_handler(tauri::generate_handler![
hosted_default,
hosted_set_session,
hosted_has_game_account,
hosted_create,
hosted_binding,
hosted_sync,
@@ -65,6 +66,11 @@ pub async fn hosted_default() -> Result<theseus::pack::hosted::Publication> {
Ok(theseus::pack::hosted::default_publication().await?)
}
#[tauri::command]
pub async fn hosted_has_game_account() -> Result<bool> {
Ok(theseus::pack::hosted::has_game_account().await?)
}
#[tauri::command]
pub async fn hosted_create(game_dir_root: Option<String>) -> Result<String> {
Ok(theseus::pack::hosted::create(game_dir_root).await?)
+55 -1
View File
@@ -1,6 +1,60 @@
{
"updated_at": "2026-09-27T00:38:49.605Z",
"updated_at": "2026-09-30T15:12:17.359Z",
"announcements": [
{
"id": "launcher-1.0.1-beta3",
"version": "1.0.1-beta3",
"publishedAt": "2026-09-30",
"title": {
"en-US": "Starlight Launcher 1.0.1-beta3",
"zh-CN": "Starlight Launcher 1.0.1-beta3"
},
"changes": {
"added": [
{
"en-US": "Added a launcher UI scale setting under Appearance, which also helps when embedded pages are cut off.",
"zh-CN": "在“界面与外观”中新增启动器缩放设置,同时可缓解内嵌网页被裁切的问题。"
},
{
"en-US": "Added a one-time welcome announcement on first launch, with an entry in the home sidebar.",
"zh-CN": "新增首次启动时的欢迎公告弹窗,并在主页侧栏提供入口。"
}
],
"fixed": [
{
"en-US": "Fixed the browse page going blank without a CurseForge API key; the navigation tabs now stay visible.",
"zh-CN": "修复未配置 CurseForge API Key 时浏览页整页空白的问题,导航栏现在保持可见。"
},
{
"en-US": "Fixed the about page license links and the update changelog link to point at the correct repository.",
"zh-CN": "修复关于页许可证链接与更新日志链接,改为指向正确的仓库。"
}
]
}
},
{
"id": "launcher-1.0.1-beta2",
"version": "1.0.1-beta2",
"publishedAt": "2026-09-28",
"title": {
"en-US": "Starlight Launcher 1.0.1-beta2",
"zh-CN": "Starlight Launcher 1.0.1-beta2"
},
"changes": {
"added": [
{
"en-US": "GitHub releases now include one complete update package for publishing to the StarLight skin site.",
"zh-CN": "GitHub 发布现会生成一个可直接发布到 StarLight 皮肤站的完整更新包。"
}
],
"fixed": [
{
"en-US": "Improved the reliability of live logs and database switching.",
"zh-CN": "提高实时日志与数据库切换的可靠性。"
}
]
}
},
{
"id": "launcher-1.0.1-beta1",
"version": "1.0.1-beta1",
+66 -3
View File
@@ -5,8 +5,9 @@ mod transport;
use crate::{
State,
state::{
AppliedContentSetPatch, EditInstance, InstanceInstallStage,
InstanceLaunchOverridesPatch, InstanceMode, ModLoader,
AppliedContentSetPatch, Credentials, EditInstance,
InstanceInstallStage, InstanceLaunchOverridesPatch, InstanceMode,
MinecraftAccountType, ModLoader,
},
util::fetch::{
DownloadRequest, Integrity, ResourceClass, configured_client,
@@ -31,6 +32,9 @@ use tokio::{
};
const API: &str = "https://skin.starlight.cool/starlight/mod/packs";
const STARLIGHT_YGGDRASIL_API_ROOT: &str =
"https://skin.starlight.cool/yggdrasil";
const ACCOUNT_REQUIRED_MESSAGE: &str = "您需要登录正版或皮肤站账号后才能操作";
const BINDING: &str = ".starlight-pack.json";
const JOURNAL: &str = ".starlight-pack-pending.json";
const MAX_HOSTED_CONCURRENT_FILES: usize = 8;
@@ -426,10 +430,46 @@ pub async fn default_publication() -> crate::Result<Publication> {
})
}
fn is_supported_game_account(
account_type: MinecraftAccountType,
yggdrasil_api_root: Option<&str>,
) -> bool {
account_type == MinecraftAccountType::Microsoft
|| (account_type == MinecraftAccountType::Yggdrasil
&& yggdrasil_api_root.map(|value| value.trim_end_matches('/'))
== Some(STARLIGHT_YGGDRASIL_API_ROOT))
}
async fn has_game_account_in(state: &State) -> crate::Result<bool> {
let accounts = Credentials::get_all_without_refresh(&state.pool).await?;
Ok(accounts.values().any(|account| {
is_supported_game_account(
account.account_type,
account
.yggdrasil
.as_ref()
.map(|value| value.api_root.as_str()),
)
}))
}
pub async fn has_game_account() -> crate::Result<bool> {
let state = State::get().await?;
has_game_account_in(&state).await
}
async fn require_game_account(state: &State) -> crate::Result<()> {
if !has_game_account_in(state).await? {
return Err(invalid(ACCOUNT_REQUIRED_MESSAGE));
}
Ok(())
}
pub async fn create(game_dir_root: Option<String>) -> crate::Result<String> {
let state = State::get().await?;
require_game_account(&state).await?;
let publication = default_publication().await?;
let runtime = &publication.manifest.runtime;
let state = State::get().await?;
// The pack's game files live in their own folder under the chosen root,
// e.g. `<root>/<pack name>`. Avoid a `versions/<name>` layout: that shape
// is reserved for externally linked launcher instances and would make the
@@ -728,6 +768,8 @@ async fn recover(instance_id: &str) -> crate::Result<()> {
}
pub async fn synchronize(instance_id: &str) -> crate::Result<SyncResult> {
let state = State::get().await?;
require_game_account(&state).await?;
let _guard = instance_gate(instance_id).lock_owned().await;
if instance_mode(instance_id).await? != InstanceMode::StarLight {
return Err(invalid(
@@ -1492,6 +1534,27 @@ async fn synchronize_with_progress(
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hosted_operations_accept_only_microsoft_or_starlight_accounts() {
assert!(is_supported_game_account(
MinecraftAccountType::Microsoft,
None
));
assert!(is_supported_game_account(
MinecraftAccountType::Yggdrasil,
Some("https://skin.starlight.cool/yggdrasil/")
));
assert!(!is_supported_game_account(
MinecraftAccountType::Yggdrasil,
Some("https://auth.example.com/yggdrasil")
));
assert!(!is_supported_game_account(
MinecraftAccountType::Offline,
None
));
}
#[tokio::test]
async fn running_pack_checks_tag_changes_without_writing_live_files() {
let root = tempfile::tempdir().unwrap();