import { useEffect, useState } from "react" import { ButtonItem, PanelSection, PanelSectionRow, TextField, DropdownItem, staticClasses, } from "@decky/ui" import { FaPlay, FaStop, FaClock, FaCopy, FaCheck, FaTimes, FaDownload, FaCog, FaFileExport, FaFileImport, FaSearch, } from "react-icons/fa" import type { RecordingState, SessionData, RecentSession, PluginSettings } from "../lib/store" import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared" import { testApiKey, checkMangohud, writeMangohudConfig, getMangohudConfig, exportConfig, importConfig, } from "../lib/api" import SessionForm from "./session-form" interface MainPanelProps { recordingState: RecordingState session: SessionData recentSessions: RecentSession[] error: string settings: PluginSettings onStart: () => void onStop: () => void onUpdateSession: (updates: Partial) => void onAddToRecent: (sess: SessionData) => void onReset: () => void setError: (msg: string) => void setGameName: (name: string, appId?: number) => void onUpdateSetting: (key: K, value: string | null) => void } const HARDWARE_OPTIONS = [ { label: "Auto-detect", data: "" }, ...KNOWN_HARDWARE_SLUGS.map((slug) => ({ label: slug, data: slug })), ] export default function MainPanel({ recordingState, session, recentSessions, error, settings, onStart, onStop, onUpdateSession, onAddToRecent, onReset, setError, setGameName, onUpdateSetting, }: MainPanelProps) { const [elapsed, setElapsed] = useState(0) const [mangohudStatus, setMangohudStatus] = useState<{ checked: boolean installed: boolean path: string version: string }>({ checked: false, installed: false, path: "", version: "" }) const [keyTestStatus, setKeyTestStatus] = useState<"idle" | "testing" | "valid" | "invalid">("idle") const [keyTestMessage, setKeyTestMessage] = useState("") const [copiedLaunchOpt, setCopiedLaunchOpt] = useState(false) const [configWritten, setConfigWritten] = useState(false) const [configVerified, setConfigVerified] = useState<{ checked: boolean valid: boolean message: string }>({ checked: false, valid: false, message: "" }) const [configStatus, setConfigStatus] = useState<{ message: string; isError: boolean } | null>(null) // Timer for recording state useEffect(() => { if (recordingState !== "recording") { setElapsed(0) return } const interval = setInterval(() => { setElapsed(Math.floor((Date.now() - session.startedAt) / 1000)) }, 1000) return () => clearInterval(interval) }, [recordingState, session.startedAt]) function formatTime(seconds: number): string { const m = Math.floor(seconds / 60) const s = seconds % 60 return `${m}:${s.toString().padStart(2, "0")}` } async function handleCheckMangohud() { const result = await checkMangohud() setMangohudStatus({ checked: true, installed: result.installed, path: result.path, version: result.version, }) if (result.debug) { console.log("[DeckyVault] MangoHud debug:", result.debug) } } async function handleTestKey() { if (!settings.apiKey) { setKeyTestStatus("invalid") setKeyTestMessage("Enter an API key first") return } setKeyTestStatus("testing") setKeyTestMessage("") const result = await testApiKey(settings.apiKey, settings.baseUrl) if (result.valid) { setKeyTestStatus("valid") setKeyTestMessage("API key is valid") } else { setKeyTestStatus("invalid") setKeyTestMessage(result.error || "Invalid API key") } } async function handleCopyLaunchOption() { try { await navigator.clipboard.writeText("mangohud %command%") setCopiedLaunchOpt(true) setTimeout(() => setCopiedLaunchOpt(false), 2000) } catch { const ta = document.createElement("textarea") ta.value = "mangohud %command%" document.body.appendChild(ta) ta.select() document.execCommand("copy") document.body.removeChild(ta) setCopiedLaunchOpt(true) setTimeout(() => setCopiedLaunchOpt(false), 2000) } } async function handleWriteConfig() { const result = await writeMangohudConfig() setConfigWritten(result.success) } async function handleVerifyConfig() { const result = await getMangohudConfig() if (!result.exists) { setConfigVerified({ checked: true, valid: false, message: "No MangoHud config found. Write one first." }) return } 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 { setConfigVerified({ checked: true, valid: false, message: "Config is missing required settings. Write it again." }) } } async function handleExportConfig() { setConfigStatus(null) const result = await exportConfig(settings) if (result.success) { setConfigStatus({ message: `Config saved to ${result.path}`, isError: false }) } else { setConfigStatus({ message: result.error || "Export failed", isError: true }) } } async function handleImportConfig() { setConfigStatus(null) const result = await importConfig() if (result.success && result.settings) { onUpdateSetting("apiKey", result.settings.apiKey || "") onUpdateSetting("exportPath", result.settings.exportPath || "/home/deck/Downloads") onUpdateSetting("baseUrl", result.settings.baseUrl || "https://deckyvault.xyz") onUpdateSetting("hardwareSlug", result.settings.hardwareSlug || null) setConfigStatus({ message: "Config imported from Downloads", isError: false }) } else { setConfigStatus({ message: result.error || "No config file found in Downloads", isError: true }) } } // ── Stopped state: show the session form ────────────────────── if (recordingState === "stopped") { return ( ) } return ( <> {/* ── Recording ──────────────────────────────────────────── */} {error && (
{error}
)} {recordingState === "idle" && ( <> setGameName(e.target.value)} placeholder="e.g. Cyberpunk 2077" />
Start Recording
)} {recordingState === "recording" && ( <>
{formatTime(elapsed)}
{session.gameName ? `Recording: ${session.gameName}` : "Recording..."}
Stop Recording
)} {recentSessions.length > 0 && recordingState === "idle" && ( {recentSessions.map((rs, i) => (
{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 )}
)} {keyTestStatus === "testing" ? "Testing..." : "Test API Key"} {keyTestStatus === "valid" && } {keyTestStatus === "invalid" && } {keyTestMessage && (
{keyTestMessage}
)}
{/* ── Usage Instructions ──────────────────────────────────── */}
Add this to your game's Steam launch options, then press Start Recording before launching.
~/deckyvault-mangohud.sh %command%
{copiedLaunchOpt ? "Copied to clipboard" : "Copy Launch Option"}
Config stored in ~/.config/MangoHud/MangoHud.conf
{/* ── MangoHud Config ────────────────────────────────────── */}
Write Config
{configWritten && (
Config written
)}
Verify Config
{configVerified.checked && (
{configVerified.valid ? : } {configVerified.message}
)}
{/* ── Configuration ────────────────────────────────────────── */} onUpdateSetting("apiKey", e.target.value)} placeholder="dv_..." bIsPassword /> onUpdateSetting("exportPath", e.target.value)} placeholder="/home/deck/Downloads" /> onUpdateSetting("baseUrl", e.target.value)} placeholder="https://deckyvault.xyz" /> onUpdateSetting("hardwareSlug", opt.data as string || null)} />
Export Config to Downloads
Import Config from Downloads
{configStatus && (
{configStatus.isError ? : } {configStatus.message}
)}
{/* ── MangoHud Setup Guide ────────────────────────────────── */}
Steam Deck (SteamOS): MangoHud is pre-installed. Add mangohud %command% to your game's Steam launch options (right-click → Properties → Launch Options).
Other Linux: Install via sudo apt install mangohud or flatpak install ...VulkanLayer.MangoHud. See{" "} github.com/flightlessmango/MangoHud.
Troubleshooting: Log empty? Check MangoHud is enabled. Not attaching? Add mangohud %command% to launch options explicitly.
) }