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:
@@ -169,6 +169,9 @@ export function PresetDetailModal({
|
|||||||
const [activeTab, setActiveTab] = useState<TabKey>("media")
|
const [activeTab, setActiveTab] = useState<TabKey>("media")
|
||||||
const [lightboxOpen, setLightboxOpen] = useState(false)
|
const [lightboxOpen, setLightboxOpen] = useState(false)
|
||||||
const [lightboxIndex, setLightboxIndex] = useState(0)
|
const [lightboxIndex, setLightboxIndex] = useState(0)
|
||||||
|
const [deletingScreenshotId, setDeletingScreenshotId] = useState<
|
||||||
|
string | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
const isOwner = session?.user?.id === preset.userId
|
const isOwner = session?.user?.id === preset.userId
|
||||||
const isAdmin = session?.user?.role === "admin"
|
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"]
|
const visibleTabs: TabKey[] = ["details", "media", "settings"]
|
||||||
if (preset.userNotes) visibleTabs.push("notes")
|
if (preset.userNotes) visibleTabs.push("notes")
|
||||||
|
|
||||||
@@ -854,31 +874,42 @@ export function PresetDetailModal({
|
|||||||
<div className='flex flex-col gap-3'>
|
<div className='flex flex-col gap-3'>
|
||||||
{preset.screenshots.map(
|
{preset.screenshots.map(
|
||||||
(ss, i) => (
|
(ss, i) => (
|
||||||
<button
|
<div key={ss.id} className='relative group'>
|
||||||
key={
|
<button
|
||||||
ss.id
|
onClick={() => {
|
||||||
}
|
setLightboxIndex(
|
||||||
onClick={() => {
|
i,
|
||||||
setLightboxIndex(
|
)
|
||||||
i,
|
setLightboxOpen(
|
||||||
)
|
true,
|
||||||
setLightboxOpen(
|
)
|
||||||
true,
|
}}
|
||||||
)
|
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}`}
|
||||||
className='block cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary/50 rounded-lg overflow-hidden w-full'
|
>
|
||||||
aria-label={`View screenshot ${i + 1}`}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
>
|
<img
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
src={ss.url}
|
||||||
<img
|
alt={`Screenshot ${i + 1}`}
|
||||||
src={
|
className='w-full h-auto object-cover border border-border hover:border-primary/50 transition-colors rounded-lg'
|
||||||
ss.url
|
loading='lazy'
|
||||||
}
|
/>
|
||||||
alt={`Screenshot ${i + 1}`}
|
</button>
|
||||||
className='w-full h-auto object-cover border border-border hover:border-primary/50 transition-colors rounded-lg'
|
{(isOwner || isAdmin) && (
|
||||||
loading='lazy'
|
<button
|
||||||
/>
|
onClick={(e) => {
|
||||||
</button>
|
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>
|
</div>
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ export const performanceImportRoutes = new Elysia({
|
|||||||
.values({
|
.values({
|
||||||
gameId: game.id,
|
gameId: game.id,
|
||||||
isLatest: true,
|
isLatest: true,
|
||||||
|
versionString: body.versionString ?? null,
|
||||||
|
buildId: body.buildId ?? null,
|
||||||
})
|
})
|
||||||
.returning({ id: gameVersions.id })
|
.returning({ id: gameVersions.id })
|
||||||
version = newVersion
|
version = newVersion
|
||||||
@@ -260,6 +262,8 @@ export const performanceImportRoutes = new Elysia({
|
|||||||
fpsHigh: t.Optional(t.Nullable(t.Number())),
|
fpsHigh: t.Optional(t.Nullable(t.Number())),
|
||||||
protonVersion: t.Optional(t.Nullable(t.String())),
|
protonVersion: t.Optional(t.Nullable(t.String())),
|
||||||
osVersion: 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()),
|
upscalerType: t.Optional(t.String()),
|
||||||
upscalerVersion: t.Optional(t.Nullable(t.String())),
|
upscalerVersion: t.Optional(t.Nullable(t.String())),
|
||||||
frameGenMethod: t.Optional(t.String()),
|
frameGenMethod: t.Optional(t.String()),
|
||||||
|
|||||||
+5
-1
@@ -14,7 +14,11 @@
|
|||||||
"db:migrate": "bun run --filter @deckyvault/web db:migrate",
|
"db:migrate": "bun run --filter @deckyvault/web db:migrate",
|
||||||
"db:push": "bun run --filter @deckyvault/web db:push",
|
"db:push": "bun run --filter @deckyvault/web db:push",
|
||||||
"db:studio": "bun run --filter @deckyvault/web db:studio",
|
"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"],
|
"trustedDependencies": ["sharp", "unrs-resolver"],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export interface DeckyVaultImportV1 {
|
|||||||
settingsJson?: unknown[] | null
|
settingsJson?: unknown[] | null
|
||||||
userNotes?: string | null
|
userNotes?: string | null
|
||||||
customSystem?: boolean
|
customSystem?: boolean
|
||||||
|
versionString?: string | null
|
||||||
|
buildId?: string | null
|
||||||
antiCheatRelevant?: boolean
|
antiCheatRelevant?: boolean
|
||||||
antiCheatName?: string | null
|
antiCheatName?: string | null
|
||||||
antiCheatStatus?: string
|
antiCheatStatus?: string
|
||||||
|
|||||||
@@ -605,7 +605,7 @@ exec mangohud "$@"
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"success": False, "error": str(e), "status": 0}
|
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/.
|
"""RPC: List recent Steam Deck screenshots from ~/Pictures/Screenshots/.
|
||||||
Returns {screenshots: [{path, name, mtime, size}], error?}.
|
Returns {screenshots: [{path, name, mtime, size}], error?}.
|
||||||
Steam saves timestamped JPGs in a 'Steam Client' subfolder and keeps
|
Steam saves timestamped JPGs in a 'Steam Client' subfolder and keeps
|
||||||
|
|||||||
Executable
+30
@@ -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/{}'"
|
||||||
@@ -34,6 +34,8 @@ export interface SessionData {
|
|||||||
hardwareName: string
|
hardwareName: string
|
||||||
osVersion: string
|
osVersion: string
|
||||||
protonVersion: string
|
protonVersion: string
|
||||||
|
versionString: string
|
||||||
|
buildId: string
|
||||||
// Manual inputs (filled by user in the form)
|
// Manual inputs (filled by user in the form)
|
||||||
upscalerType: string
|
upscalerType: string
|
||||||
upscalerVersion: string
|
upscalerVersion: string
|
||||||
@@ -66,6 +68,8 @@ function createEmptySession(): SessionData {
|
|||||||
hardwareName: "",
|
hardwareName: "",
|
||||||
osVersion: "",
|
osVersion: "",
|
||||||
protonVersion: "",
|
protonVersion: "",
|
||||||
|
versionString: "",
|
||||||
|
buildId: "",
|
||||||
upscalerType: "none",
|
upscalerType: "none",
|
||||||
upscalerVersion: "",
|
upscalerVersion: "",
|
||||||
frameGenMethod: "none",
|
frameGenMethod: "none",
|
||||||
@@ -244,6 +248,8 @@ export function buildImportPayload(sess: SessionData): DeckyVaultImportV1 {
|
|||||||
fpsHigh: sess.fpsHigh,
|
fpsHigh: sess.fpsHigh,
|
||||||
protonVersion: sess.protonVersion || null,
|
protonVersion: sess.protonVersion || null,
|
||||||
osVersion: sess.osVersion || null,
|
osVersion: sess.osVersion || null,
|
||||||
|
versionString: sess.versionString || null,
|
||||||
|
buildId: sess.buildId || null,
|
||||||
upscalerType: sess.upscalerType,
|
upscalerType: sess.upscalerType,
|
||||||
upscalerVersion: sess.upscalerVersion || null,
|
upscalerVersion: sess.upscalerVersion || null,
|
||||||
frameGenMethod: sess.frameGenMethod,
|
frameGenMethod: sess.frameGenMethod,
|
||||||
|
|||||||
Reference in New Issue
Block a user