import { useEffect, useState, useRef } from "react" import { PanelSection, PanelSectionRow, DropdownItem, staticClasses } from "@decky/ui" import { FaCheck, FaTimes, FaChartLine } from "react-icons/fa" import { fetchPluginGame, fetchPluginDevices, setPluginApiBaseUrl, type PluginGameResponse, type PluginDeviceRow, type PluginEntry, } from "../lib/plugin-api" interface Props { appId: number title: string hardwareSlug: string | null // detected device baseUrl: string } function EntryCard({ e }: { e: PluginEntry }) { const [expanded, setExpanded] = useState(false) const settingsCount = Array.isArray(e.settingsJson) ? (e.settingsJson as Array<{ settings: unknown[] }>).reduce((s, c) => s + (c.settings?.length ?? 0), 0) : 0 const label = e.isPinned ? "Pinned" : e.upvotes > 0 ? `${e.upvotes}👍` : "Recent" return (
setExpanded((v) => !v)} style={{ padding: "8px 10px", borderRadius: "8px", background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.10)", cursor: "pointer" }} >
{e.fpsAvg} FPS avg {label}
{e.fpsLow ?? "—"} low · {e.fpsOnePercentLow ?? "—"} 1% · {e.fpsHigh ?? "—"} high {e.tdpWatts ? ` · ${e.tdpWatts}W` : ""} {e.upscalerType && e.upscalerType !== "none" ? ` · ${e.upscalerType}` : ""} {e.protonVersion ? ` · Proton ${e.protonVersion}` : ""}
{expanded && (
By {e.userName ?? "unknown"} · {new Date(e.createdAt).toLocaleDateString()}
{settingsCount} settings
{e.osVersion &&
OS: {e.osVersion}
}
)}
) } export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }: Props) { const [data, setData] = useState(null) const [devices, setDevices] = useState([]) const [loading, setLoading] = useState(true) const [device, setDevice] = useState(hardwareSlug ?? "") // "" = all devices const [fetchError, setFetchError] = useState("") const reqIdRef = useRef(0) useEffect(() => { setPluginApiBaseUrl(baseUrl) let cancelled = false const id = ++reqIdRef.current async function load() { setLoading(true) try { const d = await fetchPluginGame(appId, device || null, 3) if (cancelled || id !== reqIdRef.current) return if (d.error && !d.game) { setFetchError(d.error) } else { setFetchError("") } setData(d) setLoading(false) const devs = await fetchPluginDevices(appId) if (!cancelled && id === reqIdRef.current) setDevices(devs) } catch (e) { if (!cancelled && id === reqIdRef.current) { setData(null) setFetchError("Request failed") setLoading(false) } } } load() return () => { cancelled = true } }, [appId, device, baseUrl]) if (loading) { return (
Loading DeckyVault…
) } const deviceOptions = [ { label: "All devices", data: "" }, ...(hardwareSlug ? [{ label: `Your device (${hardwareSlug})`, data: hardwareSlug }] : []), ...devices .filter((d) => d.slug !== hardwareSlug) .map((d) => ({ label: `${d.name} (${d.count})`, data: d.slug })), ] if (fetchError) { return (
Could not load DeckyVault data. Please try again later.
) } if (!data || !data.game) { return (
Not in DeckyVault yet. Open {title} on{" "} deckyvault.xyz to add it.
) } return (
In DeckyVault
{/* Est FPS — hidden when "All devices" selected to avoid mixing hardware */} {device ? ( <>
Est FPS
{data.estFps ? (
{data.estFps.avg} avg · {data.estFps.low ?? "—"} low · {data.estFps.onePct ?? "—"} 1% · {data.estFps.high ?? "—"} high · {data.estFps.count} entries
) : (
No entries for this device yet — be the first: open the DeckyVault plugin and record.
)} ) : (
Select a device to see estimated FPS.
)} {/* Device switcher */} setDevice(opt.data as string)} /> {/* Top entries */} {data.topEntries.length > 0 && ( <>
Top entries
{data.topEntries.map((e) => )} )} {/* Recent entries */} {data.recentEntries.length > 0 && ( <>
Recent entries
{data.recentEntries.map((e) => )} )}
) }