feat: add TDP to quick stats + open-on-DeckyVault button
This commit is contained in:
@@ -39,7 +39,7 @@ export type PluginEntryRow = {
|
|||||||
|
|
||||||
export type PluginGameResponse = {
|
export type PluginGameResponse = {
|
||||||
game: { id: string; steamAppId: number | null; title: string; slug: string | null } | null
|
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<typeof trimEntry>[]
|
topEntries: ReturnType<typeof trimEntry>[]
|
||||||
recentEntries: ReturnType<typeof trimEntry>[]
|
recentEntries: ReturnType<typeof trimEntry>[]
|
||||||
error?: string
|
error?: string
|
||||||
@@ -85,6 +85,9 @@ export async function buildPluginGameResponse(args: {
|
|||||||
onePct: entries.reduce<number | null>((m, e) => (m == null ? e.fpsOnePercentLow : Math.min(m, e.fpsOnePercentLow ?? m)), null),
|
onePct: entries.reduce<number | null>((m, e) => (m == null ? e.fpsOnePercentLow : Math.min(m, e.fpsOnePercentLow ?? m)), null),
|
||||||
high: entries.reduce<number | null>((m, e) => (m == null ? e.fpsHigh : Math.max(m, e.fpsHigh ?? m)), null),
|
high: entries.reduce<number | null>((m, e) => (m == null ? e.fpsHigh : Math.max(m, e.fpsHigh ?? m)), null),
|
||||||
count: entries.length,
|
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)
|
} : null)
|
||||||
if (!estFps) {
|
if (!estFps) {
|
||||||
return { game: { ...args.game }, estFps: null, topEntries: [], recentEntries: [] }
|
return { game: { ...args.game }, estFps: null, topEntries: [], recentEntries: [] }
|
||||||
@@ -215,6 +218,7 @@ export const pluginPublicRoutes = new Elysia({
|
|||||||
onePct: sql<number | null>`min(${performanceEntries.fpsOnePercentLow})`,
|
onePct: sql<number | null>`min(${performanceEntries.fpsOnePercentLow})`,
|
||||||
high: sql<number | null>`max(${performanceEntries.fpsHigh})`,
|
high: sql<number | null>`max(${performanceEntries.fpsHigh})`,
|
||||||
count: sql<number>`count(*)::int`,
|
count: sql<number>`count(*)::int`,
|
||||||
|
tdpAvg: sql<number | null>`round(avg(${performanceEntries.tdpWatts})::numeric, 1)`,
|
||||||
})
|
})
|
||||||
.from(performanceEntries)
|
.from(performanceEntries)
|
||||||
.where(baseWhere)
|
.where(baseWhere)
|
||||||
@@ -225,7 +229,7 @@ export const pluginPublicRoutes = new Elysia({
|
|||||||
game,
|
game,
|
||||||
entries: topRows as unknown as PluginEntryRow[],
|
entries: topRows as unknown as PluginEntryRow[],
|
||||||
recent: recentRows 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,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState, useRef } from "react"
|
import { useEffect, useState, useRef } from "react"
|
||||||
import { PanelSectionRow, DropdownItem, staticClasses } from "@decky/ui"
|
import { PanelSectionRow, DropdownItem, ButtonItem, staticClasses } from "@decky/ui"
|
||||||
|
import { Router } from "@decky/ui"
|
||||||
import {
|
import {
|
||||||
fetchPluginGame,
|
fetchPluginGame,
|
||||||
fetchPluginDevices,
|
fetchPluginDevices,
|
||||||
@@ -57,7 +58,10 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }:
|
|||||||
.map((d) => ({ label: `${d.name} (${d.count})`, data: d.slug })),
|
.map((d) => ({ label: `${d.name} (${d.count})`, data: d.slug })),
|
||||||
]
|
]
|
||||||
|
|
||||||
// Minimal single-row stats — no header, no entry cards
|
const gameUrl = data?.game?.slug
|
||||||
|
? `${baseUrl}/games/${data.game.slug}`
|
||||||
|
: `${baseUrl}/games`
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<PanelSectionRow>
|
<PanelSectionRow>
|
||||||
@@ -90,14 +94,18 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Quick stats row */}
|
||||||
<PanelSectionRow>
|
<PanelSectionRow>
|
||||||
<div className={staticClasses.Text} style={{ display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", padding: "4px 0" }}>
|
<div className={staticClasses.Text} style={{ padding: "4px 0", fontSize: "13px" }}>
|
||||||
{device && data.estFps ? (
|
{device && data.estFps ? (
|
||||||
<>
|
<span style={{ display: "flex", flexWrap: "wrap", gap: "4px 12px" }}>
|
||||||
<span style={{ opacity: 0.6 }}>DeckyVault est FPS:</span>
|
<span><strong>{data.estFps.avg}</strong> <span style={{ opacity: 0.4 }}>avg FPS</span></span>
|
||||||
<strong>{data.estFps.avg}</strong>
|
{data.estFps.onePct != null && <span><strong>{data.estFps.onePct}</strong> <span style={{ opacity: 0.4 }}>1% low</span></span>}
|
||||||
<span style={{ opacity: 0.4, fontSize: "11px" }}>avg · {data.estFps.low ?? "—"} low · {data.estFps.high ?? "—"} high · {data.estFps.count} entries</span>
|
{data.estFps.low != null && <span><strong>{data.estFps.low}</strong> <span style={{ opacity: 0.4 }}>min</span></span>}
|
||||||
</>
|
{data.estFps.high != null && <span><strong>{data.estFps.high}</strong> <span style={{ opacity: 0.4 }}>max</span></span>}
|
||||||
|
{data.estFps.tdpAvg != null && <span><strong>{data.estFps.tdpAvg}W</strong> <span style={{ opacity: 0.4 }}>TDP</span></span>}
|
||||||
|
<span><strong>{data.estFps.count}</strong> <span style={{ opacity: 0.4 }}>entries</span></span>
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ opacity: 0.5, fontSize: "12px" }}>
|
<span style={{ opacity: 0.5, fontSize: "12px" }}>
|
||||||
{device
|
{device
|
||||||
@@ -107,6 +115,8 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PanelSectionRow>
|
</PanelSectionRow>
|
||||||
|
|
||||||
|
{/* Device dropdown */}
|
||||||
<PanelSectionRow>
|
<PanelSectionRow>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
rgOptions={deviceOptions}
|
rgOptions={deviceOptions}
|
||||||
@@ -114,6 +124,13 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }:
|
|||||||
onChange={(opt) => setDevice(opt.data as string)}
|
onChange={(opt) => setDevice(opt.data as string)}
|
||||||
/>
|
/>
|
||||||
</PanelSectionRow>
|
</PanelSectionRow>
|
||||||
|
|
||||||
|
{/* Open on DeckyVault button */}
|
||||||
|
<PanelSectionRow>
|
||||||
|
<ButtonItem layout="below" onClick={() => Router.NavigateToExternalWeb(gameUrl)}>
|
||||||
|
View on DeckyVault
|
||||||
|
</ButtonItem>
|
||||||
|
</PanelSectionRow>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ export interface PluginEntry {
|
|||||||
|
|
||||||
export interface PluginGameResponse {
|
export interface PluginGameResponse {
|
||||||
game: { id: string; steamAppId: number | null; title: string; slug: string | null } | null
|
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[]
|
topEntries: PluginEntry[]
|
||||||
recentEntries: PluginEntry[]
|
recentEntries: PluginEntry[]
|
||||||
error?: string
|
error?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user