feat(plugin): add frontend state management (settings + session hooks)
This commit is contained in:
@@ -0,0 +1,225 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from "react"
|
||||||
|
import type { DeckyVaultImportV1, HardwareSlug } from "@deckyvault/shared"
|
||||||
|
import { getSettings, setSetting } from "./api"
|
||||||
|
|
||||||
|
// ── Types ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface PluginSettings {
|
||||||
|
apiKey: string
|
||||||
|
exportPath: string
|
||||||
|
hardwareSlug: string | null // null = auto-detect
|
||||||
|
baseUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: PluginSettings = {
|
||||||
|
apiKey: "",
|
||||||
|
exportPath: "/home/deck/Downloads",
|
||||||
|
hardwareSlug: null,
|
||||||
|
baseUrl: "https://deckyvault.xyz",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RecordingState = "idle" | "recording" | "stopped"
|
||||||
|
|
||||||
|
export interface SessionData {
|
||||||
|
appId: number | null
|
||||||
|
gameName: string
|
||||||
|
startedAt: number
|
||||||
|
// Auto-captured (filled after stop)
|
||||||
|
fpsAvg: number | null
|
||||||
|
fpsLow: number | null
|
||||||
|
fpsHigh: number | null
|
||||||
|
fpsOnePercentLow: number | null
|
||||||
|
tdpWatts: number | null
|
||||||
|
hardwareSlug: string
|
||||||
|
hardwareName: string
|
||||||
|
osVersion: string
|
||||||
|
protonVersion: string
|
||||||
|
// Manual inputs (filled by user in the form)
|
||||||
|
upscalerType: string
|
||||||
|
upscalerVersion: string
|
||||||
|
frameGenMethod: string
|
||||||
|
settingsJson: string
|
||||||
|
loadTimeSsd: string
|
||||||
|
loadTimeSd: string
|
||||||
|
launchOptions: string
|
||||||
|
userNotes: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecentSession {
|
||||||
|
appId: number | null
|
||||||
|
gameName: string
|
||||||
|
fpsAvg: number | null
|
||||||
|
date: string // ISO string
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptySession(): SessionData {
|
||||||
|
return {
|
||||||
|
appId: null,
|
||||||
|
gameName: "",
|
||||||
|
startedAt: 0,
|
||||||
|
fpsAvg: null,
|
||||||
|
fpsLow: null,
|
||||||
|
fpsHigh: null,
|
||||||
|
fpsOnePercentLow: null,
|
||||||
|
tdpWatts: null,
|
||||||
|
hardwareSlug: "",
|
||||||
|
hardwareName: "",
|
||||||
|
osVersion: "",
|
||||||
|
protonVersion: "",
|
||||||
|
upscalerType: "none",
|
||||||
|
upscalerVersion: "",
|
||||||
|
frameGenMethod: "none",
|
||||||
|
settingsJson: "",
|
||||||
|
loadTimeSsd: "",
|
||||||
|
loadTimeSd: "",
|
||||||
|
launchOptions: "",
|
||||||
|
userNotes: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Settings Hook ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function useSettings() {
|
||||||
|
const [settings, setSettings] = useState<PluginSettings>(DEFAULT_SETTINGS)
|
||||||
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const raw = await getSettings()
|
||||||
|
setSettings({
|
||||||
|
apiKey: (raw.apiKey as string) || "",
|
||||||
|
exportPath: (raw.exportPath as string) || DEFAULT_SETTINGS.exportPath,
|
||||||
|
hardwareSlug: (raw.hardwareSlug as string) || null,
|
||||||
|
baseUrl: (raw.baseUrl as string) || DEFAULT_SETTINGS.baseUrl,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load settings:", e)
|
||||||
|
} finally {
|
||||||
|
setLoaded(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateSetting = useCallback(async (key: keyof PluginSettings, value: string | null) => {
|
||||||
|
setSettings((prev) => ({ ...prev, [key]: value }))
|
||||||
|
try {
|
||||||
|
await setSetting(key, value)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to save setting ${key}:`, e)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { settings, updateSetting, loaded }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Session Hook ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function useSession() {
|
||||||
|
const [recordingState, setRecordingState] = useState<RecordingState>("idle")
|
||||||
|
const [session, setSession] = useState<SessionData>(createEmptySession())
|
||||||
|
const [recentSessions, setRecentSessions] = useState<RecentSession[]>([])
|
||||||
|
const [error, setError] = useState<string>("")
|
||||||
|
const currentAppIdRef = useRef<number | null>(null)
|
||||||
|
const currentAppNameRef = useRef<string>("")
|
||||||
|
|
||||||
|
const startRecording = useCallback(() => {
|
||||||
|
setError("")
|
||||||
|
setSession({
|
||||||
|
...createEmptySession(),
|
||||||
|
appId: currentAppIdRef.current,
|
||||||
|
gameName: currentAppNameRef.current,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
})
|
||||||
|
setRecordingState("recording")
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const stopRecording = useCallback(() => {
|
||||||
|
setRecordingState("stopped")
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateSession = useCallback((updates: Partial<SessionData>) => {
|
||||||
|
setSession((prev) => ({ ...prev, ...updates }))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const addToRecent = useCallback((sess: SessionData) => {
|
||||||
|
const recent: RecentSession = {
|
||||||
|
appId: sess.appId,
|
||||||
|
gameName: sess.gameName,
|
||||||
|
fpsAvg: sess.fpsAvg,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
setRecentSessions((prev) => [recent, ...prev].slice(0, 5))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setRecordingState("idle")
|
||||||
|
setSession(createEmptySession())
|
||||||
|
setError("")
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Called when a game starts (via SteamClient event)
|
||||||
|
const onGameStart = useCallback((appId: number, gameName: string) => {
|
||||||
|
currentAppIdRef.current = appId
|
||||||
|
currentAppNameRef.current = gameName
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Called when a game stops (via SteamClient event)
|
||||||
|
const onGameStop = useCallback(() => {
|
||||||
|
currentAppIdRef.current = null
|
||||||
|
currentAppNameRef.current = ""
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
recordingState,
|
||||||
|
session,
|
||||||
|
recentSessions,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
startRecording,
|
||||||
|
stopRecording,
|
||||||
|
updateSession,
|
||||||
|
addToRecent,
|
||||||
|
reset,
|
||||||
|
onGameStart,
|
||||||
|
onGameStop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Payload Builder ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function buildImportPayload(sess: SessionData): DeckyVaultImportV1 {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
steamAppId: sess.appId ?? 0,
|
||||||
|
hardwareSlug: sess.hardwareSlug,
|
||||||
|
fpsAvg: sess.fpsAvg ?? 0,
|
||||||
|
fpsLow: sess.fpsLow,
|
||||||
|
fpsOnePercentLow: sess.fpsOnePercentLow,
|
||||||
|
fpsHigh: sess.fpsHigh,
|
||||||
|
protonVersion: sess.protonVersion || null,
|
||||||
|
osVersion: sess.osVersion || null,
|
||||||
|
upscalerType: sess.upscalerType,
|
||||||
|
upscalerVersion: sess.upscalerVersion || null,
|
||||||
|
frameGenMethod: sess.frameGenMethod,
|
||||||
|
tdpWatts: sess.tdpWatts,
|
||||||
|
loadTimeSsd: sess.loadTimeSsd ? Number(sess.loadTimeSsd) : null,
|
||||||
|
loadTimeSd: sess.loadTimeSd ? Number(sess.loadTimeSd) : null,
|
||||||
|
launchOptions: sess.launchOptions || null,
|
||||||
|
settingsJson: sess.settingsJson ? tryParseJson(sess.settingsJson) : null,
|
||||||
|
userNotes: sess.userNotes || null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryParseJson(text: string): unknown[] | null {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
return Array.isArray(parsed) ? parsed : [parsed]
|
||||||
|
} catch {
|
||||||
|
return [{ text }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DEFAULT_SETTINGS }
|
||||||
|
export type { HardwareSlug }
|
||||||
Reference in New Issue
Block a user