diff --git a/plugins/decky-vault/src/components/session-form.tsx b/plugins/decky-vault/src/components/session-form.tsx new file mode 100644 index 0000000..1518a3f --- /dev/null +++ b/plugins/decky-vault/src/components/session-form.tsx @@ -0,0 +1,281 @@ +import { useState } from "react" +import { + ButtonItem, + PanelSection, + PanelSectionRow, + Field, + DropdownItem, + staticClasses, +} from "@decky/ui" +import { + FaFileExport, + FaCloudUploadAlt, + FaCheck, + FaTimes, +} from "react-icons/fa" +import type { SessionData } from "../lib/store" +import { buildImportPayload } from "../lib/store" +import type { PluginSettings } from "../lib/store" +import { exportToFile, uploadToDeckyvault } from "../lib/api" + +interface SessionFormProps { + session: SessionData + error: string + settings?: PluginSettings + onUpdateSession: (updates: Partial) => void + onAddToRecent: (sess: SessionData) => void + onReset: () => void + setError: (msg: string) => void +} + +const UPSCALER_OPTIONS = [ + { label: "None", value: "none" }, + { label: "FSR", value: "fsr" }, + { label: "DLSS", value: "dlss" }, + { label: "XeSS", value: "xess" }, + { label: "LSFG", value: "lsfg" }, + { label: "Other", value: "other" }, +] + +const FRAME_GEN_OPTIONS = [ + { label: "None", value: "none" }, + { label: "FSR FG", value: "fsr_fg" }, + { label: "DLSS FG", value: "dlss_fg" }, + { label: "LSFG", value: "lsfg" }, + { label: "Other", value: "other" }, +] + +export default function SessionForm({ + session, + error, + settings, + onUpdateSession, + onAddToRecent, + onReset, + setError, +}: SessionFormProps) { + const [exportStatus, setExportStatus] = useState<"idle" | "success" | "error">("idle") + const [uploadStatus, setUploadStatus] = useState<"idle" | "loading" | "success" | "error">("idle") + const [statusMessage, setStatusMessage] = useState("") + + async function handleExport() { + if (!settings) return + setError("") + setExportStatus("idle") + const payload = buildImportPayload(session) + const gameSlug = session.gameName.toLowerCase().replace(/[^a-z0-9]/g, "-") || "unknown" + const date = new Date().toISOString().slice(0, 10) + const filename = `${gameSlug}-${date}.deckyvault.json` + const fullPath = `${settings.exportPath}/${filename}` + + const result = await exportToFile(payload as unknown as Record, fullPath) + if (result.success) { + setExportStatus("success") + setStatusMessage(`Saved to ${result.path}`) + onAddToRecent(session) + } else { + setExportStatus("error") + setStatusMessage(result.error || "Export failed") + } + } + + async function handleUpload() { + if (!settings) return + if (!settings.apiKey) { + setError("No API key configured. Set one in the Settings tab.") + return + } + setError("") + setUploadStatus("loading") + setStatusMessage("") + + const payload = buildImportPayload(session) + const result = await uploadToDeckyvault( + payload as unknown as Record, + settings.apiKey, + settings.baseUrl, + ) + + if (result.success) { + setUploadStatus("success") + setStatusMessage(`Uploaded! Entry ID: ${result.data?.id}`) + onAddToRecent(session) + } else { + setUploadStatus("error") + setStatusMessage(result.error || "Upload failed") + if (result.status === 404) { + setStatusMessage("This game isn't in DeckyVault yet. Submit it on the website first, or export to file.") + } + } + } + + return ( + + {/* ── Auto-captured summary ──────────────────────────────── */} + + +
+ Game: {session.gameName || "Unknown"}
+ {session.appId && <>App ID: {session.appId}
} + FPS: {session.fpsAvg ?? "—"} avg / {session.fpsLow ?? "—"} min / {session.fpsOnePercentLow ?? "—"} 1% low / {session.fpsHigh ?? "—"} max
+ TDP: {session.tdpWatts ? `${session.tdpWatts}W` : "—"}
+ Hardware: {session.hardwareName || session.hardwareSlug || "—"}
+ OS: {session.osVersion || "—"}
+ Proton: {session.protonVersion || "—"} +
+
+
+ + {/* ── Manual inputs ──────────────────────────────────────── */} + + + onUpdateSession({ upscalerType: opt.data as string })} + /> + + + + + onUpdateSession({ upscalerVersion: e.target.value })} + placeholder="e.g. 2.4" + style={{ width: "100%", padding: "4px 8px" }} + /> + + + + + onUpdateSession({ frameGenMethod: opt.data as string })} + /> + + + + +