feat:移除了弹窗,服务器添加sls

This commit is contained in:
2026-09-08 22:39:45 +08:00
commit 6a295f9a7a
4082 changed files with 1322534 additions and 0 deletions

View File

@ -0,0 +1,54 @@
import { defineStore } from 'pinia'
export const useBreadcrumbs = defineStore('breadcrumbsStore', {
state: () => ({
names: new Map(),
icons: new Map(),
context: null,
rootContext: null,
}),
actions: {
getName(route) {
return this.names.get(route) ?? ''
},
setName(route, title) {
this.names.set(route, title)
},
setNameIcon(route, iconUrl) {
if (iconUrl) this.icons.set(route, iconUrl)
else this.icons.delete(route)
},
getIcon(route) {
return this.icons.get(route) ?? null
},
// resets breadcrumbs to only included ones as to not have stale breadcrumbs
resetToNames(breadcrumbs) {
if (!breadcrumbs) return
// names is an array of every breadcrumb.name that starts with a ?
const names = breadcrumbs
.filter((breadcrumb) => breadcrumb.name.charAt(0) === '?')
.map((breadcrumb) => breadcrumb.name.slice(1))
// remove all names that are not in the names array
for (const [route] of this.names) {
if (!names.includes(route)) {
this.names.delete(route)
}
}
for (const route of this.icons.keys()) {
if (!names.includes(route)) this.icons.delete(route)
}
},
setContext(context) {
this.context = context
},
setContextIcon(iconUrl) {
if (this.context) this.context = { ...this.context, iconUrl }
},
setRootContext(context) {
this.rootContext = context
},
setRootContextIcon(iconUrl) {
if (this.rootContext) this.rootContext = { ...this.rootContext, iconUrl }
},
},
})

View File

@ -0,0 +1,41 @@
import { defineStore } from 'pinia'
import { findMinecraftAuthError } from '@/components/ui/minecraft-auth-error-modal/minecraft-auth-errors'
export const useError = defineStore('errorsStore', {
state: () => ({
errorModal: null,
minecraftAuthErrorModal: null,
minecraftLaunchErrorHandler: null,
}),
actions: {
setErrorModal(ref) {
this.errorModal = ref
},
setMinecraftAuthErrorModal(ref) {
this.minecraftAuthErrorModal = ref
},
setMinecraftLaunchErrorHandler(handler) {
this.minecraftLaunchErrorHandler = handler
},
showError(error, context, closable = true, source = null) {
if (this.minecraftLaunchErrorHandler?.(error, context)) return
if (
error.message &&
(error.message.includes('Minecraft authentication error:') ||
findMinecraftAuthError(error.message)) &&
this.minecraftAuthErrorModal
) {
this.minecraftAuthErrorModal.show(error)
return
}
this.errorModal.show(error, context, closable, source)
},
},
})
export const handleSevereError = (err, context) => {
const error = useError()
error.showError(err, context)
console.error(err)
}

View File

