Merge branch 'dev' into prod

This commit is contained in:
2026-06-28 18:51:14 +08:00
6 changed files with 347 additions and 65 deletions
@@ -13,6 +13,7 @@ import {
EyeOff, EyeOff,
Clock, Clock,
AlertCircle, AlertCircle,
Download,
} from "lucide-react" } from "lucide-react"
import { motion, AnimatePresence } from "motion/react" import { motion, AnimatePresence } from "motion/react"
@@ -187,6 +188,27 @@ export function SettingsApiKeysTab() {
} }
} }
const handleDownloadConfig = () => {
if (!createdKey?.key) return
const config = {
apiKey: createdKey.key,
exportPath: "/home/deck/Downloads",
baseUrl: "https://deckyvault.xyz",
hardwareSlug: null,
}
const blob = new Blob([JSON.stringify(config, null, 2)], {
type: "application/json",
})
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = "deckyvault-config.json"
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
// Show the created key modal // Show the created key modal
@@ -261,6 +283,14 @@ export function SettingsApiKeysTab() {
> >
Done Done
</button> </button>
<button
onClick={handleDownloadConfig}
className='mt-2 px-4 py-2 rounded-lg border border-border text-text/70 text-sm font-medium hover:border-primary/40 hover:text-primary transition-colors cursor-pointer flex items-center justify-center gap-2 w-full'
>
<Download className='h-4 w-4' />
Download Plugin Config
</button>
</div> </div>
</motion.div> </motion.div>
) )
+34
View File
@@ -414,3 +414,37 @@ benchmark_percentiles=97,AVG,1,0.1
return {"valid": False, "error": f"Network error: {str(e.reason)}"} return {"valid": False, "error": f"Network error: {str(e.reason)}"}
except Exception as e: except Exception as e:
return {"valid": False, "error": str(e)} return {"valid": False, "error": str(e)}
async def export_config(self, settings: dict) -> dict:
"""RPC: Export current settings to Downloads/deckyvault-config.json.
Returns {success: bool, path?: str, error?: str}."""
try:
home = os.path.expanduser("~")
config_path = os.path.join(home, "Downloads", "deckyvault-config.json")
with open(config_path, 'w') as f:
json.dump(settings, f, indent=2)
return {"success": True, "path": config_path}
except Exception as e:
return {"success": False, "error": str(e)}
async def import_config(self) -> dict:
"""RPC: Import settings from the latest deckyvault-config.json in Downloads.
Returns {success: bool, settings?: dict, error?: str}."""
try:
home = os.path.expanduser("~")
config_path = os.path.join(home, "Downloads", "deckyvault-config.json")
if not os.path.exists(config_path):
return {"success": False, "error": "No deckyvault-config.json found in Downloads"}
with open(config_path, 'r') as f:
settings = json.load(f)
return {
"success": True,
"settings": {
"apiKey": settings.get("apiKey", ""),
"exportPath": settings.get("exportPath", "/home/deck/Downloads"),
"baseUrl": settings.get("baseUrl", "https://deckyvault.xyz"),
"hardwareSlug": settings.get("hardwareSlug", None),
}
}
except Exception as e:
return {"success": False, "error": str(e)}
@@ -29,20 +29,20 @@ interface SessionFormProps {
} }
const UPSCALER_OPTIONS = [ const UPSCALER_OPTIONS = [
{ label: "None", value: "none" }, { label: "None", data: "none" },
{ label: "FSR", value: "fsr" }, { label: "FSR", data: "fsr" },
{ label: "DLSS", value: "dlss" }, { label: "DLSS", data: "dlss" },
{ label: "XeSS", value: "xess" }, { label: "XeSS", data: "xess" },
{ label: "LSFG", value: "lsfg" }, { label: "LSFG", data: "lsfg" },
{ label: "Other", value: "other" }, { label: "Other", data: "other" },
] ]
const FRAME_GEN_OPTIONS = [ const FRAME_GEN_OPTIONS = [
{ label: "None", value: "none" }, { label: "None", data: "none" },
{ label: "FSR FG", value: "fsr_fg" }, { label: "FSR FG", data: "fsr_fg" },
{ label: "DLSS FG", value: "dlss_fg" }, { label: "DLSS FG", data: "dlss_fg" },
{ label: "LSFG", value: "lsfg" }, { label: "LSFG", data: "lsfg" },
{ label: "Other", value: "other" }, { label: "Other", data: "other" },
] ]
export default function SessionForm({ export default function SessionForm({
@@ -12,10 +12,14 @@ import {
FaTimes, FaTimes,
FaDownload, FaDownload,
FaCog, FaCog,
FaFileExport,
FaFileImport,
FaCopy,
FaSearch,
} from "react-icons/fa" } from "react-icons/fa"
import type { PluginSettings } from "../lib/store" import type { PluginSettings } from "../lib/store"
import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared" import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared"
import { testApiKey, checkMangohud, writeMangohudConfig, getMangohudConfig } from "../lib/api" import { testApiKey, checkMangohud, writeMangohudConfig, getMangohudConfig, exportConfig, importConfig } from "../lib/api"
interface SettingsPanelProps { interface SettingsPanelProps {
settings: PluginSettings settings: PluginSettings
@@ -26,8 +30,8 @@ interface SettingsPanelProps {
} }
const HARDWARE_OPTIONS = [ const HARDWARE_OPTIONS = [
{ label: "Auto-detect", value: "" }, { label: "Auto-detect", data: "" },
...KNOWN_HARDWARE_SLUGS.map((slug) => ({ label: slug, value: slug })), ...KNOWN_HARDWARE_SLUGS.map((slug) => ({ label: slug, data: slug })),
] ]
export default function SettingsPanel({ export default function SettingsPanel({
@@ -42,8 +46,14 @@ export default function SettingsPanel({
path: string path: string
version: string version: string
}>({ checked: false, installed: false, path: "", version: "" }) }>({ checked: false, installed: false, path: "", version: "" })
const [showMangohudGuide, setShowMangohudGuide] = useState(false)
const [configWritten, setConfigWritten] = useState(false) const [configWritten, setConfigWritten] = useState(false)
const [configStatus, setConfigStatus] = useState<{ message: string; isError: boolean } | null>(null)
const [configVerified, setConfigVerified] = useState<{
checked: boolean
valid: boolean
message: string
}>({ checked: false, valid: false, message: "" })
const [copiedLaunchOpt, setCopiedLaunchOpt] = useState(false)
async function handleTestKey() { async function handleTestKey() {
if (!settings.apiKey) { if (!settings.apiKey) {
@@ -78,6 +88,86 @@ export default function SettingsPanel({
setConfigWritten(result.success) setConfigWritten(result.success)
} }
async function handleVerifyConfig() {
const result = await getMangohudConfig()
if (!result.exists) {
setConfigVerified({
checked: true,
valid: false,
message: "No MangoHud config found. Write one first.",
})
return
}
const content = result.content
const hasOutputFolder = content.includes("output_folder=/tmp")
const hasOutputFile = content.includes("output_file=deckyvault-mangohud.log")
const hasFps = content.includes("fps")
const hasFrameTiming = content.includes("frame_timing")
const hasGpuPower = content.includes("gpu_power")
if (hasOutputFolder && hasOutputFile && hasFps) {
setConfigVerified({
checked: true,
valid: true,
message: "Config looks good — logging to /tmp/deckyvault-mangohud.log",
})
} else {
const missing: string[] = []
if (!hasOutputFolder) missing.push("output_folder=/tmp")
if (!hasOutputFile) missing.push("output_file=deckyvault-mangohud.log")
if (!hasFps) missing.push("fps")
if (!hasFrameTiming) missing.push("frame_timing")
if (!hasGpuPower) missing.push("gpu_power")
setConfigVerified({
checked: true,
valid: false,
message: `Missing: ${missing.join(", ")}. Write config again.`,
})
}
}
async function handleCopyLaunchOption() {
try {
await navigator.clipboard.writeText("mangohud %command%")
setCopiedLaunchOpt(true)
setTimeout(() => setCopiedLaunchOpt(false), 2000)
} catch {
// Fallback
const ta = document.createElement("textarea")
ta.value = "mangohud %command%"
document.body.appendChild(ta)
ta.select()
document.execCommand("copy")
document.body.removeChild(ta)
setCopiedLaunchOpt(true)
setTimeout(() => setCopiedLaunchOpt(false), 2000)
}
}
async function handleExportConfig() {
setConfigStatus(null)
const result = await exportConfig(settings)
if (result.success) {
setConfigStatus({ message: `Config saved to ${result.path}`, isError: false })
} else {
setConfigStatus({ message: result.error || "Export failed", isError: true })
}
}
async function handleImportConfig() {
setConfigStatus(null)
const result = await importConfig()
if (result.success && result.settings) {
onUpdateSetting("apiKey", result.settings.apiKey || "")
onUpdateSetting("exportPath", result.settings.exportPath || "/home/deck/Downloads")
onUpdateSetting("baseUrl", result.settings.baseUrl || "https://deckyvault.xyz")
onUpdateSetting("hardwareSlug", result.settings.hardwareSlug || null)
setConfigStatus({ message: "Config imported from Downloads", isError: false })
} else {
setConfigStatus({ message: result.error || "No config file found in Downloads", isError: true })
}
}
return ( return (
<> <>
{/* ── API Key ─────────────────────────────────────────────── */} {/* ── API Key ─────────────────────────────────────────────── */}
@@ -152,6 +242,40 @@ export default function SettingsPanel({
</PanelSectionRow> </PanelSectionRow>
</PanelSection> </PanelSection>
{/* ── Config Export/Import ────────────────────────────────── */}
<PanelSection title="Configuration">
<PanelSectionRow>
<ButtonItem layout="below" onClick={handleExportConfig}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<FaFileExport />
Export Config to Downloads
</div>
</ButtonItem>
</PanelSectionRow>
<PanelSectionRow>
<ButtonItem layout="below" onClick={handleImportConfig}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<FaFileImport />
Import Config from Downloads
</div>
</ButtonItem>
</PanelSectionRow>
{configStatus && (
<PanelSectionRow>
<div
className={staticClasses.Text}
style={{
fontSize: "12px",
padding: "4px 0",
color: configStatus.isError ? "#e74c3c" : "#2ecc71",
}}
>
{configStatus.isError ? <FaTimes /> : <FaCheck />} {configStatus.message}
</div>
</PanelSectionRow>
)}
</PanelSection>
{/* ── MangoHud Setup ──────────────────────────────────────── */} {/* ── MangoHud Setup ──────────────────────────────────────── */}
<PanelSection title="MangoHud Setup"> <PanelSection title="MangoHud Setup">
<PanelSectionRow> <PanelSectionRow>
@@ -196,6 +320,39 @@ export default function SettingsPanel({
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow>
<ButtonItem layout="below" onClick={handleVerifyConfig}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<FaSearch />
Verify Config
</div>
</ButtonItem>
</PanelSectionRow>
{configVerified.checked && (
<PanelSectionRow>
<div
className={staticClasses.Text}
style={{
fontSize: "12px",
padding: "4px 0",
color: configVerified.valid ? "#2ecc71" : "#e74c3c",
}}
>
{configVerified.valid ? <FaCheck /> : <FaTimes />} {configVerified.message}
</div>
</PanelSectionRow>
)}
<PanelSectionRow>
<ButtonItem layout="below" onClick={handleCopyLaunchOption}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<FaCopy />
{copiedLaunchOpt ? "Copied!" : "Copy Launch Option"}
</div>
</ButtonItem>
</PanelSectionRow>
{configWritten && ( {configWritten && (
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "12px", color: "#2ecc71", padding: "4px 0" }}> <div className={staticClasses.Text} style={{ fontSize: "12px", color: "#2ecc71", padding: "4px 0" }}>
@@ -204,13 +361,6 @@ export default function SettingsPanel({
</PanelSectionRow> </PanelSectionRow>
)} )}
<PanelSectionRow>
<ButtonItem layout="below" onClick={() => setShowMangohudGuide(!showMangohudGuide)}>
{showMangohudGuide ? "Hide Guide" : "Show Installation Guide"}
</ButtonItem>
</PanelSectionRow>
{showMangohudGuide && (
<PanelSectionRow> <PanelSectionRow>
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "8px", lineHeight: "1.6" }}> <div className={staticClasses.Text} style={{ fontSize: "12px", padding: "8px", lineHeight: "1.6" }}>
<strong>Steam Deck (SteamOS):</strong> <strong>Steam Deck (SteamOS):</strong>
@@ -251,7 +401,6 @@ export default function SettingsPanel({
Not attaching? Try adding <code>mangohud %command%</code> to Steam launch options explicitly. Not attaching? Try adding <code>mangohud %command%</code> to Steam launch options explicitly.
</div> </div>
</PanelSectionRow> </PanelSectionRow>
)}
</PanelSection> </PanelSection>
</> </>
) )
+48 -2
View File
@@ -7,7 +7,7 @@ import {
import { import {
definePlugin, definePlugin,
} from "@decky/api" } from "@decky/api"
import { FaDatabase } from "react-icons/fa" import { FaChartLine } from "react-icons/fa"
import MainPanel from "./components/main-panel" import MainPanel from "./components/main-panel"
import SettingsPanel from "./components/settings-panel" import SettingsPanel from "./components/settings-panel"
import { useSettings, useSession } from "./lib/store" import { useSettings, useSession } from "./lib/store"
@@ -163,12 +163,58 @@ function Content() {
) )
} }
// ── DeckyVault icon (flat SVG) ────────────────────────────────
function DeckyVaultIcon() {
return (
<svg
viewBox="0 0 2000 2000"
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
style={{
fillRule: "evenodd",
clipRule: "evenodd",
strokeLinecap: "round",
strokeLinejoin: "round",
strokeMiterlimit: 1.5,
}}
>
<g transform="matrix(1,0,0,1,0,37.09322)">
<g transform="matrix(0.861784,0,0,0.861784,133.476373,498.392836)">
<g transform="matrix(1.160384,0,0,1.160384,-154.883825,-621.369358)">
<path
d="M1729.5,1227.837L1729.5,1549.724C1729.5,1640.166 1656.072,1713.593 1565.63,1713.593L433.04,1713.593C343.332,1713.593 270.5,1640.762 270.5,1551.053L270.5,1228.241C270.5,1173.107 315.262,1128.345 370.396,1128.345L1630.008,1128.345C1684.919,1128.345 1729.5,1172.926 1729.5,1227.837ZM573.241,1362.037C573.241,1347.001 561.033,1334.793 545.996,1334.793L440.872,1334.793C425.835,1334.793 413.627,1347.001 413.627,1362.037L413.627,1467.162C413.627,1482.198 425.835,1494.406 440.872,1494.406L545.996,1494.406C561.033,1494.406 573.241,1482.198 573.241,1467.162L573.241,1362.037ZM506.173,1203.28C477.429,1203.28 454.093,1226.617 454.093,1255.361C454.093,1284.105 477.429,1307.441 506.173,1307.441C534.917,1307.441 558.253,1284.105 558.253,1255.361C558.253,1226.617 534.917,1203.28 506.173,1203.28ZM1371.919,1256.579C1371.919,1225.929 1347.036,1201.045 1316.386,1201.045L675.371,1201.045C644.721,1201.045 619.838,1225.929 619.838,1256.579L619.838,1593.602C619.838,1624.252 644.721,1649.136 675.371,1649.136L1316.386,1649.136C1347.036,1649.136 1371.919,1624.252 1371.919,1593.602L1371.919,1256.579ZM1629.368,1260.092C1619.752,1260.092 1611.946,1267.899 1611.946,1277.515C1611.946,1287.131 1619.752,1294.937 1629.368,1294.937C1638.984,1294.937 1646.791,1287.131 1646.791,1277.515C1646.791,1267.899 1638.984,1260.092 1629.368,1260.092ZM1629.368,1173.745C1619.752,1173.745 1611.946,1181.552 1611.946,1191.168C1611.946,1200.784 1619.752,1208.59 1629.368,1208.59C1638.984,1208.59 1646.791,1200.784 1646.791,1191.168C1646.791,1181.552 1638.984,1173.745 1629.368,1173.745ZM1671.673,1216.769C1662.057,1216.769 1654.25,1224.576 1654.25,1234.191C1654.25,1243.807 1662.057,1251.614 1671.673,1251.614C1681.288,1251.614 1689.095,1243.807 1689.095,1234.191C1689.095,1224.576 1681.288,1216.769 1671.673,1216.769ZM1586.56,1216.769C1576.944,1216.769 1569.137,1224.576 1569.137,1234.191C1569.137,1243.807 1576.944,1251.614 1586.56,1251.614C1596.176,1251.614 1603.983,1243.807 1603.983,1234.191C1603.983,1224.576 1596.176,1216.769 1586.56,1216.769ZM353.679,1252.925L353.679,1276.615C353.679,1282.358 358.341,1287.021 364.085,1287.021L380.656,1287.021C386.446,1287.021 391.147,1282.32 391.147,1276.53L391.147,1252.925L418.873,1252.925C422.388,1252.925 425.242,1250.071 425.242,1246.556L425.242,1221.853C425.242,1218.323 422.377,1215.457 418.847,1215.457L391.147,1215.457L391.147,1187.731C391.147,1184.216 388.293,1181.362 384.777,1181.362L360.074,1181.362C356.544,1181.362 353.679,1184.227 353.679,1187.757L353.679,1215.457L329.989,1215.457C324.246,1215.457 319.583,1220.12 319.583,1225.863L319.583,1242.434C319.583,1248.225 324.284,1252.925 330.074,1252.925L353.679,1252.925ZM1491.737,1203.28C1462.993,1203.28 1439.657,1226.617 1439.657,1255.361C1439.657,1284.105 1462.993,1307.441 1491.737,1307.441C1520.481,1307.441 1543.818,1284.105 1543.818,1255.361C1543.818,1226.617 1520.481,1203.28 1491.737,1203.28ZM1584.585,1362.037C1584.585,1347.001 1572.377,1334.793 1557.34,1334.793L1452.216,1334.793C1437.179,1334.793 1424.972,1347.001 1424.972,1362.037L1424.972,1467.162C1424.972,1482.198 1437.179,1494.406 1452.216,1494.406L1557.34,1494.406C1572.377,1494.406 1584.585,1482.198 1584.585,1467.162L1584.585,1362.037Z"
fill="white" stroke="white" strokeWidth="1"
/>
</g>
<g transform="matrix(0.883612,0,0,0.904278,106.86691,103.779729)">
<path
d="M1975,773.67L1975,1186.72C1975,1302.777 1878.573,1397 1759.802,1397L272.452,1397C154.645,1397 59,1303.541 59,1188.426L59,774.189C59,703.439 117.783,646 190.187,646L1844.344,646C1916.455,646 1975,703.207 1975,773.67Z"
fill="none" stroke="white" strokeWidth="64.9"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,-0,-24.728814)">
<path
d="M323,1000L323,891.398C323,841.926 363.195,801.76 412.704,801.76L640.543,801.76L479.436,640.773C444.428,605.791 444.428,548.988 479.436,514.006L563.141,430.363C598.149,395.381 654.994,395.381 690.002,430.363L851.108,591.351L851.108,363.68C851.108,314.208 891.303,274.042 940.812,274.042L1059.188,274.042C1108.697,274.042 1148.892,314.208 1148.892,363.68L1148.892,591.351L1309.998,430.363C1345.006,395.381 1401.851,395.381 1436.859,430.363L1520.564,514.006C1555.572,548.988 1555.572,605.791 1520.564,640.773L1359.457,801.76L1587.296,801.76C1636.805,801.76 1677,841.926 1677,891.398L1677,1000L1180.034,1000C1184.251,984.222 1186.5,967.643 1186.5,950.542C1186.5,844.971 1100.789,759.26 995.218,759.26C889.646,759.26 803.935,844.971 803.935,950.542C803.935,967.643 806.184,984.222 810.401,1000L323,1000Z"
fill="white" stroke="white" strokeWidth="50"
/>
<path
d="M323,1000L323,891.398C323,841.926 363.195,801.76 412.704,801.76L640.543,801.76L479.436,640.773C444.428,605.791 444.428,548.988 479.436,514.006L563.141,430.363C598.149,395.381 654.994,395.381 690.002,430.363L851.108,591.351L851.108,363.68C851.108,314.208 891.303,274.042 940.812,274.042L1059.188,274.042C1108.697,274.042 1148.892,314.208 1148.892,363.68L1148.892,591.351L1309.998,430.363C1345.006,395.381 1401.851,395.381 1436.859,430.363L1520.564,514.006C1555.572,548.988 1555.572,605.791 1520.564,640.773L1359.457,801.76L1587.296,801.76C1636.805,801.76 1677,841.926 1677,891.398L1677,1000L1180.034,1000C1184.251,984.222 1186.5,967.643 1186.5,950.542C1186.5,844.971 1100.789,759.26 995.218,759.26C889.646,759.26 803.935,844.971 803.935,950.542C803.935,967.643 806.184,984.222 810.401,1000L323,1000Z"
fill="none" stroke="white" strokeWidth="50"
/>
</g>
</g>
</svg>
)
}
export default definePlugin(() => { export default definePlugin(() => {
return { return {
name: "DeckyVault", name: "DeckyVault",
titleView: <div className={staticClasses.Title}>DeckyVault</div>, titleView: <div className={staticClasses.Title}>DeckyVault</div>,
content: <Content />, content: <Content />,
icon: <FaDatabase />, icon: <DeckyVaultIcon />,
alwaysRender: false, alwaysRender: false,
onDismount() { onDismount() {
console.log("[DeckyVault] Plugin unloading") console.log("[DeckyVault] Plugin unloading")
+23
View File
@@ -74,3 +74,26 @@ export const testApiKey = callable<[apiKey: string, baseUrl?: string], {
valid: boolean valid: boolean
error?: string error?: string
}>("test_api_key") }>("test_api_key")
// ── Config Export/Import ────────────────────────────────────────
export const exportConfig = callable<[settings: {
apiKey: string
exportPath: string
baseUrl: string
hardwareSlug: string | null
}], {
success: boolean
path?: string
error?: string
}>("export_config")
export const importConfig = callable<[], {
success: boolean
settings?: {
apiKey: string
exportPath: string
baseUrl: string
hardwareSlug: string | null
}
error?: string
}>("import_config")