Merge branch 'dev' into prod
This commit is contained in:
@@ -646,6 +646,39 @@ exec mangohud "$@"
|
||||
except Exception as e:
|
||||
return {"screenshots": [], "error": str(e)}
|
||||
|
||||
async def read_screenshot(self, path: str, max_width: int = 320) -> dict:
|
||||
"""RPC: Read a screenshot file and return it as a base64 data URL,
|
||||
downscaled to a thumbnail for preview display in the plugin UI.
|
||||
Returns {dataUrl: str, error?: str}."""
|
||||
import base64
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
return {"dataUrl": "", "error": "File not found"}
|
||||
with open(path, "rb") as f:
|
||||
raw = f.read()
|
||||
# Downscale via Pillow if available, else return raw bytes as-is
|
||||
try:
|
||||
from PIL import Image
|
||||
import io
|
||||
img = Image.open(io.BytesIO(raw))
|
||||
if img.mode not in ("RGB", "RGBA"):
|
||||
img = img.convert("RGB")
|
||||
if img.width > max_width:
|
||||
ratio = max_width / float(img.width)
|
||||
img = img.resize((max_width, int(img.height * ratio)))
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=70)
|
||||
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
|
||||
return {"dataUrl": f"data:image/jpeg;base64,{b64}"}
|
||||
except ImportError:
|
||||
# No Pillow — return the raw file as a data URL
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
mime = "image/png" if ext == ".png" else ("image/webp" if ext == ".webp" else "image/jpeg")
|
||||
b64 = base64.b64encode(raw).decode("ascii")
|
||||
return {"dataUrl": f"data:{mime};base64,{b64}"}
|
||||
except Exception as e:
|
||||
return {"dataUrl": "", "error": str(e)}
|
||||
|
||||
async def upload_screenshots(self, entry_id: str, screenshot_paths: list, api_key: str, base_url: str = "https://deckyvault.xyz") -> dict:
|
||||
"""RPC: Upload up to 2 screenshots to a performance entry as multipart/form-data.
|
||||
Returns {success: bool, uploaded: int, error?: str, status?: int}.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import {
|
||||
ButtonItem,
|
||||
PanelSection,
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
exportToFile,
|
||||
uploadToDeckyvault,
|
||||
listScreenshots,
|
||||
readScreenshot,
|
||||
uploadScreenshots,
|
||||
} from "../lib/api"
|
||||
|
||||
@@ -77,6 +78,41 @@ function formatSize(bytes: number): string {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function Thumbnail({ src, size = 56 }: { src?: string; size?: number }) {
|
||||
if (src) {
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
objectFit: "cover",
|
||||
borderRadius: "6px",
|
||||
flexShrink: 0,
|
||||
background: "#000",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: "6px",
|
||||
flexShrink: 0,
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<FaImages style={{ opacity: 0.4 }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SessionForm({
|
||||
session,
|
||||
error,
|
||||
@@ -96,6 +132,8 @@ export default function SessionForm({
|
||||
const [availableShots, setAvailableShots] = useState<ScreenshotFile[]>([])
|
||||
const [shotsLoading, setShotsLoading] = useState(false)
|
||||
const [shotsError, setShotsError] = useState("")
|
||||
// path → base64 data URL thumbnail for preview display
|
||||
const [thumbnails, setThumbnails] = useState<Record<string, string>>({})
|
||||
|
||||
async function openPicker() {
|
||||
if (selectedShots.length >= MAX_SCREENSHOTS) return
|
||||
@@ -108,9 +146,30 @@ export default function SessionForm({
|
||||
setShotsError(result.error)
|
||||
}
|
||||
// Filter out already-selected paths
|
||||
setAvailableShots(result.screenshots.filter((s) => !selectedShots.some((sel) => sel.path === s.path)))
|
||||
const list = result.screenshots.filter((s) => !selectedShots.some((sel) => sel.path === s.path))
|
||||
setAvailableShots(list)
|
||||
// Load thumbnails for the list (and any selected shots not yet loaded)
|
||||
loadThumbnails([...list, ...selectedShots])
|
||||
}
|
||||
|
||||
async function loadThumbnails(shots: ScreenshotFile[]) {
|
||||
for (const shot of shots) {
|
||||
if (thumbnails[shot.path]) continue // already loaded
|
||||
const res = await readScreenshot(shot.path, 320)
|
||||
if (res.dataUrl) {
|
||||
setThumbnails((prev) => ({ ...prev, [shot.path]: res.dataUrl }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load thumbnails for any selected shots without one yet
|
||||
useEffect(() => {
|
||||
if (selectedShots.length > 0) {
|
||||
loadThumbnails(selectedShots)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedShots.map((s) => s.path).join(",")])
|
||||
|
||||
function addShot(shot: ScreenshotFile) {
|
||||
if (selectedShots.length >= MAX_SCREENSHOTS) return
|
||||
setSelectedShots((prev) => [...prev, shot])
|
||||
@@ -305,7 +364,7 @@ export default function SessionForm({
|
||||
background: "rgba(255,255,255,0.06)",
|
||||
border: "1px solid rgba(255,255,255,0.10)",
|
||||
}}>
|
||||
<FaImages style={{ opacity: 0.6, flexShrink: 0 }} />
|
||||
<Thumbnail src={thumbnails[shot.path]} />
|
||||
<div className={staticClasses.Text} style={{ flex: 1, minWidth: 0, fontSize: "12px" }}>
|
||||
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{shot.name}
|
||||
@@ -381,7 +440,7 @@ export default function SessionForm({
|
||||
<PanelSectionRow key={shot.path}>
|
||||
<ButtonItem layout="below" onClick={() => addShot(shot)}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<FaImages style={{ opacity: 0.6, flexShrink: 0 }} />
|
||||
<Thumbnail src={thumbnails[shot.path]} />
|
||||
<div style={{ flex: 1, minWidth: 0, textAlign: "left" }}>
|
||||
<div style={{ fontSize: "12px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{formatShotTime(shot.mtime)}
|
||||
|
||||
@@ -86,6 +86,11 @@ export const listScreenshots = callable<[limit?: number], {
|
||||
error?: string
|
||||
}>("list_screenshots")
|
||||
|
||||
export const readScreenshot = callable<[path: string, maxWidth?: number], {
|
||||
dataUrl: string
|
||||
error?: string
|
||||
}>("read_screenshot")
|
||||
|
||||
export const uploadScreenshots = callable<[
|
||||
entryId: string,
|
||||
screenshotPaths: string[],
|
||||
|
||||
Reference in New Issue
Block a user