Merge branch 'dev' into prod
This commit is contained in:
@@ -93,7 +93,7 @@ describe("buildPluginGameResponse — shape contract", () => {
|
|||||||
const r = await buildPluginGameResponse({
|
const r = await buildPluginGameResponse({
|
||||||
game: { id: "g1", steamAppId: 123, title: "X", slug: "x" },
|
game: { id: "g1", steamAppId: 123, title: "X", slug: "x" },
|
||||||
entries, recent: entries,
|
entries, recent: entries,
|
||||||
estFps: { avg: 72.3, low: 35, onePct: 38, high: 140, count: 25 },
|
estFps: { avg: 72.3, low: 35, onePct: 38, high: 140, count: 25, tdpAvg: 13.5 },
|
||||||
})
|
})
|
||||||
expect(r.estFps).not.toBeNull()
|
expect(r.estFps).not.toBeNull()
|
||||||
expect(r.estFps!.avg).toBe(72.3)
|
expect(r.estFps!.avg).toBe(72.3)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export async function buildPluginGameResponse(args: {
|
|||||||
game: PluginGameRow | null
|
game: PluginGameRow | null
|
||||||
entries?: PluginEntryRow[]
|
entries?: PluginEntryRow[]
|
||||||
recent?: PluginEntryRow[]
|
recent?: PluginEntryRow[]
|
||||||
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
|
||||||
}): Promise<PluginGameResponse> {
|
}): Promise<PluginGameResponse> {
|
||||||
if (!args.game) {
|
if (!args.game) {
|
||||||
return { game: null, estFps: null, topEntries: [], recentEntries: [] }
|
return { game: null, estFps: null, topEntries: [], recentEntries: [] }
|
||||||
|
|||||||
@@ -16,6 +16,22 @@ interface Props {
|
|||||||
baseUrl: string
|
baseUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openExternalUrl(url: string) {
|
||||||
|
try {
|
||||||
|
if (Router?.NavigateToExternalWeb) {
|
||||||
|
Router.NavigateToExternalWeb(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[DeckyVault] NavigateToExternalWeb failed:", e)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
window.open(url, "_blank")
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[DeckyVault] window.open failed:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }: Props) {
|
export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }: Props) {
|
||||||
const [data, setData] = useState<PluginGameResponse | null>(null)
|
const [data, setData] = useState<PluginGameResponse | null>(null)
|
||||||
const [devices, setDevices] = useState<PluginDeviceRow[]>([])
|
const [devices, setDevices] = useState<PluginDeviceRow[]>([])
|
||||||
@@ -58,14 +74,14 @@ 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 })),
|
||||||
]
|
]
|
||||||
|
|
||||||
const gameUrl = data?.game?.slug
|
const gameUrl = data?.game?.steamAppId
|
||||||
? `${baseUrl}/games/${data.game.slug}`
|
? `${baseUrl}/game/${data.game.steamAppId}`
|
||||||
: `${baseUrl}/games`
|
: `${baseUrl}/games`
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<PanelSectionRow>
|
<PanelSectionRow>
|
||||||
<div className={staticClasses.Text} style={{ padding: "8px 0", fontSize: "12px", opacity: 0.5 }}>
|
<div className={staticClasses.Text} style={{ padding: "8px 0", fontSize: "12px", opacity: 0.5, textAlign: "center" }}>
|
||||||
DeckyVault loading…
|
DeckyVault loading…
|
||||||
</div>
|
</div>
|
||||||
</PanelSectionRow>
|
</PanelSectionRow>
|
||||||
@@ -75,7 +91,7 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }:
|
|||||||
if (fetchError) {
|
if (fetchError) {
|
||||||
return (
|
return (
|
||||||
<PanelSectionRow>
|
<PanelSectionRow>
|
||||||
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "8px 0", opacity: 0.5, color: "#e74c3c" }}>
|
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "8px 0", opacity: 0.5, color: "#e74c3c", textAlign: "center" }}>
|
||||||
DeckyVault: {fetchError}
|
DeckyVault: {fetchError}
|
||||||
</div>
|
</div>
|
||||||
</PanelSectionRow>
|
</PanelSectionRow>
|
||||||
@@ -85,7 +101,7 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }:
|
|||||||
if (!data || !data.game) {
|
if (!data || !data.game) {
|
||||||
return (
|
return (
|
||||||
<PanelSectionRow>
|
<PanelSectionRow>
|
||||||
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "8px 0", opacity: 0.5 }}>
|
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "8px 0", opacity: 0.5, textAlign: "center" }}>
|
||||||
Not on DeckyVault — <a href={`${baseUrl}/games`}>add it</a>
|
Not on DeckyVault — <a href={`${baseUrl}/games`}>add it</a>
|
||||||
</div>
|
</div>
|
||||||
</PanelSectionRow>
|
</PanelSectionRow>
|
||||||
@@ -94,41 +110,35 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Quick stats row */}
|
{/* Stats + device dropdown inline */}
|
||||||
<PanelSectionRow>
|
<PanelSectionRow>
|
||||||
<div className={staticClasses.Text} style={{ padding: "4px 0", fontSize: "13px" }}>
|
<div className={staticClasses.Text} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px", padding: "2px 0", fontSize: "13px" }}>
|
||||||
{device && data.estFps ? (
|
<span style={{ flex: 1, textAlign: "center" }}>
|
||||||
<span style={{ display: "flex", flexWrap: "wrap", gap: "4px 12px" }}>
|
{data.estFps ? (
|
||||||
<span><strong>{data.estFps.avg}</strong> <span style={{ opacity: 0.4 }}>avg FPS</span></span>
|
<span style={{ display: "inline-flex", flexWrap: "wrap", gap: "4px 8px", justifyContent: "center" }}>
|
||||||
|
<span><strong>{data.estFps.avg}</strong> <span style={{ opacity: 0.4 }}>avg</span></span>
|
||||||
{data.estFps.onePct != null && <span><strong>{data.estFps.onePct}</strong> <span style={{ opacity: 0.4 }}>1% low</span></span>}
|
{data.estFps.onePct != null && <span><strong>{data.estFps.onePct}</strong> <span style={{ opacity: 0.4 }}>1% low</span></span>}
|
||||||
{data.estFps.low != null && <span><strong>{data.estFps.low}</strong> <span style={{ opacity: 0.4 }}>min</span></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.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>}
|
{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 style={{ opacity: 0.4 }}>({data.estFps.count})</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ opacity: 0.5, fontSize: "12px" }}>
|
<span style={{ opacity: 0.4, fontSize: "12px" }}>No data yet</span>
|
||||||
{device
|
|
||||||
? "No entries for this device yet"
|
|
||||||
: "Select a device to see estimated FPS"}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</span>
|
||||||
</PanelSectionRow>
|
|
||||||
|
|
||||||
{/* Device dropdown */}
|
|
||||||
<PanelSectionRow>
|
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
rgOptions={deviceOptions}
|
rgOptions={deviceOptions}
|
||||||
selectedOption={device}
|
selectedOption={device}
|
||||||
onChange={(opt) => setDevice(opt.data as string)}
|
onChange={(opt) => setDevice(opt.data as string)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</PanelSectionRow>
|
</PanelSectionRow>
|
||||||
|
|
||||||
{/* Open on DeckyVault button */}
|
{/* View Details button */}
|
||||||
<PanelSectionRow>
|
<PanelSectionRow>
|
||||||
<ButtonItem layout="below" onClick={() => Router.NavigateToExternalWeb(gameUrl)}>
|
<ButtonItem layout="below" onClick={() => openExternalUrl(gameUrl)}>
|
||||||
View on DeckyVault
|
View Details
|
||||||
</ButtonItem>
|
</ButtonItem>
|
||||||
</PanelSectionRow>
|
</PanelSectionRow>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user