chore(history): squash 67 commit(s) from 2026-09-25
- feat(accounts): persist device links with opaque hashed tokens, list and revoke endpoints - feat(frontend): app shell, routing and landing page with the chat-command hero - feat(frontend): Cyrillic-first fonts (Unbounded, Onest, JetBrains Mono); add i18next and motion - docs: free mod, bilingual site, one-click download, theme editor, public profiles, rich landing in plans - feat(accounts): internal gRPC AuthenticateDevice guarded by internal key - feat(frontend): ru/en i18n with typed per-feature dictionaries and language switch - feat(accounts): GET /me profile endpoint - feat(gateway): scaffold crate with config validation and health check - feat(gateway): reverse proxy to accounts and configs services - feat(gateway): resolve identity once from access JWT or device token via gRPC - feat(gateway): per-route and global rate limits with Retry-After - feat(gateway): CORS for the site origin; docs for gateway and internal contract - feat(configs): scaffold service with schema, config validation and health check - feat(configs): four config slots per account with list, get and save - feat(configs): permanent share codes with regenerate and public load-by-code - feat(accounts): GetPublicProfiles gRPC for showcase author info - style(accounts,common): apply rustfmt to existing sources - feat(configs): public showcase with publish, browse, detail and copy-to-slot - feat(backend): public profile endpoint and showcase author filter - fix(gateway): silence clippy collapsible-if and needless-ref warnings - docs(backend): configs-service implemented; Подсистема 1 backend complete - feat(mod): add Optimize module skeleton with OptimizeState holder - feat(mod): gate glass blur behind Optimize no_glass knob - feat(mod): cut MotionBlur and DoF sample counts behind lite_post knob - feat(mod): trim procedural sky noise behind lite_sky knob - feat(mod): drop fade gradients and digit rolls behind lean_hud knob - docs(todo): mark Optimize module phase 9.2 complete - refactor(mod): drop dead Renderer2D compatibility shims - refactor(mod): prune unreachable Renderer2D overload towers - refactor(mod): remove unused Renderer2D overloads and imports - docs(todo): mark Renderer2D giant-splitting done (2179 to 1597) - refactor(mod): extract shader id constants from LoVisualRenderPipelines - docs(todo): record registry wave 2026-09-25 (Renderer2D, pipelines) - refactor(mod): move Renderer2D instance state into base class - refactor(mod): extract Renderer2DRounded drawing family - refactor(mod): extract Renderer2DPath connector and chamfer family - refactor(mod): extract Renderer2DShapes circle line and texture primitives - refactor(mod): extract Renderer2DGlass and Renderer2DItem families - refactor(mod): prune Renderer2D imports after facade split - docs(todo): record Renderer2D facade inheritance split (1597 to 475) - docs: easter eggs — .env honeypot, konami troll mode, devtools banner, IDDQD config, breakable 404 block, 418 teapot - feat(mod): introduce surface style system core (SurfaceStyle, StyleSpec, StyleConfig, SurfaceRenderer) - refactor(mod): delegate HudRenderUtil liquid glass draws to SurfaceRenderer (dedupe glass constants) - refactor(mod): route bespoke glass call sites through SurfaceRenderer.plateSpec - feat(mod): add Auto option to HUD bg effects via shared HudBgStyles resolution - feat(mod): flat fallback for no-glass optimize mode and persist global HUD config - feat(mod): default HUD bg effects to Auto so the global surface style drives widgets - feat(mod): add global cycle-style hotkey with surface style notification - feat(mod): add surface style swatch strip under the global style picker - feat(gateway): reject ambiguous paths and answer .env probes with a honeypot - fix(gateway): charge failed credentials against the rate limit, allow stale ones on /auth - feat(frontend): ClickGui theme pipeline generated from the mod, live site theming - feat(frontend): landing v2 hero — voxel/particle backdrop, live ClickGui, theme strip - docs(todo): drop the FPS A/B measurement from phase 9.3, close phase 9 - feat(gateway): answer /coffee with a 418 teapot - feat(frontend): land the rest of landing v2 — HUD, module wall, showcase, FAQ, footer - feat(frontend): one-click download from GitHub releases, changelog page, release CI - feat(frontend): theme editor with live ClickGui preview, mod-compatible export and share links - fix(frontend): landing HUD playground now shows real mod widgets (fps, coordinates, module list, keybinds, ping) - style(frontend): apply ClickGui glass effect to landing HUD playground widgets - fix(frontend): prevent color field row overflow in theme editor grid - fix(frontend): never attach stale bearer token to /auth/* requests - fix(configs): unpublish/publish can no longer bypass moderation - refactor(accounts): shrink auth/handlers.rs under the 250-line cap - fix(accounts): tolerate concurrent refresh without killing every session - fix(gateway): minor hardening from the backend review - feat(configs): IDDQD easter egg config
This commit is contained in:
parent
72bc4c7148
commit
7f4b532f99
257 changed files with 13085 additions and 6582 deletions
165
frontend/scripts/extract-themes.ts
Normal file
165
frontend/scripts/extract-themes.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
// Generates src/features/themes/presets.generated.ts from the mod's built-in theme presets.
|
||||
// Usage: bun run gen:themes
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const THEME_DIR = resolve(here, '../../mod/src/main/java/dev/loki/lovisual/features/theme')
|
||||
const PRESET_FILES = [1, 2, 3].map((n) => resolve(THEME_DIR, `presets/ThemePresets${n}.java`))
|
||||
const OUT = resolve(here, '../src/features/themes/presets.generated.ts')
|
||||
|
||||
const COLOR_KEYS = [
|
||||
'windowBg', 'windowHeader', 'windowStroke', 'surface', 'surfaceHover', 'cardEnabled',
|
||||
'cardDisabled', 'textPrimary', 'textMuted', 'accent', 'accentSoft', 'strokeSoft',
|
||||
] as const
|
||||
const GRADIENT_KEYS = ['windowGradient', 'headerGradient', 'surfaceGradient', 'cardGradient', 'strokeGradient'] as const
|
||||
|
||||
type Colors = Record<(typeof COLOR_KEYS)[number], number>
|
||||
type Gradient = { enabled: boolean; start: number; end: number; angle: number }
|
||||
|
||||
const stripComments = (src: string) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '')
|
||||
|
||||
/** Splits a comma-separated argument list, respecting nested parentheses. */
|
||||
function splitArgs(args: string): string[] {
|
||||
const out: string[] = []
|
||||
let depth = 0
|
||||
let cur = ''
|
||||
for (const ch of args) {
|
||||
if (ch === '(') depth++
|
||||
if (ch === ')') depth--
|
||||
if (ch === ',' && depth === 0) {
|
||||
out.push(cur.trim())
|
||||
cur = ''
|
||||
} else cur += ch
|
||||
}
|
||||
if (cur.trim()) out.push(cur.trim())
|
||||
return out
|
||||
}
|
||||
|
||||
/** Finds `static final <type> NAME = new <type>(...)` declarations and returns their raw argument lists. */
|
||||
function declarations(src: string, type: string): Map<string, string[]> {
|
||||
const found = new Map<string, string[]>()
|
||||
const re = new RegExp(`static\\s+final\\s+${type}\\s+(\\w+)\\s*=\\s*new\\s+${type}\\s*\\(`, 'g')
|
||||
for (let m = re.exec(src); m; m = re.exec(src)) {
|
||||
let depth = 1
|
||||
let i = re.lastIndex
|
||||
for (; depth > 0; i++) {
|
||||
if (src[i] === '(') depth++
|
||||
if (src[i] === ')') depth--
|
||||
}
|
||||
found.set(m[1], splitArgs(src.slice(re.lastIndex, i - 1)))
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
const parseInt32 = (lit: string) => Number.parseInt(lit.replace(/^0x/i, ''), 16) >>> 0
|
||||
|
||||
// Port of ThemesBlending.adjustBrightness (Java Math.round(float) == floor(x + 0.5)).
|
||||
function adjustBrightness(argb: number, delta: number): number {
|
||||
const ch = (v: number) => {
|
||||
const moved = v + Math.floor((delta >= 0 ? 255 - v : v) * delta + 0.5)
|
||||
return Math.min(255, Math.max(0, moved))
|
||||
}
|
||||
const a = (argb >>> 24) & 0xff
|
||||
return ((a << 24) | (ch((argb >>> 16) & 0xff) << 16) | (ch((argb >>> 8) & 0xff) << 8) | ch(argb & 0xff)) >>> 0
|
||||
}
|
||||
|
||||
function resolveColor(expr: string, themes: Map<string, Colors>): number {
|
||||
if (/^0x[0-9a-f]+$/i.test(expr)) return parseInt32(expr)
|
||||
const ref = /^(\w+)\.(\w+)\(\)$/.exec(expr)
|
||||
const colors = ref ? themes.get(ref[1]) : undefined
|
||||
if (!ref || !colors || !(ref[2] in colors)) throw new Error(`Cannot resolve color: ${expr}`)
|
||||
return colors[ref[2] as keyof Colors]
|
||||
}
|
||||
|
||||
function parseGradient(expr: string, themes: Map<string, Colors>): Gradient {
|
||||
const call = /^(?:Themes\.)?(flatGradient|subtleHeaderGradient)\((.+)\)$/.exec(expr)
|
||||
if (call) {
|
||||
const color = resolveColor(call[2].trim(), themes)
|
||||
if (call[1] === 'flatGradient') return { enabled: false, start: color, end: color, angle: 90 }
|
||||
return { enabled: true, start: adjustBrightness(color, 0.08), end: adjustBrightness(color, -0.06), angle: 90 }
|
||||
}
|
||||
const spec = /^new\s+GradientSpec\((.+)\)$/.exec(expr)
|
||||
if (!spec) throw new Error(`Unknown gradient expression: ${expr}`)
|
||||
const [enabled, start, end, angle] = splitArgs(spec[1])
|
||||
return {
|
||||
enabled: enabled === 'true',
|
||||
start: resolveColor(start, themes),
|
||||
end: resolveColor(end, themes),
|
||||
angle: Number.parseFloat(angle.replace(/f$/i, '')),
|
||||
}
|
||||
}
|
||||
|
||||
const hex = (argb: number) => `#${argb.toString(16).toUpperCase().padStart(8, '0')}`
|
||||
|
||||
export type ParsedEntry = { const: string; id: string; name: string; builtin: boolean; colors: Colors; gradients: Gradient[] }
|
||||
|
||||
/** Parses the preset sources (and Themes.java for id constants and display order). */
|
||||
export function parsePresets(presetSources: string[], themesSource: string): ParsedEntry[] {
|
||||
const themesJava = stripComments(themesSource)
|
||||
const ids = new Map<string, string>()
|
||||
for (const m of themesJava.matchAll(/static\s+final\s+String\s+(THEME_\w+)\s*=\s*"([^"]+)"/g)) ids.set(m[1], m[2])
|
||||
|
||||
const entries: ParsedEntry[] = []
|
||||
for (const raw of presetSources) {
|
||||
const src = stripComments(raw)
|
||||
const themes = new Map<string, Colors>()
|
||||
for (const [name, args] of declarations(src, 'Theme')) {
|
||||
if (args.length !== COLOR_KEYS.length) throw new Error(`Theme ${name}: expected 12 colors, got ${args.length}`)
|
||||
themes.set(name, Object.fromEntries(COLOR_KEYS.map((k, i) => [k, parseInt32(args[i])])) as Colors)
|
||||
}
|
||||
for (const [constName, args] of declarations(src, 'ThemeEntry')) {
|
||||
const [idExpr, nameExpr, builtin, themeRef, ...grads] = args
|
||||
const idConst = idExpr.replace(/^Themes\./, '')
|
||||
const colors = themes.get(themeRef)
|
||||
if (!colors) throw new Error(`ThemeEntry ${constName}: unknown theme ${themeRef}`)
|
||||
entries.push({
|
||||
const: constName,
|
||||
id: ids.get(idConst) ?? idExpr.replace(/^"|"$/g, ''),
|
||||
name: nameExpr.replace(/^"|"$/g, ''),
|
||||
builtin: builtin === 'true',
|
||||
colors,
|
||||
gradients: grads.map((g) => parseGradient(g, themes)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Follow the mod's picker order (Themes.PRESET_LIST) where available.
|
||||
const order = /PRESET_LIST\s*=\s*List\.of\(([\s\S]*?)\);/.exec(themesJava)?.[1] ?? ''
|
||||
const rank = (e: ParsedEntry) => {
|
||||
const i = order.indexOf(`.${e.const}`)
|
||||
return i < 0 ? Number.MAX_SAFE_INTEGER : i
|
||||
}
|
||||
return entries.map((e, i) => ({ e, i })).sort((a, b) => rank(a.e) - rank(b.e) || a.i - b.i).map(({ e }) => e)
|
||||
}
|
||||
|
||||
export function render(entries: ParsedEntry[]): string {
|
||||
const body = entries.map((e) => {
|
||||
const theme = COLOR_KEYS.map((k) => ` ${k}: '${hex(e.colors[k])}',`).join('\n')
|
||||
const grads = GRADIENT_KEYS.map((k, i) => {
|
||||
const g = e.gradients[i]
|
||||
return ` ${k}: { enabled: ${g.enabled}, start: '${hex(g.start)}', end: '${hex(g.end)}', angle: ${g.angle} },`
|
||||
}).join('\n')
|
||||
return ` {\n id: '${e.id}',\n name: '${e.name}',\n builtin: ${e.builtin},\n theme: {\n${theme}\n },\n${grads}\n },`
|
||||
})
|
||||
return [
|
||||
'// generated — do not edit. Source: mod/.../features/theme/presets/ThemePresets{1,2,3}.java',
|
||||
'// Regenerate with `bun run gen:themes`.',
|
||||
"import type { ThemeEntry } from './types'",
|
||||
'',
|
||||
'export const PRESETS: ThemeEntry[] = [',
|
||||
...body,
|
||||
']',
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const entries = parsePresets(
|
||||
PRESET_FILES.map((f) => readFileSync(f, 'utf8')),
|
||||
readFileSync(resolve(THEME_DIR, 'Themes.java'), 'utf8'),
|
||||
)
|
||||
writeFileSync(OUT, render(entries))
|
||||
console.log(`Wrote ${entries.length} presets to ${OUT}`)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue