fix(plugin): screenshot limit, version/build fields, delete button, workspace scripts

- list_screenshots: bump limit 12 → 50 so users can scroll further back
- Add versionString + buildId to DeckyVaultImportV1, SessionData, and
  buildImportPayload. Import endpoint now saves them on new game versions.
- Screenshot delete button in preset detail modal: owner/admin sees a
  trash icon on hover that calls DELETE /api/performance/:id/screenshots/:sid
- Root workspace scripts for plugin workflow:
  bun run plugin:build  | plugin:zip | plugin:release | plugin:deploy
- Add deploy.sh (SSH scripts to push plugin to Deck)
This commit is contained in:
2026-06-29 00:33:20 +08:00
parent 8cff20c13e
commit 868faafb4d
7 changed files with 104 additions and 27 deletions
+38 -7
View File
@@ -169,6 +169,9 @@ export function PresetDetailModal({
const [activeTab, setActiveTab] = useState<TabKey>("media")
const [lightboxOpen, setLightboxOpen] = useState(false)
const [lightboxIndex, setLightboxIndex] = useState(0)
const [deletingScreenshotId, setDeletingScreenshotId] = useState<
string | null
>(null)
const isOwner = session?.user?.id === preset.userId
const isAdmin = session?.user?.role === "admin"
@@ -259,6 +262,23 @@ export function PresetDetailModal({
}
}
async function handleDeleteScreenshot(screenshotId: string) {
setDeletingScreenshotId(screenshotId)
try {
const res = await fetch(
`/api/performance/${preset.id}/screenshots/${screenshotId}`,
{ method: "DELETE" },
)
if (res.ok) {
router.refresh()
}
} catch (err) {
console.error("Failed to delete screenshot:", err)
} finally {
setDeletingScreenshotId(null)
}
}
const visibleTabs: TabKey[] = ["details", "media", "settings"]
if (preset.userNotes) visibleTabs.push("notes")
@@ -854,10 +874,8 @@ export function PresetDetailModal({
<div className='flex flex-col gap-3'>
{preset.screenshots.map(
(ss, i) => (
<div key={ss.id} className='relative group'>
<button
key={
ss.id
}
onClick={() => {
setLightboxIndex(
i,
@@ -866,19 +884,32 @@ export function PresetDetailModal({
true,
)
}}
className='block cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary/50 rounded-lg overflow-hidden w-full'
className='block w-full cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary/50 rounded-lg overflow-hidden'
aria-label={`View screenshot ${i + 1}`}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={
ss.url
}
src={ss.url}
alt={`Screenshot ${i + 1}`}
className='w-full h-auto object-cover border border-border hover:border-primary/50 transition-colors rounded-lg'
loading='lazy'
/>
</button>
{(isOwner || isAdmin) && (
<button
onClick={(e) => {
e.stopPropagation()
handleDeleteScreenshot(ss.id)
}}
disabled={deletingScreenshotId === ss.id}
className='absolute top-2 right-2 p-1.5 rounded-md bg-black/50 text-white/60 hover:text-red-400 hover:bg-red-500/20 opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer disabled:cursor-not-allowed'
aria-label={`Delete screenshot ${i + 1}`}
title='Delete screenshot'
>
<TrashIcon className='h-3.5 w-3.5' />
</button>
)}
</div>
),
)}
</div>
+4
View File
@@ -88,6 +88,8 @@ export const performanceImportRoutes = new Elysia({
.values({
gameId: game.id,
isLatest: true,
versionString: body.versionString ?? null,
buildId: body.buildId ?? null,
})
.returning({ id: gameVersions.id })
version = newVersion
@@ -260,6 +262,8 @@ export const performanceImportRoutes = new Elysia({
fpsHigh: t.Optional(t.Nullable(t.Number())),
protonVersion: t.Optional(t.Nullable(t.String())),
osVersion: t.Optional(t.Nullable(t.String())),
versionString: t.Optional(t.Nullable(t.String())),
buildId: t.Optional(t.Nullable(t.String())),
upscalerType: t.Optional(t.String()),
upscalerVersion: t.Optional(t.Nullable(t.String())),
frameGenMethod: t.Optional(t.String()),
+5 -1
View File
@@ -14,7 +14,11 @@
"db:migrate": "bun run --filter @deckyvault/web db:migrate",
"db:push": "bun run --filter @deckyvault/web db:push",
"db:studio": "bun run --filter @deckyvault/web db:studio",
"db:seed": "bun run --filter @deckyvault/web db:seed"
"db:seed": "bun run --filter @deckyvault/web db:seed",
"plugin:build": "bun run --filter @deckyvault/plugin build",
"plugin:zip": "bun run --filter @deckyvault/plugin zip",
"plugin:release": "bun run --filter @deckyvault/plugin release",
"plugin:deploy": "bun run --filter @deckyvault/plugin build && bash plugins/decky-vault/scripts/deploy.sh"
},
"trustedDependencies": ["sharp", "unrs-resolver"],
"overrides": {
+2
View File
@@ -38,6 +38,8 @@ export interface DeckyVaultImportV1 {
settingsJson?: unknown[] | null
userNotes?: string | null
customSystem?: boolean
versionString?: string | null
buildId?: string | null
antiCheatRelevant?: boolean
antiCheatName?: string | null
antiCheatStatus?: string
+1 -1
View File
@@ -605,7 +605,7 @@ exec mangohud "$@"
except Exception as e:
return {"success": False, "error": str(e), "status": 0}
async def list_screenshots(self, limit: int = 12) -> dict:
async def list_screenshots(self, limit: int = 50) -> dict:
"""RPC: List recent Steam Deck screenshots from ~/Pictures/Screenshots/.
Returns {screenshots: [{path, name, mtime, size}], error?}.
Steam saves timestamped JPGs in a 'Steam Client' subfolder and keeps
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Deploy the built plugin dist/ + main.py to the Steam Deck and restart the
# plugin loader. The build step is handled separately (bun run plugin:build
# calls rollup first, then this script runs).
set -euo pipefail
DECK_IP="${DECK_IP:-192.168.254.112}"
DECK_USER="${DECK_USER:-deck}"
DECK_PASS="${DECK_PASS:-22122012003}"
PLUGIN_DIR="/home/deck/homebrew/plugins/decky-vault"
cd "$(dirname "$0")/.."
echo " Deploying to ${DECK_USER}@${DECK_IP}"
sshpass -p "$DECK_PASS" ssh -o StrictHostKeyChecking=no "${DECK_USER}@${DECK_IP}" \
"mkdir -p /tmp/dv-deploy && rm -rf /tmp/dv-deploy/*"
sshpass -p "$DECK_PASS" scp -o StrictHostKeyChecking=no -r dist main.py \
"${DECK_USER}@${DECK_IP}:/tmp/dv-deploy/"
sshpass -p "$DECK_PASS" ssh -o StrictHostKeyChecking=no "${DECK_USER}@${DECK_IP}" \
"echo '${DECK_PASS}' | sudo -S cp /tmp/dv-deploy/dist/index.js ${PLUGIN_DIR}/dist/index.js && \
echo '${DECK_PASS}' | sudo -S cp /tmp/dv-deploy/main.py ${PLUGIN_DIR}/main.py && \
echo '${DECK_PASS}' | sudo -S systemctl restart plugin_loader.service && \
sleep 1 && echo 'restarted'"
echo "✓ Deployed — plugin loaded:"
sshpass -p "$DECK_PASS" ssh -o StrictHostKeyChecking=no "${DECK_USER}@${DECK_IP}" \
"ls -t /home/deck/homebrew/logs/decky-vault/ | head -1 | xargs -I{} tail -1 '/home/deck/homebrew/logs/decky-vault/{}'"
+6
View File
@@ -34,6 +34,8 @@ export interface SessionData {
hardwareName: string
osVersion: string
protonVersion: string
versionString: string
buildId: string
// Manual inputs (filled by user in the form)
upscalerType: string
upscalerVersion: string
@@ -66,6 +68,8 @@ function createEmptySession(): SessionData {
hardwareName: "",
osVersion: "",
protonVersion: "",
versionString: "",
buildId: "",
upscalerType: "none",
upscalerVersion: "",
frameGenMethod: "none",
@@ -244,6 +248,8 @@ export function buildImportPayload(sess: SessionData): DeckyVaultImportV1 {
fpsHigh: sess.fpsHigh,
protonVersion: sess.protonVersion || null,
osVersion: sess.osVersion || null,
versionString: sess.versionString || null,
buildId: sess.buildId || null,
upscalerType: sess.upscalerType,
upscalerVersion: sess.upscalerVersion || null,
frameGenMethod: sess.frameGenMethod,