Merge branch 'dev' into prod
This commit is contained in:
@@ -646,6 +646,39 @@ exec mangohud "$@"
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"screenshots": [], "error": str(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:
|
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.
|
"""RPC: Upload up to 2 screenshots to a performance entry as multipart/form-data.
|
||||||
Returns {success: bool, uploaded: int, error?: str, status?: int}.
|
Returns {success: bool, uploaded: int, error?: str, status?: int}.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import {
|
import {
|
||||||
ButtonItem,
|
ButtonItem,
|
||||||
PanelSection,
|
PanelSection,
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
exportToFile,
|
exportToFile,
|
||||||
uploadToDeckyvault,
|
uploadToDeckyvault,
|
||||||
listScreenshots,
|
listScreenshots,
|
||||||
|
readScreenshot,
|
||||||
uploadScreenshots,
|
uploadScreenshots,
|
||||||
} from "../lib/api"
|
} from "../lib/api"
|
||||||
|
|
||||||
@@ -77,6 +78,41 @@ function formatSize(bytes: number): string {
|
|||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
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({
|
export default function SessionForm({
|
||||||
session,
|
session,
|
||||||
error,
|
error,
|
||||||
@@ -96,6 +132,8 @@ export default function SessionForm({
|
|||||||
const [availableShots, setAvailableShots] = useState<ScreenshotFile[]>([])
|
const [availableShots, setAvailableShots] = useState<ScreenshotFile[]>([])
|
||||||
const [shotsLoading, setShotsLoading] = useState(false)
|
const [shotsLoading, setShotsLoading] = useState(false)
|
||||||
const [shotsError, setShotsError] = useState("")
|
const [shotsError, setShotsError] = useState("")
|
||||||
|
// path → base64 data URL thumbnail for preview display
|
||||||
|
const [thumbnails, setThumbnails] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
async function openPicker() {
|
async function openPicker() {
|
||||||
if (selectedShots.length >= MAX_SCREENSHOTS) return
|
if (selectedShots.length >= MAX_SCREENSHOTS) return
|
||||||
@@ -108,9 +146,30 @@ export default function SessionForm({
|
|||||||
setShotsError(result.error)
|
setShotsError(result.error)
|
||||||
}
|
}
|
||||||
// Filter out already-selected paths
|
// 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) {
|
function addShot(shot: ScreenshotFile) {
|
||||||
if (selectedShots.length >= MAX_SCREENSHOTS) return
|
if (selectedShots.length >= MAX_SCREENSHOTS) return
|
||||||
setSelectedShots((prev) => [...prev, shot])
|
setSelectedShots((prev) => [...prev, shot])
|
||||||
@@ -305,7 +364,7 @@ export default function SessionForm({
|
|||||||
background: "rgba(255,255,255,0.06)",
|
background: "rgba(255,255,255,0.06)",
|
||||||
border: "1px solid rgba(255,255,255,0.10)",
|
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 className={staticClasses.Text} style={{ flex: 1, minWidth: 0, fontSize: "12px" }}>
|
||||||
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||||
{shot.name}
|
{shot.name}
|
||||||
@@ -381,7 +440,7 @@ export default function SessionForm({
|
|||||||
<PanelSectionRow key={shot.path}>
|
<PanelSectionRow key={shot.path}>
|
||||||
<ButtonItem layout="below" onClick={() => addShot(shot)}>
|
<ButtonItem layout="below" onClick={() => addShot(shot)}>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
<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={{ flex: 1, minWidth: 0, textAlign: "left" }}>
|
||||||
<div style={{ fontSize: "12px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
<div style={{ fontSize: "12px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||||
{formatShotTime(shot.mtime)}
|
{formatShotTime(shot.mtime)}
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ export const listScreenshots = callable<[limit?: number], {
|
|||||||
error?: string
|
error?: string
|
||||||
}>("list_screenshots")
|
}>("list_screenshots")
|
||||||
|
|
||||||
|
export const readScreenshot = callable<[path: string, maxWidth?: number], {
|
||||||
|
dataUrl: string
|
||||||
|
error?: string
|
||||||
|
}>("read_screenshot")
|
||||||
|
|
||||||
export const uploadScreenshots = callable<[
|
export const uploadScreenshots = callable<[
|
||||||
entryId: string,
|
entryId: string,
|
||||||
screenshotPaths: string[],
|
screenshotPaths: string[],
|
||||||
|
|||||||
Reference in New Issue
Block a user