refactor: redesign plugin panel layout

Consolidate 7 flat sections into 5 logical groups with clear hierarchy:
1. Recording (primary action)
2. Account (QR pairing + manual key + test, with readiness indicator)
3. MangoHud Setup (merge config write/verify + launch option + numbered guide)
4. System (MangoHud check + game/app info)
5. Advanced (server, paths, hardware, config backup)

Add reusable helpers (SectionHint, StatusRow, InfoRow, CodeBlock, BtnContent)
for consistent styling. Improve recording timer with radial glow, error/success
states with colored backgrounds, and recent recordings as cards.
This commit is contained in:
2026-06-28 22:12:57 +08:00
parent 1de08ed4ec
commit 02553445b1
+325 -199
View File
@@ -21,6 +21,10 @@ import {
FaSearch, FaSearch,
FaQrcode, FaQrcode,
FaLink, FaLink,
FaMicrochip,
FaGamepad,
FaServer,
FaWrench,
} from "react-icons/fa" } from "react-icons/fa"
import type { RecordingState, SessionData, RecentSession, PluginSettings } from "../lib/store" import type { RecordingState, SessionData, RecentSession, PluginSettings } from "../lib/store"
import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared" import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared"
@@ -58,16 +62,85 @@ const HARDWARE_OPTIONS = [
...KNOWN_HARDWARE_SLUGS.map((slug) => ({ label: slug, data: slug })), ...KNOWN_HARDWARE_SLUGS.map((slug) => ({ label: slug, data: slug })),
] ]
// ── Reusable layout helpers ─────────────────────────────────────
// Small, consistent building blocks so every section looks uniform.
const COLORS = {
ok: "#2ecc71",
err: "#e74c3c",
accent: "#1b9bf3",
muted: "rgba(255,255,255,0.55)",
panel: "rgba(255,255,255,0.06)",
border: "rgba(255,255,255,0.10)",
}
/** Muted intro line shown directly under a PanelSection title. */
function SectionHint({ children }: { children: React.ReactNode }) {
return (
<PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "2px 0 6px 0", lineHeight: "1.45", opacity: 0.6 }}>
{children}
</div>
</PanelSectionRow>
)
}
/** A labelled status row with a colored icon: ✓/✗ + label + value. */
function StatusRow({
ok,
label,
value,
}: {
ok: boolean | null
label: string
value?: string
}) {
const icon = ok === null ? null : ok ? <FaCheck style={{ color: COLORS.ok }} /> : <FaTimes style={{ color: COLORS.err }} />
return (
<PanelSectionRow>
<div className={staticClasses.Text} style={{ display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", padding: "3px 0" }}>
{icon}
<span style={{ opacity: 0.7 }}>{label}</span>
{value && <span style={{ fontWeight: 600 }}>{value}</span>}
</div>
</PanelSectionRow>
)
}
/** A labelled key/value row used for system info (Game, App ID, etc). */
function InfoRow({ icon, label, value }: { icon: React.ReactNode; label: string; value: React.ReactNode }) {
return (
<PanelSectionRow>
<div className={staticClasses.Text} style={{ display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", padding: "3px 0" }}>
<span style={{ opacity: 0.55, display: "flex", alignItems: "center" }}>{icon}</span>
<span style={{ opacity: 0.7, flexShrink: 0 }}>{label}</span>
<span style={{ fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{value}</span>
</div>
</PanelSectionRow>
)
}
/** Standard centered icon+label content for ButtonItem. */
function BtnContent({ icon, label }: { icon: React.ReactNode; label: string }) {
return (
<div style={{ display: "flex", alignItems: "center", gap: "8px", justifyContent: "center" }}>
{icon}
{label}
</div>
)
}
/** A numbered setup step with a circular badge. */
function SetupStep({ number, title, body }: { number: number; title: string; body: string }) { function SetupStep({ number, title, body }: { number: number; title: string; body: string }) {
return ( return (
<PanelSectionRow> <PanelSectionRow>
<div style={{ display: "flex", gap: "10px", padding: "6px 0", alignItems: "flex-start" }}> <div style={{ display: "flex", gap: "10px", padding: "5px 0", alignItems: "flex-start" }}>
<div style={{ <div style={{
flexShrink: 0, flexShrink: 0,
width: "22px", width: "22px",
height: "22px", height: "22px",
borderRadius: "50%", borderRadius: "50%",
background: "#1b9bf3", background: COLORS.accent,
color: "white", color: "white",
fontSize: "12px", fontSize: "12px",
fontWeight: 700, fontWeight: 700,
@@ -86,6 +159,33 @@ function SetupStep({ number, title, body }: { number: number; title: string; bod
) )
} }
/** Monospace code block with optional copy button. */
function CodeBlock({ value, copied, onCopy }: { value: string; copied: boolean; onCopy: () => void }) {
return (
<>
<PanelSectionRow>
<div style={{
background: COLORS.panel,
borderRadius: "8px",
padding: "10px 14px",
fontFamily: "monospace",
fontSize: "13px",
textAlign: "center",
wordBreak: "break-all",
border: `1px solid ${COLORS.border}`,
}}>
{value}
</div>
</PanelSectionRow>
<PanelSectionRow>
<ButtonItem layout="below" onClick={onCopy}>
<BtnContent icon={<FaCopy />} label={copied ? "Copied to clipboard" : "Copy"} />
</ButtonItem>
</PanelSectionRow>
</>
)
}
export default function MainPanel({ export default function MainPanel({
recordingState, recordingState,
session, session,
@@ -197,6 +297,10 @@ export default function MainPanel({
async function handleWriteConfig() { async function handleWriteConfig() {
const result = await writeMangohudConfig() const result = await writeMangohudConfig()
setConfigWritten(result.success) setConfigWritten(result.success)
if (result.success) {
// Reset verify state since config changed
setConfigVerified({ checked: false, valid: false, message: "" })
}
} }
async function handleVerifyConfig() { async function handleVerifyConfig() {
@@ -208,7 +312,6 @@ export default function MainPanel({
const content = result.content const content = result.content
const hasOutputFolder = content.includes("output_folder=/tmp") const hasOutputFolder = content.includes("output_folder=/tmp")
const hasFps = content.includes("fps") const hasFps = content.includes("fps")
const hasFrameTiming = content.includes("frame_timing")
if (hasOutputFolder && hasFps) { if (hasOutputFolder && hasFps) {
setConfigVerified({ checked: true, valid: true, message: "Config looks good" }) setConfigVerified({ checked: true, valid: true, message: "Config looks good" })
} else { } else {
@@ -303,6 +406,10 @@ export default function MainPanel({
return () => stopPairPolling() return () => stopPairPolling()
}, []) }, [])
// ── Derived readiness flags ─────────────────────────────
const hasApiKey = !!settings.apiKey
const isLinked = pairState.status === "linked" || hasApiKey
// ── Stopped state: show the session form ────────────────────── // ── Stopped state: show the session form ──────────────────────
if (recordingState === "stopped") { if (recordingState === "stopped") {
return ( return (
@@ -320,16 +427,30 @@ export default function MainPanel({
return ( return (
<> <>
{/* ── Recording ──────────────────────────────────────────── */} {/* ════════════════════════════════════════════════════════
1. RECORDING — the primary action, always first
════════════════════════════════════════════════════════ */}
<PanelSection title="Recording"> <PanelSection title="Recording">
{error && ( {error && (
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ color: "#e74c3c", padding: "8px" }}>{error}</div> <div className={staticClasses.Text} style={{
color: COLORS.err,
padding: "8px 10px",
fontSize: "12px",
background: "rgba(231,76,60,0.10)",
borderRadius: "6px",
border: `1px solid rgba(231,76,60,0.25)`,
}}>
{error}
</div>
</PanelSectionRow> </PanelSectionRow>
)} )}
{recordingState === "idle" && ( {recordingState === "idle" && (
<> <>
<SectionHint>
Enter the game name (or let it auto-detect), then start recording once you're in-game.
</SectionHint>
<PanelSectionRow> <PanelSectionRow>
<TextField <TextField
label="Game Name" label="Game Name"
@@ -340,10 +461,7 @@ export default function MainPanel({
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<ButtonItem layout="below" onClick={onStart}> <ButtonItem layout="below" onClick={onStart}>
<div style={{ display: "flex", alignItems: "center", gap: "8px", justifyContent: "center" }}> <BtnContent icon={<FaPlay />} label="Start Recording" />
<FaPlay />
Start Recording
</div>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
</> </>
@@ -352,112 +470,109 @@ export default function MainPanel({
{recordingState === "recording" && ( {recordingState === "recording" && (
<> <>
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ padding: "8px 0", textAlign: "center" }}> <div className={staticClasses.Text} style={{
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: "6px", marginBottom: "4px" }}> padding: "14px 0",
<FaClock /> textAlign: "center",
<strong>{formatTime(elapsed)}</strong> background: `radial-gradient(circle at center, ${COLORS.err}22 0%, transparent 70%)`,
borderRadius: "10px",
}}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: "8px", marginBottom: "4px" }}>
<FaClock style={{ color: COLORS.err }} />
<strong style={{ fontSize: "22px", fontFamily: "monospace" }}>{formatTime(elapsed)}</strong>
</div> </div>
<div style={{ fontSize: "13px", opacity: 0.7 }}> <div style={{ fontSize: "13px", opacity: 0.7 }}>
{session.gameName ? `Recording: ${session.gameName}` : "Recording..."} {session.gameName ? session.gameName : "Recording"}
</div> </div>
</div> </div>
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<ButtonItem layout="below" onClick={onStop}> <ButtonItem layout="below" onClick={onStop}>
<div style={{ display: "flex", alignItems: "center", gap: "8px", justifyContent: "center" }}> <BtnContent icon={<FaStop />} label="Stop Recording" />
<FaStop />
Stop Recording
</div>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
</> </>
)} )}
{recentSessions.length > 0 && recordingState === "idle" && ( {recentSessions.length > 0 && recordingState === "idle" && (
<PanelSection title="Recent Recordings"> <>
<PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "11px", opacity: 0.5, padding: "10px 0 4px 0", textTransform: "uppercase", letterSpacing: "0.05em" }}>
Recent
</div>
</PanelSectionRow>
{recentSessions.map((rs, i) => ( {recentSessions.map((rs, i) => (
<PanelSectionRow key={i}> <PanelSectionRow key={i}>
<div className={staticClasses.Text} style={{ padding: "4px 0", fontSize: "13px" }}> <div className={staticClasses.Text} style={{
<strong>{rs.gameName || "Unknown game"}</strong> padding: "6px 10px",
<br /> fontSize: "13px",
<span style={{ opacity: 0.6 }}> background: COLORS.panel,
borderRadius: "6px",
border: `1px solid ${COLORS.border}`,
}}>
<div style={{ fontWeight: 600 }}>{rs.gameName || "Unknown game"}</div>
<div style={{ fontSize: "11px", opacity: 0.6, marginTop: "2px" }}>
{rs.fpsAvg ? `${rs.fpsAvg} FPS avg` : "No data"} · {new Date(rs.date).toLocaleDateString()} {rs.fpsAvg ? `${rs.fpsAvg} FPS avg` : "No data"} · {new Date(rs.date).toLocaleDateString()}
</span> </div>
</div> </div>
</PanelSectionRow> </PanelSectionRow>
))} ))}
</PanelSection> </>
)} )}
</PanelSection> </PanelSection>
{/* ── Status ──────────────────────────────────────────────── */} {/* ════════════════════════════════════════════════════════
<PanelSection title="Status"> 2. ACCOUNT — pairing + API key (prerequisite for upload)
<PanelSectionRow> ════════════════════════════════════════════════════════ */}
<ButtonItem layout="below" onClick={handleCheckMangohud}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<FaCog />
Check MangoHud Status
</div>
</ButtonItem>
</PanelSectionRow>
{mangohudStatus.checked && (
<PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "13px", padding: "4px 0" }}>
{mangohudStatus.installed ? (
<><FaCheck style={{ color: "#2ecc71" }} /> MangoHud {mangohudStatus.version}</>
) : (
<><FaTimes style={{ color: "#e74c3c" }} /> MangoHud not found</>
)}
</div>
</PanelSectionRow>
)}
{session.gameName && (
<PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "13px", padding: "4px 0" }}>
<strong>Game:</strong> {session.gameName}
{session.appId && <> <strong>App ID:</strong> {session.appId}</>}
</div>
</PanelSectionRow>
)}
<PanelSectionRow>
<ButtonItem layout="below" onClick={handleTestKey} disabled={keyTestStatus === "testing"}>
{keyTestStatus === "testing" ? "Testing..." : "Test API Key"}
{keyTestStatus === "valid" && <FaCheck style={{ color: "#2ecc71", marginLeft: "8px" }} />}
{keyTestStatus === "invalid" && <FaTimes style={{ color: "#e74c3c", marginLeft: "8px" }} />}
</ButtonItem>
</PanelSectionRow>
{keyTestMessage && (
<PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "12px", color: keyTestStatus === "valid" ? "#2ecc71" : "#e74c3c", padding: "4px 0" }}>
{keyTestMessage}
</div>
</PanelSectionRow>
)}
</PanelSection>
{/* ── Account ─────────────────────────────────────────── */}
<PanelSection title="Account"> <PanelSection title="Account">
{/* Readiness indicator */}
<StatusRow ok={isLinked ? true : null} label="Plugin" value={isLinked ? "Linked" : "Not linked"} />
{pairState.status === "idle" && ( {pairState.status === "idle" && (
<> <>
<SectionHint>
Scan a QR code with your phone to link your DeckyVault account no manual key entry needed.
</SectionHint>
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "4px 0", lineHeight: "1.5", opacity: 0.7 }}> <ButtonItem layout="below" onClick={handleStartPairing}>
Link this plugin to your DeckyVault account by scanning a QR code with your phone no manual key entry needed. <BtnContent icon={<FaQrcode />} label="Pair with Phone" />
</ButtonItem>
</PanelSectionRow>
{/* Manual key entry + test */}
<PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "11px", opacity: 0.5, padding: "8px 0 2px 0", textTransform: "uppercase", letterSpacing: "0.05em" }}>
Or paste a key manually
</div> </div>
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<ButtonItem layout="below" onClick={handleStartPairing}> <TextField
<div style={{ display: "flex", alignItems: "center", gap: "8px", justifyContent: "center" }}> label="API Key"
<FaQrcode /> value={settings.apiKey}
Pair with Phone onChange={(e) => onUpdateSetting("apiKey", e.target.value)}
</div> placeholder="dv_..."
bIsPassword
/>
</PanelSectionRow>
<PanelSectionRow>
<ButtonItem layout="below" onClick={handleTestKey} disabled={keyTestStatus === "testing"}>
<BtnContent
icon={keyTestStatus === "testing" ? <FaClock /> : keyTestStatus === "valid" ? <FaCheck /> : keyTestStatus === "invalid" ? <FaTimes /> : <FaLink />}
label={keyTestStatus === "testing" ? "Testing…" : "Test API Key"}
/>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
{keyTestMessage && (
<PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "12px", color: keyTestStatus === "valid" ? COLORS.ok : COLORS.err, padding: "3px 0" }}>
{keyTestMessage}
</div>
</PanelSectionRow>
)}
</> </>
)} )}
{pairState.status === "starting" && ( {pairState.status === "starting" && (
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ padding: "12px 0", textAlign: "center", fontSize: "13px", opacity: 0.7 }}> <div className={staticClasses.Text} style={{ padding: "16px 0", textAlign: "center", fontSize: "13px", opacity: 0.7 }}>
Starting pairing session Starting pairing session
</div> </div>
</PanelSectionRow> </PanelSectionRow>
@@ -465,13 +580,9 @@ export default function MainPanel({
{(pairState.status === "showing-qr" || pairState.status === "polling") && ( {(pairState.status === "showing-qr" || pairState.status === "polling") && (
<> <>
<SectionHint>Scan with your phone camera, then tap Confirm on the page.</SectionHint>
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "4px 0", lineHeight: "1.5", opacity: 0.8 }}> <div style={{ display: "flex", justifyContent: "center", padding: "12px", background: "#fff", borderRadius: "12px" }}>
Scan this code with your phone's camera, then confirm on the page that opens.
</div>
</PanelSectionRow>
<PanelSectionRow>
<div style={{ display: "flex", justifyContent: "center", padding: "12px 0", background: "#fff", borderRadius: "12px" }}>
<QRCodeSVG value={pairState.qrUrl} size={180} level="M" /> <QRCodeSVG value={pairState.qrUrl} size={180} level="M" />
</div> </div>
</PanelSectionRow> </PanelSectionRow>
@@ -482,10 +593,7 @@ export default function MainPanel({
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<ButtonItem layout="below" onClick={handleCancelPairing}> <ButtonItem layout="below" onClick={handleCancelPairing}>
<div style={{ display: "flex", alignItems: "center", gap: "8px", justifyContent: "center" }}> <BtnContent icon={<FaTimes />} label="Cancel" />
<FaTimes />
Cancel
</div>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
</> </>
@@ -494,21 +602,26 @@ export default function MainPanel({
{pairState.status === "linked" && ( {pairState.status === "linked" && (
<> <>
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "13px", color: "#2ecc71", padding: "4px 0", textAlign: "center" }}> <div className={staticClasses.Text} style={{
<FaCheck /> Plugin linked to your account! fontSize: "13px",
color: COLORS.ok,
padding: "10px",
textAlign: "center",
background: "rgba(46,204,113,0.10)",
borderRadius: "8px",
border: "1px solid rgba(46,204,113,0.25)",
}}>
<FaCheck /> Linked to your account
</div> </div>
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "11px", opacity: 0.6, padding: "4px 0", textAlign: "center" }}> <div className={staticClasses.Text} style={{ fontSize: "11px", opacity: 0.6, padding: "2px 0", textAlign: "center" }}>
API key saved. You can now upload performance entries. API key saved. You can now upload entries.
</div> </div>
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<ButtonItem layout="below" onClick={handleCancelPairing}> <ButtonItem layout="below" onClick={handleCancelPairing}>
<div style={{ display: "flex", alignItems: "center", gap: "8px", justifyContent: "center" }}> <BtnContent icon={<FaLink />} label="Done" />
<FaLink />
Done
</div>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
</> </>
@@ -517,106 +630,122 @@ export default function MainPanel({
{pairState.status === "error" && ( {pairState.status === "error" && (
<> <>
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "12px", color: "#e74c3c", padding: "4px 0" }}> <div className={staticClasses.Text} style={{
<FaTimes /> {pairState.error} fontSize: "12px",
color: COLORS.err,
padding: "8px 10px",
background: "rgba(231,76,60,0.10)",
borderRadius: "6px",
border: "1px solid rgba(231,76,60,0.25)",
}}>
{pairState.error}
</div> </div>
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<ButtonItem layout="below" onClick={handleStartPairing}> <ButtonItem layout="below" onClick={handleStartPairing}>
<div style={{ display: "flex", alignItems: "center", gap: "8px", justifyContent: "center" }}> <BtnContent icon={<FaQrcode />} label="Try Again" />
<FaQrcode />
Try Again
</div>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<ButtonItem layout="below" onClick={handleCancelPairing}> <ButtonItem layout="below" onClick={handleCancelPairing}>
<div style={{ display: "flex", alignItems: "center", gap: "8px", justifyContent: "center" }}> <BtnContent icon={<FaTimes />} label="Dismiss" />
Dismiss
</div>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
</> </>
)} )}
</PanelSection> </PanelSection>
{/* ── Usage Instructions ──────────────────────────────────── */} {/* ════════════════════════════════════════════════════════
<PanelSection title="Usage Instructions"> 3. MANGOHUD SETUP — config + launch option + guide,
<PanelSectionRow> all in one coherent section
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "4px 0", lineHeight: "1.5" }}> ════════════════════════════════════════════════════════ */}
Add this to your game's Steam launch options, then launch the game. Press Start Recording once you're in-game and ready to benchmark. <PanelSection title="MangoHud Setup">
</div> <SectionHint>Follow these steps once to enable performance logging.</SectionHint>
</PanelSectionRow>
<PanelSectionRow>
<div style={{
background: "rgba(255,255,255,0.1)",
borderRadius: "8px",
padding: "10px 14px",
fontFamily: "monospace",
fontSize: "14px",
textAlign: "center",
}}>
~/deckyvault-mangohud.sh %command%
</div>
</PanelSectionRow>
<PanelSectionRow>
<ButtonItem layout="below" onClick={handleCopyLaunchOption}>
<div style={{ display: "flex", alignItems: "center", gap: "8px", justifyContent: "center" }}>
<FaCopy />
{copiedLaunchOpt ? "Copied to clipboard" : "Copy Launch Option"}
</div>
</ButtonItem>
</PanelSectionRow>
<PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "11px", opacity: 0.6, padding: "4px 0" }}>
Config stored in ~/.config/MangoHud/MangoHud.conf
</div>
</PanelSectionRow>
</PanelSection>
{/* ── MangoHud Config ────────────────────────────────────── */} <SetupStep number={1} title="Write the Config" body="Creates the logging config and wrapper script automatically." />
<PanelSection title="MangoHud Config">
<PanelSectionRow> <PanelSectionRow>
<ButtonItem layout="below" onClick={handleWriteConfig}> <ButtonItem layout="below" onClick={handleWriteConfig}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}> <BtnContent icon={<FaDownload />} label="Write Config" />
<FaDownload />
Write Config
</div>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
{configWritten && ( {configWritten && (
<PanelSectionRow> <StatusRow ok={true} label="Config" value="written" />
<div className={staticClasses.Text} style={{ fontSize: "12px", color: "#2ecc71", padding: "4px 0" }}>
<FaCheck /> Config written
</div>
</PanelSectionRow>
)} )}
<SetupStep number={2} title="Add the Launch Option" body="Steam → right-click game → Properties → Launch Options, then paste:" />
<CodeBlock value="~/deckyvault-mangohud.sh %command%" copied={copiedLaunchOpt} onCopy={handleCopyLaunchOption} />
<SetupStep number={3} title="Launch & Record" body="Start the game from Steam, then press Start Recording once in-game." />
{/* Verify + config details */}
<PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "11px", opacity: 0.5, padding: "10px 0 2px 0", textTransform: "uppercase", letterSpacing: "0.05em" }}>
Verify
</div>
</PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<ButtonItem layout="below" onClick={handleVerifyConfig}> <ButtonItem layout="below" onClick={handleVerifyConfig}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}> <BtnContent icon={<FaSearch />} label="Verify Config" />
<FaSearch />
Verify Config
</div>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
{configVerified.checked && ( {configVerified.checked && (
<PanelSectionRow> <StatusRow ok={configVerified.valid} label="" value={configVerified.message} />
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "4px 0", color: configVerified.valid ? "#2ecc71" : "#e74c3c" }}> )}
{configVerified.valid ? <FaCheck /> : <FaTimes />} {configVerified.message}
</div> <PanelSectionRow>
</PanelSectionRow> <div className={staticClasses.Text} style={{
fontSize: "11px",
padding: "10px 0 0 0",
lineHeight: "1.5",
opacity: 0.5,
borderTop: `1px solid ${COLORS.border}`,
marginTop: "8px",
}}>
Config stored in <span style={{ fontFamily: "monospace", opacity: 0.8 }}>~/.config/MangoHud/MangoHud.conf</span>.
Steam Deck has MangoHud pre-installed; on other Linux, install via <span style={{ fontFamily: "monospace", opacity: 0.8 }}>apt</span> or Flatpak.
</div>
</PanelSectionRow>
</PanelSection>
{/* ════════════════════════════════════════════════════════
4. SYSTEM — environment / detection status
════════════════════════════════════════════════════════ */}
<PanelSection title="System">
<SectionHint>Detect your hardware, OS, and the currently running game.</SectionHint>
<PanelSectionRow>
<ButtonItem layout="below" onClick={handleCheckMangohud}>
<BtnContent icon={<FaCog />} label="Check MangoHud" />
</ButtonItem>
</PanelSectionRow>
{mangohudStatus.checked && (
<StatusRow
ok={mangohudStatus.installed}
label="MangoHud"
value={mangohudStatus.installed ? mangohudStatus.version : "not found"}
/>
)}
{session.gameName && (
<InfoRow icon={<FaGamepad />} label="Game" value={session.gameName} />
)}
{session.appId && (
<InfoRow icon={<FaMicrochip />} label="App ID" value={String(session.appId)} />
)} )}
</PanelSection> </PanelSection>
{/* ── Configuration ────────────────────────────────────────── */} {/* ════════════════════════════════════════════════════════
<PanelSection title="Configuration"> 5. ADVANCED — server, paths, hardware, config portability
════════════════════════════════════════════════════════ */}
<PanelSection title="Advanced">
<SectionHint>Change these only if you need a custom server or export location.</SectionHint>
<PanelSectionRow> <PanelSectionRow>
<TextField <TextField
label="API Key" label="Server URL"
value={settings.apiKey} value={settings.baseUrl}
onChange={(e) => onUpdateSetting("apiKey", e.target.value)} onChange={(e) => onUpdateSetting("baseUrl", e.target.value)}
placeholder="dv_..." placeholder="https://deckyvault.xyz"
bIsPassword
/> />
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
@@ -627,14 +756,6 @@ export default function MainPanel({
placeholder="/home/deck/Downloads" placeholder="/home/deck/Downloads"
/> />
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow>
<TextField
label="Server URL"
value={settings.baseUrl}
onChange={(e) => onUpdateSetting("baseUrl", e.target.value)}
placeholder="https://deckyvault.xyz"
/>
</PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<DropdownItem <DropdownItem
label="Default Hardware" label="Default Hardware"
@@ -643,47 +764,52 @@ export default function MainPanel({
onChange={(opt) => onUpdateSetting("hardwareSlug", opt.data as string || null)} onChange={(opt) => onUpdateSetting("hardwareSlug", opt.data as string || null)}
/> />
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "11px", opacity: 0.5, padding: "10px 0 2px 0", textTransform: "uppercase", letterSpacing: "0.05em" }}>
<FaServer style={{ marginRight: "6px" }} />
Config Backup
</div>
</PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<ButtonItem layout="below" onClick={handleExportConfig}> <ButtonItem layout="below" onClick={handleExportConfig}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}> <BtnContent icon={<FaFileExport />} label="Export to Downloads" />
<FaFileExport />
Export Config to Downloads
</div>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<ButtonItem layout="below" onClick={handleImportConfig}> <ButtonItem layout="below" onClick={handleImportConfig}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}> <BtnContent icon={<FaFileImport />} label="Import from Downloads" />
<FaFileImport />
Import Config from Downloads
</div>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
{configStatus && ( {configStatus && (
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "4px 0", color: configStatus.isError ? "#e74c3c" : "#2ecc71" }}> <div className={staticClasses.Text} style={{
fontSize: "12px",
padding: "6px 10px",
borderRadius: "6px",
color: configStatus.isError ? COLORS.err : COLORS.ok,
background: configStatus.isError ? "rgba(231,76,60,0.10)" : "rgba(46,204,113,0.10)",
border: `1px solid ${configStatus.isError ? "rgba(231,76,60,0.25)" : "rgba(46,204,113,0.25)"}`,
}}>
{configStatus.isError ? <FaTimes /> : <FaCheck />} {configStatus.message} {configStatus.isError ? <FaTimes /> : <FaCheck />} {configStatus.message}
</div> </div>
</PanelSectionRow> </PanelSectionRow>
)} )}
</PanelSection>
{/* ── MangoHud Setup Guide ────────────────────────────── */}
<PanelSection title="MangoHud Setup Guide">
<PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "4px 0 8px 0", lineHeight: "1.5", opacity: 0.7 }}>
Follow these steps once to enable performance logging.
</div>
</PanelSectionRow>
<SetupStep number={1} title="Write MangoHud Config" body="Tap 'Write Config' above. This creates the logging config and a wrapper script automatically." />
<SetupStep number={2} title="Add the Launch Option" body="Right-click your game in Steam Properties Launch Options, and paste the launch option above." />
<SetupStep number={3} title="Launch the Game" body="Start the game from Steam. MangoHud loads automatically using the wrapper script." />
<SetupStep number={4} title="Record While Playing" body="Once in-game, open this panel and press Start Recording. Press Stop when done." />
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "11px", padding: "10px 0 0 0", lineHeight: "1.5", opacity: 0.5, borderTop: "1px solid rgba(255,255,255,0.08)", marginTop: "8px" }}> <div className={staticClasses.Text} style={{
Steam Deck ships with MangoHud pre-installed. On other Linux distros, install it with <span style={{ fontFamily: "monospace", opacity: 0.8 }}>sudo apt install mangohud</span> or via Flatpak. fontSize: "11px",
padding: "10px 0 0 0",
lineHeight: "1.5",
opacity: 0.45,
borderTop: `1px solid ${COLORS.border}`,
marginTop: "8px",
display: "flex",
alignItems: "flex-start",
gap: "6px",
}}>
<FaWrench style={{ marginTop: "2px", flexShrink: 0 }} />
<span>Export saves your settings (including API key) as a JSON file you can move between devices.</span>
</div> </div>
</PanelSectionRow> </PanelSectionRow>
</PanelSection> </PanelSection>