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

This commit is contained in:
Disy committed 2026-09-26 13:06:51 +08:00
1 parent 2359b80250
commit d2a319a4d5
45 files changed
+235 -178

No files matched your search

@@ -12,10 +12,14 @@
>
{{ formatMessage(messages.score, { score }) }}
</span>
<span class="shrink-0 whitespace-nowrap rounded-full bg-surface-3 px-2 py-0.5 tabular-nums text-secondary">
<span
class="shrink-0 whitespace-nowrap rounded-full bg-surface-3 px-2 py-0.5 tabular-nums text-secondary"
>
{{ formatMessage(messages.highestLevel, { level: highestLevel + 1 }) }}
</span>
<span class="shrink-0 whitespace-nowrap rounded-full bg-surface-3 px-2 py-0.5 tabular-nums text-secondary">
<span
class="shrink-0 whitespace-nowrap rounded-full bg-surface-3 px-2 py-0.5 tabular-nums text-secondary"
>
{{ formatMessage(messages.best, { score: bestScore }) }}
</span>
<NewButton type="base" size="sm" class="shrink-0" @click="settleGame">
@@ -80,7 +84,10 @@
</div>
</div>
</Transition>
<div v-if="gameOver && !endedManually" class="tide-drain pointer-events-none absolute inset-0 z-20" />
<div
v-if="gameOver && !endedManually"
class="tide-drain pointer-events-none absolute inset-0 z-20"
/>
<Transition name="panel">
<div
v-if="gameOver"
@@ -115,16 +122,26 @@
</div>
<div class="grid w-full min-w-0 grid-cols-3 gap-1.5">
<div class="flex min-w-0 flex-col items-center rounded-lg bg-surface-3 px-1 py-1">
<span class="text-[9px] text-secondary">{{ formatMessage(messages.levelLabel) }}</span>
<span class="text-sm font-bold tabular-nums text-contrast">{{ highestLevel + 1 }}</span>
<span class="text-[9px] text-secondary">{{
formatMessage(messages.levelLabel)
}}</span>
<span class="text-sm font-bold tabular-nums text-contrast">{{
highestLevel + 1
}}</span>
</div>
<div class="flex min-w-0 flex-col items-center rounded-lg bg-surface-3 px-1 py-1">
<span class="text-[9px] text-secondary">{{ formatMessage(messages.bestLabel) }}</span>
<span class="text-[9px] text-secondary">{{
formatMessage(messages.bestLabel)
}}</span>
<span class="text-sm font-bold tabular-nums text-contrast">{{ bestScore }}</span>
</div>
<div class="flex min-w-0 flex-col items-center rounded-lg bg-surface-3 px-1 py-1">
<span class="text-[9px] text-secondary">{{ formatMessage(messages.overtimesLabel) }}</span>
<span class="text-sm font-bold tabular-nums text-contrast">{{ overtimeCount }}</span>
<span class="text-[9px] text-secondary">{{
formatMessage(messages.overtimesLabel)
}}</span>
<span class="text-sm font-bold tabular-nums text-contrast">{{
overtimeCount
}}</span>
</div>
</div>
<div class="flex flex-wrap justify-center gap-2">
@@ -150,8 +167,8 @@ import blueStar from '@/assets/stars/blue.png'
import greenStar from '@/assets/stars/green.png'
import pinkStar from '@/assets/stars/pink.png'
import redStar from '@/assets/stars/red.png'
import yellowStar from '@/assets/stars/yellow.png'
import superStar from '@/assets/stars/super.png'
import yellowStar from '@/assets/stars/yellow.png'
interface Piece {
id: number
@@ -396,7 +413,14 @@ function mergePieces(a: Piece, b: Piece) {
score.value += points[level]
highestLevel.value = Math.max(highestLevel.value, level)
const isRainbow = level === colors.length - 1
spawnBurst(x, y, isRainbow ? rainbowColors : [colors[level]], 10 + level * 3, 130 + level * 20, 3.4)
spawnBurst(
x,
y,
isRainbow ? rainbowColors : [colors[level]],
10 + level * 3,
130 + level * 20,
3.4,
)
spawnShockwave(x, y, 46 + level * 26, 'rgba(255, 255, 255, 0.9)', Math.min(3, 1.4 + level * 0.3))
shakePower = Math.max(shakePower, Math.min(2 + level * 1.2, 7))
spawnFloater(x, y, `+${points[level]}`, 15 + level * 3, colors[level])
@@ -423,7 +447,14 @@ function easeOutBack(t: number) {
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2)
}
function spawnBurst(x: number, y: number, palette: string[], count: number, speed: number, size: number) {
function spawnBurst(
x: number,
y: number,
palette: string[],
count: number,
speed: number,
size: number,
) {
for (let i = 0; i < count; i++) {
if (particles.length >= 260) return
const angle = Math.random() * Math.PI * 2
@@ -508,7 +539,8 @@ function update(delta: number) {
if (overtimeStartAt >= 0) {
const overtimeElapsed = elapsed - overtimeStartAt
if (overtimeElapsed < 0.8) {
surfaceRatio = overtimeFromRatio + (0.05 - overtimeFromRatio) * easeInOutCubic(overtimeElapsed / 0.8)
surfaceRatio =
overtimeFromRatio + (0.05 - overtimeFromRatio) * easeInOutCubic(overtimeElapsed / 0.8)
} else {
overtimeStartAt = -1
}
@@ -670,17 +702,7 @@ function prepareBallSprite(src: string, level: number) {
sprite.height = Math.max(1, Math.round(bounds.h * scale))
const ctx = sprite.getContext('2d')
if (!ctx) return
ctx.drawImage(
image,
bounds.x,
bounds.y,
bounds.w,
bounds.h,
0,
0,
sprite.width,
sprite.height,
)
ctx.drawImage(image, bounds.x, bounds.y, bounds.w, bounds.h, 0, 0, sprite.width, sprite.height)
ballSprites[level] = sprite
}
image.src = src
@@ -692,9 +714,7 @@ function drawMergePiece(piece: Piece) {
const spawnProgress = Math.min(1, (elapsed - piece.spawnAt) / 0.22)
const spawnScale = spawnProgress >= 1 ? 1 : easeOutBack(spawnProgress)
const radius =
piece.radius *
spawnScale *
(isRainbow ? 1 + 0.05 * Math.sin(elapsed * 3.2 + piece.id) : 1)
piece.radius * spawnScale * (isRainbow ? 1 + 0.05 * Math.sin(elapsed * 3.2 + piece.id) : 1)
context.save()
context.translate(piece.x, piece.y)
const hue = (elapsed * 55) % 360
@@ -792,9 +812,7 @@ function draw() {
context.beginPath()
for (let x = 0; x <= width; x += 8) {
const waveY =
surfaceY +
Math.sin(x * 0.045 + elapsed * 1.8) * 2.5 +
Math.sin(x * 0.013 + elapsed * 0.9) * 2
surfaceY + Math.sin(x * 0.045 + elapsed * 1.8) * 2.5 + Math.sin(x * 0.013 + elapsed * 0.9) * 2
if (x === 0) context.moveTo(x, waveY)
else context.lineTo(x, waveY)
}
@@ -807,7 +825,12 @@ function draw() {
const progress = Math.min(1, (elapsed - waveStart) / 0.8)
const waveY = height * (1 - progress)
const bandHeight = Math.max(22, height * 0.1)
const gradient = context.createLinearGradient(0, waveY - bandHeight, 0, waveY + bandHeight * 0.5)
const gradient = context.createLinearGradient(
0,
waveY - bandHeight,
0,
waveY + bandHeight * 0.5,
)
gradient.addColorStop(0, 'rgba(255, 255, 255, 0)')
gradient.addColorStop(0.55, 'rgba(96, 178, 255, 0.5)')
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
@@ -11,8 +11,8 @@ import {
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { save } from '@tauri-apps/plugin-dialog'
import { join } from '@tauri-apps/api/path'
import { save } from '@tauri-apps/plugin-dialog'
import { readDir, stat } from '@tauri-apps/plugin-fs'
import { ref } from 'vue'
@@ -20,8 +20,8 @@ import { process_listener } from '@/helpers/events'
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
import { kill, run } from '@/helpers/instance'
import { getDisplayInstanceIcon } from '@/helpers/instance-icons'
import { get_by_instance_id } from '@/helpers/process'
import { isInstanceLaunching } from '@/helpers/instance-launch-state'
import { get_by_instance_id } from '@/helpers/process'
import { showInstanceInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js'
@@ -98,7 +98,7 @@ const checkProcess = async () => {
if (Array.isArray(runningProcesses)) internalPlaying.value = runningProcesses.length > 0
}
const play = async (e, context) => {
const play = async (e) => {
e?.stopPropagation()
if (loading.value) return
await run(props.instance.id).catch(async (err) => {
@@ -111,7 +111,7 @@ const play = async (e, context) => {
await checkProcess()
}
const stop = async (e, context) => {
const stop = async (e) => {
e?.stopPropagation()
await kill(props.instance.id).catch(handleError)
await checkProcess()
@@ -205,7 +205,7 @@ onUnmounted(() => unlisten())
: formatMessage(isPlaying ? messages.launchAnother : commonMessages.playButton)
"
:disabled="offline && !installed"
@click="(e) => play(e, 'InstanceCard')"
@click="play"
@mousehover="checkProcess"
>
<!-- Translate for optical centering -->
@@ -295,7 +295,7 @@ onUnmounted(() => unlisten())
'scale-75 opacity-0 transition-all group-hover:scale-100 group-hover:opacity-100':
!disabled,
}"
@click="(e) => play(e, 'InstanceCard')"
@click="play"
@mousehover="checkProcess"
>
<PlayIcon class="translate-x-[1px]" />
@@ -522,7 +522,7 @@ async function copyShareUrl(): Promise<void> {
}
}
function _openAIAnalysis(): void {
function openAIAnalysis(): void {
if (!lastAnalysis?.combined_log) {
notifyNoLogContent()
return
@@ -603,7 +603,7 @@ onUnmounted(() => {
unlistenProcess?.()
})
defineExpose({ handleLaunchError, handleWarning, isLaunchFailure, showPreview, openAIAnalysis: _openAIAnalysis })
defineExpose({ handleLaunchError, handleWarning, isLaunchFailure, showPreview, openAIAnalysis })
</script>
<template>
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { clearHostedSession } from '@/helpers/hosted-packs'
import {
receiveSkinSiteMessage,
@@ -10,6 +9,7 @@ import {
skinSiteStatus,
skinSiteUser,
} from '@/composables/skin-site-session'
import { clearHostedSession } from '@/helpers/hosted-packs'
const frame = ref<HTMLIFrameElement>()
let lastMessage = 0
@@ -67,6 +67,7 @@ withDefaults(
}>(),
{
count: 1,
symlinkTarget: null,
},
)
@@ -127,7 +127,7 @@ interface IconBackground {
style: Record<string, string>
}
function background(
function makeBackground(
id: string,
name: MessageDescriptor,
top: string,
@@ -171,16 +171,16 @@ const backgrounds = [
backgroundSize: '16px 16px',
},
} satisfies IconBackground,
background('grass', backgroundNames.grass, '#7fc95b', '#2f7d32'),
background('ocean', backgroundNames.ocean, '#55b7ff', '#3157c8'),
background('amethyst', backgroundNames.amethyst, '#c084fc', '#6d28d9'),
background('sunset', backgroundNames.sunset, '#ffba52', '#e94b64'),
background('cherry', backgroundNames.cherry, '#ff9ec4', '#b93670'),
background('nether', backgroundNames.nether, '#ef5a46', '#6d1717'),
background('slime', backgroundNames.slime, '#b6ee55', '#44972f'),
background('deep-dark', backgroundNames.deepDark, '#245369', '#0c1f2b'),
background('stone', backgroundNames.stone, '#aeb5bd', '#525b66'),
background('midnight', backgroundNames.midnight, '#4d5f8f', '#171b2d'),
makeBackground('grass', backgroundNames.grass, '#7fc95b', '#2f7d32'),
makeBackground('ocean', backgroundNames.ocean, '#55b7ff', '#3157c8'),
makeBackground('amethyst', backgroundNames.amethyst, '#c084fc', '#6d28d9'),
makeBackground('sunset', backgroundNames.sunset, '#ffba52', '#e94b64'),
makeBackground('cherry', backgroundNames.cherry, '#ff9ec4', '#b93670'),
makeBackground('nether', backgroundNames.nether, '#ef5a46', '#6d1717'),
makeBackground('slime', backgroundNames.slime, '#b6ee55', '#44972f'),
makeBackground('deep-dark', backgroundNames.deepDark, '#245369', '#0c1f2b'),
makeBackground('stone', backgroundNames.stone, '#aeb5bd', '#525b66'),
makeBackground('midnight', backgroundNames.midnight, '#4d5f8f', '#171b2d'),
]
const { formatMessage } = useVIntl()
@@ -12,13 +12,13 @@ import {
import { getVersion } from '@tauri-apps/api/app'
import { invoke } from '@tauri-apps/api/core'
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
import { inject, nextTick, ref, watch } from 'vue'
import { nextTick, ref, watch } from 'vue'
import UpdateAnnouncementHistory from '@/components/ui/announcement/UpdateAnnouncementHistory.vue'
import {
betaDatabaseExists,
copyReleaseDatabaseToBeta,
copyDatabaseBetweenChannels,
copyReleaseDatabaseToBeta,
getCurrentAppDatabasePath,
getUpdateChannel,
getUpdatePreferences,
@@ -446,11 +446,14 @@ async function confirmDatabaseOperation() {
}),
})
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
handleError(
new Error(
targetChannel === activeChannel
? formatMessage(messages.databaseOperationActiveTarget)
: formatMessage(messages.databaseOperationFailed),
`${
targetChannel === activeChannel
? formatMessage(messages.databaseOperationActiveTarget)
: formatMessage(messages.databaseOperationFailed)
}: ${detail}`,
),
)
} finally {
@@ -468,7 +471,7 @@ function cancelDatabaseOperation() {
<div class="flex flex-col gap-6">
<SettingsSection :title="formatMessage(messages.title)">
<div class="update-channel-panel">
<p id="settings-target-updates-channel" class="m-0 text-sm leading-[1.45] text-secondary">
<p id="settings-target-updates-source" class="m-0 text-sm leading-[1.45] text-secondary">
{{ formatMessage(messages.description) }}
</p>
<div
@@ -1,4 +1,4 @@
import { defineMessage, type MessageDescriptor } from '@modrinth/ui'
import type { MessageDescriptor } from '@modrinth/ui'
export type SettingsCategoryId =
| 'interface'
@@ -28,100 +28,100 @@ export interface SettingsCategoryDefinition {
export const settingsCategoryDefinitions: SettingsCategoryDefinition[] = [
{
id: 'interface',
name: defineMessage({
name: {
id: 'app.settings.tabs.interface',
defaultMessage: 'Interface & appearance',
}),
},
group: 'launcher',
onboardingId: 'settings-tab-interface',
},
{
id: 'home-navigation',
name: defineMessage({
name: {
id: 'app.settings.tabs.home-navigation',
defaultMessage: 'Home & navigation',
}),
},
group: 'launcher',
onboardingId: 'settings-tab-home-navigation',
},
{
id: 'language-translation',
name: defineMessage({
name: {
id: 'app.settings.tabs.language-translation',
defaultMessage: 'Language & translation',
}),
},
group: 'launcher',
onboardingId: 'settings-tab-language-translation',
},
{
id: 'ai',
name: defineMessage({ id: 'app.settings.tabs.ai', defaultMessage: 'AI' }),
name: { id: 'app.settings.tabs.ai', defaultMessage: 'AI' },
group: 'launcher',
flushContent: true,
onboardingId: 'settings-tab-ai',
},
{
id: 'java-performance',
name: defineMessage({
name: {
id: 'app.settings.tabs.java-performance',
defaultMessage: 'Java & performance',
}),
},
group: 'game',
onboardingId: 'settings-tab-java-performance',
},
{
id: 'launch-defaults',
name: defineMessage({
name: {
id: 'app.settings.tabs.launch-defaults',
defaultMessage: 'Launch & instance defaults',
}),
},
group: 'game',
onboardingId: 'settings-tab-launch-defaults',
},
{
id: 'content-downloads',
name: defineMessage({
name: {
id: 'app.settings.tabs.content-downloads',
defaultMessage: 'Content & downloads',
}),
},
group: 'game',
onboardingId: 'settings-tab-content-downloads',
},
{
id: 'network-multiplayer',
name: defineMessage({
name: {
id: 'app.settings.tabs.network-multiplayer',
defaultMessage: 'Network & multiplayer',
}),
},
group: 'game',
onboardingId: 'settings-tab-network-multiplayer',
},
{
id: 'storage-backups',
name: defineMessage({
name: {
id: 'app.settings.tabs.storage-backups',
defaultMessage: 'Storage & backups',
}),
},
group: 'data-privacy',
onboardingId: 'settings-tab-storage-backups',
},
{
id: 'updates',
name: defineMessage({ id: 'app.settings.tabs.updates', defaultMessage: 'Updates' }),
name: { id: 'app.settings.tabs.updates', defaultMessage: 'Updates' },
group: 'support',
onboardingId: 'settings-tab-updates',
},
{
id: 'about',
name: defineMessage({ id: 'app.settings.tabs.about', defaultMessage: 'About' }),
name: { id: 'app.settings.tabs.about', defaultMessage: 'About' },
group: 'support',
},
{
id: 'feature-flags',
name: defineMessage({
name: {
id: 'settings.feature-flags.title',
defaultMessage: 'Feature flags',
}),
},
group: 'developer',
developerOnly: true,
},
@@ -15,7 +15,8 @@ import {
UsersIcon,
} from '@modrinth/assets'
import { commonMessages, defineMessages, type MessageDescriptor } from '@modrinth/ui'
import { defineAsyncComponent, type Component } from 'vue'
import { type Component, defineAsyncComponent } from 'vue'
import {
getVisibleSettingsCategoryDefinitions,
type SettingsCategoryDefinition,
@@ -29,8 +29,8 @@ import { useMinecraftLaunchError } from '@/composables/useMinecraftLaunchError'
import { get_project } from '@/helpers/cache'
import { process_listener } from '@/helpers/events'
import { kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import { isInstanceLaunching } from '@/helpers/instance-launch-state'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
import { showInstanceInFolder } from '@/helpers/utils'
import { handleSevereError } from '@/store/error'