diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx index 56876d9..1cf3cbf 100644 --- a/plugins/decky-vault/src/components/main-panel.tsx +++ b/plugins/decky-vault/src/components/main-panel.tsx @@ -21,6 +21,10 @@ import { FaSearch, FaQrcode, FaLink, + FaMicrochip, + FaGamepad, + FaServer, + FaWrench, } from "react-icons/fa" import type { RecordingState, SessionData, RecentSession, PluginSettings } from "../lib/store" import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared" @@ -58,16 +62,85 @@ const HARDWARE_OPTIONS = [ ...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 ( + +
+ {children} +
+
+ ) +} + +/** 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 ? : + return ( + +
+ {icon} + {label} + {value && {value}} +
+
+ ) +} + +/** 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 ( + +
+ {icon} + {label} + {value} +
+
+ ) +} + +/** Standard centered icon+label content for ButtonItem. */ +function BtnContent({ icon, label }: { icon: React.ReactNode; label: string }) { + return ( +
+ {icon} + {label} +
+ ) +} + +/** A numbered setup step with a circular badge. */ function SetupStep({ number, title, body }: { number: number; title: string; body: string }) { return ( -
+
void }) { + return ( + <> + +
+ {value} +
+
+ + + } label={copied ? "Copied to clipboard" : "Copy"} /> + + + + ) +} + export default function MainPanel({ recordingState, session, @@ -197,6 +297,10 @@ export default function MainPanel({ async function handleWriteConfig() { const result = await writeMangohudConfig() setConfigWritten(result.success) + if (result.success) { + // Reset verify state since config changed + setConfigVerified({ checked: false, valid: false, message: "" }) + } } async function handleVerifyConfig() { @@ -208,7 +312,6 @@ export default function MainPanel({ const content = result.content const hasOutputFolder = content.includes("output_folder=/tmp") const hasFps = content.includes("fps") - const hasFrameTiming = content.includes("frame_timing") if (hasOutputFolder && hasFps) { setConfigVerified({ checked: true, valid: true, message: "Config looks good" }) } else { @@ -303,6 +406,10 @@ export default function MainPanel({ return () => stopPairPolling() }, []) + // ── Derived readiness flags ───────────────────────────── + const hasApiKey = !!settings.apiKey + const isLinked = pairState.status === "linked" || hasApiKey + // ── Stopped state: show the session form ────────────────────── if (recordingState === "stopped") { return ( @@ -320,16 +427,30 @@ export default function MainPanel({ return ( <> - {/* ── Recording ──────────────────────────────────────────── */} + {/* ════════════════════════════════════════════════════════ + 1. RECORDING — the primary action, always first + ════════════════════════════════════════════════════════ */} {error && ( -
{error}
+
+ {error} +
)} {recordingState === "idle" && ( <> + + Enter the game name (or let it auto-detect), then start recording once you're in-game. + -
- - Start Recording -
+ } label="Start Recording" />
@@ -352,112 +470,109 @@ export default function MainPanel({ {recordingState === "recording" && ( <> -
-
- - {formatTime(elapsed)} +
+
+ + {formatTime(elapsed)}
- {session.gameName ? `Recording: ${session.gameName}` : "Recording..."} + {session.gameName ? session.gameName : "Recording…"}
-
- - Stop Recording -
+ } label="Stop Recording" />
)} {recentSessions.length > 0 && recordingState === "idle" && ( - + <> + +
+ Recent +
+
{recentSessions.map((rs, i) => ( -
- {rs.gameName || "Unknown game"} -
- +
+
{rs.gameName || "Unknown game"}
+
{rs.fpsAvg ? `${rs.fpsAvg} FPS avg` : "No data"} · {new Date(rs.date).toLocaleDateString()} - +
))} - + )} - {/* ── Status ──────────────────────────────────────────────── */} - - - -
- - Check MangoHud Status -
-
-
- {mangohudStatus.checked && ( - -
- {mangohudStatus.installed ? ( - <> MangoHud {mangohudStatus.version} - ) : ( - <> MangoHud not found - )} -
-
- )} - {session.gameName && ( - -
- Game: {session.gameName} - {session.appId && <> App ID: {session.appId}} -
-
- )} - - - {keyTestStatus === "testing" ? "Testing..." : "Test API Key"} - {keyTestStatus === "valid" && } - {keyTestStatus === "invalid" && } - - - {keyTestMessage && ( - -
- {keyTestMessage} -
-
- )} -
- - {/* ── Account ─────────────────────────────────────────── */} + {/* ════════════════════════════════════════════════════════ + 2. ACCOUNT — pairing + API key (prerequisite for upload) + ════════════════════════════════════════════════════════ */} + {/* Readiness indicator */} + + {pairState.status === "idle" && ( <> + + Scan a QR code with your phone to link your DeckyVault account — no manual key entry needed. + -
- Link this plugin to your DeckyVault account by scanning a QR code with your phone — no manual key entry needed. + + } label="Pair with Phone" /> + + + {/* Manual key entry + test */} + +
+ Or paste a key manually
- -
- - Pair with Phone -
+ onUpdateSetting("apiKey", e.target.value)} + placeholder="dv_..." + bIsPassword + /> +
+ + + : keyTestStatus === "valid" ? : keyTestStatus === "invalid" ? : } + label={keyTestStatus === "testing" ? "Testing…" : "Test API Key"} + /> + {keyTestMessage && ( + +
+ {keyTestMessage} +
+
+ )} )} {pairState.status === "starting" && ( -
+
Starting pairing session…
@@ -465,13 +580,9 @@ export default function MainPanel({ {(pairState.status === "showing-qr" || pairState.status === "polling") && ( <> + Scan with your phone camera, then tap Confirm on the page. -
- Scan this code with your phone's camera, then confirm on the page that opens. -
-
- -
+
@@ -482,10 +593,7 @@ export default function MainPanel({ -
- - Cancel -
+ } label="Cancel" />
@@ -494,21 +602,26 @@ export default function MainPanel({ {pairState.status === "linked" && ( <> -
- Plugin linked to your account! +
+ Linked to your account
-
- API key saved. You can now upload performance entries. +
+ API key saved. You can now upload entries.
-
- - Done -
+ } label="Done" />
@@ -517,106 +630,122 @@ export default function MainPanel({ {pairState.status === "error" && ( <> -
- {pairState.error} +
+ {pairState.error}
-
- - Try Again -
+ } label="Try Again" />
-
- Dismiss -
+ } label="Dismiss" />
)} - {/* ── Usage Instructions ──────────────────────────────────── */} - - -
- 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. -
-
- -
- ~/deckyvault-mangohud.sh %command% -
-
- - -
- - {copiedLaunchOpt ? "Copied to clipboard" : "Copy Launch Option"} -
-
-
- -
- Config stored in ~/.config/MangoHud/MangoHud.conf -
-
-
+ {/* ════════════════════════════════════════════════════════ + 3. MANGOHUD SETUP — config + launch option + guide, + all in one coherent section + ════════════════════════════════════════════════════════ */} + + Follow these steps once to enable performance logging. - {/* ── MangoHud Config ────────────────────────────────────── */} - + -
- - Write Config -
+ } label="Write Config" />
{configWritten && ( - -
- Config written -
-
+ )} + + + + + + + {/* Verify + config details */} + +
+ Verify +
+
-
- - Verify Config -
+ } label="Verify Config" />
{configVerified.checked && ( - -
- {configVerified.valid ? : } {configVerified.message} -
-
+ + )} + + +
+ Config stored in ~/.config/MangoHud/MangoHud.conf. + Steam Deck has MangoHud pre-installed; on other Linux, install via apt or Flatpak. +
+
+
+ + {/* ════════════════════════════════════════════════════════ + 4. SYSTEM — environment / detection status + ════════════════════════════════════════════════════════ */} + + Detect your hardware, OS, and the currently running game. + + + + } label="Check MangoHud" /> + + + {mangohudStatus.checked && ( + + )} + + {session.gameName && ( + } label="Game" value={session.gameName} /> + )} + {session.appId && ( + } label="App ID" value={String(session.appId)} /> )} - {/* ── Configuration ────────────────────────────────────────── */} - + {/* ════════════════════════════════════════════════════════ + 5. ADVANCED — server, paths, hardware, config portability + ════════════════════════════════════════════════════════ */} + + Change these only if you need a custom server or export location. + onUpdateSetting("apiKey", e.target.value)} - placeholder="dv_..." - bIsPassword + label="Server URL" + value={settings.baseUrl} + onChange={(e) => onUpdateSetting("baseUrl", e.target.value)} + placeholder="https://deckyvault.xyz" /> @@ -627,14 +756,6 @@ export default function MainPanel({ placeholder="/home/deck/Downloads" /> - - onUpdateSetting("baseUrl", e.target.value)} - placeholder="https://deckyvault.xyz" - /> - onUpdateSetting("hardwareSlug", opt.data as string || null)} /> + + +
+ + Config Backup +
+
-
- - Export Config to Downloads -
+ } label="Export to Downloads" />
-
- - Import Config from Downloads -
+ } label="Import from Downloads" />
{configStatus && ( -
+
{configStatus.isError ? : } {configStatus.message}
)} - - - {/* ── MangoHud Setup Guide ────────────────────────────── */} - - -
- Follow these steps once to enable performance logging. -
-
- - - - - -
- Steam Deck ships with MangoHud pre-installed. On other Linux distros, install it with sudo apt install mangohud or via Flatpak. +
+ + Export saves your settings (including API key) as a JSON file you can move between devices.