diff --git a/apps/web/lib/api/plugin-public.ts b/apps/web/lib/api/plugin-public.ts index c2e9a41..cb601a2 100644 --- a/apps/web/lib/api/plugin-public.ts +++ b/apps/web/lib/api/plugin-public.ts @@ -39,7 +39,7 @@ export type PluginEntryRow = { export type PluginGameResponse = { game: { id: string; steamAppId: number | null; title: string; slug: string | null } | null - estFps: { avg: number; low: number | null; onePct: number | null; high: number | null; count: number } | null + estFps: { avg: number; low: number | null; onePct: number | null; high: number | null; count: number; tdpAvg: number | null } | null topEntries: ReturnType[] recentEntries: ReturnType[] error?: string @@ -85,6 +85,9 @@ export async function buildPluginGameResponse(args: { onePct: entries.reduce((m, e) => (m == null ? e.fpsOnePercentLow : Math.min(m, e.fpsOnePercentLow ?? m)), null), high: entries.reduce((m, e) => (m == null ? e.fpsHigh : Math.max(m, e.fpsHigh ?? m)), null), count: entries.length, + tdpAvg: entries.length > 0 + ? Math.round((entries.filter((e) => e.tdpWatts != null).reduce((a, b) => a + (b.tdpWatts ?? 0), 0) / Math.max(1, entries.filter((e) => e.tdpWatts != null).length)) * 10) / 10 + : null, } : null) if (!estFps) { return { game: { ...args.game }, estFps: null, topEntries: [], recentEntries: [] } @@ -215,6 +218,7 @@ export const pluginPublicRoutes = new Elysia({ onePct: sql`min(${performanceEntries.fpsOnePercentLow})`, high: sql`max(${performanceEntries.fpsHigh})`, count: sql`count(*)::int`, + tdpAvg: sql`round(avg(${performanceEntries.tdpWatts})::numeric, 1)`, }) .from(performanceEntries) .where(baseWhere) @@ -225,7 +229,7 @@ export const pluginPublicRoutes = new Elysia({ game, entries: topRows as unknown as PluginEntryRow[], recent: recentRows as unknown as PluginEntryRow[], - estFps: agg.avg ? { avg: agg.avg, low: agg.low, onePct: agg.onePct, high: agg.high, count: agg.count } : null, + estFps: agg.avg ? { avg: agg.avg, low: agg.low, onePct: agg.onePct, high: agg.high, count: agg.count, tdpAvg: agg.tdpAvg ?? null } : null, }) }, { diff --git a/plugins/decky-vault/src/components/LibraryAppPanel.tsx b/plugins/decky-vault/src/components/LibraryAppPanel.tsx index 74e98be..020efda 100644 --- a/plugins/decky-vault/src/components/LibraryAppPanel.tsx +++ b/plugins/decky-vault/src/components/LibraryAppPanel.tsx @@ -1,13 +1,12 @@ import { useEffect, useState, useRef } from "react" -import { PanelSection, PanelSectionRow, DropdownItem, staticClasses } from "@decky/ui" -import { FaCheck, FaTimes, FaChartLine } from "react-icons/fa" +import { PanelSectionRow, DropdownItem, ButtonItem, staticClasses } from "@decky/ui" +import { Router } from "@decky/ui" import { fetchPluginGame, fetchPluginDevices, setPluginApiBaseUrl, type PluginGameResponse, type PluginDeviceRow, - type PluginEntry, } from "../lib/plugin-api" interface Props { @@ -17,40 +16,6 @@ interface Props { 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([]) @@ -68,16 +33,12 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }: 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("") - } + setFetchError(d.error && !d.game ? d.error : "") setData(d) setLoading(false) const devs = await fetchPluginDevices(appId) if (!cancelled && id === reqIdRef.current) setDevices(devs) - } catch (e) { + } catch { if (!cancelled && id === reqIdRef.current) { setData(null) setFetchError("Request failed") @@ -89,16 +50,6 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }: return () => { cancelled = true } }, [appId, device, baseUrl]) - if (loading) { - return ( - - -
Loading DeckyVault…
-
-
- ) - } - const deviceOptions = [ { label: "All devices", data: "" }, ...(hardwareSlug ? [{ label: `Your device (${hardwareSlug})`, data: hardwareSlug }] : []), @@ -107,103 +58,79 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }: .map((d) => ({ label: `${d.name} (${d.count})`, data: d.slug })), ] + const gameUrl = data?.game?.slug + ? `${baseUrl}/games/${data.game.slug}` + : `${baseUrl}/games` + + if (loading) { + return ( + +
+ DeckyVault loading… +
+
+ ) + } + if (fetchError) { return ( - - -
- Could not load DeckyVault data: {fetchError} -
-
-
+ +
+ DeckyVault: {fetchError} +
+
) } if (!data || !data.game) { return ( - - -
- Not in DeckyVault yet. Open {title} on{" "} - deckyvault.xyz to add it. -
-
-
+ +
+ Not on DeckyVault — add it +
+
) } return ( - + <> + {/* Quick stats row */} -
- In DeckyVault +
+ {device && data.estFps ? ( + + {data.estFps.avg} avg FPS + {data.estFps.onePct != null && {data.estFps.onePct} 1% low} + {data.estFps.low != null && {data.estFps.low} min} + {data.estFps.high != null && {data.estFps.high} max} + {data.estFps.tdpAvg != null && {data.estFps.tdpAvg}W TDP} + {data.estFps.count} entries + + ) : ( + + {device + ? "No entries for this device yet" + : "Select a device to see estimated FPS"} + + )}
- {/* 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 */} + {/* Device dropdown */} 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) => )} - - )} - + {/* Open on DeckyVault button */} + + Router.NavigateToExternalWeb(gameUrl)}> + View on DeckyVault + + + ) } \ No newline at end of file diff --git a/plugins/decky-vault/src/lib/plugin-api.ts b/plugins/decky-vault/src/lib/plugin-api.ts index 3ddd9ed..614c56f 100644 --- a/plugins/decky-vault/src/lib/plugin-api.ts +++ b/plugins/decky-vault/src/lib/plugin-api.ts @@ -22,7 +22,7 @@ export interface PluginEntry { export interface PluginGameResponse { game: { id: string; steamAppId: number | null; title: string; slug: string | null } | null - estFps: { avg: number; low: number | null; onePct: number | null; high: number | null; count: number } | null + estFps: { avg: number; low: number | null; onePct: number | null; high: number | null; count: number; tdpAvg: number | null } | null topEntries: PluginEntry[] recentEntries: PluginEntry[] error?: string