Web frontend: perf panel, cover warmup, incremental browse rendering

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 11:42:33 +02:00
parent 9fd106b757
commit 2db4368fc9
24 changed files with 1178 additions and 255 deletions

View File

@@ -0,0 +1,158 @@
/** Per-knob performance settings, shown under `?pi=1`. Stored in this browser's
* localStorage (see lib/perfSettings.ts) and applied by a reload, because the knobs are
* read once at module load. Nothing changes until "Speichern". */
import { useState } from "react";
import {
FULL,
PERF,
PI,
resetPerfSettings,
savePerfSettings,
type PerfSettings,
} from "../lib/perfSettings";
type BoolKey = { [K in keyof PerfSettings]: PerfSettings[K] extends boolean ? K : never }[keyof PerfSettings];
type NumKey = { [K in keyof PerfSettings]: PerfSettings[K] extends number ? K : never }[keyof PerfSettings];
const TOGGLES: [BoolKey, string, string][] = [
["ambience", "Animierter Hintergrund", "Canvas im Abspielbildschirm (größter CPU-Verbraucher)"],
["cardBlur", "Weichzeichner auf Karten", "Backdrop-Blur je Album-Karte und Titelzeile"],
["viewTransitions", "Übergänge", "Ein-/Ausblenden beim Öffnen von Gruppen und Alben"],
["decorativeAnimations", "Deko-Animationen", "Blasen im Zimmer, schwimmender Delfin"],
["cardShadows", "Schatten auf Karten", "Schlagschatten unter den Album-Karten"],
["tileShadows", "Schatten auf Kacheln", "Schlagschatten unter den Kacheln im Raster"],
["petAnimations", "Tippen: Tiere bewegen sich", "Aus: Tiere stehen still"],
["tippenBubbles", "Tippen: Blasen", "Aufsteigende Blasen im Hintergrund"],
["tippenViewTransitions", "Tippen: Übergänge", "Ein-/Ausblenden der Bildschirme"],
["serviceWorker", "Service Worker", "Aus: vorhandener Worker wird entfernt"],
["coverWarmup", "Cover vorladen", "Cover im Hintergrund dekodieren"],
];
const NUMBERS: [NumKey, string, string, number, number, number][] = [
["playbackClockFps", "Fortschrittsbalken: Updates/s", "0 = jeder Frame", 0, 60, 1],
["ambienceScale", "Hintergrund: Auflösung", "Faktor der Canvas-Größe", 0.1, 1, 0.05],
["ambienceMaxPx", "Hintergrund: max. Pixel", "Obergrenze je Achse", 256, 4096, 64],
["ambienceMaxFps", "Hintergrund: max. FPS", "0 = unbegrenzt", 0, 60, 1],
["ambienceMaxBubbles", "Hintergrund: max. Blasen", "0 = unbegrenzt", 0, 500, 5],
];
const pill = (background: string, color: string) =>
({
border: "none",
cursor: "pointer",
background,
color,
fontSize: 15,
fontWeight: 800,
padding: "11px 20px",
borderRadius: 999,
}) as const;
export function PerfPanel({ onClose }: { onClose: () => void }) {
const [draft, setDraft] = useState<PerfSettings>({ ...PERF });
const save = () => {
savePerfSettings(draft);
location.reload();
};
const reset = () => {
resetPerfSettings();
location.reload();
};
return (
<div className="overlay" style={{ zIndex: 7, background: "oklch(15% 0.03 210 / .75)" }}>
<div className="sheet" style={{ padding: 28, width: 480, maxHeight: "100%", overflow: "auto" }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ fontSize: 20, fontWeight: 900, color: "var(--ink)", flex: 1 }}>
Leistung
</div>
<button
onClick={onClose}
aria-label="Schließen"
style={{
...pill("oklch(88% 0.02 210)", "var(--ink)"),
fontSize: 18,
fontWeight: 900,
width: 36,
height: 36,
padding: 0,
}}
>
×
</button>
</div>
<div style={{ display: "flex", gap: 10, marginTop: 16 }}>
<button onClick={() => setDraft({ ...PI })} style={pill("oklch(88% 0.02 210)", "var(--ink)")}>
Pi-Vorgabe
</button>
<button onClick={() => setDraft({ ...FULL })} style={pill("oklch(88% 0.02 210)", "var(--ink)")}>
Alles an
</button>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 12, marginTop: 18 }}>
{TOGGLES.map(([key, label, hint]) => (
<label key={key} style={{ display: "flex", alignItems: "center", gap: 12 }}>
<input
type="checkbox"
checked={draft[key]}
onChange={(event) => setDraft({ ...draft, [key]: event.target.checked })}
style={{ width: 22, height: 22 }}
/>
<span style={{ display: "flex", flexDirection: "column", gap: 2 }}>
<span style={{ fontSize: 14, fontWeight: 800, color: "var(--ink)" }}>{label}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: "oklch(50% 0.03 210)" }}>
{hint}
</span>
</span>
</label>
))}
{NUMBERS.map(([key, label, hint, min, max, step]) => (
<label key={key} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<span style={{ fontSize: 14, fontWeight: 800, color: "var(--ink)" }}>{label}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: "oklch(50% 0.03 210)" }}>
{hint}
</span>
<input
type="number"
min={min}
max={max}
step={step}
value={draft[key]}
onChange={(event) => {
const value = event.target.valueAsNumber;
if (Number.isFinite(value)) setDraft({ ...draft, [key]: value });
}}
style={{
padding: "8px 12px",
borderRadius: 10,
border: "2px solid oklch(85% 0.02 210)",
fontSize: 16,
fontWeight: 700,
}}
/>
</label>
))}
<div style={{ display: "flex", gap: 10, marginTop: 6 }}>
<button onClick={save} style={pill("var(--accent)", "#fff")}>
Speichern &amp; neu laden
</button>
<button onClick={reset} style={pill("oklch(88% 0.02 210)", "var(--ink)")}>
Zurücksetzen
</button>
</div>
<div style={{ fontSize: 12, fontWeight: 600, color: "oklch(50% 0.03 210)", lineHeight: 1.5 }}>
Wird nur in diesem Browser gespeichert und gilt nur mit <code>?pi=1</code>.
</div>
</div>
</div>
</div>
);
}