@ -0,0 +1,142 @@
import { defineStore } from 'pinia'
import {
cancelModTranslation,
dismissModTranslationTask,
listenToModTranslationTasks,
listModTranslationTasks,
translateMod,
} from '@/lab/mod-translation/backend'
import {
mergeTaskSnapshot,
reduceModTranslationJob,
replayContiguousTaskEvents,
} from '@/lab/mod-translation/job-state'
import type {
ModTranslationAnalysis,
ModTranslationJob,
ModTranslationOptions,
ModTranslationTaskEvent,
ModTranslationTaskSnapshot,
} from '@/lab/mod-translation/types.ts'
let unlistenTasks: (() => void) | undefined
let initPromise: Promise<void> | undefined
const earlyEvents = new Map<string, ModTranslationTaskEvent[]>()
async function ensureModTranslationRuntime(): Promise<void> {
if (initPromise) return await initPromise
initPromise = (async () => {
unlistenTasks = await listenToModTranslationTasks((event) => {
useModTranslationStore().applyTaskEvent(event)
})
const snapshots = await listModTranslationTasks()
useModTranslationStore().restoreSnapshots(snapshots)
})().catch((error) => {
unlistenTasks?.()
unlistenTasks = undefined
initPromise = undefined
throw error
})
return await initPromise
}
export const useModTranslationStore = defineStore('modTranslation', {
state: () => ({
inputPath: '',
analysis: null as ModTranslationAnalysis | null,
analysisInputPath: '',
providerId: '',
modelId: '',
options: {
batchSize: 40,
deepBatchSize: 24,
generateModName: false,
repairEnabled: true,
classTextEnabled: false,
maxClassBatch: 16,
} as ModTranslationOptions,
jobs: [] as ModTranslationJob[],
}),
getters: {
activeJobs(state): ModTranslationJob[] {
return state.jobs.filter((job) => job.status === 'running')
},
},
actions: {
async init() {
await ensureModTranslationRuntime()
},
setAnalysis(analysis: ModTranslationAnalysis | null, inputPath = '') {
this.analysis = analysis
this.analysisInputPath = analysis ? inputPath : ''
},
async startTranslation() {
if (!this.inputPath || !this.providerId || !this.modelId) return
await ensureModTranslationRuntime()
const outputPath = this.inputPath.replace(/\.jar$/i, '-zh_cn.jar')
const reusableAnalysis =
this.analysis && this.analysisInputPath === this.inputPath ? this.analysis : null
const snapshot = await translateMod({
inputPath: this.inputPath,
outputPath,
providerId: this.providerId,
modelId: this.modelId,
analysisId: reusableAnalysis?.analysisId,
inputHash: reusableAnalysis?.inputHash,
options: this.options,
})
this.upsertSnapshot(snapshot)
this.setAnalysis(null)
},
async cancelJob(taskId: string) {
await cancelModTranslation(taskId)
},
async removeJob(taskId: string) {
await dismissModTranslationTask(taskId)
this.jobs = this.jobs.filter((job) => job.taskId !== taskId)
},
restoreSnapshots(snapshots: ModTranslationTaskSnapshot[]) {
for (const snapshot of snapshots) this.upsertSnapshot(snapshot)
},
upsertSnapshot(snapshot: ModTranslationTaskSnapshot) {
const index = this.jobs.findIndex((job) => job.taskId === snapshot.taskId)
const current = index >= 0 ? this.jobs[index] : undefined
let job = mergeTaskSnapshot(current, snapshot)
const replayed = replayContiguousTaskEvents(job, earlyEvents.get(snapshot.taskId) ?? [])
job = replayed.job
if (replayed.pending.length) earlyEvents.set(snapshot.taskId, replayed.pending)
else earlyEvents.delete(snapshot.taskId)
if (index >= 0) this.jobs.splice(index, 1, job)
else this.jobs.unshift(job)
this.jobs.sort((left, right) => right.startedAt.localeCompare(left.startedAt))
},
applyTaskEvent(event: ModTranslationTaskEvent) {
const index = this.jobs.findIndex((job) => job.taskId === event.taskId)
if (index < 0) {
const buffered = earlyEvents.get(event.taskId) ?? []
if (!buffered.some((candidate) => candidate.sequence === event.sequence)) {
buffered.push(event)
earlyEvents.set(event.taskId, buffered.slice(-400))
}
return
}
let job = this.jobs[index]
if (event.sequence <= job.lastSequence) return
if (event.sequence > job.lastSequence + 1) {
const buffered = earlyEvents.get(event.taskId) ?? []
if (!buffered.some((candidate) => candidate.sequence === event.sequence)) {
buffered.push(event)
earlyEvents.set(event.taskId, buffered.slice(-400))
}
return
}
job = reduceModTranslationJob(job, event)
const replayed = replayContiguousTaskEvents(job, earlyEvents.get(event.taskId) ?? [])
job = replayed.job
if (replayed.pending.length) earlyEvents.set(event.taskId, replayed.pending)
else earlyEvents.delete(event.taskId)
this.jobs.splice(index, 1, job)
},
},
})

View File

@ -0,0 +1,4 @@
import { useBreadcrumbs } from './breadcrumbs'
import { useTheming } from './theme.ts'
export { useBreadcrumbs, useTheming }

View File

@ -0,0 +1,20 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { applySystemAccentHue, DEFAULT_CUSTOM_ACCENT_COLOR, hexToHsl } from './theme.ts'
test('applies the system hue with the custom accent saturation and lightness', () => {
const system = hexToHsl('#0078d4')
const custom = hexToHsl(DEFAULT_CUSTOM_ACCENT_COLOR)
const result = hexToHsl(applySystemAccentHue('#0078d4'))
assert.ok(Math.abs(result.h - system.h) < 1)
assert.ok(Math.abs(result.s - custom.s) < 1)
assert.ok(Math.abs(result.l - custom.l) < 1)
})
test('preserves hue values at the red boundary', () => {
const result = hexToHsl(applySystemAccentHue('#ff0000'))
assert.ok(result.h < 1 || result.h > 359)
})

View File

@ -0,0 +1,258 @@
import { defineStore } from 'pinia'
let systemThemeMq: MediaQueryList | null = null
export const DEFAULT_FEATURE_FLAGS = {
project_background: false,
page_path: false,
worlds_tab: false,
worlds_in_home: true,
server_project_qa: false,
show_version_environment_column: false,
server_ram_as_bytes_always_on: false,
always_show_app_controls: false,
skip_non_essential_warnings: false,
skip_unknown_pack_warning: false,
i18n_debug: false,
show_instance_play_time: true,
page_transitions: true,
advanced_filters_collapsed: true,
auto_install_dependencies: true,
}
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const
export const ACCENT_COLOR_OPTIONS = ['pink', 'orange', 'green', 'blue', 'purple'] as const
export const DEFAULT_CUSTOM_ACCENT_COLOR = '#db2777'
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
export type FeatureFlags = Record<FeatureFlag, boolean>
export type ColorTheme = (typeof THEME_OPTIONS)[number]
export type AccentColor = (typeof ACCENT_COLOR_OPTIONS)[number]
export type CustomAccentColor = `custom:#${string}`
export type AccentColorSetting = AccentColor | 'system' | CustomAccentColor
export type HomeLayout = 'standard' | 'minimal'
export type CloseBehavior = 'ask' | 'close' | 'lightweight'
/**
* Extracts the `#rrggbb` hex from a `custom:#rrggbb` accent setting,
* or returns null if the value is not a valid custom accent.
*/
export function parseCustomAccentColor(value: string): string | null {
const match = /^custom:(#[0-9a-fA-F]{6})$/.exec(value)
return match ? match[1].toLowerCase() : null
}
export function hexToHsl(hex: string): { h: number; s: number; l: number } {
const n = parseInt(hex.slice(1), 16)
const r = ((n >> 16) & 255) / 255
const g = ((n >> 8) & 255) / 255
const b = (n & 255) / 255
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const l = (max + min) / 2
const d = max - min
const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1))
let h = 0
if (d !== 0) {
if (max === r) h = ((g - b) / d) % 6
else if (max === g) h = (b - r) / d + 2
else h = (r - g) / d + 4
h *= 60
if (h < 0) h += 360
}
return { h, s: s * 100, l: l * 100 }
}
export function hslToHex(h: number, s: number, l: number): string {
const sn = s / 100
const ln = l / 100
const c = (1 - Math.abs(2 * ln - 1)) * sn
const hp = (((h % 360) + 360) % 360) / 60
const x = c * (1 - Math.abs((hp % 2) - 1))
let [r, g, b] = [0, 0, 0]
if (hp < 1) [r, g, b] = [c, x, 0]
else if (hp < 2) [r, g, b] = [x, c, 0]
else if (hp < 3) [r, g, b] = [0, c, x]
else if (hp < 4) [r, g, b] = [0, x, c]
else if (hp < 5) [r, g, b] = [x, 0, c]
else [r, g, b] = [c, 0, x]
const m = ln - c / 2
const toHex = (channel: number) =>
Math.round((channel + m) * 255)
.toString(16)
.padStart(2, '0')
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
}
export function applySystemAccentHue(systemHex: string): string {
const { h } = hexToHsl(systemHex)
const { s, l } = hexToHsl(DEFAULT_CUSTOM_ACCENT_COLOR)
return hslToHex(h, s, l)
}
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max)
/**
* Derives per-theme accent variants from a single base color, mirroring the
* relationship between the preset accents' light and dark values: hue is kept,
* the light variant is clamped dark enough for light surfaces, and the dark
* variant is lifted ~18 lightness points with a saturation boost.
*/
export function deriveAccentVariants(hex: string): { light: string; dark: string } {
const { h, s, l } = hexToHsl(hex)
const lightL = clamp(l, 34, 57)
return {
light: hslToHex(h, s, lightL),
dark: hslToHex(h, Math.min(s * 1.15, 100), Math.min(lightL + 18, 80)),
}
}
export type ThemeStore = {
selectedTheme: ColorTheme
selectedAccentColor: AccentColorSetting
systemAccentColor: string | null
systemAccentSupported: boolean | null
advancedRendering: boolean
hideNametagSkinsPage: boolean
toggleSidebar: boolean
customBackgroundPath: string | null
customBackgroundBlur: number
customBackgroundOpacity: number
transparentBackground: boolean
transparentBackgroundOpacity: number
transparentBackgroundBlur: boolean
sidebarInstanceCount: number
autoHideDownloadsButton: boolean
homeLayout: HomeLayout
minimalHomeInstanceId: string | null
closeBehavior: CloseBehavior
devMode: boolean
featureFlags: FeatureFlags
}
export const DEFAULT_THEME_STORE: ThemeStore = {
selectedTheme: 'dark',
selectedAccentColor: 'pink',
systemAccentColor: null,
systemAccentSupported: null,
advancedRendering: true,
hideNametagSkinsPage: false,
toggleSidebar: false,
customBackgroundPath: null,
customBackgroundBlur: 12,
customBackgroundOpacity: 65,
transparentBackground: false,
transparentBackgroundOpacity: 55,
transparentBackgroundBlur: false,
sidebarInstanceCount: 0,
autoHideDownloadsButton: false,
homeLayout: 'standard',
minimalHomeInstanceId: null,
closeBehavior: 'ask',
devMode: false,
featureFlags: DEFAULT_FEATURE_FLAGS,
}
export const useTheming = defineStore('themeStore', {
state: () => DEFAULT_THEME_STORE,
actions: {
setThemeState(newTheme: ColorTheme) {
if (THEME_OPTIONS.includes(newTheme)) {
this.selectedTheme = newTheme
} else {
console.warn('Selected theme is not present. Check themeOptions.')
}
this.setThemeClass()
},
setAccentColor(newAccentColor: AccentColorSetting) {
if (
newAccentColor === 'system' ||
parseCustomAccentColor(newAccentColor) !== null ||
ACCENT_COLOR_OPTIONS.includes(newAccentColor as AccentColor)
) {
this.selectedAccentColor = newAccentColor
} else {
console.warn('Selected accent color is not available.')
}
const html = document.documentElement
for (const accentColor of ACCENT_COLOR_OPTIONS) {
html.classList.remove(`accent-${accentColor}`)
}
html.classList.remove('accent-custom')
const customHex =
parseCustomAccentColor(this.selectedAccentColor) ??
(this.selectedAccentColor === 'system' ? this.systemAccentColor : null)
if (customHex) {
const variants = deriveAccentVariants(customHex)
html.style.setProperty('--custom-accent-light', variants.light)
html.style.setProperty('--custom-accent-dark', variants.dark)
html.classList.add('accent-custom')
} else if (this.selectedAccentColor === 'system') {
html.style.removeProperty('--custom-accent-light')
html.style.removeProperty('--custom-accent-dark')
html.classList.add('accent-pink')
} else {
html.style.removeProperty('--custom-accent-light')
html.style.removeProperty('--custom-accent-dark')
html.classList.add(`accent-${this.selectedAccentColor}`)
}
},
setSystemAccentColor(systemHex: string) {
if (!/^#[0-9a-fA-F]{6}$/.test(systemHex)) {
this.setSystemAccentUnavailable()
return
}
this.systemAccentColor = applySystemAccentHue(systemHex)
this.systemAccentSupported = true
if (this.selectedAccentColor === 'system') this.setAccentColor('system')
},
setSystemAccentUnavailable() {
this.systemAccentColor = null
this.systemAccentSupported = false
if (this.selectedAccentColor === 'system') this.setAccentColor('system')
},
setThemeClass() {
const html = document.getElementsByTagName('html')[0]
for (const theme of THEME_OPTIONS) {
html.classList.remove(`${theme}-mode`)
}
systemThemeMq?.removeEventListener('change', this.setThemeClass)
systemThemeMq = null
let theme = this.selectedTheme
if (this.selectedTheme === 'system') {
systemThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
systemThemeMq.addEventListener('change', this.setThemeClass)
theme = systemThemeMq.matches ? 'dark' : 'light'
}
html.classList.add(`${theme}-mode`)
},
/**
* Applies the transparent window mode to the root element. The palette
* lives on `body` in `global.scss`; this only supplies the toggle and the
* alpha every translucent surface is derived from.
*/
setTransparentBackgroundClass() {
const html = document.documentElement
html.classList.toggle('transparent-window', this.transparentBackground)
html.style.setProperty(
'--transparent-window-alpha',
`${Math.min(Math.max(this.transparentBackgroundOpacity, 0), 100)}%`,
)
},
getFeatureFlag(key: FeatureFlag) {
return this.featureFlags[key] ?? DEFAULT_FEATURE_FLAGS[key]
},
getThemeOptions() {
return THEME_OPTIONS
},
},
})